@pylonsync/create-pylon 0.11.1 → 0.11.3
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,13 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type Metadata } from "@pylonsync/react";
|
|
3
|
-
import {
|
|
4
|
-
WRAP,
|
|
5
|
-
Eyebrow,
|
|
6
|
-
Divider,
|
|
7
|
-
SectionHead,
|
|
8
|
-
ImagePlaceholder,
|
|
9
|
-
LiveBadge,
|
|
10
|
-
} from "@/components/marketing";
|
|
3
|
+
import { WRAP, Divider, ImagePlaceholder, LiveBadge } from "@/components/marketing";
|
|
11
4
|
import { BookingWidget } from "./booking-widget";
|
|
12
5
|
import { siteConfig } from "@/lib/site.config";
|
|
13
6
|
|
|
@@ -21,10 +14,13 @@ export const metadata: Metadata = {
|
|
|
21
14
|
},
|
|
22
15
|
};
|
|
23
16
|
|
|
24
|
-
// `app/(marketing)/page.tsx` → `/` (the group segment adds no URL prefix).
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
17
|
+
// `app/(marketing)/page.tsx` → `/` (the group segment adds no URL prefix).
|
|
18
|
+
// Server-rendered single-page site for an appointment business, laid out like
|
|
19
|
+
// a barbershop poster: cream page, condensed uppercase display type, full-width
|
|
20
|
+
// rules between sections. Hero, price list, reviews, location, and FAQ are
|
|
21
|
+
// static server HTML (SEO + first paint); the booking section (#book) is a
|
|
22
|
+
// client island (<BookingWidget>) with live slot availability. All copy comes
|
|
23
|
+
// from siteConfig.
|
|
28
24
|
//
|
|
29
25
|
// Does not read `auth`, so the public page stays cacheable; the nav's signed-in
|
|
30
26
|
// state is resolved in the layout.
|
|
@@ -32,45 +28,42 @@ export default function LandingPage() {
|
|
|
32
28
|
const { hero, services, booking, reviews, location, faq } = siteConfig;
|
|
33
29
|
|
|
34
30
|
return (
|
|
35
|
-
<div className="bg-
|
|
31
|
+
<div className="bg-cream text-ink">
|
|
36
32
|
{/* ============================= HERO ============================= */}
|
|
37
|
-
<section className={`${WRAP} pt-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
33
|
+
<section className={`${WRAP} pt-10 pb-12 sm:pt-14`}>
|
|
34
|
+
<h1 className="font-display text-balance text-[4.25rem] leading-[0.9] tracking-[0.005em] sm:text-[7rem] lg:text-[9.5rem]">
|
|
35
|
+
{hero.headline}
|
|
36
|
+
</h1>
|
|
37
|
+
|
|
38
|
+
<div className="mt-8 grid gap-8 lg:mt-10 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)] lg:gap-12">
|
|
39
|
+
<div className="flex flex-col justify-between gap-8">
|
|
40
|
+
<p className="max-w-xl text-[17px] leading-relaxed sm:text-[19px]">{hero.subcopy}</p>
|
|
41
|
+
<div>
|
|
42
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
43
|
+
<a
|
|
44
|
+
href="#book"
|
|
45
|
+
className="inline-flex h-12 items-center bg-brand px-7 font-display text-[22px] tracking-[0.04em] text-cream transition-opacity hover:opacity-90"
|
|
46
|
+
>
|
|
47
|
+
{hero.ctaLabel}
|
|
48
|
+
</a>
|
|
49
|
+
<a
|
|
50
|
+
href="#services"
|
|
51
|
+
className="font-display text-[22px] tracking-[0.04em] underline decoration-1 underline-offset-[6px] hover:text-brand"
|
|
52
|
+
>
|
|
53
|
+
See the prices
|
|
54
|
+
</a>
|
|
55
|
+
</div>
|
|
56
|
+
<dl className="mt-10 grid grid-cols-3 gap-4 border-t border-ink pt-5">
|
|
57
|
+
<QuickFact label="Hours" value={hero.quickFacts.hours} />
|
|
58
|
+
<QuickFact label="Area" value={hero.quickFacts.area} />
|
|
59
|
+
<QuickFact label="Call" value={hero.quickFacts.phone} />
|
|
60
|
+
</dl>
|
|
59
61
|
</div>
|
|
60
|
-
<dl className="mt-10 grid max-w-lg grid-cols-3 gap-4 border-t border-zinc-200/70 pt-6">
|
|
61
|
-
<QuickFact label="Hours" value={hero.quickFacts.hours} />
|
|
62
|
-
<QuickFact label="Area" value={hero.quickFacts.area} />
|
|
63
|
-
<QuickFact label="Call" value={hero.quickFacts.phone} />
|
|
64
|
-
</dl>
|
|
65
62
|
</div>
|
|
66
63
|
|
|
67
|
-
{/* Hero photo
|
|
68
|
-
<div className="relative
|
|
69
|
-
<ImagePlaceholder
|
|
70
|
-
shape="portrait"
|
|
71
|
-
title="A photo of your shop"
|
|
72
|
-
hint="Swap for an <img> in app/(marketing)/page.tsx"
|
|
73
|
-
/>
|
|
64
|
+
{/* Hero photo. Swap public/images/hero.jpg for a shot of your shop. */}
|
|
65
|
+
<div className="relative w-full">
|
|
66
|
+
<ImagePlaceholder shape="portrait" title="The shop" src="/images/hero.jpg" />
|
|
74
67
|
<div className="absolute left-4 top-4">
|
|
75
68
|
<LiveBadge>Availability updates live</LiveBadge>
|
|
76
69
|
</div>
|
|
@@ -80,114 +73,130 @@ export default function LandingPage() {
|
|
|
80
73
|
|
|
81
74
|
{/* =========================== SERVICES ========================== */}
|
|
82
75
|
<Divider />
|
|
83
|
-
<section id="services" className={`${WRAP} py-
|
|
84
|
-
<
|
|
85
|
-
<
|
|
76
|
+
<section id="services" className={`${WRAP} py-12`}>
|
|
77
|
+
<h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{services.headline}</h2>
|
|
78
|
+
<ol className="mt-8 border-t border-ink">
|
|
86
79
|
{services.items.map((s) => (
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
{s.description ? (
|
|
94
|
-
<p className="mt-1.5 text-[13.5px] leading-relaxed text-zinc-500">
|
|
95
|
-
{s.description}
|
|
96
|
-
</p>
|
|
97
|
-
) : null}
|
|
98
|
-
<p className="mt-2 text-[12px] uppercase tracking-wide text-zinc-400">
|
|
80
|
+
<li key={s.slug} className="border-b border-ink py-5">
|
|
81
|
+
<div className="flex items-baseline gap-4">
|
|
82
|
+
<h3 className="shrink-0 font-display text-[2rem] leading-none sm:text-[2.5rem]">
|
|
83
|
+
{s.name}
|
|
84
|
+
</h3>
|
|
85
|
+
<span className="hidden shrink-0 text-[13px] text-ink/60 sm:inline">
|
|
99
86
|
{s.durationMin} min
|
|
100
|
-
</
|
|
87
|
+
</span>
|
|
88
|
+
<span
|
|
89
|
+
aria-hidden
|
|
90
|
+
className="mb-[0.35em] min-w-6 flex-1 border-b border-dotted border-ink/60"
|
|
91
|
+
/>
|
|
92
|
+
<span className="shrink-0 font-display text-[2rem] leading-none text-brand sm:text-[2.5rem]">
|
|
93
|
+
{s.price}
|
|
94
|
+
</span>
|
|
101
95
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
{s.description ? (
|
|
97
|
+
<p className="mt-2 max-w-xl text-[14px] leading-relaxed text-ink/70">
|
|
98
|
+
<span className="sm:hidden">{s.durationMin} min. </span>
|
|
99
|
+
{s.description}
|
|
100
|
+
</p>
|
|
101
|
+
) : null}
|
|
102
|
+
</li>
|
|
104
103
|
))}
|
|
105
|
-
</
|
|
104
|
+
</ol>
|
|
106
105
|
</section>
|
|
107
106
|
|
|
108
107
|
{/* ============================ BOOKING ========================== */}
|
|
109
108
|
{booking.enabled ? (
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
<section id="book" className="bg-ink text-cream">
|
|
110
|
+
<div className={`${WRAP} py-14`}>
|
|
111
|
+
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] lg:gap-12">
|
|
112
|
+
<div>
|
|
113
|
+
<h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">
|
|
114
|
+
{booking.headline}
|
|
115
|
+
</h2>
|
|
116
|
+
<p className="mt-5 max-w-sm text-[15px] leading-relaxed text-cream/75">
|
|
117
|
+
{booking.subcopy}
|
|
118
|
+
</p>
|
|
119
|
+
<p className="mt-6 text-[13px] text-cream/60">
|
|
120
|
+
Or call{" "}
|
|
121
|
+
<a href={`tel:${location.phone}`} className="text-cream underline underline-offset-4">
|
|
122
|
+
{location.phone}
|
|
123
|
+
</a>
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
119
126
|
<BookingWidget />
|
|
120
127
|
</div>
|
|
121
|
-
</
|
|
122
|
-
|
|
128
|
+
</div>
|
|
129
|
+
</section>
|
|
123
130
|
) : null}
|
|
124
131
|
|
|
125
132
|
{/* ============================ REVIEWS ========================== */}
|
|
126
133
|
{reviews && reviews.items.length > 0 ? (
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
<section className={`${WRAP} py-12`}>
|
|
135
|
+
<h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{reviews.headline}</h2>
|
|
136
|
+
<div className="mt-8 grid gap-10 border-t border-ink pt-8 md:grid-cols-3 md:gap-8">
|
|
137
|
+
{reviews.items.map((r) => (
|
|
138
|
+
<figure key={r.name}>
|
|
139
|
+
<blockquote className="font-display text-balance text-[1.75rem] leading-[1.05] sm:text-[2rem]">
|
|
140
|
+
“{r.quote}”
|
|
141
|
+
</blockquote>
|
|
142
|
+
<figcaption className="mt-4 text-[13px] text-ink/70">
|
|
143
|
+
{r.name}
|
|
137
144
|
{r.rating ? (
|
|
138
|
-
<
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
</div>
|
|
145
|
+
<span className="ml-2 text-brand" aria-label={`${r.rating} out of 5`}>
|
|
146
|
+
{r.rating}/5
|
|
147
|
+
</span>
|
|
142
148
|
) : null}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
</figcaption>
|
|
149
|
-
</figure>
|
|
150
|
-
))}
|
|
151
|
-
</div>
|
|
152
|
-
</section>
|
|
153
|
-
</>
|
|
149
|
+
</figcaption>
|
|
150
|
+
</figure>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
</section>
|
|
154
154
|
) : null}
|
|
155
155
|
|
|
156
156
|
{/* =========================== LOCATION ========================== */}
|
|
157
157
|
<Divider />
|
|
158
|
-
<section className={`${WRAP} py-
|
|
159
|
-
<div className="grid gap-8 lg:grid-cols-
|
|
158
|
+
<section id="visit" className={`${WRAP} py-12`}>
|
|
159
|
+
<div className="grid gap-8 lg:grid-cols-2 lg:gap-12">
|
|
160
160
|
<div>
|
|
161
|
-
<
|
|
162
|
-
<
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
</
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
161
|
+
<h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{location.headline}</h2>
|
|
162
|
+
<dl className="mt-6 grid gap-5 text-[15px] leading-relaxed">
|
|
163
|
+
<div>
|
|
164
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">Address</dt>
|
|
165
|
+
<dd className="mt-1">{location.address}</dd>
|
|
166
|
+
</div>
|
|
167
|
+
<div>
|
|
168
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">Hours</dt>
|
|
169
|
+
<dd className="mt-1">{location.hoursText}</dd>
|
|
170
|
+
</div>
|
|
171
|
+
<div>
|
|
172
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">Contact</dt>
|
|
173
|
+
<dd className="mt-1">
|
|
174
|
+
<a href={`tel:${location.phone}`} className="text-brand">
|
|
175
|
+
{location.phone}
|
|
176
|
+
</a>{" "}
|
|
177
|
+
·{" "}
|
|
178
|
+
<a href={`mailto:${location.email}`} className="underline underline-offset-4">
|
|
179
|
+
{location.email}
|
|
180
|
+
</a>
|
|
181
|
+
</dd>
|
|
182
|
+
</div>
|
|
183
|
+
</dl>
|
|
184
|
+
<a
|
|
185
|
+
href="#book"
|
|
186
|
+
className="mt-8 inline-flex h-12 items-center bg-ink px-7 font-display text-[22px] tracking-[0.04em] text-cream transition-opacity hover:opacity-90"
|
|
187
|
+
>
|
|
188
|
+
{hero.ctaLabel}
|
|
189
|
+
</a>
|
|
181
190
|
</div>
|
|
182
191
|
{location.mapEmbedUrl ? (
|
|
183
192
|
<iframe
|
|
184
193
|
title="Map"
|
|
185
194
|
src={location.mapEmbedUrl}
|
|
186
|
-
className="h-
|
|
195
|
+
className="h-72 w-full border border-ink"
|
|
187
196
|
loading="lazy"
|
|
188
197
|
/>
|
|
189
198
|
) : (
|
|
190
|
-
<div className="grid h-
|
|
199
|
+
<div className="grid h-72 place-items-center border border-ink bg-paper text-sm text-ink/60">
|
|
191
200
|
Drop a Google Maps embed URL in <code className="mx-1">location.mapEmbedUrl</code>
|
|
192
201
|
</div>
|
|
193
202
|
)}
|
|
@@ -198,21 +207,18 @@ export default function LandingPage() {
|
|
|
198
207
|
{faq && faq.items.length > 0 ? (
|
|
199
208
|
<>
|
|
200
209
|
<Divider />
|
|
201
|
-
<section className={`${WRAP} py-
|
|
202
|
-
<
|
|
203
|
-
<
|
|
204
|
-
{faq.headline}
|
|
205
|
-
</h2>
|
|
206
|
-
<div className="mt-8 divide-y divide-zinc-200/70 border-y border-zinc-200/70">
|
|
210
|
+
<section className={`${WRAP} py-12`}>
|
|
211
|
+
<h2 className="font-display text-[3rem] leading-none sm:text-[4rem]">{faq.headline}</h2>
|
|
212
|
+
<div className="mt-8 divide-y divide-ink border-y border-ink">
|
|
207
213
|
{faq.items.map((f) => (
|
|
208
214
|
<details key={f.q} className="group py-5">
|
|
209
|
-
<summary className="flex cursor-pointer items-center justify-between text-[
|
|
215
|
+
<summary className="flex cursor-pointer items-center justify-between text-[16px] font-medium marker:hidden [&::-webkit-details-marker]:hidden">
|
|
210
216
|
{f.q}
|
|
211
|
-
<span className="text-brand transition-transform group-open:rotate-45"
|
|
217
|
+
<span className="font-display text-[24px] leading-none text-brand transition-transform group-open:rotate-45">
|
|
218
|
+
+
|
|
219
|
+
</span>
|
|
212
220
|
</summary>
|
|
213
|
-
<p className="mt-3 max-w-2xl text-[
|
|
214
|
-
{f.a}
|
|
215
|
-
</p>
|
|
221
|
+
<p className="mt-3 max-w-2xl text-[14.5px] leading-relaxed text-ink/70">{f.a}</p>
|
|
216
222
|
</details>
|
|
217
223
|
))}
|
|
218
224
|
</div>
|
|
@@ -226,8 +232,8 @@ export default function LandingPage() {
|
|
|
226
232
|
function QuickFact({ label, value }: { label: string; value: string }) {
|
|
227
233
|
return (
|
|
228
234
|
<div>
|
|
229
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-
|
|
230
|
-
<dd className="mt-1 text-[
|
|
235
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-ink/60">{label}</dt>
|
|
236
|
+
<dd className="mt-1 text-[14px] font-medium">{value}</dd>
|
|
231
237
|
</div>
|
|
232
238
|
);
|
|
233
239
|
}
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
--brand: #4f46e5;
|
|
20
20
|
--brand-soft: #eef2ff;
|
|
21
21
|
--paper: #fafafa;
|
|
22
|
+
/* The marketing pages: a cream poster, near-black type. Set from lib/site.config.ts. */
|
|
23
|
+
--ink: #161412;
|
|
24
|
+
--cream: #f4efe6;
|
|
22
25
|
--background: oklch(1 0 0);
|
|
23
26
|
--foreground: oklch(0.141 0.005 285.823);
|
|
24
27
|
--card: oklch(1 0 0);
|
|
@@ -112,6 +115,8 @@
|
|
|
112
115
|
--color-brand: var(--brand);
|
|
113
116
|
--color-brand-soft: var(--brand-soft);
|
|
114
117
|
--color-paper: var(--paper);
|
|
118
|
+
--color-ink: var(--ink);
|
|
119
|
+
--color-cream: var(--cream);
|
|
115
120
|
--color-chart-1: var(--chart-1);
|
|
116
121
|
--color-chart-2: var(--chart-2);
|
|
117
122
|
--color-chart-3: var(--chart-3);
|
|
@@ -153,3 +158,9 @@
|
|
|
153
158
|
cursor: pointer;
|
|
154
159
|
}
|
|
155
160
|
}
|
|
161
|
+
|
|
162
|
+
/* Headlines, service names, and pull quotes. Bebas Neue is declared in app.ts. */
|
|
163
|
+
.font-display {
|
|
164
|
+
font-family: var(--font-display, "Oswald", "Arial Narrow", Impact, sans-serif);
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
}
|
|
@@ -143,6 +143,15 @@ const manifest = buildManifest({
|
|
|
143
143
|
display: "swap",
|
|
144
144
|
preload: true,
|
|
145
145
|
}),
|
|
146
|
+
// The condensed display face for the headline, service names, and pull quotes.
|
|
147
|
+
font({
|
|
148
|
+
family: "Bebas Neue",
|
|
149
|
+
variable: "--font-display",
|
|
150
|
+
weights: ["400"],
|
|
151
|
+
subsets: ["latin"],
|
|
152
|
+
display: "swap",
|
|
153
|
+
preload: true,
|
|
154
|
+
}),
|
|
146
155
|
],
|
|
147
156
|
routes: await discoverAppRoutes(),
|
|
148
157
|
});
|
|
@@ -8,14 +8,6 @@ import React from "react";
|
|
|
8
8
|
// Shared container: a contained, centered column.
|
|
9
9
|
export const WRAP = "mx-auto w-full max-w-5xl px-6";
|
|
10
10
|
|
|
11
|
-
export function Eyebrow({ children }: { children: React.ReactNode }) {
|
|
12
|
-
return (
|
|
13
|
-
<p className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-brand">
|
|
14
|
-
{children}
|
|
15
|
-
</p>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
11
|
// "New / Coming soon"-style pill for the hero.
|
|
20
12
|
export function Badge({ children }: { children: React.ReactNode }) {
|
|
21
13
|
return (
|
|
@@ -26,36 +18,9 @@ export function Badge({ children }: { children: React.ReactNode }) {
|
|
|
26
18
|
);
|
|
27
19
|
}
|
|
28
20
|
|
|
21
|
+
// A full-bleed rule between sections, like the rules on a printed price board.
|
|
29
22
|
export function Divider() {
|
|
30
|
-
return
|
|
31
|
-
<div className={WRAP}>
|
|
32
|
-
<div className="border-t border-zinc-200/70" />
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function SectionHead({
|
|
38
|
-
eyebrow,
|
|
39
|
-
title,
|
|
40
|
-
body,
|
|
41
|
-
}: {
|
|
42
|
-
eyebrow: string;
|
|
43
|
-
title: string;
|
|
44
|
-
body?: string;
|
|
45
|
-
}) {
|
|
46
|
-
return (
|
|
47
|
-
<div>
|
|
48
|
-
<Eyebrow>{eyebrow}</Eyebrow>
|
|
49
|
-
<h2 className="mt-4 text-balance text-2xl font-semibold leading-[1.15] tracking-[-0.02em] sm:text-3xl">
|
|
50
|
-
{title}
|
|
51
|
-
</h2>
|
|
52
|
-
{body ? (
|
|
53
|
-
<p className="mt-4 max-w-xl text-[15px] leading-relaxed text-zinc-500">
|
|
54
|
-
{body}
|
|
55
|
-
</p>
|
|
56
|
-
) : null}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
23
|
+
return <div className="border-t border-ink" />;
|
|
59
24
|
}
|
|
60
25
|
|
|
61
26
|
// A grid of value props — icon + title + body.
|
|
@@ -105,11 +70,14 @@ export function ImagePlaceholder({
|
|
|
105
70
|
shape = "landscape",
|
|
106
71
|
title,
|
|
107
72
|
hint,
|
|
73
|
+
src,
|
|
108
74
|
className = "",
|
|
109
75
|
}: {
|
|
110
76
|
shape?: "landscape" | "portrait" | "square" | "circle";
|
|
111
77
|
title: string;
|
|
112
78
|
hint?: string;
|
|
79
|
+
/** A real image. The template ships one under public/images; swap it for yours. */
|
|
80
|
+
src?: string;
|
|
113
81
|
className?: string;
|
|
114
82
|
}) {
|
|
115
83
|
const aspect =
|
|
@@ -118,7 +86,14 @@ export function ImagePlaceholder({
|
|
|
118
86
|
: shape === "square" || shape === "circle"
|
|
119
87
|
? "aspect-square"
|
|
120
88
|
: "aspect-[4/3]";
|
|
121
|
-
const radius = shape === "circle" ? "rounded-full" : "
|
|
89
|
+
const radius = shape === "circle" ? "rounded-full" : "";
|
|
90
|
+
if (src) {
|
|
91
|
+
return (
|
|
92
|
+
<div className={`relative overflow-hidden bg-zinc-100 ${aspect} ${radius} ${className}`}>
|
|
93
|
+
<img src={src} alt={title} className="absolute inset-0 size-full object-cover" loading="lazy" />
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
122
97
|
return (
|
|
123
98
|
<div
|
|
124
99
|
className={`relative grid place-items-center overflow-hidden border-2 border-dashed border-zinc-300 bg-zinc-50 ${aspect} ${radius} ${className}`}
|
|
@@ -149,7 +124,7 @@ export function ImagePlaceholder({
|
|
|
149
124
|
// visible (e.g. "Availability updates live"). Pure decoration; no client JS.
|
|
150
125
|
export function LiveBadge({ children }: { children: React.ReactNode }) {
|
|
151
126
|
return (
|
|
152
|
-
<span className="inline-flex items-center gap-2
|
|
127
|
+
<span className="inline-flex items-center gap-2 border border-ink bg-cream px-3 py-1.5 text-[12.5px] font-medium text-ink">
|
|
153
128
|
<span className="relative flex size-2">
|
|
154
129
|
<span className="absolute inline-flex size-2 animate-ping rounded-full bg-green-500/60" />
|
|
155
130
|
<span className="relative inline-flex size-2 rounded-full bg-green-600" />
|
|
@@ -20,7 +20,8 @@ export type BaseConfig = {
|
|
|
20
20
|
copyrightName: string;
|
|
21
21
|
socials: Social[];
|
|
22
22
|
};
|
|
23
|
-
|
|
23
|
+
// brand: the one accent (deep red). ink/cream: the poster's type and page colors.
|
|
24
|
+
colors: { brand: string; brandSoft: string; paper: string; ink: string; cream: string };
|
|
24
25
|
seo: { title: string; description: string };
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -46,13 +47,11 @@ export type LocalServiceConfig = BaseConfig & {
|
|
|
46
47
|
quickFacts: { hours: string; area: string; phone: string };
|
|
47
48
|
};
|
|
48
49
|
services: {
|
|
49
|
-
eyebrow: string;
|
|
50
50
|
headline: string;
|
|
51
51
|
items: ServiceItem[];
|
|
52
52
|
};
|
|
53
53
|
booking: {
|
|
54
54
|
enabled: boolean;
|
|
55
|
-
eyebrow: string;
|
|
56
55
|
headline: string;
|
|
57
56
|
subcopy: string;
|
|
58
57
|
slotMinutes: number; // granularity of offered start times, e.g. 30
|
|
@@ -62,9 +61,8 @@ export type LocalServiceConfig = BaseConfig & {
|
|
|
62
61
|
hours: Record<number, DayHours>;
|
|
63
62
|
confirmationMessage: string;
|
|
64
63
|
};
|
|
65
|
-
reviews?: {
|
|
64
|
+
reviews?: { headline: string; items: Review[] };
|
|
66
65
|
location: {
|
|
67
|
-
eyebrow: string;
|
|
68
66
|
headline: string;
|
|
69
67
|
address: string;
|
|
70
68
|
mapEmbedUrl?: string;
|
|
@@ -72,7 +70,7 @@ export type LocalServiceConfig = BaseConfig & {
|
|
|
72
70
|
phone: string;
|
|
73
71
|
email: string;
|
|
74
72
|
};
|
|
75
|
-
faq?: {
|
|
73
|
+
faq?: { headline: string; items: Faq[] };
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
/* ----------------------------- config ---------------------------- */
|
|
@@ -84,7 +82,7 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
84
82
|
domain: "northgatebarbers.com",
|
|
85
83
|
email: "hello@northgatebarbers.example",
|
|
86
84
|
footerBlurb:
|
|
87
|
-
"A neighborhood barbershop in Dallas.
|
|
85
|
+
"A neighborhood barbershop in Dallas. Haircuts, beard trims, and hot-towel shaves. Book a chair online.",
|
|
88
86
|
copyrightName: "Northgate Barbers",
|
|
89
87
|
socials: [
|
|
90
88
|
{
|
|
@@ -95,7 +93,7 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
95
93
|
],
|
|
96
94
|
},
|
|
97
95
|
|
|
98
|
-
colors: { brand: "#
|
|
96
|
+
colors: { brand: "#9b1c1c", brandSoft: "#f6dcd7", paper: "#ebe4d6", ink: "#161412", cream: "#f4efe6" },
|
|
99
97
|
|
|
100
98
|
seo: {
|
|
101
99
|
title: "Northgate Barbers — classic cuts in Dallas. Book online.",
|
|
@@ -105,9 +103,9 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
105
103
|
|
|
106
104
|
hero: {
|
|
107
105
|
tagline: "Dallas · est. 2014",
|
|
108
|
-
headline: "
|
|
106
|
+
headline: "Haircuts, beard trims, and hot-towel shaves.",
|
|
109
107
|
subcopy:
|
|
110
|
-
"
|
|
108
|
+
"A neighborhood barbershop on Lower Greenville since 2014. Walk in or book a chair online. The calendar updates as chairs fill.",
|
|
111
109
|
ctaLabel: "Book a chair",
|
|
112
110
|
quickFacts: {
|
|
113
111
|
hours: "Tue–Sat, 9–6",
|
|
@@ -117,8 +115,7 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
117
115
|
},
|
|
118
116
|
|
|
119
117
|
services: {
|
|
120
|
-
|
|
121
|
-
headline: "Services and prices, up front.",
|
|
118
|
+
headline: "Prices",
|
|
122
119
|
items: [
|
|
123
120
|
{
|
|
124
121
|
slug: "haircut",
|
|
@@ -153,8 +150,7 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
153
150
|
|
|
154
151
|
booking: {
|
|
155
152
|
enabled: true,
|
|
156
|
-
|
|
157
|
-
headline: "Find a time that's open.",
|
|
153
|
+
headline: "Book a chair",
|
|
158
154
|
subcopy:
|
|
159
155
|
"Pick a service and a day from the live availability calendar. Book without creating an account or making a phone call.",
|
|
160
156
|
slotMinutes: 30,
|
|
@@ -174,8 +170,7 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
174
170
|
},
|
|
175
171
|
|
|
176
172
|
reviews: {
|
|
177
|
-
|
|
178
|
-
headline: "Regulars say it best.",
|
|
173
|
+
headline: "What regulars say",
|
|
179
174
|
items: [
|
|
180
175
|
{
|
|
181
176
|
quote:
|
|
@@ -199,8 +194,7 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
199
194
|
},
|
|
200
195
|
|
|
201
196
|
location: {
|
|
202
|
-
|
|
203
|
-
headline: "Find us on Greenville.",
|
|
197
|
+
headline: "Find us",
|
|
204
198
|
address: "1845 Greenville Ave, Dallas, TX 75206",
|
|
205
199
|
mapEmbedUrl: "",
|
|
206
200
|
hoursText: "Tue–Wed 9–6 · Thu–Fri 9–7 · Sat 9–4 · Sun–Mon closed",
|
|
@@ -209,20 +203,19 @@ export const siteConfig: LocalServiceConfig = {
|
|
|
209
203
|
},
|
|
210
204
|
|
|
211
205
|
faq: {
|
|
212
|
-
|
|
213
|
-
headline: "Good to know.",
|
|
206
|
+
headline: "Questions",
|
|
214
207
|
items: [
|
|
215
208
|
{
|
|
216
209
|
q: "Do you take walk-ins?",
|
|
217
|
-
a: "
|
|
210
|
+
a: "Yes, when a chair is open. Booking online holds your time, and you can see what is free.",
|
|
218
211
|
},
|
|
219
212
|
{
|
|
220
213
|
q: "What if I need to cancel?",
|
|
221
|
-
a: "
|
|
214
|
+
a: "Call the shop. There is no charge for cancellations with a few hours' notice.",
|
|
222
215
|
},
|
|
223
216
|
{
|
|
224
217
|
q: "How should I pay?",
|
|
225
|
-
a: "Cash or card in the shop. Booking online
|
|
218
|
+
a: "Cash or card in the shop. Booking online does not charge you. You pay after the cut.",
|
|
226
219
|
},
|
|
227
220
|
],
|
|
228
221
|
},
|
|
Binary file
|