@weft-js/create 0.1.1 → 0.1.2
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/README.md +60 -10
- package/bin/create-weft.mjs +62 -14
- package/package.json +26 -16
- package/scripts/tarballs.mjs +56 -6
- package/src/data.mjs +90 -18
- package/src/index.mjs +210 -51
- package/src/stack/pages.mjs +151 -0
- package/src/stack/schema.mjs +195 -0
- package/src/stack.mjs +102 -313
- package/src/templates.mjs +174 -0
- package/src/wizard.mjs +65 -16
- package/templates/admin/files/drizzle/.schema-hash +1 -0
- package/templates/admin/files/drizzle/0000_admin.sql +136 -0
- package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
- package/templates/admin/files/drizzle/meta/_journal.json +13 -0
- package/templates/admin/files/public/favicon.svg +1 -0
- package/templates/admin/files/src/auth.server.ts +24 -0
- package/templates/admin/files/src/components/AuthPage.tsx +33 -0
- package/templates/admin/files/src/components/Failed.tsx +19 -0
- package/templates/admin/files/src/components/Logo.tsx +16 -0
- package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
- package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
- package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
- package/templates/admin/files/src/components/Overview.tsx +113 -0
- package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
- package/templates/admin/files/src/db/schema.ts +68 -0
- package/templates/admin/files/src/lib/orders.ts +60 -0
- package/templates/admin/files/src/routes/404.tsx +20 -0
- package/templates/admin/files/src/routes/_error.tsx +20 -0
- package/templates/admin/files/src/routes/_layout.tsx +6 -0
- package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
- package/templates/admin/files/src/routes/admin/index.tsx +45 -0
- package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
- package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
- package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
- package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
- package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
- package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
- package/templates/admin/files/src/routes/index.tsx +13 -0
- package/templates/admin/files/src/routes/login.tsx +53 -0
- package/templates/admin/files/src/routes/signup.tsx +60 -0
- package/templates/admin/files/src/server/orders.ts +193 -0
- package/templates/admin/files/src/server/reports.ts +92 -0
- package/templates/admin/files/src/server/session.ts +42 -0
- package/templates/admin/files/src/theme.css +43 -0
- package/templates/admin/files/weft.config.ts +31 -0
- package/templates/admin/template.json +22 -0
- package/templates/catalog/files/drizzle/.schema-hash +1 -0
- package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
- package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
- package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
- package/templates/catalog/files/public/favicon.svg +1 -0
- package/templates/catalog/files/seed.sql +13 -0
- package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
- package/templates/catalog/files/src/components/Aisles.tsx +31 -0
- package/templates/catalog/files/src/components/CartLines.tsx +96 -0
- package/templates/catalog/files/src/components/CartLink.tsx +29 -0
- package/templates/catalog/files/src/components/Failed.tsx +19 -0
- package/templates/catalog/files/src/components/Featured.tsx +19 -0
- package/templates/catalog/files/src/components/Filters.tsx +40 -0
- package/templates/catalog/files/src/components/Logo.tsx +16 -0
- package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
- package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
- package/templates/catalog/files/src/components/ProductList.tsx +65 -0
- package/templates/catalog/files/src/components/Related.tsx +19 -0
- package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
- package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
- package/templates/catalog/files/src/db/schema.ts +53 -0
- package/templates/catalog/files/src/lib/catalog.ts +35 -0
- package/templates/catalog/files/src/routes/404.tsx +18 -0
- package/templates/catalog/files/src/routes/_error.tsx +18 -0
- package/templates/catalog/files/src/routes/_layout.tsx +14 -0
- package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
- package/templates/catalog/files/src/routes/cart.tsx +29 -0
- package/templates/catalog/files/src/routes/index.tsx +84 -0
- package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
- package/templates/catalog/files/src/server/cart.ts +125 -0
- package/templates/catalog/files/src/server/catalog.ts +107 -0
- package/templates/catalog/files/src/theme.css +159 -0
- package/templates/catalog/files/weft.config.ts +30 -0
- package/templates/catalog/template.json +20 -0
- package/templates/landing/files/.weft/plugins.lock.json +42 -0
- package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
- package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
- package/templates/landing/files/assets/hero.webp +0 -0
- package/templates/landing/files/drizzle/.schema-hash +1 -0
- package/templates/landing/files/drizzle/0000_landing.sql +6 -0
- package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
- package/templates/landing/files/drizzle/meta/_journal.json +13 -0
- package/templates/landing/files/public/favicon.svg +1 -0
- package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
- package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
- package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
- package/templates/landing/files/src/db/schema.ts +7 -0
- package/templates/landing/files/src/routes/404.tsx +24 -0
- package/templates/landing/files/src/routes/_error.tsx +24 -0
- package/templates/landing/files/src/routes/_layout.tsx +14 -0
- package/templates/landing/files/src/routes/index.tsx +130 -0
- package/templates/landing/files/src/routes/joined.tsx +30 -0
- package/templates/landing/files/src/routes/pricing.tsx +131 -0
- package/templates/landing/files/src/routes/privacy.tsx +56 -0
- package/templates/landing/files/src/server/waitlist.ts +13 -0
- package/templates/landing/files/src/theme.css +108 -0
- package/templates/landing/files/weft.config.ts +44 -0
- package/templates/landing/template.json +17 -0
- package/templates/todo/files/drizzle/.schema-hash +1 -0
- package/templates/todo/files/drizzle/0000_todo.sql +82 -0
- package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
- package/templates/todo/files/drizzle/meta/_journal.json +13 -0
- package/templates/todo/files/public/favicon.svg +1 -0
- package/templates/todo/files/src/components/AuthPage.tsx +33 -0
- package/templates/todo/files/src/components/Logo.tsx +14 -0
- package/templates/todo/files/src/components/NewTodo.tsx +27 -0
- package/templates/todo/files/src/components/TodoList.tsx +101 -0
- package/templates/todo/files/src/db/schema.ts +18 -0
- package/templates/todo/files/src/lib/views.ts +13 -0
- package/templates/todo/files/src/routes/404.tsx +20 -0
- package/templates/todo/files/src/routes/_error.tsx +20 -0
- package/templates/todo/files/src/routes/_layout.tsx +6 -0
- package/templates/todo/files/src/routes/index.tsx +13 -0
- package/templates/todo/files/src/routes/login.tsx +53 -0
- package/templates/todo/files/src/routes/signup.tsx +57 -0
- package/templates/todo/files/src/routes/todos.tsx +87 -0
- package/templates/todo/files/src/server/todos.ts +98 -0
- package/templates/todo/files/src/theme.css +71 -0
- package/templates/todo/files/weft.config.ts +30 -0
- package/templates/todo/template.json +18 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Link, type Meta } from '@weft-js/start'
|
|
2
|
+
import { Center, Heading, Stack, Text } from '@weft-js/warp'
|
|
3
|
+
|
|
4
|
+
export const meta: Meta = { title: 'Something went wrong' }
|
|
5
|
+
|
|
6
|
+
export default function ServerError() {
|
|
7
|
+
return (
|
|
8
|
+
<section class="page-head">
|
|
9
|
+
<Center max="narrow" gutter>
|
|
10
|
+
<Stack gap="group" align="center">
|
|
11
|
+
<Heading level={1} role="display">
|
|
12
|
+
Something went wrong on our side
|
|
13
|
+
</Heading>
|
|
14
|
+
<Text class="lede" subtle>
|
|
15
|
+
It isn't you. Give it a minute and try again.
|
|
16
|
+
</Text>
|
|
17
|
+
<Link href="/" class="cta">
|
|
18
|
+
Back to the home page
|
|
19
|
+
</Link>
|
|
20
|
+
</Stack>
|
|
21
|
+
</Center>
|
|
22
|
+
</section>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import '../theme.css'
|
|
2
|
+
import type { RouteProps } from '@weft-js/start'
|
|
3
|
+
import { SiteFooter } from '#/components/SiteFooter'
|
|
4
|
+
import { SiteHeader } from '#/components/SiteHeader'
|
|
5
|
+
|
|
6
|
+
export default function Root(props: RouteProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div class="site">
|
|
9
|
+
<SiteHeader />
|
|
10
|
+
<main id="content">{props.children}</main>
|
|
11
|
+
<SiteFooter />
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Link, type Meta } from '@weft-js/start'
|
|
2
|
+
import { Card, Center, Grid, Heading, Split, Stack, Text } from '@weft-js/warp'
|
|
3
|
+
import { Media } from '@weft-js/core/media'
|
|
4
|
+
import hero from '../../assets/hero.webp?media'
|
|
5
|
+
import { WaitlistForm } from '#/components/WaitlistForm'
|
|
6
|
+
|
|
7
|
+
export const meta: Meta = {
|
|
8
|
+
title: { absolute: 'Emberline: hear what customers keep asking for' },
|
|
9
|
+
description: 'Emberline turns customer calls into a ranked list of what to build next.',
|
|
10
|
+
}
|
|
11
|
+
export const prerender = true
|
|
12
|
+
|
|
13
|
+
const features = [
|
|
14
|
+
{
|
|
15
|
+
title: 'Every call, heard',
|
|
16
|
+
body: "Emberline reads your sales and support calls and pulls out each request, in the customer's own words.",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: 'Ranked, not guessed',
|
|
20
|
+
body: 'Requests are grouped and weighted by how often they come up and which accounts asked.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'Proof beside every card',
|
|
24
|
+
body: 'Each item links back to the quotes behind it, so the roadmap argument ends quickly.',
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const steps = [
|
|
29
|
+
{
|
|
30
|
+
title: 'Connect your calls',
|
|
31
|
+
body: 'Point Emberline at your call recorder and help desk. Setup takes a few minutes.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: 'Read the signals',
|
|
35
|
+
body: 'Requests arrive grouped into Now, Next and Later, with a count beside each one.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: 'Close the loop',
|
|
39
|
+
body: 'When a feature ships, tell every customer who asked for it, with one digest.',
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
function heroArt() {
|
|
44
|
+
return (
|
|
45
|
+
<Media
|
|
46
|
+
src={hero}
|
|
47
|
+
priority
|
|
48
|
+
sizes="(max-width: 599px) 100vw, min(46vw, 696px)"
|
|
49
|
+
class="hero-art"
|
|
50
|
+
alt="The Emberline board: customer requests grouped into Now, Next and Later, with a quote from a renewal call"
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function Home() {
|
|
56
|
+
return (
|
|
57
|
+
<Stack gap="none">
|
|
58
|
+
<section class="hero">
|
|
59
|
+
<Center max="wide" gutter>
|
|
60
|
+
<Split gap="section" side="end" width="half">
|
|
61
|
+
<Stack gap="group" class="hero-copy" id="join">
|
|
62
|
+
<Text role="eyebrow" tone="accent">
|
|
63
|
+
Early access opens this winter
|
|
64
|
+
</Text>
|
|
65
|
+
<Heading level={1} role="display">
|
|
66
|
+
Hear what customers keep asking for
|
|
67
|
+
</Heading>
|
|
68
|
+
<Text class="lede" subtle>
|
|
69
|
+
Emberline listens to your customer calls and turns them into a ranked list of what
|
|
70
|
+
to build next.
|
|
71
|
+
</Text>
|
|
72
|
+
<WaitlistForm />
|
|
73
|
+
</Stack>
|
|
74
|
+
{heroArt()}
|
|
75
|
+
</Split>
|
|
76
|
+
</Center>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<section class="band">
|
|
80
|
+
<Center max="wide" gutter>
|
|
81
|
+
<Stack gap="section">
|
|
82
|
+
<Heading level={2} role="title" class="band-head">
|
|
83
|
+
Stop losing what customers tell you
|
|
84
|
+
</Heading>
|
|
85
|
+
<Grid gap="group" min="sm" cols={3}>
|
|
86
|
+
{features.map((feature) => (
|
|
87
|
+
<Card key={feature.title} title={feature.title}>
|
|
88
|
+
<Text subtle>{feature.body}</Text>
|
|
89
|
+
</Card>
|
|
90
|
+
))}
|
|
91
|
+
</Grid>
|
|
92
|
+
</Stack>
|
|
93
|
+
</Center>
|
|
94
|
+
</section>
|
|
95
|
+
|
|
96
|
+
<section class="band band-sunken">
|
|
97
|
+
<Center max="wide" gutter>
|
|
98
|
+
<Stack gap="section">
|
|
99
|
+
<Heading level={2} role="title">
|
|
100
|
+
How it works
|
|
101
|
+
</Heading>
|
|
102
|
+
<Grid as="ol" gap="group" min="sm" cols={3} class="steps">
|
|
103
|
+
{steps.map((step) => (
|
|
104
|
+
<li key={step.title}>
|
|
105
|
+
<Stack gap="related">
|
|
106
|
+
<Heading level={3}>{step.title}</Heading>
|
|
107
|
+
<Text subtle>{step.body}</Text>
|
|
108
|
+
</Stack>
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</Grid>
|
|
112
|
+
</Stack>
|
|
113
|
+
</Center>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
<section class="closing">
|
|
117
|
+
<Center max="narrow" gutter>
|
|
118
|
+
<Stack gap="group" align="center">
|
|
119
|
+
<Heading level={2} role="title">
|
|
120
|
+
Your customers already told you what to build.
|
|
121
|
+
</Heading>
|
|
122
|
+
<Link href="#join" class="cta">
|
|
123
|
+
Get early access
|
|
124
|
+
</Link>
|
|
125
|
+
</Stack>
|
|
126
|
+
</Center>
|
|
127
|
+
</section>
|
|
128
|
+
</Stack>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Link, type Meta } from '@weft-js/start'
|
|
2
|
+
import { Center, Heading, Stack, Text } from '@weft-js/warp'
|
|
3
|
+
|
|
4
|
+
export const meta: Meta = {
|
|
5
|
+
title: "You're on the list",
|
|
6
|
+
description: 'We will write when your place in the Emberline queue comes up.',
|
|
7
|
+
extra: [{ name: 'robots', content: 'noindex' }],
|
|
8
|
+
}
|
|
9
|
+
export const prerender = true
|
|
10
|
+
|
|
11
|
+
export default function Joined() {
|
|
12
|
+
return (
|
|
13
|
+
<section class="page-head">
|
|
14
|
+
<Center max="narrow" gutter>
|
|
15
|
+
<Stack gap="group" align="center">
|
|
16
|
+
<Heading level={1} role="display">
|
|
17
|
+
You're on the list
|
|
18
|
+
</Heading>
|
|
19
|
+
<Text class="lede" subtle>
|
|
20
|
+
We'll write when your place in line comes up. Everyone hears from us in the order they
|
|
21
|
+
joined.
|
|
22
|
+
</Text>
|
|
23
|
+
<Link href="/pricing" class="cta" data-emphasis="outline">
|
|
24
|
+
See pricing
|
|
25
|
+
</Link>
|
|
26
|
+
</Stack>
|
|
27
|
+
</Center>
|
|
28
|
+
</section>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { CheckIcon } from '@weft-js/warp-icons'
|
|
2
|
+
import { Link, type Meta } from '@weft-js/start'
|
|
3
|
+
import { Accordion, Badge, Card, Center, Grid, Heading, Stack, Text } from '@weft-js/warp'
|
|
4
|
+
|
|
5
|
+
export const meta: Meta = {
|
|
6
|
+
title: 'Pricing',
|
|
7
|
+
description: 'Free for one person. Simple per-seat pricing for teams.',
|
|
8
|
+
}
|
|
9
|
+
export const prerender = true
|
|
10
|
+
|
|
11
|
+
const plans = [
|
|
12
|
+
{
|
|
13
|
+
name: 'Starter',
|
|
14
|
+
price: '$0',
|
|
15
|
+
unit: 'forever',
|
|
16
|
+
blurb: 'For a founder reading their own calls.',
|
|
17
|
+
items: ['50 calls a month', 'Signals board', 'CSV export'],
|
|
18
|
+
cta: 'Join for free',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'Team',
|
|
22
|
+
price: '$24',
|
|
23
|
+
unit: 'per seat, per month',
|
|
24
|
+
blurb: 'For product teams that share a roadmap.',
|
|
25
|
+
items: ['Unlimited calls', 'CRM and Slack digests', 'Team spaces', 'Priority support'],
|
|
26
|
+
cta: 'Get early access',
|
|
27
|
+
featured: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Enterprise',
|
|
31
|
+
price: 'Talk to us',
|
|
32
|
+
unit: 'billed yearly',
|
|
33
|
+
blurb: 'For companies with records to keep.',
|
|
34
|
+
items: ['Everything in Team', 'Single sign-on', 'Audit log', 'Data residency'],
|
|
35
|
+
cta: 'Get in line',
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const questions = [
|
|
40
|
+
{
|
|
41
|
+
value: 'open',
|
|
42
|
+
title: 'When does it open?',
|
|
43
|
+
content:
|
|
44
|
+
'The first teams start this winter. Everyone on the waitlist hears from us in the order they joined.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: 'price',
|
|
48
|
+
title: 'Is the early price locked in?',
|
|
49
|
+
content:
|
|
50
|
+
'Yes. Teams that join during early access keep their per-seat price for as long as they stay.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: 'data',
|
|
54
|
+
title: 'What happens to my data if I leave?',
|
|
55
|
+
content: "It's yours. Export every signal and quote as CSV at any time, on any plan.",
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export default function Pricing() {
|
|
60
|
+
return (
|
|
61
|
+
<Stack gap="none">
|
|
62
|
+
<section class="page-head">
|
|
63
|
+
<Center gutter>
|
|
64
|
+
<Stack gap="related" align="center">
|
|
65
|
+
<Heading level={1} role="display">
|
|
66
|
+
Simple pricing that grows with your team
|
|
67
|
+
</Heading>
|
|
68
|
+
<Text class="lede" subtle>
|
|
69
|
+
Free for one person. Pay per seat when the whole team comes along.
|
|
70
|
+
</Text>
|
|
71
|
+
</Stack>
|
|
72
|
+
</Center>
|
|
73
|
+
</section>
|
|
74
|
+
|
|
75
|
+
<section class="band band-tight">
|
|
76
|
+
<Center max="wide" gutter>
|
|
77
|
+
<Grid gap="group" min="sm" cols={3}>
|
|
78
|
+
{plans.map((plan) => (
|
|
79
|
+
<Card key={plan.name} class="plan" data-featured={plan.featured ? '' : undefined}>
|
|
80
|
+
<Stack gap="group" class="plan-body">
|
|
81
|
+
<Stack gap="tight">
|
|
82
|
+
<Heading level={2} role="title">
|
|
83
|
+
{plan.name}
|
|
84
|
+
</Heading>
|
|
85
|
+
<Text subtle>{plan.blurb}</Text>
|
|
86
|
+
</Stack>
|
|
87
|
+
{plan.featured && (
|
|
88
|
+
<Badge tone="accent" emphasis="solid" class="plan-flag">
|
|
89
|
+
Most popular
|
|
90
|
+
</Badge>
|
|
91
|
+
)}
|
|
92
|
+
<Stack gap="hair">
|
|
93
|
+
<Text class="plan-price">{plan.price}</Text>
|
|
94
|
+
<Text role="caption" subtle>
|
|
95
|
+
{plan.unit}
|
|
96
|
+
</Text>
|
|
97
|
+
</Stack>
|
|
98
|
+
<Stack as="ul" gap="tight" class="plan-items">
|
|
99
|
+
{plan.items.map((item) => (
|
|
100
|
+
<li key={item}>
|
|
101
|
+
<CheckIcon /> {item}
|
|
102
|
+
</li>
|
|
103
|
+
))}
|
|
104
|
+
</Stack>
|
|
105
|
+
<Link
|
|
106
|
+
href="/#join"
|
|
107
|
+
class="cta"
|
|
108
|
+
data-emphasis={plan.featured ? undefined : 'outline'}
|
|
109
|
+
>
|
|
110
|
+
{plan.cta}
|
|
111
|
+
</Link>
|
|
112
|
+
</Stack>
|
|
113
|
+
</Card>
|
|
114
|
+
))}
|
|
115
|
+
</Grid>
|
|
116
|
+
</Center>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<section class="band band-sunken">
|
|
120
|
+
<Center max="narrow" gutter>
|
|
121
|
+
<Stack gap="group">
|
|
122
|
+
<Heading level={2} role="title">
|
|
123
|
+
Questions
|
|
124
|
+
</Heading>
|
|
125
|
+
<Accordion items={questions} variant="line" level={3} />
|
|
126
|
+
</Stack>
|
|
127
|
+
</Center>
|
|
128
|
+
</section>
|
|
129
|
+
</Stack>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Meta } from '@weft-js/start'
|
|
2
|
+
import { Center, Heading, Stack, Text } from '@weft-js/warp'
|
|
3
|
+
|
|
4
|
+
export const meta: Meta = {
|
|
5
|
+
title: 'Privacy',
|
|
6
|
+
description: 'What Emberline collects, why, and how to have it removed.',
|
|
7
|
+
}
|
|
8
|
+
export const prerender = true
|
|
9
|
+
|
|
10
|
+
const sections = [
|
|
11
|
+
{
|
|
12
|
+
title: 'What we collect',
|
|
13
|
+
body: 'The waitlist stores your email address and the time you joined. This site sets no cookies and runs no analytics.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
title: 'Why we collect it',
|
|
17
|
+
body: 'We use your address to tell you when your place in line comes up, and for nothing else. We send no newsletters.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: 'Who sees it',
|
|
21
|
+
body: "Only the people who run Emberline, and the database host that stores it on our behalf. We don't sell or share addresses.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: 'How long we keep it',
|
|
25
|
+
body: 'Until the waitlist closes or you ask us to remove it, whichever comes first.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: 'Removing your address',
|
|
29
|
+
body: "Reply to any message from us and we'll delete your address within seven days.",
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export default function Privacy() {
|
|
34
|
+
return (
|
|
35
|
+
<section class="page-head">
|
|
36
|
+
<Center max="measure" gutter>
|
|
37
|
+
<Stack gap="section">
|
|
38
|
+
<Stack gap="related">
|
|
39
|
+
<Heading level={1} role="display">
|
|
40
|
+
Privacy
|
|
41
|
+
</Heading>
|
|
42
|
+
<Text subtle>Last updated 1 November 2026</Text>
|
|
43
|
+
</Stack>
|
|
44
|
+
{sections.map((section) => (
|
|
45
|
+
<Stack as="section" gap="related" key={section.title}>
|
|
46
|
+
<Heading level={2} role="title">
|
|
47
|
+
{section.title}
|
|
48
|
+
</Heading>
|
|
49
|
+
<Text>{section.body}</Text>
|
|
50
|
+
</Stack>
|
|
51
|
+
))}
|
|
52
|
+
</Stack>
|
|
53
|
+
</Center>
|
|
54
|
+
</section>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mutation, redirect } from '@weft-js/start'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { waitlist } from '#/db/schema'
|
|
4
|
+
|
|
5
|
+
// Joining twice looks the same as joining once, so the form can't be used to find out who's on the list.
|
|
6
|
+
// The redirect is what makes this work with JavaScript off: a plain POST lands on the confirmation page too.
|
|
7
|
+
export const joinWaitlist = mutation({
|
|
8
|
+
input: z.object({ email: z.email("That doesn't look like an email address.") }),
|
|
9
|
+
run: async ({ input, ctx }) => {
|
|
10
|
+
await ctx.db.insert(waitlist).values({ email: input.email.toLowerCase() }).onConflictDoNothing()
|
|
11
|
+
redirect('/joined', 303)
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Emberline's own CSS, over warp's tokens. The brand, font and paper colours are warp() options in weft.config.ts;
|
|
3
|
+
* this file is the page's shape and the ember line. Change those options first, then the rules here.
|
|
4
|
+
*/
|
|
5
|
+
html { scroll-behavior: smooth; scroll-padding-block-start: var(--warp-space-page); }
|
|
6
|
+
|
|
7
|
+
body { margin: 0; }
|
|
8
|
+
.site { display: flex; flex-direction: column; min-block-size: 100dvh; }
|
|
9
|
+
.site > main { flex: 1; }
|
|
10
|
+
|
|
11
|
+
/* the ember line */
|
|
12
|
+
.site-head::before {
|
|
13
|
+
content: "";
|
|
14
|
+
display: block;
|
|
15
|
+
block-size: 3px;
|
|
16
|
+
background: linear-gradient(90deg, var(--warp-color-accent-solid), var(--warp-color-caution-solid), transparent);
|
|
17
|
+
}
|
|
18
|
+
/* warp's <Center max="wide" gutter>, for the chrome: nothing in it runs, so it stays plain markup */
|
|
19
|
+
.wrap {
|
|
20
|
+
box-sizing: content-box;
|
|
21
|
+
max-inline-size: var(--warp-layout-wide);
|
|
22
|
+
margin-inline: auto;
|
|
23
|
+
padding-inline: max(var(--warp-space-page), var(--warp-safe-left)) max(var(--warp-space-page), var(--warp-safe-right));
|
|
24
|
+
}
|
|
25
|
+
.site-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--warp-space-group); }
|
|
26
|
+
.site-links, .site-head nav, .site-foot nav { display: flex; align-items: center; gap: var(--warp-space-group); }
|
|
27
|
+
.site-head .wrap { padding-block: var(--warp-space-related); }
|
|
28
|
+
.site-head nav a { padding-block: var(--warp-space-tight); color: var(--warp-color-accent-text); text-decoration: none; transition: color var(--warp-duration-fast, 120ms) ease; }
|
|
29
|
+
.site-head nav a:hover, .site-head nav a:focus-visible, .site-head nav a[aria-current] { color: var(--warp-color-text); }
|
|
30
|
+
.site-head nav a[aria-current] { box-shadow: inset 0 -2px var(--warp-color-accent-solid); }
|
|
31
|
+
.site-foot { margin-block-start: auto; border-block-start: 1px solid var(--warp-color-border); }
|
|
32
|
+
.site-foot > .wrap { padding-block: var(--warp-space-section); }
|
|
33
|
+
.site-foot p { margin: 0; color: var(--warp-color-text-subtle); }
|
|
34
|
+
.site-foot-brand { display: grid; gap: var(--warp-space-tight); }
|
|
35
|
+
.site-foot p.site-foot-legal {
|
|
36
|
+
margin-block-start: var(--warp-space-group);
|
|
37
|
+
color: var(--warp-color-text-faint);
|
|
38
|
+
font-size: var(--warp-text-caption-size);
|
|
39
|
+
line-height: var(--warp-text-caption-leading);
|
|
40
|
+
}
|
|
41
|
+
.site-foot nav { flex-wrap: wrap; }
|
|
42
|
+
@media (width < 600px) { .site-foot .site-bar { flex-direction: column; align-items: stretch; gap: var(--warp-space-section); } }
|
|
43
|
+
|
|
44
|
+
.brand {
|
|
45
|
+
font-size: var(--warp-text-heading-size);
|
|
46
|
+
font-weight: var(--warp-text-heading-weight);
|
|
47
|
+
color: var(--warp-color-text);
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* a link that reads as the page's main action */
|
|
52
|
+
.cta {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
padding: var(--warp-space-related) var(--warp-space-group);
|
|
57
|
+
border: 1px solid transparent;
|
|
58
|
+
border-radius: var(--warp-radius-control);
|
|
59
|
+
background: var(--warp-color-accent-solid);
|
|
60
|
+
color: var(--warp-color-accent-on-solid);
|
|
61
|
+
font-weight: var(--warp-text-heading-weight);
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
transition: background-color var(--warp-duration-fast, 120ms) ease;
|
|
65
|
+
}
|
|
66
|
+
.cta:hover { background: var(--warp-color-accent-solid-hover); }
|
|
67
|
+
.cta[data-size="sm"] { padding: var(--warp-space-tight) var(--warp-space-related); }
|
|
68
|
+
.cta[data-emphasis="outline"] { background: transparent; border-color: var(--warp-color-accent-border); color: var(--warp-color-accent-text); }
|
|
69
|
+
.cta[data-emphasis="outline"]:hover { background: var(--warp-color-accent-soft); }
|
|
70
|
+
.cta[data-emphasis="ghost"] { background: transparent; color: inherit; }
|
|
71
|
+
.cta[data-emphasis="ghost"]:hover { background: var(--warp-color-surface-hover); }
|
|
72
|
+
|
|
73
|
+
.hero { padding-block: var(--warp-space-page); }
|
|
74
|
+
.hero-copy { align-self: center; }
|
|
75
|
+
.hero-art {
|
|
76
|
+
display: block;
|
|
77
|
+
inline-size: 100%;
|
|
78
|
+
block-size: auto;
|
|
79
|
+
border-radius: var(--warp-radius-surface);
|
|
80
|
+
}
|
|
81
|
+
.lede { font-size: var(--warp-text-heading-size); line-height: var(--warp-text-body-leading); }
|
|
82
|
+
|
|
83
|
+
.waitlist-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--warp-space-related); align-items: start; }
|
|
84
|
+
.waitlist-error { color: var(--warp-color-critical-text); }
|
|
85
|
+
.waitlist-error:empty { display: none; }
|
|
86
|
+
|
|
87
|
+
.band { padding-block: var(--warp-space-page); }
|
|
88
|
+
.band-sunken { background: var(--warp-color-surface-sunken); }
|
|
89
|
+
.band-tight { padding-block-start: 0; }
|
|
90
|
+
.band-head { max-inline-size: 36rem; }
|
|
91
|
+
|
|
92
|
+
.steps { counter-reset: step; list-style: none; padding: 0; margin: 0; }
|
|
93
|
+
.steps > li { counter-increment: step; border-block-start: 2px solid var(--warp-color-text); padding-block-start: var(--warp-space-related); }
|
|
94
|
+
.steps > li::before { content: counter(step); display: block; margin-block-end: var(--warp-space-related); font-family: var(--warp-font-mono); color: var(--warp-color-accent-text); }
|
|
95
|
+
|
|
96
|
+
.closing { padding-block: var(--warp-space-page); text-align: center; }
|
|
97
|
+
.page-head { padding-block: var(--warp-space-page); }
|
|
98
|
+
.page-head [data-align="center"] { text-align: center; }
|
|
99
|
+
|
|
100
|
+
.plan { position: relative; }
|
|
101
|
+
.plan h2 { color: var(--warp-color-text); }
|
|
102
|
+
.plan[data-featured] { outline: 2px solid var(--warp-color-accent-solid); outline-offset: -2px; }
|
|
103
|
+
.plan-flag { position: absolute; inset-block-start: var(--warp-space-group); inset-inline-end: var(--warp-space-group); }
|
|
104
|
+
.plan-body { block-size: 100%; }
|
|
105
|
+
.plan-price { color: var(--warp-color-text); font-size: var(--warp-text-title-size); font-weight: var(--warp-text-title-weight); }
|
|
106
|
+
.plan-items { list-style: none; padding: 0; margin: 0; flex: 1; }
|
|
107
|
+
.plan-items svg { color: var(--warp-color-accent-text); vertical-align: -2px; }
|
|
108
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineConfig } from '@weft-js/core/config'
|
|
2
|
+
import { warp } from '@weft-js/warp/plugin'
|
|
3
|
+
import { text } from '@weft-js/text'
|
|
4
|
+
import { media } from '@weft-js/plugin-media'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
site: {
|
|
8
|
+
name: 'Emberline',
|
|
9
|
+
locale: 'en_US',
|
|
10
|
+
title: '%s · Emberline',
|
|
11
|
+
description:
|
|
12
|
+
'Emberline listens to your customer calls and turns them into a ranked list of what to build next.',
|
|
13
|
+
head: '<link rel="icon" href="/favicon.svg" type="image/svg+xml">',
|
|
14
|
+
},
|
|
15
|
+
plugins: [
|
|
16
|
+
warp({
|
|
17
|
+
seed: { accent: '#c2410c' },
|
|
18
|
+
dials: { expression: 'vivid' },
|
|
19
|
+
radius: 'round',
|
|
20
|
+
fonts: {
|
|
21
|
+
display: '"Figtree", "Figtree Fallback", ui-sans-serif, system-ui, sans-serif',
|
|
22
|
+
body: '"Figtree", "Figtree Fallback", ui-sans-serif, system-ui, sans-serif',
|
|
23
|
+
},
|
|
24
|
+
tokens: {
|
|
25
|
+
'--warp-color-page': 'light-dark(#f8f1e4, #1d1613)',
|
|
26
|
+
'--warp-color-surface': 'light-dark(#fdf8ee, #261d19)',
|
|
27
|
+
'--warp-color-surface-raised': 'light-dark(#fffcf5, #281e1a)',
|
|
28
|
+
'--warp-color-surface-sunken': 'light-dark(#f5eee0, #171110)',
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
text({
|
|
32
|
+
measure: 'off',
|
|
33
|
+
fonts: {
|
|
34
|
+
body: {
|
|
35
|
+
family: 'Figtree',
|
|
36
|
+
src: 'assets/fonts/figtree.woff2',
|
|
37
|
+
fallback: 'Arial',
|
|
38
|
+
axes: { wght: [300, 900] },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
media(),
|
|
43
|
+
],
|
|
44
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"requires": { "db": "postgres", "auth": false },
|
|
3
|
+
"pages": ["/", "/pricing", "/privacy", "/joined"],
|
|
4
|
+
"dependencies": { "@weft-js/warp-icons": "0.0.0", "@weft-js/plugin-media": "0.0.0", "@weft-js/text": "0.0.0" },
|
|
5
|
+
"warp": {
|
|
6
|
+
"seed": { "accent": "#c2410c" },
|
|
7
|
+
"dials": { "expression": "vivid" },
|
|
8
|
+
"radius": "round",
|
|
9
|
+
"fonts": { "display": "\"Figtree\", \"Figtree Fallback\", ui-sans-serif, system-ui, sans-serif", "body": "\"Figtree\", \"Figtree Fallback\", ui-sans-serif, system-ui, sans-serif" },
|
|
10
|
+
"tokens": {
|
|
11
|
+
"--warp-color-page": "light-dark(#f8f1e4, #1d1613)",
|
|
12
|
+
"--warp-color-surface": "light-dark(#fdf8ee, #261d19)",
|
|
13
|
+
"--warp-color-surface-raised": "light-dark(#fffcf5, #281e1a)",
|
|
14
|
+
"--warp-color-surface-sunken": "light-dark(#f5eee0, #171110)"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2fea01fb1d5e6e88
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS "todos" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
3
|
+
"user_id" varchar(255) NOT NULL,
|
|
4
|
+
"title" text NOT NULL,
|
|
5
|
+
"done" boolean DEFAULT false NOT NULL,
|
|
6
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE TABLE IF NOT EXISTS "account" (
|
|
10
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
11
|
+
"accountId" text NOT NULL,
|
|
12
|
+
"providerId" text NOT NULL,
|
|
13
|
+
"userId" text NOT NULL,
|
|
14
|
+
"accessToken" text,
|
|
15
|
+
"refreshToken" text,
|
|
16
|
+
"idToken" text,
|
|
17
|
+
"accessTokenExpiresAt" timestamp with time zone,
|
|
18
|
+
"refreshTokenExpiresAt" timestamp with time zone,
|
|
19
|
+
"scope" text,
|
|
20
|
+
"password" text,
|
|
21
|
+
"createdAt" timestamp with time zone NOT NULL,
|
|
22
|
+
"updatedAt" timestamp with time zone NOT NULL
|
|
23
|
+
);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
CREATE TABLE IF NOT EXISTS "jwks" (
|
|
26
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
27
|
+
"publicKey" text NOT NULL,
|
|
28
|
+
"privateKey" text NOT NULL,
|
|
29
|
+
"createdAt" timestamp with time zone NOT NULL,
|
|
30
|
+
"expiresAt" timestamp with time zone,
|
|
31
|
+
"alg" text,
|
|
32
|
+
"crv" text
|
|
33
|
+
);
|
|
34
|
+
--> statement-breakpoint
|
|
35
|
+
CREATE TABLE IF NOT EXISTS "session" (
|
|
36
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
37
|
+
"expiresAt" timestamp with time zone NOT NULL,
|
|
38
|
+
"token" text NOT NULL,
|
|
39
|
+
"createdAt" timestamp with time zone NOT NULL,
|
|
40
|
+
"updatedAt" timestamp with time zone NOT NULL,
|
|
41
|
+
"ipAddress" text,
|
|
42
|
+
"userAgent" text,
|
|
43
|
+
"userId" text NOT NULL,
|
|
44
|
+
CONSTRAINT "session_token_unique" UNIQUE("token")
|
|
45
|
+
);
|
|
46
|
+
--> statement-breakpoint
|
|
47
|
+
CREATE TABLE IF NOT EXISTS "user" (
|
|
48
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
49
|
+
"name" text NOT NULL,
|
|
50
|
+
"email" text NOT NULL,
|
|
51
|
+
"emailVerified" boolean NOT NULL,
|
|
52
|
+
"image" text,
|
|
53
|
+
"createdAt" timestamp with time zone NOT NULL,
|
|
54
|
+
"updatedAt" timestamp with time zone NOT NULL,
|
|
55
|
+
CONSTRAINT "user_email_unique" UNIQUE("email")
|
|
56
|
+
);
|
|
57
|
+
--> statement-breakpoint
|
|
58
|
+
CREATE TABLE IF NOT EXISTS "verification" (
|
|
59
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
60
|
+
"identifier" text NOT NULL,
|
|
61
|
+
"value" text NOT NULL,
|
|
62
|
+
"expiresAt" timestamp with time zone NOT NULL,
|
|
63
|
+
"createdAt" timestamp with time zone,
|
|
64
|
+
"updatedAt" timestamp with time zone
|
|
65
|
+
);
|
|
66
|
+
--> statement-breakpoint
|
|
67
|
+
DO $$ BEGIN
|
|
68
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"todos"'::regclass AND c.contype = 'f' AND a.attname = 'user_id') THEN
|
|
69
|
+
ALTER TABLE "todos" ADD CONSTRAINT "todos_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
|
70
|
+
END IF;
|
|
71
|
+
END $$;--> statement-breakpoint
|
|
72
|
+
DO $$ BEGIN
|
|
73
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"account"'::regclass AND c.contype = 'f' AND a.attname = 'userId') THEN
|
|
74
|
+
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
|
75
|
+
END IF;
|
|
76
|
+
END $$;--> statement-breakpoint
|
|
77
|
+
DO $$ BEGIN
|
|
78
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"session"'::regclass AND c.contype = 'f' AND a.attname = 'userId') THEN
|
|
79
|
+
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
|
80
|
+
END IF;
|
|
81
|
+
END $$;--> statement-breakpoint
|
|
82
|
+
CREATE INDEX IF NOT EXISTS "todos_user" ON "todos" USING btree ("user_id","created_at","id");
|