@pylonsync/create-pylon 0.11.1 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-pylon.js +1 -1
- package/package.json +1 -1
- package/templates/_root/AGENTS.md +1 -1
- package/templates/agency/AGENTS.md +1 -1
- package/templates/agency/app/(marketing)/contact-form.tsx +22 -32
- package/templates/agency/app/(marketing)/layout.tsx +36 -64
- package/templates/agency/app/(marketing)/not-found.tsx +6 -8
- package/templates/agency/app/(marketing)/page.tsx +93 -144
- package/templates/agency/app/(marketing)/work/[slug]/page.tsx +69 -71
- package/templates/agency/app/(marketing)/work/page.tsx +20 -17
- package/templates/agency/app/globals.css +15 -6
- package/templates/agency/app.ts +10 -0
- package/templates/agency/components/marketing.tsx +40 -104
- package/templates/agency/lib/site.config.ts +34 -52
- package/templates/agency/public/images/hero.jpg +0 -0
- package/templates/agency/public/images/team/claire-donovan.jpg +0 -0
- package/templates/agency/public/images/team/dana-okafor.jpg +0 -0
- package/templates/agency/public/images/team/marcus-lee.jpg +0 -0
- package/templates/agency/public/images/work/atlas-health.jpg +0 -0
- package/templates/agency/public/images/work/cohort.jpg +0 -0
- package/templates/agency/public/images/work/ledger.jpg +0 -0
- package/templates/agency/public/images/work/vela.jpg +0 -0
- package/templates/ai-chat/AGENTS.md +1 -1
- package/templates/ai-studio/AGENTS.md +1 -1
- package/templates/barebones/AGENTS.md +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/consumer/AGENTS.md +1 -1
- package/templates/consumer/README.md +39 -23
- package/templates/consumer/app/(app)/explore/page.tsx +13 -0
- package/templates/consumer/app/(app)/layout.tsx +17 -0
- package/templates/consumer/app/(app)/p/[id]/page.tsx +13 -0
- package/templates/consumer/app/(app)/page.tsx +15 -0
- package/templates/consumer/app/(app)/u/[username]/page.tsx +14 -0
- package/templates/consumer/app/error.tsx +1 -1
- package/templates/consumer/app/globals.css +25 -0
- package/templates/consumer/app/layout.tsx +3 -10
- package/templates/consumer/app/llms.ts +10 -10
- package/templates/consumer/app/not-found.tsx +9 -22
- package/templates/consumer/app/robots.ts +3 -4
- package/templates/consumer/app/sitemap.ts +7 -21
- package/templates/consumer/app.ts +152 -33
- package/templates/consumer/components/social/app-chrome.tsx +109 -0
- package/templates/consumer/components/social/avatar.tsx +50 -0
- package/templates/consumer/components/social/comment-form.tsx +51 -0
- package/templates/consumer/components/social/create-post.tsx +155 -0
- package/templates/consumer/components/social/edit-profile.tsx +103 -0
- package/templates/consumer/components/social/explore-screen.tsx +23 -0
- package/templates/consumer/components/social/home-screen.tsx +161 -0
- package/templates/consumer/components/social/post-card.tsx +77 -0
- package/templates/consumer/components/social/post-grid.tsx +46 -0
- package/templates/consumer/components/social/post-parts.tsx +146 -0
- package/templates/consumer/components/social/post-screen.tsx +173 -0
- package/templates/consumer/components/social/profile-screen.tsx +146 -0
- package/templates/consumer/components/social/rich-text.tsx +22 -0
- package/templates/consumer/components/social/session.tsx +43 -0
- package/templates/consumer/components/social/use-social.ts +86 -0
- package/templates/consumer/functions/addComment.ts +27 -0
- package/templates/consumer/functions/createPost.ts +39 -0
- package/templates/consumer/functions/ensureProfile.ts +39 -0
- package/templates/consumer/functions/seedFeed.ts +38 -0
- package/templates/consumer/functions/updateProfile.ts +61 -0
- package/templates/consumer/lib/seed.ts +192 -0
- package/templates/consumer/lib/site.ts +6 -0
- package/templates/consumer/lib/social.ts +245 -0
- package/templates/consumer/tests/social.test.ts +157 -0
- package/templates/creator/AGENTS.md +1 -1
- package/templates/creator/app/(marketing)/layout.tsx +35 -69
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +15 -26
- package/templates/creator/app/(marketing)/not-found.tsx +8 -6
- package/templates/creator/app/(marketing)/page.tsx +90 -106
- package/templates/creator/app/globals.css +14 -0
- package/templates/creator/app/layout.tsx +1 -0
- package/templates/creator/app.ts +10 -0
- package/templates/creator/components/marketing.tsx +28 -89
- package/templates/creator/lib/site.config.ts +51 -29
- package/templates/creator/public/images/headshot.jpg +0 -0
- package/templates/crm/AGENTS.md +1 -1
- package/templates/crm/lib/seed.ts +153 -21
- package/templates/directory/AGENTS.md +1 -1
- package/templates/directory/app/(marketing)/directory-browse.tsx +99 -137
- package/templates/directory/app/(marketing)/layout.tsx +37 -88
- package/templates/directory/app/(marketing)/not-found.tsx +11 -10
- package/templates/directory/app/(marketing)/page.tsx +12 -33
- package/templates/directory/app/(marketing)/submit/page.tsx +9 -10
- package/templates/directory/app/(marketing)/submit-form.tsx +26 -26
- package/templates/directory/app/globals.css +15 -4
- package/templates/directory/app.ts +10 -0
- package/templates/directory/components/marketing.tsx +27 -91
- package/templates/directory/lib/site.config.ts +13 -24
- package/templates/helpdesk/AGENTS.md +1 -1
- package/templates/helpdesk/lib/seed.ts +188 -104
- package/templates/inventory/AGENTS.md +1 -1
- package/templates/inventory/lib/seed.ts +175 -44
- package/templates/invoices/AGENTS.md +1 -1
- package/templates/invoices/lib/seed.ts +74 -89
- package/templates/local-service/AGENTS.md +1 -1
- package/templates/local-service/app/(marketing)/booking-widget.tsx +31 -31
- package/templates/local-service/app/(marketing)/layout.tsx +25 -37
- package/templates/local-service/app/(marketing)/not-found.tsx +4 -4
- package/templates/local-service/app/(marketing)/page.tsx +145 -139
- package/templates/local-service/app/globals.css +11 -0
- package/templates/local-service/app/layout.tsx +2 -0
- package/templates/local-service/app.ts +9 -0
- package/templates/local-service/components/marketing.tsx +14 -39
- package/templates/local-service/lib/site.config.ts +16 -23
- package/templates/local-service/public/images/hero.jpg +0 -0
- package/templates/marketplace/AGENTS.md +1 -1
- package/templates/marketplace/client/SellForm.tsx +2 -0
- package/templates/projects/AGENTS.md +1 -1
- package/templates/projects/lib/seed.ts +103 -32
- package/templates/restaurant/AGENTS.md +1 -1
- package/templates/restaurant/app/(marketing)/layout.tsx +26 -48
- package/templates/restaurant/app/(marketing)/page.tsx +146 -123
- package/templates/restaurant/app/globals.css +11 -0
- package/templates/restaurant/app/layout.tsx +1 -0
- package/templates/restaurant/app.ts +10 -0
- package/templates/restaurant/components/marketing.tsx +10 -0
- package/templates/restaurant/lib/site.config.ts +16 -14
- package/templates/restaurant/public/images/bar.jpg +0 -0
- package/templates/restaurant/public/images/branzino.jpg +0 -0
- package/templates/restaurant/public/images/hearth.jpg +0 -0
- package/templates/restaurant/public/images/hero.jpg +0 -0
- package/templates/saas/AGENTS.md +1 -1
- package/templates/shop/AGENTS.md +1 -1
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/todo/AGENTS.md +1 -1
- package/templates/waitlist/AGENTS.md +1 -1
- package/templates/waitlist/app/(marketing)/layout.tsx +39 -85
- package/templates/waitlist/app/(marketing)/not-found.tsx +8 -9
- package/templates/waitlist/app/(marketing)/page.tsx +51 -86
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +43 -83
- package/templates/waitlist/app/globals.css +23 -0
- package/templates/waitlist/app/layout.tsx +1 -0
- package/templates/waitlist/app.ts +18 -0
- package/templates/waitlist/components/marketing.tsx +67 -81
- package/templates/waitlist/lib/site.config.ts +52 -59
- package/templates/consumer/app/feed-client.tsx +0 -159
- package/templates/consumer/app/page.tsx +0 -27
- package/templates/consumer/functions/_keep.ts +0 -13
- package/templates/consumer/functions/seedPosts.ts +0 -56
- package/templates/consumer/tests/example.test.ts +0 -12
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import React, { Suspense, use } from "react";
|
|
2
2
|
import { Link, type Metadata, type PageProps, type ServerData } from "@pylonsync/react";
|
|
3
|
-
import {
|
|
4
|
-
WRAP,
|
|
5
|
-
Eyebrow,
|
|
6
|
-
Divider,
|
|
7
|
-
SectionHead,
|
|
8
|
-
ImagePlaceholder,
|
|
9
|
-
ProjectCard,
|
|
10
|
-
initials,
|
|
11
|
-
} from "@/components/marketing";
|
|
3
|
+
import { WRAP, TEXT_LINK, Divider, SectionTitle, ImagePlaceholder, ProjectCard } from "@/components/marketing";
|
|
12
4
|
import { LiveSlots, ContactForm } from "./contact-form";
|
|
13
5
|
import { SeedProjects } from "./seeder";
|
|
14
6
|
import { siteConfig } from "@/lib/site.config";
|
|
@@ -20,10 +12,10 @@ export const metadata: Metadata = {
|
|
|
20
12
|
openGraph: { title: siteConfig.seo.title, description: siteConfig.seo.description, type: "website" },
|
|
21
13
|
};
|
|
22
14
|
|
|
23
|
-
// The homepage
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
15
|
+
// The homepage work grid reads the live Project portfolio on the server (the
|
|
16
|
+
// `selected` + `published` ones, ordered), so curating it in the dashboard
|
|
17
|
+
// re-curates the homepage. Before the portfolio is seeded, it falls back to the
|
|
18
|
+
// config case studies so the section is never empty on first paint.
|
|
27
19
|
function selectedFromConfig(): ProjectView[] {
|
|
28
20
|
return siteConfig.work.items
|
|
29
21
|
.filter((c) => c.selected)
|
|
@@ -37,6 +29,8 @@ function selectedFromConfig(): ProjectView[] {
|
|
|
37
29
|
}));
|
|
38
30
|
}
|
|
39
31
|
|
|
32
|
+
const WORK_GRID = "grid gap-x-8 gap-y-12 sm:grid-cols-2";
|
|
33
|
+
|
|
40
34
|
function SelectedWork({ serverData }: { serverData: ServerData }) {
|
|
41
35
|
const rows = use(serverData.list<ProjectRow>("Project"));
|
|
42
36
|
const fromDb = rows
|
|
@@ -46,7 +40,7 @@ function SelectedWork({ serverData }: { serverData: ServerData }) {
|
|
|
46
40
|
const projects = fromDb.length > 0 ? fromDb : selectedFromConfig();
|
|
47
41
|
|
|
48
42
|
return (
|
|
49
|
-
<div className=
|
|
43
|
+
<div className={WORK_GRID}>
|
|
50
44
|
{projects.map((p) => (
|
|
51
45
|
<ProjectCard key={p.slug} p={p} />
|
|
52
46
|
))}
|
|
@@ -54,129 +48,92 @@ function SelectedWork({ serverData }: { serverData: ServerData }) {
|
|
|
54
48
|
);
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
function WorkSkeleton({ count }: { count: number }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className={WORK_GRID}>
|
|
54
|
+
{Array.from({ length: count }, (_, i) => (
|
|
55
|
+
<div key={i}>
|
|
56
|
+
<div className="aspect-[4/3] animate-pulse rounded-sm bg-zinc-100" />
|
|
57
|
+
<div className="mt-4 h-5 w-1/3 animate-pulse rounded bg-zinc-100" />
|
|
58
|
+
<div className="mt-2 h-3 w-3/4 animate-pulse rounded bg-zinc-100" />
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `app/page.tsx` → `/`. Server-rendered studio site. The hero is one serif
|
|
66
|
+
// sentence, then the work grid, services, process, team, testimonials, and
|
|
67
|
+
// contact. The live "slots open" line and the contact form (#contact) are
|
|
68
|
+
// client islands driven by the public Capacity row. All copy comes from
|
|
69
|
+
// siteConfig.
|
|
62
70
|
export default function LandingPage({ serverData }: PageProps) {
|
|
63
|
-
const { hero,
|
|
71
|
+
const { hero, services, work, process, team, testimonials, contact } = siteConfig;
|
|
64
72
|
|
|
65
73
|
return (
|
|
66
|
-
<div className="bg-white text-
|
|
74
|
+
<div className="bg-white text-ink">
|
|
67
75
|
{/* ============================== HERO ============================== */}
|
|
68
|
-
<section className={`${WRAP} pt-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
<a href="#contact" className="inline-flex items-center rounded-full bg-brand px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90">
|
|
76
|
+
<section className={`${WRAP} pt-14 pb-16 sm:pt-20 sm:pb-20`}>
|
|
77
|
+
<h1 className="font-display max-w-5xl text-balance text-[2.75rem] leading-[1.02] tracking-[-0.015em] sm:text-[4rem] lg:text-[5rem]">
|
|
78
|
+
{hero.headline}
|
|
79
|
+
</h1>
|
|
80
|
+
<div className="mt-8 grid gap-6 sm:mt-10 sm:grid-cols-[1fr_auto] sm:items-end">
|
|
81
|
+
<div className="max-w-xl">
|
|
82
|
+
<p className="text-[17px] leading-relaxed text-zinc-700">{hero.subcopy}</p>
|
|
83
|
+
<div className="mt-5 flex flex-wrap items-center gap-x-6 gap-y-2 text-[15px]">
|
|
84
|
+
<a href="#contact" className={TEXT_LINK}>
|
|
78
85
|
{hero.ctaLabel}
|
|
79
86
|
</a>
|
|
80
|
-
<a href="#work" className=
|
|
81
|
-
{hero.secondaryCtaLabel}
|
|
87
|
+
<a href="#work" className={TEXT_LINK}>
|
|
88
|
+
{hero.secondaryCtaLabel}
|
|
82
89
|
</a>
|
|
83
90
|
</div>
|
|
84
|
-
<div className="mt-8">
|
|
85
|
-
<LiveSlots />
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
{/* Hero photo — replace the placeholder with a real shot (the studio,
|
|
90
|
-
the team at work, a flagship project). */}
|
|
91
|
-
<div className="relative mx-auto w-full max-w-sm lg:max-w-none">
|
|
92
|
-
<ImagePlaceholder
|
|
93
|
-
shape="portrait"
|
|
94
|
-
title="A photo of your studio or work"
|
|
95
|
-
hint="Swap for an <img> in app/page.tsx"
|
|
96
|
-
/>
|
|
97
91
|
</div>
|
|
92
|
+
<LiveSlots />
|
|
98
93
|
</div>
|
|
99
94
|
</section>
|
|
100
95
|
|
|
101
|
-
{/* ==============================
|
|
102
|
-
<section className={`${WRAP} pb-
|
|
103
|
-
<
|
|
104
|
-
{
|
|
105
|
-
</
|
|
106
|
-
<div className="mt-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
</span>
|
|
111
|
-
))}
|
|
96
|
+
{/* ============================== WORK ============================= */}
|
|
97
|
+
<section id="work" className={`${WRAP} pb-20`}>
|
|
98
|
+
<Suspense fallback={<WorkSkeleton count={2} />}>
|
|
99
|
+
<SelectedWork serverData={serverData} />
|
|
100
|
+
</Suspense>
|
|
101
|
+
<div className="mt-10 text-[15px]">
|
|
102
|
+
<Link href="/work" className={TEXT_LINK}>
|
|
103
|
+
All work
|
|
104
|
+
</Link>
|
|
112
105
|
</div>
|
|
113
106
|
</section>
|
|
114
107
|
|
|
115
108
|
{/* ============================ SERVICES =========================== */}
|
|
116
109
|
<Divider />
|
|
117
|
-
<section id="services" className={`${WRAP} py-16`}>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{s.
|
|
123
|
-
<span className="
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<h3 className="mt-4 text-[16px] font-semibold text-zinc-900">{s.title}</h3>
|
|
128
|
-
<p className="mt-2 max-w-md text-[14px] leading-relaxed text-zinc-500">{s.body}</p>
|
|
129
|
-
</div>
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
</section>
|
|
133
|
-
|
|
134
|
-
{/* ============================== WORK ============================= */}
|
|
135
|
-
<Divider />
|
|
136
|
-
<section id="work" className={`${WRAP} py-16`}>
|
|
137
|
-
<div className="flex items-end justify-between gap-4">
|
|
138
|
-
<SectionHead eyebrow={work.eyebrow} title={work.headline} />
|
|
139
|
-
<Link
|
|
140
|
-
href="/work"
|
|
141
|
-
className="hidden shrink-0 text-[13.5px] font-medium text-zinc-600 transition-colors hover:text-zinc-900 sm:inline-flex"
|
|
142
|
-
>
|
|
143
|
-
All work →
|
|
144
|
-
</Link>
|
|
145
|
-
</div>
|
|
146
|
-
<Suspense
|
|
147
|
-
fallback={
|
|
148
|
-
<div className="mt-10 grid gap-6 sm:grid-cols-2">
|
|
149
|
-
{[0, 1].map((i) => (
|
|
150
|
-
<div key={i}>
|
|
151
|
-
<div className="aspect-[4/3] animate-pulse rounded-2xl bg-zinc-100" />
|
|
152
|
-
<div className="mt-4 h-4 w-1/3 animate-pulse rounded bg-zinc-100" />
|
|
153
|
-
<div className="mt-2 h-3 w-3/4 animate-pulse rounded bg-zinc-100" />
|
|
110
|
+
<section id="services" className={`${WRAP} py-16 sm:py-20`}>
|
|
111
|
+
<div className="grid gap-10 lg:grid-cols-[1fr_2fr]">
|
|
112
|
+
<SectionTitle title={services.headline} />
|
|
113
|
+
<ol className="divide-y divide-zinc-200 border-y border-zinc-200">
|
|
114
|
+
{services.items.map((s, i) => (
|
|
115
|
+
<li key={s.title} className="grid gap-2 py-6 sm:grid-cols-[3rem_1fr] sm:gap-6">
|
|
116
|
+
<span className="text-[13px] tabular-nums text-zinc-500">{String(i + 1).padStart(2, "0")}</span>
|
|
117
|
+
<div>
|
|
118
|
+
<h3 className="font-display text-[1.5rem] leading-tight">{s.title}</h3>
|
|
119
|
+
<p className="mt-2 max-w-lg text-[15px] leading-relaxed text-zinc-600">{s.body}</p>
|
|
154
120
|
</div>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
>
|
|
159
|
-
<SelectedWork serverData={serverData} />
|
|
160
|
-
</Suspense>
|
|
161
|
-
<div className="mt-8 sm:hidden">
|
|
162
|
-
<Link href="/work" className="text-[14px] font-medium text-brand">
|
|
163
|
-
See all work →
|
|
164
|
-
</Link>
|
|
121
|
+
</li>
|
|
122
|
+
))}
|
|
123
|
+
</ol>
|
|
165
124
|
</div>
|
|
166
125
|
</section>
|
|
167
126
|
|
|
168
127
|
{/* ============================ PROCESS ============================ */}
|
|
169
128
|
<Divider />
|
|
170
|
-
<section className={`${WRAP} py-16`}>
|
|
171
|
-
<
|
|
172
|
-
<ol className="mt-10 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
|
129
|
+
<section className={`${WRAP} py-16 sm:py-20`}>
|
|
130
|
+
<SectionTitle title={process.headline} />
|
|
131
|
+
<ol className="mt-10 grid gap-8 border-t border-zinc-200 pt-8 sm:grid-cols-2 lg:grid-cols-4">
|
|
173
132
|
{process.steps.map((step, i) => (
|
|
174
133
|
<li key={step.title}>
|
|
175
|
-
<span className="
|
|
176
|
-
|
|
177
|
-
</
|
|
178
|
-
<h3 className="mt-4 text-[15px] font-semibold text-zinc-900">{step.title}</h3>
|
|
179
|
-
<p className="mt-2 text-[14px] leading-relaxed text-zinc-500">{step.body}</p>
|
|
134
|
+
<span className="text-[13px] tabular-nums text-zinc-500">{String(i + 1).padStart(2, "0")}</span>
|
|
135
|
+
<h3 className="font-display mt-2 text-[1.5rem] leading-tight">{step.title}</h3>
|
|
136
|
+
<p className="mt-2 text-[15px] leading-relaxed text-zinc-600">{step.body}</p>
|
|
180
137
|
</li>
|
|
181
138
|
))}
|
|
182
139
|
</ol>
|
|
@@ -184,15 +141,15 @@ export default function LandingPage({ serverData }: PageProps) {
|
|
|
184
141
|
|
|
185
142
|
{/* ============================== TEAM ============================= */}
|
|
186
143
|
<Divider />
|
|
187
|
-
<section className={`${WRAP} py-16`}>
|
|
188
|
-
<
|
|
189
|
-
<div className="mt-10 grid gap-
|
|
144
|
+
<section className={`${WRAP} py-16 sm:py-20`}>
|
|
145
|
+
<SectionTitle title={team.headline} />
|
|
146
|
+
<div className="mt-10 grid grid-cols-2 gap-6 sm:grid-cols-3 sm:gap-8">
|
|
190
147
|
{team.members.map((m) => (
|
|
191
148
|
<div key={m.name}>
|
|
192
|
-
{/* Team headshot
|
|
193
|
-
<ImagePlaceholder shape="square" title=
|
|
194
|
-
<h3 className="mt-4 text-[15px] font-
|
|
195
|
-
<p className="text-[
|
|
149
|
+
{/* Team headshot: public/images/team/<name>.jpg. */}
|
|
150
|
+
<ImagePlaceholder shape="square" title={m.name} src={`/images/team/${slugify(m.name)}.jpg`} />
|
|
151
|
+
<h3 className="mt-4 text-[15px] font-medium">{m.name}</h3>
|
|
152
|
+
<p className="text-[14px] text-zinc-500">{m.role}</p>
|
|
196
153
|
</div>
|
|
197
154
|
))}
|
|
198
155
|
</div>
|
|
@@ -202,20 +159,15 @@ export default function LandingPage({ serverData }: PageProps) {
|
|
|
202
159
|
{testimonials ? (
|
|
203
160
|
<>
|
|
204
161
|
<Divider />
|
|
205
|
-
<section className={`${WRAP} py-16`}>
|
|
206
|
-
<
|
|
207
|
-
<div className="mt-10 grid gap-
|
|
162
|
+
<section className={`${WRAP} py-16 sm:py-20`}>
|
|
163
|
+
<SectionTitle title={testimonials.headline} />
|
|
164
|
+
<div className="mt-10 grid gap-10 border-t border-zinc-200 pt-8 sm:grid-cols-3 sm:gap-8">
|
|
208
165
|
{testimonials.items.map((t) => (
|
|
209
|
-
<figure key={t.name}
|
|
210
|
-
<blockquote className="
|
|
211
|
-
<figcaption className="mt-
|
|
212
|
-
<span className="
|
|
213
|
-
|
|
214
|
-
</span>
|
|
215
|
-
<span className="text-[13px] leading-tight">
|
|
216
|
-
<span className="block font-medium text-zinc-900">{t.name}</span>
|
|
217
|
-
<span className="text-zinc-500">{t.role}</span>
|
|
218
|
-
</span>
|
|
166
|
+
<figure key={t.name}>
|
|
167
|
+
<blockquote className="font-display text-[1.375rem] leading-snug">“{t.quote}”</blockquote>
|
|
168
|
+
<figcaption className="mt-4 text-[14px] leading-tight">
|
|
169
|
+
<span className="block font-medium">{t.name}</span>
|
|
170
|
+
<span className="text-zinc-500">{t.role}</span>
|
|
219
171
|
</figcaption>
|
|
220
172
|
</figure>
|
|
221
173
|
))}
|
|
@@ -225,20 +177,17 @@ export default function LandingPage({ serverData }: PageProps) {
|
|
|
225
177
|
) : null}
|
|
226
178
|
|
|
227
179
|
{/* ============================= CONTACT =========================== */}
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<p className="mt-4 max-w-md text-[15px] leading-relaxed text-zinc-500">{contact.subcopy}</p>
|
|
237
|
-
<div className="mt-6">
|
|
238
|
-
<LiveSlots />
|
|
180
|
+
<section id="contact" className="bg-paper">
|
|
181
|
+
<div className={`${WRAP} py-16 sm:py-20`}>
|
|
182
|
+
<div className="grid gap-10 lg:grid-cols-[1fr_1.2fr] lg:gap-16">
|
|
183
|
+
<div>
|
|
184
|
+
<SectionTitle title={contact.headline} body={contact.subcopy} />
|
|
185
|
+
<div className="mt-6">
|
|
186
|
+
<LiveSlots />
|
|
187
|
+
</div>
|
|
239
188
|
</div>
|
|
189
|
+
<ContactForm />
|
|
240
190
|
</div>
|
|
241
|
-
<ContactForm />
|
|
242
191
|
</div>
|
|
243
192
|
</section>
|
|
244
193
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ServerData,
|
|
8
8
|
type SsrResponse,
|
|
9
9
|
} from "@pylonsync/react";
|
|
10
|
-
import { ImagePlaceholder } from "@/components/marketing";
|
|
10
|
+
import { WRAP, WRAP_NARROW, TEXT_LINK, ImagePlaceholder } from "@/components/marketing";
|
|
11
11
|
import { SeedProjects } from "../../seeder";
|
|
12
12
|
import { siteConfig } from "@/lib/site.config";
|
|
13
13
|
import { slugify, viewFromRow, type ProjectRow, type ProjectView } from "@/lib/agency";
|
|
@@ -58,8 +58,6 @@ export const generateMetadata: GenerateMetadata = async ({
|
|
|
58
58
|
// so a click off the work grid hits cache instead of a live render.
|
|
59
59
|
export const revalidate = 300;
|
|
60
60
|
|
|
61
|
-
const WRAP_NARROW = "mx-auto w-full max-w-3xl px-6";
|
|
62
|
-
|
|
63
61
|
function CaseStudy({
|
|
64
62
|
serverData,
|
|
65
63
|
response,
|
|
@@ -74,79 +72,79 @@ function CaseStudy({
|
|
|
74
72
|
if (!p) {
|
|
75
73
|
response.setStatus(404);
|
|
76
74
|
return (
|
|
77
|
-
<div className={`${WRAP_NARROW} py-24
|
|
78
|
-
<p className="
|
|
79
|
-
<Link href="/work" className=
|
|
80
|
-
|
|
75
|
+
<div className={`${WRAP_NARROW} py-24`}>
|
|
76
|
+
<p className="font-display text-[2rem]">That case study does not exist.</p>
|
|
77
|
+
<Link href="/work" className={`mt-4 inline-block text-[15px] ${TEXT_LINK}`}>
|
|
78
|
+
All work
|
|
81
79
|
</Link>
|
|
82
80
|
</div>
|
|
83
81
|
);
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
return (
|
|
87
|
-
<article className=
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-brand">
|
|
94
|
-
{p.client}
|
|
95
|
-
{p.year ? ` · ${p.year}` : ""}
|
|
96
|
-
</p>
|
|
97
|
-
<h1 className="mt-3 text-balance text-[2rem] font-semibold leading-[1.08] tracking-[-0.02em] sm:text-[2.5rem]">
|
|
85
|
+
<article className="pt-10 pb-20 sm:pt-14">
|
|
86
|
+
<header className={WRAP}>
|
|
87
|
+
<Link href="/work" className={`text-[14px] ${TEXT_LINK}`}>
|
|
88
|
+
All work
|
|
89
|
+
</Link>
|
|
90
|
+
<h1 className="font-display mt-8 max-w-4xl text-balance text-[2.75rem] leading-[1.02] tracking-[-0.015em] sm:text-[4rem]">
|
|
98
91
|
{p.title}
|
|
99
92
|
</h1>
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
93
|
+
<div className="mt-6 grid gap-6 sm:grid-cols-[1fr_auto] sm:items-end">
|
|
94
|
+
<p className="max-w-xl text-[17px] leading-relaxed text-zinc-700">{p.summary}</p>
|
|
95
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-1 text-[14px] sm:text-right">
|
|
96
|
+
<dt className="text-zinc-500">Client</dt>
|
|
97
|
+
<dd>{p.client}</dd>
|
|
98
|
+
{p.year ? (
|
|
99
|
+
<>
|
|
100
|
+
<dt className="text-zinc-500">Year</dt>
|
|
101
|
+
<dd>{p.year}</dd>
|
|
102
|
+
</>
|
|
103
|
+
) : null}
|
|
104
|
+
{p.tags.length > 0 ? (
|
|
105
|
+
<>
|
|
106
|
+
<dt className="text-zinc-500">Scope</dt>
|
|
107
|
+
<dd>{p.tags.join(", ")}</dd>
|
|
108
|
+
</>
|
|
109
|
+
) : null}
|
|
110
|
+
{p.liveUrl ? (
|
|
111
|
+
<>
|
|
112
|
+
<dt className="text-zinc-500">Site</dt>
|
|
113
|
+
<dd>
|
|
114
|
+
<a href={p.liveUrl} target="_blank" rel="noopener noreferrer" className={TEXT_LINK}>
|
|
115
|
+
Visit
|
|
116
|
+
</a>
|
|
117
|
+
</dd>
|
|
118
|
+
</>
|
|
119
|
+
) : null}
|
|
120
|
+
</dl>
|
|
121
|
+
</div>
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
{/* Product shot: public/images/work/<slug>.jpg. */}
|
|
124
|
+
<div className="mt-10">
|
|
125
|
+
<ImagePlaceholder shape="landscape" title={`${p.title} product shot`} src={`/images/work/${p.slug}.jpg`} />
|
|
126
|
+
</div>
|
|
127
|
+
</header>
|
|
126
128
|
|
|
127
|
-
<div className=
|
|
128
|
-
<CaseSection
|
|
129
|
-
<CaseSection
|
|
130
|
-
<CaseSection
|
|
129
|
+
<div className={`${WRAP_NARROW} mt-14 space-y-12`}>
|
|
130
|
+
<CaseSection title="The challenge" body={p.challenge} />
|
|
131
|
+
<CaseSection title="Our approach" body={p.approach} />
|
|
132
|
+
<CaseSection title="The outcome" body={p.outcome} />
|
|
131
133
|
{!p.challenge && !p.approach && !p.outcome ? (
|
|
132
|
-
<p className="text-[
|
|
133
|
-
|
|
134
|
+
<p className="text-[17px] leading-relaxed text-zinc-700">
|
|
135
|
+
The full write-up is not published yet. In short, {p.summary.charAt(0).toLowerCase() + p.summary.slice(1)}
|
|
134
136
|
</p>
|
|
135
137
|
) : null}
|
|
136
|
-
</div>
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
>
|
|
148
|
-
Start a project
|
|
149
|
-
</Link>
|
|
139
|
+
<div className="border-t border-zinc-200 pt-10">
|
|
140
|
+
<h2 className="font-display text-[2rem] leading-tight">Have a project like this?</h2>
|
|
141
|
+
<p className="mt-3 text-[16px] leading-relaxed text-zinc-600">
|
|
142
|
+
We take on a few projects at a time. Tell us what you are building.
|
|
143
|
+
</p>
|
|
144
|
+
<Link href="/#contact" className={`mt-4 inline-block text-[15px] ${TEXT_LINK}`}>
|
|
145
|
+
Start a project
|
|
146
|
+
</Link>
|
|
147
|
+
</div>
|
|
150
148
|
</div>
|
|
151
149
|
|
|
152
150
|
<SeedProjects />
|
|
@@ -154,12 +152,12 @@ function CaseStudy({
|
|
|
154
152
|
);
|
|
155
153
|
}
|
|
156
154
|
|
|
157
|
-
function CaseSection({
|
|
155
|
+
function CaseSection({ title, body }: { title: string; body?: string | null }) {
|
|
158
156
|
if (!body) return null;
|
|
159
157
|
return (
|
|
160
158
|
<section>
|
|
161
|
-
<h2 className="font-
|
|
162
|
-
<p className="mt-
|
|
159
|
+
<h2 className="font-display text-[2rem] leading-tight">{title}</h2>
|
|
160
|
+
<p className="mt-4 whitespace-pre-wrap text-[17px] leading-relaxed text-zinc-700">{body}</p>
|
|
163
161
|
</section>
|
|
164
162
|
);
|
|
165
163
|
}
|
|
@@ -169,14 +167,14 @@ function CaseSection({ label, body }: { label: string; body?: string | null }) {
|
|
|
169
167
|
// unknown or the project is a draft.
|
|
170
168
|
export default function CaseStudyPage({ params, serverData, response }: PageProps) {
|
|
171
169
|
return (
|
|
172
|
-
<div className="bg-white text-
|
|
170
|
+
<div className="bg-white text-ink">
|
|
173
171
|
<Suspense
|
|
174
172
|
fallback={
|
|
175
|
-
<div className={`${
|
|
176
|
-
<div className="h-4 w-
|
|
177
|
-
<div className="mt-
|
|
178
|
-
<div className="mt-
|
|
179
|
-
<div className="mt-10 aspect-[4/3] animate-pulse rounded-
|
|
173
|
+
<div className={`${WRAP} pt-10 sm:pt-14`}>
|
|
174
|
+
<div className="h-4 w-16 animate-pulse rounded bg-zinc-100" />
|
|
175
|
+
<div className="mt-8 h-14 w-2/3 animate-pulse rounded bg-zinc-100" />
|
|
176
|
+
<div className="mt-6 h-4 w-1/2 animate-pulse rounded bg-zinc-100" />
|
|
177
|
+
<div className="mt-10 aspect-[4/3] animate-pulse rounded-sm bg-zinc-100" />
|
|
180
178
|
</div>
|
|
181
179
|
}
|
|
182
180
|
>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { Suspense, use } from "react";
|
|
2
2
|
import { Link, type Metadata, type PageProps, type ServerData } from "@pylonsync/react";
|
|
3
|
-
import { WRAP,
|
|
3
|
+
import { WRAP, TEXT_LINK, ProjectCard } from "@/components/marketing";
|
|
4
4
|
import { SeedProjects } from "../seeder";
|
|
5
5
|
import { siteConfig } from "@/lib/site.config";
|
|
6
6
|
import { slugify, viewFromRow, type ProjectRow, type ProjectView } from "@/lib/agency";
|
|
7
7
|
|
|
8
8
|
export const metadata: Metadata = {
|
|
9
9
|
title: `Work — ${siteConfig.brand.name}`,
|
|
10
|
-
description: `Case studies from ${siteConfig.brand.name}
|
|
10
|
+
description: `Case studies from ${siteConfig.brand.name}.`,
|
|
11
11
|
openGraph: { title: `Work — ${siteConfig.brand.name}`, type: "website" },
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -26,6 +26,8 @@ function allFromConfig(): ProjectView[] {
|
|
|
26
26
|
}));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const WORK_GRID = "mt-12 grid gap-x-8 gap-y-12 sm:grid-cols-2";
|
|
30
|
+
|
|
29
31
|
function WorkGrid({ serverData }: { serverData: ServerData }) {
|
|
30
32
|
const rows = use(serverData.list<ProjectRow>("Project"));
|
|
31
33
|
const fromDb = rows
|
|
@@ -35,10 +37,10 @@ function WorkGrid({ serverData }: { serverData: ServerData }) {
|
|
|
35
37
|
const projects = fromDb.length > 0 ? fromDb : allFromConfig();
|
|
36
38
|
|
|
37
39
|
if (projects.length === 0) {
|
|
38
|
-
return <p className="mt-
|
|
40
|
+
return <p className="mt-12 text-[15px] text-zinc-600">No published work yet.</p>;
|
|
39
41
|
}
|
|
40
42
|
return (
|
|
41
|
-
<div className=
|
|
43
|
+
<div className={WORK_GRID}>
|
|
42
44
|
{projects.map((p) => (
|
|
43
45
|
<ProjectCard key={p.slug} p={p} />
|
|
44
46
|
))}
|
|
@@ -48,20 +50,21 @@ function WorkGrid({ serverData }: { serverData: ServerData }) {
|
|
|
48
50
|
|
|
49
51
|
export default function WorkIndexPage({ serverData }: PageProps) {
|
|
50
52
|
return (
|
|
51
|
-
<div className="bg-white text-
|
|
52
|
-
<section className={`${WRAP}
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
<div className="bg-white text-ink">
|
|
54
|
+
<section className={`${WRAP} pt-14 pb-16 sm:pt-20 sm:pb-20`}>
|
|
55
|
+
<h1 className="font-display text-[2.75rem] leading-[1.02] tracking-[-0.015em] sm:text-[4rem]">
|
|
56
|
+
{siteConfig.work.headline}
|
|
57
|
+
</h1>
|
|
58
|
+
<p className="mt-4 max-w-xl text-[16px] leading-relaxed text-zinc-600">
|
|
59
|
+
Every project we have published, each with a short case study.
|
|
60
|
+
</p>
|
|
58
61
|
<Suspense
|
|
59
62
|
fallback={
|
|
60
|
-
<div className=
|
|
63
|
+
<div className={WORK_GRID}>
|
|
61
64
|
{[0, 1, 2, 3].map((i) => (
|
|
62
65
|
<div key={i}>
|
|
63
|
-
<div className="aspect-[4/3] animate-pulse rounded-
|
|
64
|
-
<div className="mt-4 h-
|
|
66
|
+
<div className="aspect-[4/3] animate-pulse rounded-sm bg-zinc-100" />
|
|
67
|
+
<div className="mt-4 h-5 w-1/3 animate-pulse rounded bg-zinc-100" />
|
|
65
68
|
</div>
|
|
66
69
|
))}
|
|
67
70
|
</div>
|
|
@@ -70,9 +73,9 @@ export default function WorkIndexPage({ serverData }: PageProps) {
|
|
|
70
73
|
<WorkGrid serverData={serverData} />
|
|
71
74
|
</Suspense>
|
|
72
75
|
|
|
73
|
-
<div className="mt-
|
|
74
|
-
<Link href="/#contact" className=
|
|
75
|
-
|
|
76
|
+
<div className="mt-14 text-[15px]">
|
|
77
|
+
<Link href="/#contact" className={TEXT_LINK}>
|
|
78
|
+
Start a project
|
|
76
79
|
</Link>
|
|
77
80
|
</div>
|
|
78
81
|
</section>
|