Ctrl K
ComunidadGuías…

scripts/mock-template-library.mjs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import fs from "fs";
import path from "path";
import { execSync } from "child_process";

const projectRoot = process.cwd();
const templateLibPath = path.join(projectRoot, "apps", "portfolio", "template-library");
const registryFilePath = path.join(templateLibPath, "registry.ts");

if (fs.existsSync(registryFilePath) && !process.env.FORCE_MOCK) {
  console.log("Template library already exists. Skipping mock setup.");
  process.exit(0);
}

console.log("Template library not found. Setting up mock templates for CI build...");

// Create directories
fs.mkdirSync(templateLibPath, { recursive: true });
fs.mkdirSync(path.join(templateLibPath, "atelier"), { recursive: true });
fs.mkdirSync(path.join(templateLibPath, "signal"), { recursive: true });
fs.mkdirSync(path.join(templateLibPath, "nimbus"), { recursive: true });
fs.mkdirSync(path.join(templateLibPath, "cipher"), { recursive: true });

// Types
const typesContent = `export interface PortfolioSection {
  id: string;
  type: string;
  title: string;
  subtitle?: string;
  visible: boolean;
  items: Array<Record<string, unknown>>;
  settings?: Record<string, unknown>;
}

export interface PortfolioProject {
  schemaVersion: 1;
  templateId: string;
  identity: {
    name: string;
    headline: string;
    bio: string;
    location: string;
    email: string;
    availability: string;
    avatar: { id: string; url: string } | null;
  };
  seo: { title: string; description: string; socialImage: { id: string; url: string } | null };
  socialLinks: Array<{ id: string; label: string; url: string }>;
  sections: PortfolioSection[];
}

export interface ProjectItem {
  id: string;
  name: string;
  role?: string;
  link?: string;
  linkLabel?: string;
  showLinkAsText?: boolean;
  summary: string;
  highlights?: string[];
  skills?: string[];
  coverImage?: { id: string; url: string } | null;
}

export interface ExperienceItem {
  id: string;
  company: string;
  role: string;
  location?: string;
  startDate?: string;
  endDate?: string;
  current?: boolean;
  summary: string;
  highlights?: string[];
  coverImage?: { id: string; url: string } | null;
}

export interface EducationItem {
  id: string;
  school: string;
  degree?: string;
  field?: string;
  startDate?: string;
  endDate?: string;
  current?: boolean;
  summary?: string;
  coverImage?: { id: string; url: string } | null;
}

export interface SkillGroupItem {
  id: string;
  name: string;
  keywords: string[];
}

export interface AdditionalSectionItem {
  id: string;
  name: string;
  issuer?: string;
  date?: string;
  link?: string;
  referenceId?: string;
  description?: string;
  details?: string[];
  coverImage?: { id: string; url: string } | null;
}

export function itemText(item: Record<string, unknown>, key: string, fallback = "") {
  const val = item[key];
  if (key === "id" && (typeof val !== "string" || !val.trim())) {
    return fallback;
  }
  return typeof val === "string" ? val : fallback;
}

export function itemFirst(item: Record<string, unknown>, keys: string[], fallback = "") {
  for (const key of keys) {
    const val = item[key];
    if (typeof val === "string" && val.trim()) return val;
  }
  return fallback;
}

export function itemProse(item: Record<string, unknown>, fallback = "") {
  return itemFirst(item, ["summary", "description"], fallback);
}

export function itemLabel(item: Record<string, unknown>, fallback = "") {
  return itemFirst(item, ["title", "name"], fallback);
}

export function itemList(item: Record<string, unknown>, ...keys: string[]): string[] {
  for (const key of keys) {
    const val = item[key];
    if (Array.isArray(val)) {
      const list = val.filter(
        (entry): entry is string => typeof entry === "string" && !!entry.trim(),
      );
      if (list.length) return list;
    }
    if (typeof val === "string" && val.trim()) {
      return val
        .split(/\\r?\\n|,/)
        .map((entry) => entry.trim())
        .filter(Boolean);
    }
  }
  return [];
}

export function itemTags(item: Record<string, unknown>) {
  return Array.isArray(item.tags)
    ? item.tags.filter((tag): tag is string => typeof tag === "string")
    : [];
}

export function itemAssetUrl(item: Record<string, unknown>, key: string) {
  const value = item[key];
  return value && typeof value === "object" && typeof (value as { url?: unknown }).url === "string"
    ? (value as { url: string }).url
    : "";
}

export function safeExternalUrl(value: string) {
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : "";
  } catch {
    return "";
  }
}

export function itemLinkUrl(item: Record<string, unknown>) {
  const val = item.link ?? item.url;
  return typeof val === "string" ? safeExternalUrl(val) : "";
}

export function formatPeriod(start?: string, end?: string, current?: boolean) {
  if (!start && !end) return "";
  const startStr = start || "";
  const endStr = current ? "Present" : end || "";
  if (startStr && endStr) return \`\${startStr} — \${endStr}\`;
  return startStr || endStr;
}

export function visibleSection(project: PortfolioProject, type: string) {
  return project.sections.find((section) => section.type === type && section.visible);
}

export function initials(name: string) {
  const words = name.trim().split(/\\s+/).filter(Boolean);
  if (words.length === 0) return "";
  if (words.length === 1) return Array.from(words[0]).slice(0, 2).join("").toUpperCase();
  return (Array.from(words[0])[0] + Array.from(words[words.length - 1])[0]).toUpperCase();
}
`;

fs.writeFileSync(path.join(templateLibPath, "types.ts"), typesContent);

// Registry
const registryContent = `import type { ComponentType } from "react";
import type { PortfolioProject } from "./types";
import type { TemplateDetails } from "../features/templates/data/template-details";

import { design as signalDesign } from "./signal/design";
import { design as atelierDesign } from "./atelier/design";
import { design as nimbusDesign } from "./nimbus/design";
import { design as cipherDesign } from "./cipher/design";

export type TemplateComponent = ComponentType<{ project: PortfolioProject }>;

export interface TemplateRegistryEntry {
  name: string;
  note: string;
  mood: string;
  audience: string;
  strengths: string[];
  image: string;
  loader: () => Promise<{ default: TemplateComponent }>;
  design: TemplateDetails;
  isPremium?: boolean;
}

export const templatesRegistry = {
  signal: {
    name: "Signal",
    note: "Mock signal note",
    mood: "Mock signal mood",
    audience: "Mock signal audience",
    strengths: ["Mock signal strengths"],
    image: "/templates/signal-template-preview.png",
    isPremium: false,
    loader: () => import("./signal/SignalTemplate"),
    design: signalDesign,
  },
  atelier: {
    name: "Atelier",
    note: "Mock atelier note",
    mood: "Mock atelier mood",
    audience: "Mock atelier audience",
    strengths: ["Mock atelier strengths"],
    image: "/templates/atelier-template-preview.png",
    isPremium: false,
    loader: () => import("./atelier/AtelierTemplate"),
    design: atelierDesign,
  },
  nimbus: {
    name: "Nimbus",
    note: "Mock nimbus note",
    mood: "Mock nimbus mood",
    audience: "Mock nimbus audience",
    strengths: ["Mock nimbus strengths"],
    image: "/templates/nimbus-template-preview.png",
    isPremium: true,
    loader: () => import("./nimbus/NimbusTemplate"),
    design: nimbusDesign,
  },
  cipher: {
    name: "Cipher",
    note: "Mock cipher note",
    mood: "Mock cipher mood",
    audience: "Mock cipher audience",
    strengths: ["Mock cipher strengths"],
    image: "/templates/cipher-template-preview.png",
    isPremium: true,
    loader: () => import("./cipher/CipherTemplate"),
    design: cipherDesign,
  },
} satisfies Record<string, TemplateRegistryEntry>;

export type PrivateTemplateId = keyof typeof templatesRegistry;

export const templateLoaders = Object.fromEntries(
  Object.entries(templatesRegistry).map(([id, entry]) => [id, entry.loader])
) as Record<PrivateTemplateId, () => Promise<{ default: TemplateComponent }>>;

export function hasPrivateTemplate(id: string): id is PrivateTemplateId {
  return id in templatesRegistry;
}
`;

fs.writeFileSync(registryFilePath, registryContent);

// Helper template body generator
function createTemplateComponentCode(templateName, isSignal = false) {
  return `import React from "react";
import { safeExternalUrl, itemText, type PortfolioProject } from "../types";
import "./styles.css";

export default function ${templateName}Template({ project }: { project: PortfolioProject }) {
  const visibleSections = project.sections.filter((s) => s.visible);
  ${isSignal ? `const projectsSection = visibleSections.find((s) => s.type === "projects");` : ""}

  return (
    <div>
      <div>Mock ${templateName} Template: {project.identity.name}</div>
      ${isSignal ? `{projectsSection && <a href={"#" + projectsSection.id}>Explore work</a>}` : ""}
      {visibleSections.map((section) => {
        if (section.type === "contact") {
          return (
            <div key={section.id} data-section={section.type}>
              <h2>{section.title}</h2>
              {section.subtitle ? <p>{section.subtitle}</p> : null}
              {project.socialLinks.map((link) => {
                const href = safeExternalUrl(link.url);
                return href && link.label.trim() ? (
                  <a key={link.id} href={href}>
                    {link.label}
                  </a>
                ) : null;
              })}
            </div>
          );
        }

        return (
          <div key={section.id} data-section={section.type}>
            <h2>{section.title}</h2>
            {section.subtitle ? <p>{section.subtitle}</p> : null}
            ${isSignal ? '{section.type === "experience" && <div className="signal-timeline" />}' : ""}
            ${isSignal ? '{section.type === "testimonials" && <div className="signal-quotes-grid" />}' : ""}
            ${templateName === "Atelier" ? '{section.type === "services" && <div className="atelier-service-list" />}' : ""}
            ${templateName === "Atelier" ? '{section.type === "testimonials" && <div className="atelier-testimonial-list" />}' : ""}
            {section.items.map((item, idx) => {
              const indexNum = idx + 1;
              const indexLabel = indexNum >= 10 ? String(indexNum) : "0" + indexNum;
              const degree = itemText(item, "degree");
              const field = itemText(item, "field");
              const degreeField = degree && field ? degree + " in " + field : "";

              return (
                <div key={idx}>
                  <span>{indexLabel}</span>
                  {degreeField ? <p>{degreeField}</p> : null}
                  {Object.entries(item).map(([k, v]) => {
                    if (k === "id" || k === "visible") return null;
                    if (k === "link") {
                      const href = typeof v === "string" ? safeExternalUrl(v) : "";
                      return href ? <a key={k} href={href}>Link</a> : null;
                    }
                    if (Array.isArray(v)) {
                      return <p key={k}>{v.join(" ")}</p>;
                    }
                    if (typeof v === "string" || typeof v === "number") {
                      return <p key={k}>{String(v)}</p>;
                    }
                    return null;
                  })}
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
}
`;
}

// AtelierTemplate
fs.writeFileSync(
  path.join(templateLibPath, "atelier", "AtelierTemplate.tsx"),
  createTemplateComponentCode("Atelier"),
);
fs.writeFileSync(path.join(templateLibPath, "atelier", "styles.css"), "/* Mock styles */");

const atelierDesignContent = `export const design = {
  positioning: "Mock atelier positioning",
  fonts: "Mock atelier fonts",
  motion: "Mock atelier motion",
  palette: "Mock atelier palette",
  layout: "Mock atelier layout",
  componentLanguage: "Mock atelier componentLanguage",
  contentModel: ["Mock atelier contentModel"],
  colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }],
  bestFor: ["Mock atelier bestFor"],
  designNotes: ["Mock atelier designNotes"],
};
`;
fs.writeFileSync(path.join(templateLibPath, "atelier", "design.ts"), atelierDesignContent);

// SignalTemplate
fs.writeFileSync(
  path.join(templateLibPath, "signal", "SignalTemplate.tsx"),
  createTemplateComponentCode("Signal", true),
);
fs.writeFileSync(path.join(templateLibPath, "signal", "styles.css"), "/* Mock styles */");

// SignalDesign
const signalDesignContent = `export const design = {
  positioning: "Mock signal positioning",
  fonts: "Mock signal fonts",
  motion: "Mock signal motion",
  palette: "Mock signal palette",
  layout: "Mock signal layout",
  componentLanguage: "Mock signal componentLanguage",
  contentModel: ["Mock signal contentModel"],
  colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }],
  bestFor: ["Mock signal bestFor"],
  designNotes: ["Mock signal designNotes"],
};
`;
fs.writeFileSync(path.join(templateLibPath, "signal", "design.ts"), signalDesignContent);

// NimbusTemplate
fs.writeFileSync(
  path.join(templateLibPath, "nimbus", "NimbusTemplate.tsx"),
  createTemplateComponentCode("Nimbus"),
);
fs.writeFileSync(path.join(templateLibPath, "nimbus", "styles.css"), "/* Mock styles */");

// NimbusDesign
const nimbusDesignContent = `export const design = {
  positioning: "Mock nimbus positioning",
  fonts: "Mock nimbus fonts",
  motion: "Mock nimbus motion",
  palette: "Mock nimbus palette",
  layout: "Mock nimbus layout",
  componentLanguage: "Mock nimbus componentLanguage",
  contentModel: ["Mock nimbus contentModel"],
  colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }],
  bestFor: ["Mock nimbus bestFor"],
  designNotes: ["Mock nimbus designNotes"],
};
`;
fs.writeFileSync(path.join(templateLibPath, "nimbus", "design.ts"), nimbusDesignContent);

// CipherTemplate
const cipherContent = `import React from "react";
import { safeExternalUrl, type PortfolioProject } from "../types";
import "./styles.css";

export default function CipherTemplate({ project }: { project: PortfolioProject }) {
  const visibleSections = project.sections.filter((s) => s.visible);
  return (
    <div>
      <div>Mock Cipher Template: {project.identity.name}</div>
      {visibleSections.map((section) => (
        <div key={section.id} data-section={section.type}>
          <h2>{section.title}</h2>
          {section.items.map((item, idx) => (
            <div key={idx}>
              {Object.entries(item).map(([k, v]) => {
                if (k === "id" || k === "visible") return null;
                if (k === "link") {
                  const href = typeof v === "string" ? safeExternalUrl(v) : "";
                  return href ? <a key={k} href={href}>Link</a> : null;
                }
                if (Array.isArray(v)) return <p key={k}>{v.join(" ")}</p>;
                if (typeof v === "string" || typeof v === "number") return <p key={k}>{String(v)}</p>;
                return null;
              })}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}
`;
fs.writeFileSync(path.join(templateLibPath, "cipher", "CipherTemplate.tsx"), cipherContent);
fs.writeFileSync(path.join(templateLibPath, "cipher", "styles.css"), "/* Mock styles */");

// CipherDesign
const cipherDesignContent = `export const design = {
  positioning: "Mock cipher positioning",
  fonts: "Mock cipher fonts",
  motion: "Mock cipher motion",
  palette: "Mock cipher palette",
  layout: "Mock cipher layout",
  componentLanguage: "Mock cipher componentLanguage",
  contentModel: ["Mock cipher contentModel"],
  colorScheme: [{ name: "Mock", value: "#000", className: "bg-black" }],
  bestFor: ["Mock cipher bestFor"],
  designNotes: ["Mock cipher designNotes"],
};
`;
fs.writeFileSync(path.join(templateLibPath, "cipher", "design.ts"), cipherDesignContent);

console.log("Mock templates set up successfully.");

try {
  console.log("Formatting mocked files...");
  execSync("npx prettier --write apps/portfolio/template-library", { stdio: "inherit" });
  console.log("Formatting completed.");
} catch (err) {
  console.error("Failed to run prettier on mocked files:", err.message);
}