Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/portfolio/app/templates/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ const PortfolioTemplateDetailPage = async ({ params }: PageProps) => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(templateSchema) }}
/>
{details.faqs && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: details.faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
}),
}}
/>
)}

<div className="text-ink-2 bg-paper min-h-dvh overflow-x-clip pt-28 font-['Outfit','Avenir_Next','Trebuchet_MS',sans-serif]">
<TemplatesNavigation showPricing={true} backHref="/templates" backLabel="All templates" />
Expand Down
13 changes: 9 additions & 4 deletions apps/portfolio/components/DraftPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { useEffect } from "react";
import dynamic from "next/dynamic";

import { demoPortfolio, type TemplateId } from "@/lib/portfolio";
import { templateLoaders } from "@/template-library/registry";

const templates = {
signal: dynamic(() => import("@/template-library/signal/SignalTemplate")),
atelier: dynamic(() => import("@/template-library/atelier/AtelierTemplate")),
};
const templates = Object.fromEntries(
Object.entries(templateLoaders).map(([id, loader]) => [id, dynamic(loader)]),
) as Record<
TemplateId,
React.ComponentType<{ project: import("@/lib/portfolio").PortfolioContent }>
>;

export function DraftPreview({ templateId }: { templateId: TemplateId }) {
const Template = templates[templateId];
Expand Down Expand Up @@ -75,6 +78,8 @@ export function DraftPreview({ templateId }: { templateId: TemplateId }) {
};
}, []);

if (!Template) return null;

return (
<div className="select-none" style={{ userSelect: "none", WebkitUserSelect: "none" }}>
<Template project={{ ...demoPortfolio, templateId }} />
Expand Down
11 changes: 6 additions & 5 deletions apps/portfolio/components/LivePortfolioPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";

import { parsePortfolioContent, type PortfolioContent } from "@/lib/portfolio";
import { parsePortfolioContent, type PortfolioContent, type TemplateId } from "@/lib/portfolio";
import { templateLoaders } from "@/template-library/registry";

const templates = {
signal: dynamic(() => import("@/template-library/signal/SignalTemplate")),
atelier: dynamic(() => import("@/template-library/atelier/AtelierTemplate")),
};
const templates = Object.fromEntries(
Object.entries(templateLoaders).map(([id, loader]) => [id, dynamic(loader)]),
) as Record<TemplateId, React.ComponentType<{ project: PortfolioContent }>>;

export function LivePortfolioPreview({ initialContent }: { initialContent: PortfolioContent }) {
const [content, setContent] = useState(initialContent);
Expand All @@ -29,6 +29,7 @@ export function LivePortfolioPreview({ initialContent }: { initialContent: Portf
}, []);

const Template = templates[content.templateId];
if (!Template) return null;

return <Template project={content} />;
}
21 changes: 21 additions & 0 deletions apps/portfolio/features/templates/components/TemplateBestFit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ const TemplateBestFit = ({
<div className="bg-accent border-accent text-accent-ink rounded-3xl border p-5 text-sm leading-6 font-semibold sm:col-span-2">
Motion direction: {details.motion}
</div>

{details.system?.targetProfessions && (
<div className="mt-8 border-t border-white/10 pt-8 sm:col-span-2">
<h3 className="mb-4 text-lg font-bold tracking-tight text-white">
Suitability by target role
</h3>
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3">
{details.system.targetProfessions.map((prof) => (
<div
key={prof.role}
className="rounded-2xl border border-white/10 bg-white/2 p-4.5"
>
<h4 className="text-accent dark:text-accent mb-1.5 animate-pulse font-mono text-[10px] font-bold tracking-wider uppercase">
{prof.role}
</h4>
<p className="text-xs leading-relaxed text-white/70">{prof.why}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
</section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { TemplateDetails } from "../data/template-details";
import type { TemplateSummary } from "@/templates/catalog/templates";

import TemplateCta from "./TemplateCta";
import TemplateFaqs from "./TemplateFaqs";
import TemplateBestFit from "./TemplateBestFit";
import TemplateDetailHero from "./TemplateDetailHero";
import TemplateStyleGuide from "./TemplateStyleGuide";
Expand Down Expand Up @@ -52,7 +53,7 @@ const TemplateDetailContainer = ({ template, details }: ContainerProps) => {
<div ref={specAnchorRef} className="scroll-mt-28" />

<div className={devMode ? "block" : "hidden"}>
<TemplateFullSystemSpec details={details} />
<TemplateFullSystemSpec details={details} templateId={template.id} />
</div>

<div className={devMode ? "hidden" : "block"}>
Expand Down Expand Up @@ -83,6 +84,8 @@ const TemplateDetailContainer = ({ template, details }: ContainerProps) => {

<TemplateBestFit template={template} details={details} />

{details.faqs && <TemplateFaqs faqs={details.faqs} templateName={template.name} />}

<TemplateCta template={template} />

<div className="fixed bottom-8 left-8 z-80 hidden md:block">
Expand Down
104 changes: 104 additions & 0 deletions apps/portfolio/features/templates/components/TemplateFaqs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"use client";

import { useState } from "react";
import { HelpCircle, ChevronDown } from "lucide-react";

type FaqItem = {
question: string;
answer: string;
};

type FaqProps = {
faqs: FaqItem[];
templateName: string;
};

export default function TemplateFaqs({ faqs, templateName }: FaqProps) {
const [openIndex, setOpenIndex] = useState<number | null>(null);

const toggle = (idx: number) => {
setOpenIndex((prev) => (prev === idx ? null : idx));
};

if (!faqs || faqs.length === 0) return null;

return (
<section className="mx-auto w-[min(1200px,calc(100%-48px))] pb-24">
<div className="border-ink-2 bg-panel rounded-4xl border-2 p-6 shadow-[14px_16px_0_rgba(37,99,235,0.12)] sm:p-8">
<div className="grid gap-8 lg:grid-cols-[0.85fr_1.15fr]">
<div>
<p className="text-accent flex items-center gap-2 text-xs font-bold tracking-[0.16em] uppercase">
<HelpCircle size={14} className="animate-pulse" /> Frequently Asked Questions
</p>

<h2 className="text-ink-2 mt-4 max-w-xl text-[clamp(2.8rem,5vw,5.5rem)] leading-[1.05] font-bold tracking-tighter">
Got questions about {templateName}?
</h2>

<p className="text-ink-2/62 mt-5 max-w-md text-sm leading-7">
Explore answers to common questions about this layout, design styling choices, and
performance optimizations.
</p>
</div>

<div className="space-y-3.5">
{faqs.map((faq, idx) => {
const isOpen = openIndex === idx;
const titleId = `template-faq-title-${idx}`;
const contentId = `template-faq-content-${idx}`;

return (
<article
key={idx}
className={`bg-paper rounded-2xl border-2 transition-all duration-300 ${
isOpen
? "border-accent shadow-[6px_8px_0_rgba(37,99,235,0.06)]"
: "border-ink-2/10 hover:border-ink-2/25 shadow-sm"
}`}
>
<button
type="button"
onClick={() => toggle(idx)}
aria-expanded={isOpen}
aria-controls={contentId}
className="flex w-full cursor-pointer items-center justify-between gap-4 p-5 text-left transition-transform active:scale-[0.99] sm:p-6"
>
<h3
id={titleId}
className="text-ink-2 text-sm font-bold tracking-tight sm:text-base"
>
{faq.question}
</h3>

<span
className={`bg-panel border-ink-2/10 flex size-7 shrink-0 items-center justify-center rounded-full border transition-all duration-300 ${
isOpen
? "text-accent bg-accent/8 border-accent/20 rotate-180"
: "text-ink-2/45"
}`}
>
<ChevronDown className="size-4" />
</span>
</button>

<div
id={contentId}
role="region"
aria-labelledby={titleId}
className={`overflow-hidden transition-all duration-300 ease-out ${
isOpen ? "max-h-[300px] opacity-100" : "pointer-events-none max-h-0 opacity-0"
}`}
>
<p className="text-ink-2/65 border-ink-2/5 border-t px-5 pt-3.5 pb-5.5 text-xs leading-relaxed sm:px-6 sm:text-sm">
{faq.answer}
</p>
</div>
</article>
);
})}
</div>
</div>
</div>
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,46 @@
import { useState } from "react";

import type { TemplateDetails } from "../data/template-details";
import TemplateGuidelines from "./TemplateGuidelines";

type SystemSpecProps = {
details: TemplateDetails;
templateId: string;
};

const TemplateFullSystemSpec = ({ details }: SystemSpecProps) => {
const TemplateFullSystemSpec = ({ details, templateId }: SystemSpecProps) => {
const { system } = details;

const [activeTab, setActiveTab] = useState<"blueprint" | "layout" | "components" | "json">(
"blueprint",
);
const [activeTab, setActiveTab] = useState<
"blueprint" | "layout" | "components" | "json" | "guidelines"
>("blueprint");

if (!system) return null;

const sanitizedSystem = {
overview: {
genre: system.overview?.genre,
canvas: system.overview?.canvas,
anchorHue: system.overview?.anchorHue,
},
colors: system.colors,
typography: {
fonts: system.typography?.fonts,
hierarchy:
system.typography?.hierarchy?.map((h) => ({
token: h.token,
size: h.size,
weight: h.weight,
})) || [],
},
shapes: system.shapes,
};

const tabs = [
{ id: "blueprint", label: "Visual Blueprint" },
{ id: "layout", label: "Layout & Shapes" },
{ id: "components", label: "Component Spec" },
{ id: "guidelines", label: "Guidelines" },
{ id: "json", label: "JSON Spec" },
] as const;

Expand Down Expand Up @@ -131,31 +153,43 @@ const TemplateFullSystemSpec = ({ details }: SystemSpecProps) => {

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="border-ink-2/10 dark:bg-paper-2 rounded-2xl border bg-[#faf9f6] p-4.5">
<div className="border-ink-2/10 mb-3 h-10 rounded-xl border bg-[#39e5a1]" />
<div
className="border-ink-2/10 mb-3 h-10 rounded-xl border"
style={{ backgroundColor: system.colors.brand.accentDark || "#39e5a1" }}
/>
<h4 className="text-ink-2 text-sm font-bold">Accent (Dark)</h4>
<p className="text-ink-2/62 mt-1 font-mono text-[11px]">
{system.colors.brand.accentDark}
</p>
</div>

<div className="border-ink-2/10 dark:bg-paper-2 rounded-2xl border bg-[#faf9f6] p-4.5">
<div className="border-ink-2/10 mb-3 h-10 rounded-xl border bg-[#1a8f65]" />
<div
className="border-ink-2/10 mb-3 h-10 rounded-xl border"
style={{ backgroundColor: system.colors.brand.accentLight || "#1a8f65" }}
/>
<h4 className="text-ink-2 text-sm font-bold">Accent (Light)</h4>
<p className="text-ink-2/62 mt-1 font-mono text-[11px]">
{system.colors.brand.accentLight}
</p>
</div>

<div className="border-ink-2/10 dark:bg-paper-2 rounded-2xl border bg-[#faf9f6] p-4.5">
<div className="border-ink-2/10 mb-3 h-10 rounded-xl border bg-[#0c100e]" />
<div
className="border-ink-2/10 mb-3 h-10 rounded-xl border"
style={{ backgroundColor: system.colors.surface.paperDark || "#0c100e" }}
/>
<h4 className="text-ink-2 text-sm font-bold">Obsidian Paper</h4>
<p className="text-ink-2/62 mt-1 font-mono text-[11px]">
{system.colors.surface.paperDark}
</p>
</div>

<div className="border-ink-2/10 dark:bg-paper-2 rounded-2xl border bg-[#faf9f6] p-4.5">
<div className="border-ink-2/10 mb-3 h-10 rounded-xl border bg-[#fbfcf9]" />
<div
className="border-ink-2/10 mb-3 h-10 rounded-xl border"
style={{ backgroundColor: system.colors.surface.paperLight || "#fbfcf9" }}
/>
<h4 className="text-ink-2 text-sm font-bold">Chalk Paper</h4>
<p className="text-ink-2/62 mt-1 font-mono text-[11px]">
{system.colors.surface.paperLight}
Expand Down Expand Up @@ -348,26 +382,37 @@ const TemplateFullSystemSpec = ({ details }: SystemSpecProps) => {
<span>Target Class</span>

<span className="bg-ink-2/5 text-ink-2/80 rounded px-1.5 py-0.5">
.signal-{key}
.{templateId}-{key}
</span>
</div>
</div>
))}
</div>
)}

{activeTab === "guidelines" && (
<TemplateGuidelines
guidelines={details.guidelines}
templateName={details.positioning}
/>
)}

{activeTab === "json" && (
<div className="border-ink-2/10 animate-fade-in max-h-[500px] overflow-x-auto rounded-3xl border bg-[#0f1412] p-6 font-mono text-xs text-emerald-400 shadow-inner">
<div className="mb-4 flex items-center justify-between border-b border-white/10 pb-3 text-[11px] text-white/50">
<span>programmatic_design_spec.json</span>
<button
onClick={() => navigator.clipboard.writeText(JSON.stringify(system, null, 2))}
onClick={() =>
navigator.clipboard.writeText(JSON.stringify(sanitizedSystem, null, 2))
}
className="cursor-pointer rounded border border-white/10 bg-white/5 px-2.5 py-1 font-bold text-white transition hover:bg-white/10 active:scale-95"
>
Copy JSON
</button>
</div>
<pre className="leading-5">{JSON.stringify(system, null, 2)}</pre>
<pre className="leading-5 break-words whitespace-pre-wrap">
{JSON.stringify(sanitizedSystem, null, 2)}
</pre>
</div>
)}
</div>
Expand Down
Loading
Loading