@pylonsync/create-pylon 0.3.356 → 0.3.358
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/package.json +1 -1
- package/templates/marketplace/README.md +29 -14
- package/templates/marketplace/app/error.tsx +30 -16
- package/templates/marketplace/app/globals.css +140 -24
- package/templates/marketplace/app/layout.tsx +60 -31
- package/templates/marketplace/app/listing/[id]/page.tsx +160 -60
- package/templates/marketplace/app/me/page.tsx +3 -3
- package/templates/marketplace/app/not-found.tsx +19 -10
- package/templates/marketplace/app/page.tsx +287 -97
- package/templates/marketplace/app/sell/page.tsx +11 -9
- package/templates/marketplace/app.ts +5 -4
- package/templates/marketplace/client/AuthNav.tsx +4 -8
- package/templates/marketplace/client/LiveListingStatus.tsx +60 -0
- package/templates/marketplace/client/LiveTicker.tsx +22 -10
- package/templates/marketplace/client/LoginCard.tsx +108 -62
- package/templates/marketplace/client/MarketProvider.tsx +16 -2
- package/templates/marketplace/client/MyMarket.tsx +126 -56
- package/templates/marketplace/client/OfferPanel.tsx +222 -112
- package/templates/marketplace/client/ScrollToListingsLink.tsx +29 -0
- package/templates/marketplace/client/SeedOnEmpty.tsx +7 -6
- package/templates/marketplace/client/SellForm.tsx +198 -40
- package/templates/marketplace/client/ThemeToggle.tsx +62 -0
- package/templates/marketplace/client/WatchButton.tsx +19 -8
- package/templates/marketplace/client/market.ts +10 -4
- package/templates/marketplace/components/ui/alert.tsx +66 -0
- package/templates/marketplace/components/ui/badge.tsx +5 -1
- package/templates/marketplace/components/ui/button.tsx +9 -7
- package/templates/marketplace/components/ui/card.tsx +2 -2
- package/templates/marketplace/components/ui/empty.tsx +104 -0
- package/templates/marketplace/components/ui/field.tsx +246 -0
- package/templates/marketplace/components/ui/input.tsx +2 -2
- package/templates/marketplace/components/ui/label.tsx +4 -3
- package/templates/marketplace/components/ui/native-select.tsx +62 -0
- package/templates/marketplace/components/ui/separator.tsx +26 -0
- package/templates/marketplace/components/ui/skeleton.tsx +13 -0
- package/templates/marketplace/components/ui/spinner.tsx +16 -0
- package/templates/marketplace/components/ui/textarea.tsx +2 -2
- package/templates/marketplace/{ui → components/ui}/tokens.css +1 -1
- package/templates/marketplace/components.json +20 -0
- package/templates/marketplace/functions/seedMarket.ts +14 -12
- package/templates/marketplace/lib/catalog.ts +32 -0
- package/templates/marketplace/package.json +10 -9
- package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +76 -8
- package/templates/marketplace/tsconfig.json +13 -2
- package/templates/marketplace/components/ui/select.tsx +0 -39
- package/templates/marketplace/ui/badge.tsx +0 -30
- package/templates/marketplace/ui/button.tsx +0 -49
- package/templates/marketplace/ui/card.tsx +0 -48
- package/templates/marketplace/ui/input.tsx +0 -17
- package/templates/marketplace/ui/label.tsx +0 -18
- package/templates/marketplace/ui/textarea.tsx +0 -17
- package/templates/marketplace/ui/utils.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.358",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
server-rendered
|
|
3
|
+
A polished, two-sided resale marketplace built with
|
|
4
|
+
[Pylon](https://pylonsync.com), with server-rendered product discovery and
|
|
5
|
+
realtime offers.
|
|
5
6
|
|
|
6
7
|
Anyone can list an item; anyone else can make an offer or buy it now; sellers
|
|
7
8
|
watch offers arrive and accept or decline them. Every write reaches each open
|
|
@@ -16,7 +17,8 @@ __RUN_DEV__
|
|
|
16
17
|
Open http://localhost:4321. The grid seeds itself on first load. Open a
|
|
17
18
|
second tab, post something from `/sell` in one, and watch it hit the "just
|
|
18
19
|
listed" ticker in the other with no refresh. Sign in (the `/sell` form prefills
|
|
19
|
-
a demo account) to list, make offers, and watch your
|
|
20
|
+
a demo account) to list, save finds, make offers, and watch your dashboard
|
|
21
|
+
update live.
|
|
20
22
|
|
|
21
23
|
## How it works
|
|
22
24
|
|
|
@@ -24,35 +26,48 @@ a demo account) to list, make offers, and watch your `/me` inbox light up live.
|
|
|
24
26
|
page (`/listing/:slug`) render on the server with real rows (`serverData` +
|
|
25
27
|
React 19 `use()`). View source and the products are *in the HTML*.
|
|
26
28
|
- **Realtime where it matters.** The "just listed" ticker, the live offers on a
|
|
27
|
-
listing, and your `/me`
|
|
29
|
+
listing, and your `/me` dashboard all ride the sync engine: one `db.useQuery` per
|
|
28
30
|
view, no polling. The public surface connects with an anonymous **guest
|
|
29
31
|
session** (read-only); writing (list/offer/buy) requires a real sign-in.
|
|
30
32
|
- **Unspoofable ownership.** `sellerId`/`buyerId` use `field.owner()`, so the
|
|
31
|
-
framework stamps them from the session and rejects forged values
|
|
33
|
+
framework stamps them from the session and rejects forged values. Listings
|
|
32
34
|
and offers can be created with a plain optimistic `db.insert` and still can't
|
|
33
35
|
be spoofed. The heavier logic (accept = mark sold + auto-decline the rest)
|
|
34
|
-
runs in `functions/respondToOffer.ts
|
|
36
|
+
runs in `functions/respondToOffer.ts`, where it enforces "only the seller".
|
|
37
|
+
- **Commerce-ready discovery.** Browse by category, search title, description,
|
|
38
|
+
seller, or category, and sort by recency or price. The logic is pure and
|
|
39
|
+
covered by tests in `tests/example.test.ts`.
|
|
35
40
|
|
|
36
41
|
## Privacy and policies
|
|
37
42
|
|
|
38
43
|
- `Listing` + `Offer` are **public-read** (buyers and sellers both see live
|
|
39
44
|
state); writes are owner-scoped.
|
|
40
|
-
- `Watch` (your saved listings) is **private
|
|
45
|
+
- `Watch` (your saved listings) is **private**. You can only read and write your
|
|
46
|
+
own rows.
|
|
41
47
|
- `User` rows are readable only to signed-in users (for seller/buyer names);
|
|
42
48
|
`passwordHash` is `serverOnly` and never serialized. Auth writes go through
|
|
43
49
|
`/api/auth/password/*`, not the entity API.
|
|
44
50
|
|
|
45
51
|
## Listing photos
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
to
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
Seeded listings include purpose-matched
|
|
54
|
+
[Unsplash](https://unsplash.com/license) photography. New listings collect a
|
|
55
|
+
JPG, PNG, or WebP upload (up to 8 MB), with a direct-link fallback and preview
|
|
56
|
+
before publishing. Uploads use Pylon's direct-to-storage three-step flow
|
|
57
|
+
(`/api/files/init` → signed upload URL → `/api/files/confirm`) so large files
|
|
58
|
+
do not pass through the app server. `imageUrl` stays optional so older rows
|
|
59
|
+
fall back to deterministic category artwork. The generated hero asset is a
|
|
60
|
+
44 KB WebP under `public/images`.
|
|
61
|
+
|
|
62
|
+
## Payments and fulfillment
|
|
63
|
+
|
|
64
|
+
`buyNow` atomically records an accepted offer, marks the listing sold, and
|
|
65
|
+
declines competing bids. It does not transfer money. Add your payment provider
|
|
66
|
+
and shipping or pickup workflow before accepting real transactions.
|
|
52
67
|
|
|
53
68
|
## Rebrand it
|
|
54
69
|
|
|
55
|
-
The brand ("
|
|
70
|
+
The brand ("Reprise") lives in `app/layout.tsx`; the demo catalog +
|
|
56
71
|
seed account are in `functions/seedMarket.ts`. The design tokens are in
|
|
57
72
|
`ui/tokens.css` + `app/globals.css`.
|
|
58
73
|
|
|
@@ -60,7 +75,7 @@ seed account are in `functions/seedMarket.ts`. The design tokens are in
|
|
|
60
75
|
|
|
61
76
|
```
|
|
62
77
|
app.ts User + Listing + Offer + Watch + policies
|
|
63
|
-
app/page.tsx SSR
|
|
78
|
+
app/page.tsx SSR discovery, search, sort, category facets
|
|
64
79
|
app/listing/[id]/page.tsx SSR listing detail (+ generateMetadata)
|
|
65
80
|
app/sell/page.tsx list an item (sign-in gated)
|
|
66
81
|
app/me/page.tsx your listings, offers, watchlist (live)
|
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ErrorBoundaryProps } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Empty,
|
|
6
|
+
EmptyContent,
|
|
7
|
+
EmptyDescription,
|
|
8
|
+
EmptyHeader,
|
|
9
|
+
EmptyTitle,
|
|
10
|
+
} from "@/components/ui/empty";
|
|
3
11
|
|
|
4
12
|
// `app/error.tsx` → the error boundary for this segment. Hydrated + interactive:
|
|
5
13
|
// `reset()` re-attempts the route. The thrown error reaches the client as
|
|
6
14
|
// `{ message, digest }` only — the stack stays in the dev overlay / server logs.
|
|
7
15
|
export default function Error({ error, reset }: ErrorBoundaryProps) {
|
|
8
16
|
return (
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
</
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
<Empty className="mx-auto min-h-[60vh] max-w-3xl border-0 px-6">
|
|
18
|
+
<EmptyHeader>
|
|
19
|
+
<EmptyTitle className="text-2xl">Something went wrong</EmptyTitle>
|
|
20
|
+
<EmptyDescription className="max-w-md">
|
|
21
|
+
Reprise could not load this page. Try again, or return to browse if
|
|
22
|
+
the problem continues.
|
|
23
|
+
</EmptyDescription>
|
|
24
|
+
{error.digest ? (
|
|
25
|
+
<p className="text-xs text-muted-foreground">
|
|
26
|
+
Reference: <code>{error.digest}</code>
|
|
27
|
+
</p>
|
|
28
|
+
) : null}
|
|
29
|
+
</EmptyHeader>
|
|
30
|
+
<EmptyContent>
|
|
31
|
+
<Button type="button" onClick={reset}>
|
|
32
|
+
Try again
|
|
33
|
+
</Button>
|
|
34
|
+
<Button asChild variant="link">
|
|
35
|
+
<a href="/">Back to browse</a>
|
|
36
|
+
</Button>
|
|
37
|
+
</EmptyContent>
|
|
38
|
+
</Empty>
|
|
25
39
|
);
|
|
26
40
|
}
|
|
@@ -1,39 +1,88 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
@import "tw-animate-css";
|
|
3
|
-
@import "../ui/tokens.css";
|
|
3
|
+
@import "../components/ui/tokens.css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
|
4
6
|
|
|
5
7
|
/* Tailwind v4 scans these for class names (relative to this file). */
|
|
6
8
|
@source "./";
|
|
7
9
|
@source "../client";
|
|
10
|
+
@source "../components";
|
|
8
11
|
@source "../../_shared/src";
|
|
9
12
|
|
|
10
13
|
@theme {
|
|
11
|
-
--font-sans:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
--color-
|
|
16
|
-
--color-
|
|
17
|
-
--color-card
|
|
18
|
-
--color-
|
|
19
|
-
--color-popover
|
|
20
|
-
--color-
|
|
21
|
-
--color-primary
|
|
22
|
-
--color-
|
|
23
|
-
--color-secondary
|
|
24
|
-
--color-
|
|
25
|
-
--color-muted
|
|
26
|
-
--color-
|
|
27
|
-
--color-accent
|
|
14
|
+
--font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
15
|
+
sans-serif;
|
|
16
|
+
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
17
|
+
|
|
18
|
+
--color-background: oklch(0.975 0.004 85);
|
|
19
|
+
--color-foreground: oklch(0.17 0.004 85);
|
|
20
|
+
--color-card: oklch(0.995 0.002 85);
|
|
21
|
+
--color-card-foreground: oklch(0.17 0.004 85);
|
|
22
|
+
--color-popover: oklch(0.995 0.002 85);
|
|
23
|
+
--color-popover-foreground: oklch(0.17 0.004 85);
|
|
24
|
+
--color-primary: oklch(0.19 0.004 85);
|
|
25
|
+
--color-primary-foreground: oklch(0.98 0.002 85);
|
|
26
|
+
--color-secondary: oklch(0.94 0.006 85);
|
|
27
|
+
--color-secondary-foreground: oklch(0.21 0.004 85);
|
|
28
|
+
--color-muted: oklch(0.945 0.006 85);
|
|
29
|
+
--color-muted-foreground: oklch(0.48 0.008 85);
|
|
30
|
+
--color-accent: oklch(0.93 0.009 85);
|
|
31
|
+
--color-accent-foreground: oklch(0.21 0.004 85);
|
|
28
32
|
--color-destructive: oklch(0.577 0.245 27.325);
|
|
29
33
|
--color-destructive-foreground: oklch(0.985 0 0);
|
|
30
|
-
--color-
|
|
31
|
-
--color-
|
|
32
|
-
--color-
|
|
34
|
+
--color-success: oklch(0.62 0.14 153);
|
|
35
|
+
--color-success-foreground: oklch(0.38 0.1 153);
|
|
36
|
+
--color-warning: oklch(0.72 0.14 75);
|
|
37
|
+
--color-warning-foreground: oklch(0.42 0.1 65);
|
|
38
|
+
--color-border: oklch(0.89 0.007 85);
|
|
39
|
+
--color-input: oklch(0.86 0.008 85);
|
|
40
|
+
--color-ring: oklch(0.53 0.015 85);
|
|
41
|
+
|
|
42
|
+
--radius-lg: 1rem;
|
|
43
|
+
--radius-md: 0.625rem;
|
|
44
|
+
--radius-sm: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:root {
|
|
48
|
+
color-scheme: light;
|
|
49
|
+
--shadow-border:
|
|
50
|
+
0 0 0 1px rgb(0 0 0 / 0.06),
|
|
51
|
+
0 1px 2px -1px rgb(0 0 0 / 0.06),
|
|
52
|
+
0 8px 24px -16px rgb(41 37 33 / 0.18);
|
|
53
|
+
--shadow-border-hover:
|
|
54
|
+
0 0 0 1px rgb(0 0 0 / 0.09),
|
|
55
|
+
0 2px 4px -2px rgb(0 0 0 / 0.08),
|
|
56
|
+
0 16px 32px -18px rgb(41 37 33 / 0.24);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:root[data-theme="light"] {
|
|
60
|
+
color-scheme: light;
|
|
61
|
+
}
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
--
|
|
63
|
+
:root[data-theme="dark"] {
|
|
64
|
+
color-scheme: dark;
|
|
65
|
+
--color-background: oklch(0.15 0.004 85);
|
|
66
|
+
--color-foreground: oklch(0.94 0.004 85);
|
|
67
|
+
--color-card: oklch(0.19 0.005 85);
|
|
68
|
+
--color-card-foreground: oklch(0.94 0.004 85);
|
|
69
|
+
--color-popover: oklch(0.19 0.005 85);
|
|
70
|
+
--color-popover-foreground: oklch(0.94 0.004 85);
|
|
71
|
+
--color-primary: oklch(0.93 0.004 85);
|
|
72
|
+
--color-primary-foreground: oklch(0.17 0.004 85);
|
|
73
|
+
--color-secondary: oklch(0.24 0.006 85);
|
|
74
|
+
--color-secondary-foreground: oklch(0.94 0.004 85);
|
|
75
|
+
--color-muted: oklch(0.23 0.006 85);
|
|
76
|
+
--color-muted-foreground: oklch(0.7 0.007 85);
|
|
77
|
+
--color-accent: oklch(0.25 0.008 85);
|
|
78
|
+
--color-accent-foreground: oklch(0.94 0.004 85);
|
|
79
|
+
--color-border: oklch(0.3 0.007 85);
|
|
80
|
+
--color-input: oklch(0.34 0.008 85);
|
|
81
|
+
--color-ring: oklch(0.68 0.01 85);
|
|
82
|
+
--color-success-foreground: oklch(0.82 0.12 153);
|
|
83
|
+
--color-warning-foreground: oklch(0.84 0.12 75);
|
|
84
|
+
--shadow-border: 0 0 0 1px rgb(255 255 255 / 0.08);
|
|
85
|
+
--shadow-border-hover: 0 0 0 1px rgb(255 255 255 / 0.14);
|
|
37
86
|
}
|
|
38
87
|
|
|
39
88
|
html,
|
|
@@ -41,6 +90,70 @@ body {
|
|
|
41
90
|
height: 100%;
|
|
42
91
|
}
|
|
43
92
|
|
|
93
|
+
html {
|
|
94
|
+
-webkit-tap-highlight-color: rgb(23 23 23 / 0.08);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
h1,
|
|
98
|
+
h2,
|
|
99
|
+
h3 {
|
|
100
|
+
scroll-margin-top: 6rem;
|
|
101
|
+
text-wrap: balance;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
button,
|
|
105
|
+
a,
|
|
106
|
+
input,
|
|
107
|
+
select,
|
|
108
|
+
textarea {
|
|
109
|
+
touch-action: manipulation;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
:where(a, button, input, select, textarea):focus-visible {
|
|
113
|
+
outline: 2px solid var(--color-ring);
|
|
114
|
+
outline-offset: 2px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
p,
|
|
118
|
+
li {
|
|
119
|
+
text-wrap: pretty;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.hide-scrollbar {
|
|
123
|
+
scrollbar-width: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.hide-scrollbar::-webkit-scrollbar {
|
|
127
|
+
display: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@media (prefers-color-scheme: dark) {
|
|
131
|
+
:root:not([data-theme]) {
|
|
132
|
+
color-scheme: dark;
|
|
133
|
+
--color-background: oklch(0.15 0.004 85);
|
|
134
|
+
--color-foreground: oklch(0.94 0.004 85);
|
|
135
|
+
--color-card: oklch(0.19 0.005 85);
|
|
136
|
+
--color-card-foreground: oklch(0.94 0.004 85);
|
|
137
|
+
--color-popover: oklch(0.19 0.005 85);
|
|
138
|
+
--color-popover-foreground: oklch(0.94 0.004 85);
|
|
139
|
+
--color-primary: oklch(0.93 0.004 85);
|
|
140
|
+
--color-primary-foreground: oklch(0.17 0.004 85);
|
|
141
|
+
--color-secondary: oklch(0.24 0.006 85);
|
|
142
|
+
--color-secondary-foreground: oklch(0.94 0.004 85);
|
|
143
|
+
--color-muted: oklch(0.23 0.006 85);
|
|
144
|
+
--color-muted-foreground: oklch(0.7 0.007 85);
|
|
145
|
+
--color-accent: oklch(0.25 0.008 85);
|
|
146
|
+
--color-accent-foreground: oklch(0.94 0.004 85);
|
|
147
|
+
--color-border: oklch(0.3 0.007 85);
|
|
148
|
+
--color-input: oklch(0.34 0.008 85);
|
|
149
|
+
--color-ring: oklch(0.68 0.01 85);
|
|
150
|
+
--color-success-foreground: oklch(0.82 0.12 153);
|
|
151
|
+
--color-warning-foreground: oklch(0.84 0.12 75);
|
|
152
|
+
--shadow-border: 0 0 0 1px rgb(255 255 255 / 0.08);
|
|
153
|
+
--shadow-border-hover: 0 0 0 1px rgb(255 255 255 / 0.14);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
44
157
|
/* Live "just listed" ticker. Items are doubled in the markup, so translating
|
|
45
158
|
the track by -50% loops seamlessly. */
|
|
46
159
|
@keyframes market-marquee {
|
|
@@ -61,4 +174,7 @@ body {
|
|
|
61
174
|
.market-marquee {
|
|
62
175
|
animation: none;
|
|
63
176
|
}
|
|
177
|
+
.animate-pulse {
|
|
178
|
+
animation: none;
|
|
179
|
+
}
|
|
64
180
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Separator } from "@/components/ui/separator";
|
|
3
5
|
import { AuthNav } from "../client/AuthNav";
|
|
6
|
+
import { ThemeToggle } from "../client/ThemeToggle";
|
|
4
7
|
|
|
5
8
|
// Root layout. Server-rendered shell; Pylon's SSR head adapter injects the
|
|
6
9
|
// compiled Tailwind <link> from app/globals.css automatically.
|
|
@@ -14,45 +17,71 @@ export default function RootLayout({
|
|
|
14
17
|
<head>
|
|
15
18
|
<meta charSet="utf-8" />
|
|
16
19
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
-
<
|
|
18
|
-
|
|
20
|
+
<script
|
|
21
|
+
dangerouslySetInnerHTML={{
|
|
22
|
+
__html:
|
|
23
|
+
"try{var t=localStorage.getItem('reprise:theme');document.documentElement.dataset.theme=t==='light'||t==='dark'?t:matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}catch(e){}",
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
<meta
|
|
27
|
+
name="theme-color"
|
|
28
|
+
content="#f8f6f2"
|
|
29
|
+
media="(prefers-color-scheme: light)"
|
|
30
|
+
/>
|
|
31
|
+
<meta
|
|
32
|
+
name="theme-color"
|
|
33
|
+
content="#181817"
|
|
34
|
+
media="(prefers-color-scheme: dark)"
|
|
35
|
+
/>
|
|
36
|
+
<link rel="preconnect" href="https://images.unsplash.com" />
|
|
19
37
|
</head>
|
|
20
38
|
<body className="min-h-screen bg-background text-foreground antialiased">
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
<a
|
|
40
|
+
href="#main-content"
|
|
41
|
+
className="fixed left-4 top-4 z-50 -translate-y-20 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background shadow-lg transition-transform focus:translate-y-0"
|
|
42
|
+
>
|
|
43
|
+
Skip to content
|
|
44
|
+
</a>
|
|
45
|
+
<header className="sticky top-0 z-20 h-16 border-b bg-background/85 backdrop-blur-xl">
|
|
46
|
+
<div className="mx-auto flex h-full max-w-6xl items-center justify-between gap-3 px-5">
|
|
47
|
+
<Link
|
|
48
|
+
href="/"
|
|
49
|
+
translate="no"
|
|
50
|
+
className="flex min-h-11 items-center gap-2.5 text-sm font-semibold tracking-[-0.01em]"
|
|
51
|
+
>
|
|
52
|
+
<span className="grid size-8 place-items-center rounded-lg bg-foreground text-sm font-semibold text-background shadow-sm">
|
|
53
|
+
R
|
|
26
54
|
</span>
|
|
27
|
-
|
|
55
|
+
Reprise
|
|
28
56
|
</Link>
|
|
29
|
-
<nav
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
>
|
|
34
|
-
Browse
|
|
35
|
-
</
|
|
36
|
-
<
|
|
37
|
-
href="/sell"
|
|
38
|
-
|
|
39
|
-
>
|
|
40
|
-
|
|
41
|
-
</
|
|
42
|
-
<
|
|
43
|
-
href="/me"
|
|
44
|
-
className="rounded-md px-3 py-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
45
|
-
>
|
|
46
|
-
My Market
|
|
47
|
-
</Link>
|
|
57
|
+
<nav
|
|
58
|
+
aria-label="Primary navigation"
|
|
59
|
+
className="flex items-center gap-0.5 text-sm sm:gap-1"
|
|
60
|
+
>
|
|
61
|
+
<Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex">
|
|
62
|
+
<Link href="/">Browse</Link>
|
|
63
|
+
</Button>
|
|
64
|
+
<Button asChild variant="ghost" size="sm">
|
|
65
|
+
<Link href="/sell">Sell</Link>
|
|
66
|
+
</Button>
|
|
67
|
+
<Button asChild variant="ghost" size="sm" className="hidden md:inline-flex">
|
|
68
|
+
<Link href="/me">Dashboard</Link>
|
|
69
|
+
</Button>
|
|
70
|
+
<ThemeToggle />
|
|
48
71
|
<AuthNav />
|
|
49
72
|
</nav>
|
|
50
73
|
</div>
|
|
51
74
|
</header>
|
|
52
|
-
<main
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
<main
|
|
76
|
+
id="main-content"
|
|
77
|
+
className="mx-auto min-h-[calc(100dvh-8rem)] max-w-6xl px-5 py-6 sm:py-8"
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</main>
|
|
81
|
+
<Separator />
|
|
82
|
+
<footer className="px-5 py-7 text-center text-xs text-muted-foreground">
|
|
83
|
+
Reprise, built with Pylon. Server-rendered listings and realtime offers
|
|
84
|
+
from one binary.
|
|
56
85
|
</footer>
|
|
57
86
|
</body>
|
|
58
87
|
</html>
|