@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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/templates/marketplace/README.md +29 -14
  3. package/templates/marketplace/app/error.tsx +30 -16
  4. package/templates/marketplace/app/globals.css +140 -24
  5. package/templates/marketplace/app/layout.tsx +60 -31
  6. package/templates/marketplace/app/listing/[id]/page.tsx +160 -60
  7. package/templates/marketplace/app/me/page.tsx +3 -3
  8. package/templates/marketplace/app/not-found.tsx +19 -10
  9. package/templates/marketplace/app/page.tsx +287 -97
  10. package/templates/marketplace/app/sell/page.tsx +11 -9
  11. package/templates/marketplace/app.ts +5 -4
  12. package/templates/marketplace/client/AuthNav.tsx +4 -8
  13. package/templates/marketplace/client/LiveListingStatus.tsx +60 -0
  14. package/templates/marketplace/client/LiveTicker.tsx +22 -10
  15. package/templates/marketplace/client/LoginCard.tsx +108 -62
  16. package/templates/marketplace/client/MarketProvider.tsx +16 -2
  17. package/templates/marketplace/client/MyMarket.tsx +126 -56
  18. package/templates/marketplace/client/OfferPanel.tsx +222 -112
  19. package/templates/marketplace/client/ScrollToListingsLink.tsx +29 -0
  20. package/templates/marketplace/client/SeedOnEmpty.tsx +7 -6
  21. package/templates/marketplace/client/SellForm.tsx +198 -40
  22. package/templates/marketplace/client/ThemeToggle.tsx +62 -0
  23. package/templates/marketplace/client/WatchButton.tsx +19 -8
  24. package/templates/marketplace/client/market.ts +10 -4
  25. package/templates/marketplace/components/ui/alert.tsx +66 -0
  26. package/templates/marketplace/components/ui/badge.tsx +5 -1
  27. package/templates/marketplace/components/ui/button.tsx +9 -7
  28. package/templates/marketplace/components/ui/card.tsx +2 -2
  29. package/templates/marketplace/components/ui/empty.tsx +104 -0
  30. package/templates/marketplace/components/ui/field.tsx +246 -0
  31. package/templates/marketplace/components/ui/input.tsx +2 -2
  32. package/templates/marketplace/components/ui/label.tsx +4 -3
  33. package/templates/marketplace/components/ui/native-select.tsx +62 -0
  34. package/templates/marketplace/components/ui/separator.tsx +26 -0
  35. package/templates/marketplace/components/ui/skeleton.tsx +13 -0
  36. package/templates/marketplace/components/ui/spinner.tsx +16 -0
  37. package/templates/marketplace/components/ui/textarea.tsx +2 -2
  38. package/templates/marketplace/{ui → components/ui}/tokens.css +1 -1
  39. package/templates/marketplace/components.json +20 -0
  40. package/templates/marketplace/functions/seedMarket.ts +14 -12
  41. package/templates/marketplace/lib/catalog.ts +32 -0
  42. package/templates/marketplace/package.json +10 -9
  43. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
  44. package/templates/marketplace/tests/example.test.ts +76 -8
  45. package/templates/marketplace/tsconfig.json +13 -2
  46. package/templates/marketplace/components/ui/select.tsx +0 -39
  47. package/templates/marketplace/ui/badge.tsx +0 -30
  48. package/templates/marketplace/ui/button.tsx +0 -49
  49. package/templates/marketplace/ui/card.tsx +0 -48
  50. package/templates/marketplace/ui/input.tsx +0 -17
  51. package/templates/marketplace/ui/label.tsx +0 -18
  52. package/templates/marketplace/ui/textarea.tsx +0 -17
  53. package/templates/marketplace/ui/utils.ts +0 -6
@@ -1,21 +1,34 @@
1
1
  import React, { Suspense, use } from "react";
2
2
  import {
3
3
  Link,
4
+ useRouteData,
4
5
  type GenerateMetadata,
5
6
  type Metadata,
6
7
  type PageProps,
7
8
  type ServerData,
8
9
  type SsrResponse,
9
10
  } from "@pylonsync/react";
10
- import { Badge } from "../../../ui/badge";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Card } from "@/components/ui/card";
13
+ import {
14
+ Empty,
15
+ EmptyContent,
16
+ EmptyDescription,
17
+ EmptyHeader,
18
+ EmptyTitle,
19
+ } from "@/components/ui/empty";
20
+ import { Separator } from "@/components/ui/separator";
21
+ import { Skeleton } from "@/components/ui/skeleton";
11
22
  import { OfferPanel } from "../../../client/OfferPanel";
12
23
  import { CategoryIcon } from "../../_components/CategoryIcon";
13
24
  import { WatchButton } from "../../../client/WatchButton";
25
+ import { LiveListingStatus } from "../../../client/LiveListingStatus";
14
26
  import {
15
27
  gradient,
16
28
  money,
17
29
  conditionLabel,
18
30
  type Listing,
31
+ type Offer,
19
32
  } from "../../../client/market";
20
33
 
21
34
  // Resolve a listing from the URL segment, which is its slug
@@ -47,12 +60,12 @@ export const generateMetadata: GenerateMetadata = async ({
47
60
  serverData,
48
61
  }): Promise<Metadata> => {
49
62
  const l = await resolveListing(serverData, params.id);
50
- if (!l) return { title: "Listing not found · Pylon Market" };
63
+ if (!l) return { title: "Listing not found | Reprise" };
51
64
  return {
52
- title: `${l.title} ${money(l.price)} · Pylon Market`,
65
+ title: `${l.title} | ${money(l.price)} | Reprise`,
53
66
  description:
54
67
  l.description?.slice(0, 155) ||
55
- `${l.title} for sale on Pylon Market (${conditionLabel(l.condition)}).`,
68
+ `${l.title} for sale on Reprise (${conditionLabel(l.condition)}).`,
56
69
  };
57
70
  };
58
71
 
@@ -65,88 +78,170 @@ function Detail({
65
78
  response: SsrResponse;
66
79
  id: string;
67
80
  }) {
68
- const listing = use(resolveListing(serverData, id));
81
+ // Listing cards seed this route with the row they already rendered.
82
+ // useRouteData paints that real content immediately, resets scroll at the
83
+ // start of navigation, then upgrades it to the authoritative server row in
84
+ // place. Direct loads still suspend for SSR as normal.
85
+ const listing = useRouteData<Listing | null>(
86
+ () => resolveListing(serverData, id),
87
+ [serverData, id],
88
+ );
69
89
 
70
90
  if (!listing) {
71
91
  response.setStatus(404);
72
92
  return (
73
- <div className="rounded-xl border border-dashed p-12 text-center">
74
- <p className="font-medium">This listing is gone.</p>
75
- <Link href="/" className="mt-2 inline-block text-sm underline">
76
- Back to the market
77
- </Link>
78
- </div>
93
+ <Empty className="mx-auto min-h-[60vh] max-w-xl border-0">
94
+ <EmptyHeader>
95
+ <p className="text-sm font-medium text-muted-foreground">Unavailable</p>
96
+ <EmptyTitle className="text-3xl tracking-[-0.03em]">
97
+ This listing is no longer available
98
+ </EmptyTitle>
99
+ <EmptyDescription className="max-w-md">
100
+ It may have sold or been removed by the seller. Browse the latest
101
+ finds to discover something similar.
102
+ </EmptyDescription>
103
+ </EmptyHeader>
104
+ <EmptyContent>
105
+ <Button asChild size="lg">
106
+ <Link href="/">Browse latest finds</Link>
107
+ </Button>
108
+ </EmptyContent>
109
+ </Empty>
79
110
  );
80
111
  }
81
112
 
82
113
  return (
83
- <div className="space-y-6">
114
+ <div className="flex flex-col gap-6 pb-10">
84
115
  <Link
85
116
  href="/"
86
- className="text-sm text-muted-foreground hover:text-foreground"
117
+ className="inline-flex min-h-10 items-center text-sm text-muted-foreground transition-colors hover:text-foreground"
87
118
  >
88
- ← Back to the market
119
+ ← Back to browse
89
120
  </Link>
90
121
 
91
- <div className="grid gap-8 md:grid-cols-2">
92
- <div
93
- className="relative flex aspect-square items-center justify-center rounded-2xl text-white/90"
94
- style={{ background: gradient(listing.seed || listing.id) }}
95
- >
96
- <CategoryIcon category={listing.category} className="size-28" />
122
+ <div className="grid gap-8 lg:grid-cols-[minmax(0,1.16fr)_minmax(360px,.84fr)] lg:gap-12">
123
+ <div className="relative aspect-[4/5] overflow-hidden rounded-[24px] bg-muted shadow-[var(--shadow-border)] sm:aspect-[6/5] lg:aspect-[4/5]">
124
+ {listing.imageUrl ? (
125
+ <img
126
+ src={listing.imageUrl}
127
+ alt={listing.title}
128
+ width="1200"
129
+ height="1500"
130
+ fetchPriority="high"
131
+ decoding="async"
132
+ className="h-full w-full object-cover outline outline-1 -outline-offset-1 outline-border"
133
+ />
134
+ ) : (
135
+ <div
136
+ className="flex h-full w-full items-center justify-center text-white/90"
137
+ style={{ background: gradient(listing.seed || listing.id) }}
138
+ >
139
+ <CategoryIcon category={listing.category} className="size-28" />
140
+ </div>
141
+ )}
97
142
  <WatchButton
98
143
  listingId={listing.id}
99
144
  listingTitle={listing.title}
100
- className="absolute right-3 top-3"
145
+ className="absolute right-4 top-4"
146
+ />
147
+ <LiveListingStatus
148
+ listingId={listing.id}
149
+ initialStatus={listing.status}
150
+ mode="overlay"
101
151
  />
102
- {listing.status === "sold" ? (
103
- <span className="absolute inset-0 grid place-items-center rounded-2xl bg-black/55 text-2xl font-bold uppercase tracking-wide">
104
- Sold
105
- </span>
106
- ) : null}
107
152
  </div>
108
153
 
109
- <div className="flex flex-col gap-4">
110
- <div className="space-y-1">
111
- <div className="flex items-center gap-2 text-xs uppercase tracking-wider text-muted-foreground">
112
- <span>{listing.category}</span>
113
- <span>·</span>
114
- <Badge variant="outline" className="text-[10px]">
115
- {conditionLabel(listing.condition)}
116
- </Badge>
117
- </div>
118
- <h1 className="text-2xl font-semibold tracking-tight">
154
+ <div className="flex min-w-0 flex-col">
155
+ <div>
156
+ <p className="text-sm capitalize text-muted-foreground">
157
+ {listing.category} / {conditionLabel(listing.condition)}
158
+ </p>
159
+ <h1 className="mt-2 text-balance text-3xl font-semibold leading-tight tracking-[-0.035em] sm:text-4xl">
119
160
  {listing.title}
120
161
  </h1>
121
- <p className="text-3xl font-semibold tabular-nums">{money(listing.price)}</p>
122
- <p className="text-sm text-muted-foreground">
123
- Listed by <span className="font-medium">{listing.sellerName}</span>
162
+ <p className="mt-3 text-4xl font-semibold tracking-[-0.03em] tabular-nums">
163
+ {money(listing.price)}
164
+ </p>
165
+ <p className="mt-2 text-sm text-muted-foreground">
166
+ Listed by{" "}
167
+ <span className="font-medium text-foreground">{listing.sellerName}</span>
124
168
  </p>
125
169
  </div>
126
170
 
127
- {listing.description ? (
128
- <p className="whitespace-pre-wrap text-sm leading-relaxed text-foreground/80">
129
- {listing.description}
171
+ <Separator className="my-6" />
172
+
173
+ <section>
174
+ <h2 className="text-sm font-medium">About this item</h2>
175
+ <p className="mt-2 whitespace-pre-wrap text-pretty text-sm leading-6 text-muted-foreground">
176
+ {listing.description || "The seller has not added a description yet."}
130
177
  </p>
131
- ) : null}
132
-
133
- {/* Realtime client island: live offers, accept/decline. */}
134
- <div className="mt-2">
135
- <OfferPanel
136
- listingId={listing.id}
137
- sellerId={listing.sellerId}
138
- sellerName={listing.sellerName}
139
- title={listing.title}
140
- price={listing.price}
141
- status={listing.status}
142
- />
178
+ </section>
179
+
180
+ <Card className="my-6 grid grid-cols-2 gap-px overflow-hidden rounded-xl bg-border">
181
+ <div className="bg-card p-4">
182
+ <p className="text-xs text-muted-foreground">Condition</p>
183
+ <p className="mt-1 text-sm font-medium">{conditionLabel(listing.condition)}</p>
184
+ </div>
185
+ <div className="bg-card p-4">
186
+ <p className="text-xs text-muted-foreground">Offer status</p>
187
+ <p className="mt-1 text-sm font-medium">
188
+ <LiveListingStatus
189
+ listingId={listing.id}
190
+ initialStatus={listing.status}
191
+ />
192
+ </p>
193
+ </div>
194
+ </Card>
195
+
196
+ <div className="mt-6">
197
+ <Suspense fallback={<ListingOffers listing={listing} />}>
198
+ <LoadedListingOffers serverData={serverData} listing={listing} />
199
+ </Suspense>
143
200
  </div>
201
+
202
+ <p className="mt-4 text-pretty text-xs leading-5 text-muted-foreground">
203
+ Reprise keeps offers and listing status in sync. Confirm payment and
204
+ delivery details with the seller before completing a transaction.
205
+ </p>
144
206
  </div>
145
207
  </div>
146
208
  </div>
147
209
  );
148
210
  }
149
211
 
212
+ function ListingOffers({
213
+ listing,
214
+ initialOffers,
215
+ }: {
216
+ listing: Listing;
217
+ initialOffers?: Offer[];
218
+ }) {
219
+ return (
220
+ <OfferPanel
221
+ listingId={listing.id}
222
+ sellerId={listing.sellerId}
223
+ sellerName={listing.sellerName}
224
+ title={listing.title}
225
+ price={listing.price}
226
+ status={listing.status}
227
+ initialOffers={initialOffers}
228
+ />
229
+ );
230
+ }
231
+
232
+ function LoadedListingOffers({
233
+ serverData,
234
+ listing,
235
+ }: {
236
+ serverData: ServerData;
237
+ listing: Listing;
238
+ }) {
239
+ const initialOffers = use(
240
+ serverData.query<Offer>("Offer", { listingId: listing.id }),
241
+ );
242
+ return <ListingOffers listing={listing} initialOffers={initialOffers} />;
243
+ }
244
+
150
245
  export default function ListingPage({
151
246
  params,
152
247
  serverData,
@@ -156,16 +251,21 @@ export default function ListingPage({
156
251
  <Suspense
157
252
  fallback={
158
253
  <div className="grid gap-8 md:grid-cols-2">
159
- <div className="aspect-square animate-pulse rounded-2xl bg-muted" />
160
- <div className="space-y-3">
161
- <div className="h-6 w-2/3 animate-pulse rounded bg-muted" />
162
- <div className="h-8 w-1/3 animate-pulse rounded bg-muted" />
163
- <div className="h-24 animate-pulse rounded bg-muted" />
254
+ <Skeleton className="aspect-[4/5] rounded-[24px]" />
255
+ <div className="flex flex-col gap-3">
256
+ <Skeleton className="h-6 w-2/3" />
257
+ <Skeleton className="h-8 w-1/3" />
258
+ <Skeleton className="h-24" />
164
259
  </div>
165
260
  </div>
166
261
  }
167
262
  >
168
- <Detail serverData={serverData} response={response} id={params.id} />
263
+ <Detail
264
+ key={params.id}
265
+ serverData={serverData}
266
+ response={response}
267
+ id={params.id}
268
+ />
169
269
  </Suspense>
170
270
  );
171
271
  }
@@ -3,9 +3,9 @@ import { type Metadata } from "@pylonsync/react";
3
3
  import { MyMarket } from "../../client/MyMarket";
4
4
 
5
5
  export const metadata: Metadata = {
6
- title: "My Market · Pylon Market",
7
- description: "Your listings and offers.",
8
- robots: "noindex", // personal dashboard keep it out of search
6
+ title: "Dashboard | Reprise",
7
+ description: "Your listings, saved finds, and offers.",
8
+ robots: "noindex", // personal dashboard; keep it out of search
9
9
  };
10
10
 
11
11
  // Fully interactive dashboard (three live queries scoped to you), so the
@@ -1,20 +1,29 @@
1
1
  import React from "react";
2
2
  import { Link, type NotFoundProps } 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/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when a
5
13
  // page calls `response.notFound()` — e.g. a listing slug that doesn't resolve).
6
14
  // Hydrated, so the link is a client nav.
7
15
  export default function NotFound(_props: NotFoundProps) {
8
16
  return (
9
- <div className="mx-auto flex min-h-[60vh] max-w-3xl flex-col items-center justify-center px-6 text-center">
10
- <h1 className="text-3xl font-semibold tracking-tight">404</h1>
11
- <p className="mt-2 text-muted-foreground">We couldn&apos;t find that listing.</p>
12
- <Link
13
- href="/"
14
- className="mt-6 inline-flex h-10 items-center rounded-md bg-foreground px-5 text-sm font-medium text-background transition hover:opacity-90"
15
- >
16
- Back to browse
17
- </Link>
18
- </div>
17
+ <Empty className="mx-auto min-h-[60vh] max-w-3xl border-0 px-6">
18
+ <EmptyHeader>
19
+ <EmptyTitle className="text-3xl">404</EmptyTitle>
20
+ <EmptyDescription>We couldn&apos;t find that page.</EmptyDescription>
21
+ </EmptyHeader>
22
+ <EmptyContent>
23
+ <Button asChild>
24
+ <Link href="/">Back to browse</Link>
25
+ </Button>
26
+ </EmptyContent>
27
+ </Empty>
19
28
  );
20
29
  }