@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
@@ -0,0 +1,104 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="empty"
9
+ className={cn(
10
+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="empty-header"
22
+ className={cn(
23
+ "flex max-w-sm flex-col items-center gap-2 text-center",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ const emptyMediaVariants = cva(
32
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
33
+ {
34
+ variants: {
35
+ variant: {
36
+ default: "bg-transparent",
37
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ variant: "default",
42
+ },
43
+ }
44
+ )
45
+
46
+ function EmptyMedia({
47
+ className,
48
+ variant = "default",
49
+ ...props
50
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
51
+ return (
52
+ <div
53
+ data-slot="empty-icon"
54
+ data-variant={variant}
55
+ className={cn(emptyMediaVariants({ variant, className }))}
56
+ {...props}
57
+ />
58
+ )
59
+ }
60
+
61
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
62
+ return (
63
+ <div
64
+ data-slot="empty-title"
65
+ className={cn("text-lg font-medium tracking-tight", className)}
66
+ {...props}
67
+ />
68
+ )
69
+ }
70
+
71
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
72
+ return (
73
+ <div
74
+ data-slot="empty-description"
75
+ className={cn(
76
+ "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="empty-content"
88
+ className={cn(
89
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
90
+ className
91
+ )}
92
+ {...props}
93
+ />
94
+ )
95
+ }
96
+
97
+ export {
98
+ Empty,
99
+ EmptyHeader,
100
+ EmptyTitle,
101
+ EmptyDescription,
102
+ EmptyContent,
103
+ EmptyMedia,
104
+ }
@@ -0,0 +1,246 @@
1
+ import { useMemo } from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { Label } from "@/components/ui/label"
6
+ import { Separator } from "@/components/ui/separator"
7
+
8
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9
+ return (
10
+ <fieldset
11
+ data-slot="field-set"
12
+ className={cn(
13
+ "flex flex-col gap-6",
14
+ "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function FieldLegend({
23
+ className,
24
+ variant = "legend",
25
+ ...props
26
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27
+ return (
28
+ <legend
29
+ data-slot="field-legend"
30
+ data-variant={variant}
31
+ className={cn(
32
+ "mb-3 font-medium",
33
+ "data-[variant=legend]:text-base",
34
+ "data-[variant=label]:text-sm",
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43
+ return (
44
+ <div
45
+ data-slot="field-group"
46
+ className={cn(
47
+ "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ const fieldVariants = cva(
56
+ "group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
57
+ {
58
+ variants: {
59
+ orientation: {
60
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
61
+ horizontal: [
62
+ "flex-row items-center",
63
+ "[&>[data-slot=field-label]]:flex-auto",
64
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
65
+ ],
66
+ responsive: [
67
+ "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
68
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70
+ ],
71
+ },
72
+ },
73
+ defaultVariants: {
74
+ orientation: "vertical",
75
+ },
76
+ }
77
+ )
78
+
79
+ function Field({
80
+ className,
81
+ orientation = "vertical",
82
+ ...props
83
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84
+ return (
85
+ <div
86
+ role="group"
87
+ data-slot="field"
88
+ data-orientation={orientation}
89
+ className={cn(fieldVariants({ orientation }), className)}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96
+ return (
97
+ <div
98
+ data-slot="field-content"
99
+ className={cn(
100
+ "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
101
+ className
102
+ )}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function FieldLabel({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof Label>) {
112
+ return (
113
+ <Label
114
+ data-slot="field-label"
115
+ className={cn(
116
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
117
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
118
+ "has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
119
+ className
120
+ )}
121
+ {...props}
122
+ />
123
+ )
124
+ }
125
+
126
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
127
+ return (
128
+ <div
129
+ data-slot="field-label"
130
+ className={cn(
131
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
132
+ className
133
+ )}
134
+ {...props}
135
+ />
136
+ )
137
+ }
138
+
139
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
140
+ return (
141
+ <p
142
+ data-slot="field-description"
143
+ className={cn(
144
+ "text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
145
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
146
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ function FieldSeparator({
155
+ children,
156
+ className,
157
+ ...props
158
+ }: React.ComponentProps<"div"> & {
159
+ children?: React.ReactNode
160
+ }) {
161
+ return (
162
+ <div
163
+ data-slot="field-separator"
164
+ data-content={!!children}
165
+ className={cn(
166
+ "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
167
+ className
168
+ )}
169
+ {...props}
170
+ >
171
+ <Separator className="absolute inset-0 top-1/2" />
172
+ {children && (
173
+ <span
174
+ className="relative mx-auto block w-fit bg-card px-2 text-muted-foreground"
175
+ data-slot="field-separator-content"
176
+ >
177
+ {children}
178
+ </span>
179
+ )}
180
+ </div>
181
+ )
182
+ }
183
+
184
+ function FieldError({
185
+ className,
186
+ children,
187
+ errors,
188
+ ...props
189
+ }: React.ComponentProps<"div"> & {
190
+ errors?: Array<{ message?: string } | undefined>
191
+ }) {
192
+ const content = useMemo(() => {
193
+ if (children) {
194
+ return children
195
+ }
196
+
197
+ if (!errors?.length) {
198
+ return null
199
+ }
200
+
201
+ const uniqueErrors = [
202
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
203
+ ]
204
+
205
+ if (uniqueErrors?.length == 1) {
206
+ return uniqueErrors[0]?.message
207
+ }
208
+
209
+ return (
210
+ <ul className="ml-4 flex list-disc flex-col gap-1">
211
+ {uniqueErrors.map(
212
+ (error, index) =>
213
+ error?.message && <li key={index}>{error.message}</li>
214
+ )}
215
+ </ul>
216
+ )
217
+ }, [children, errors])
218
+
219
+ if (!content) {
220
+ return null
221
+ }
222
+
223
+ return (
224
+ <div
225
+ role="alert"
226
+ data-slot="field-error"
227
+ className={cn("text-sm font-normal text-destructive", className)}
228
+ {...props}
229
+ >
230
+ {content}
231
+ </div>
232
+ )
233
+ }
234
+
235
+ export {
236
+ Field,
237
+ FieldLabel,
238
+ FieldDescription,
239
+ FieldError,
240
+ FieldGroup,
241
+ FieldLegend,
242
+ FieldSeparator,
243
+ FieldSet,
244
+ FieldContent,
245
+ FieldTitle,
246
+ }
@@ -12,9 +12,9 @@ function Input({
12
12
  type={type}
13
13
  data-slot="input"
14
14
  className={cn(
15
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors",
15
+ "flex min-h-11 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm transition-colors",
16
16
  "placeholder:text-muted-foreground",
17
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
17
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
18
18
  "disabled:cursor-not-allowed disabled:opacity-50",
19
19
  // Date/time inputs render a native picker indicator that ignores the
20
20
  // text color; nudge it so it stays visible in dark mode.
@@ -1,16 +1,17 @@
1
1
  import * as React from "react";
2
+ import { Label as LabelPrimitive } from "radix-ui";
2
3
 
3
4
  import { cn } from "@/lib/utils";
4
5
 
5
6
  function Label({
6
7
  className,
7
8
  ...props
8
- }: React.LabelHTMLAttributes<HTMLLabelElement>) {
9
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
9
10
  return (
10
- <label
11
+ <LabelPrimitive.Root
11
12
  data-slot="label"
12
13
  className={cn(
13
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
14
+ "flex items-center gap-2 text-sm font-medium leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
14
15
  className,
15
16
  )}
16
17
  {...props}
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import { ChevronDownIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function NativeSelect({
7
+ className,
8
+ size = "default",
9
+ ...props
10
+ }: Omit<React.ComponentProps<"select">, "size"> & { size?: "sm" | "default" }) {
11
+ return (
12
+ <div
13
+ className="group/native-select relative w-full has-[select:disabled]:opacity-50"
14
+ data-slot="native-select-wrapper"
15
+ >
16
+ <select
17
+ data-slot="native-select"
18
+ data-size={size}
19
+ className={cn(
20
+ "h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 dark:bg-input/30 dark:hover:bg-input/50",
21
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
22
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ <ChevronDownIcon
28
+ className="pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none"
29
+ aria-hidden="true"
30
+ data-slot="native-select-icon"
31
+ />
32
+ </div>
33
+ )
34
+ }
35
+
36
+ function NativeSelectOption({
37
+ className,
38
+ ...props
39
+ }: React.ComponentProps<"option">) {
40
+ return (
41
+ <option
42
+ data-slot="native-select-option"
43
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function NativeSelectOptGroup({
50
+ className,
51
+ ...props
52
+ }: React.ComponentProps<"optgroup">) {
53
+ return (
54
+ <optgroup
55
+ data-slot="native-select-optgroup"
56
+ className={cn("bg-[Canvas] text-[CanvasText]", className)}
57
+ {...props}
58
+ />
59
+ )
60
+ }
61
+
62
+ export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
@@ -0,0 +1,26 @@
1
+ import * as React from "react"
2
+ import { Separator as SeparatorPrimitive } from "radix-ui"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Separator({
7
+ className,
8
+ orientation = "horizontal",
9
+ decorative = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12
+ return (
13
+ <SeparatorPrimitive.Root
14
+ data-slot="separator"
15
+ decorative={decorative}
16
+ orientation={orientation}
17
+ className={cn(
18
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ export { Separator }
@@ -0,0 +1,13 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("animate-pulse rounded-md bg-accent", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,16 @@
1
+ import { Loader2Icon } from "lucide-react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
6
+ return (
7
+ <Loader2Icon
8
+ role="status"
9
+ aria-label="Loading"
10
+ className={cn("size-4 animate-spin", className)}
11
+ {...props}
12
+ />
13
+ )
14
+ }
15
+
16
+ export { Spinner }
@@ -10,9 +10,9 @@ function Textarea({
10
10
  <textarea
11
11
  data-slot="textarea"
12
12
  className={cn(
13
- "flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-colors",
13
+ "flex min-h-24 w-full rounded-lg border border-input bg-background px-3 py-2.5 text-sm",
14
14
  "placeholder:text-muted-foreground",
15
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
15
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
16
16
  "disabled:cursor-not-allowed disabled:opacity-50",
17
17
  className,
18
18
  )}
@@ -2,7 +2,7 @@
2
2
  * Design-token base layer (vendored from examples/_shared so the market is a
3
3
  * self-contained, standalone-deployable example — no unpublished workspace dep).
4
4
  *
5
- * globals.css includes this with `@import "../ui/tokens.css";` AFTER
5
+ * globals.css includes this with `@import "../components/ui/tokens.css";` AFTER
6
6
  * `@import "tailwindcss";` and AFTER its own `@theme` block.
7
7
  * The tokens reference semantic colors so any example's theme cascades
8
8
  * automatically.
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -10,20 +10,21 @@ const DEMO: Array<{
10
10
  price: number;
11
11
  category: string;
12
12
  condition: string;
13
+ imageUrl: string;
13
14
  seed: string;
14
15
  }> = [
15
- { seller: "maple-fox", title: "Herman Miller Aeron (size B)", description: "Fully loaded, posture-fit SL. Light desk use, no squeaks.", price: 540, category: "furniture", condition: "like-new", seed: "a1f3" },
16
- { seller: "amber-lynx", title: "Kodak Retina IIa rangefinder", description: "1950s folding 35mm. Clean glass, accurate shutter, leather case.", price: 185, category: "cameras", condition: "good", seed: "b7c2" },
17
- { seller: "swift-otter", title: "Specialized Allez road bike, 54cm", description: "Shimano Claris, fresh bar tape, recently tuned. Fast commuter.", price: 420, category: "bikes", condition: "good", seed: "c4e9" },
18
- { seller: "cosmic-wren", title: "Vintage Technics SL-1200 MK2", description: "Legendary direct-drive turntable. Spins true, new slipmat.", price: 650, category: "audio", condition: "good", seed: "d8a1" },
19
- { seller: "ivory-sparrow", title: "Le Creuset 5.5qt Dutch oven", description: "Flame orange, enamel intact. The one everyone wants.", price: 120, category: "kitchen", condition: "like-new", seed: "e2b6" },
20
- { seller: "slate-heron", title: 'LG 27" 4K UltraFine monitor', description: "USB-C, 60Hz, color-accurate. Box + cables included.", price: 240, category: "electronics", condition: "good", seed: "f5d0" },
21
- { seller: "maple-fox", title: "Eames-style lounge + ottoman", description: "Walnut + tan leather replica. Sturdy, very comfortable.", price: 380, category: "furniture", condition: "good", seed: "a9c4" },
22
- { seller: "amber-lynx", title: "Patagonia Black Hole 55L duffel", description: "One trip old. Weatherproof, zips perfect. Forest green.", price: 75, category: "outdoor", condition: "like-new", seed: "b3f8" },
23
- { seller: "cosmic-wren", title: "Fender Player Stratocaster, sunburst", description: "Maple neck, plays great. Small buckle rash on back.", price: 560, category: "instruments", condition: "good", seed: "d1e7" },
24
- { seller: "slate-heron", title: "iPad Air (5th gen) 64GB", description: "Space gray, 100% battery health, screen flawless.", price: 330, category: "electronics", condition: "like-new", seed: "f0a2" },
25
- { seller: "ivory-sparrow", title: "Mid-century teak sideboard", description: "Three drawers, two cabinets. Real teak, refinished top.", price: 295, category: "furniture", condition: "good", seed: "e7c3" },
26
- { seller: "swift-otter", title: "Brooks Brothers wool overcoat, 40R", description: "Charcoal herringbone, fully lined. Dry-cleaned, ready to wear.", price: 90, category: "apparel", condition: "good", seed: "c2b5" },
16
+ { seller: "maple-fox", title: "Herman Miller Aeron (size B)", description: "Fully loaded, posture-fit SL. Light desk use, no squeaks.", price: 540, category: "furniture", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1758607010203-d6b7f6ec9262?auto=format&fit=crop&w=1200&q=82", seed: "a1f3" },
17
+ { seller: "amber-lynx", title: "Kodak Retina IIa rangefinder", description: "1950s folding 35mm. Clean glass, accurate shutter, leather case.", price: 185, category: "cameras", condition: "good", imageUrl: "https://images.unsplash.com/photo-1516035069371-29a1b244cc32?auto=format&fit=crop&w=1200&q=82", seed: "b7c2" },
18
+ { seller: "swift-otter", title: "Specialized Allez road bike, 54cm", description: "Shimano Claris, fresh bar tape, recently tuned. Fast commuter.", price: 420, category: "bikes", condition: "good", imageUrl: "https://images.unsplash.com/photo-1532298229144-0ec0c57515c7?auto=format&fit=crop&w=1200&q=82", seed: "c4e9" },
19
+ { seller: "cosmic-wren", title: "Vintage Technics SL-1200 MK2", description: "Legendary direct-drive turntable. Spins true, new slipmat.", price: 650, category: "audio", condition: "good", imageUrl: "https://images.unsplash.com/photo-1599723895995-80e160d14fe4?auto=format&fit=crop&w=1200&q=82", seed: "d8a1" },
20
+ { seller: "ivory-sparrow", title: "Le Creuset 5.5qt Dutch oven", description: "Flame orange, enamel intact. The one everyone wants.", price: 120, category: "kitchen", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1559137703-44aeed6161b6?auto=format&fit=crop&w=1200&q=82", seed: "e2b6" },
21
+ { seller: "slate-heron", title: 'LG 27" 4K UltraFine monitor', description: "USB-C, 60Hz, color-accurate. Box + cables included.", price: 240, category: "electronics", condition: "good", imageUrl: "https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?auto=format&fit=crop&w=1200&q=82", seed: "f5d0" },
22
+ { seller: "maple-fox", title: "Eames-style lounge + ottoman", description: "Walnut + tan leather replica. Sturdy, very comfortable.", price: 380, category: "furniture", condition: "good", imageUrl: "https://images.unsplash.com/photo-1567538096630-e0c55bd6374c?auto=format&fit=crop&w=1200&q=82", seed: "a9c4" },
23
+ { seller: "amber-lynx", title: "Patagonia Black Hole 55L duffel", description: "One trip old. Weatherproof, zips perfect. Forest green.", price: 75, category: "outdoor", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1699319656128-6e353d6b9c7c?auto=format&fit=crop&w=1200&q=82", seed: "b3f8" },
24
+ { seller: "cosmic-wren", title: "Fender Player Stratocaster, sunburst", description: "Maple neck, plays great. Small buckle rash on back.", price: 560, category: "instruments", condition: "good", imageUrl: "https://images.unsplash.com/photo-1525201548942-d8732f6617a0?auto=format&fit=crop&w=1200&q=82", seed: "d1e7" },
25
+ { seller: "slate-heron", title: "iPad Air (5th gen) 64GB", description: "Space gray, excellent battery health, screen flawless.", price: 330, category: "electronics", condition: "like-new", imageUrl: "https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?auto=format&fit=crop&w=1200&q=82", seed: "f0a2" },
26
+ { seller: "ivory-sparrow", title: "Mid-century teak sideboard", description: "Three drawers, two cabinets. Real teak, refinished top.", price: 295, category: "furniture", condition: "good", imageUrl: "https://images.unsplash.com/photo-1558997519-83ea9252edf8?auto=format&fit=crop&w=1200&q=82", seed: "e7c3" },
27
+ { seller: "swift-otter", title: "Brooks Brothers wool overcoat, 40R", description: "Charcoal herringbone, fully lined. Dry-cleaned, ready to wear.", price: 90, category: "apparel", condition: "good", imageUrl: "https://images.unsplash.com/photo-1551488831-00ddcb6c6bd3?auto=format&fit=crop&w=1200&q=82", seed: "c2b5" },
27
28
  ];
28
29
 
29
30
  interface SeedMarketArgs {
@@ -80,6 +81,7 @@ export default mutation<SeedMarketArgs, SeedMarketResult>({
80
81
  category: d.category,
81
82
  condition: d.condition,
82
83
  status: "active",
84
+ imageUrl: d.imageUrl,
83
85
  seed: d.seed,
84
86
  createdAt: new Date(now - (start + n) * 7 * 60_000).toISOString(),
85
87
  });
@@ -0,0 +1,32 @@
1
+ import type { Listing } from "../client/market";
2
+
3
+ export type CatalogSort = "latest" | "price-low" | "price-high";
4
+
5
+ export function browseListings(
6
+ listings: Listing[],
7
+ options: { category?: string; query?: string; sort?: string },
8
+ ): Listing[] {
9
+ const category = options.category || "all";
10
+ const query = options.query?.trim().toLocaleLowerCase() ?? "";
11
+ const sort: CatalogSort =
12
+ options.sort === "price-low" || options.sort === "price-high"
13
+ ? options.sort
14
+ : "latest";
15
+
16
+ const filtered = listings.filter((listing) => {
17
+ if (category !== "all" && listing.category !== category) return false;
18
+ if (!query) return true;
19
+ return [
20
+ listing.title,
21
+ listing.description,
22
+ listing.category,
23
+ listing.sellerName,
24
+ ].some((value) => value.toLocaleLowerCase().includes(query));
25
+ });
26
+
27
+ return [...filtered].sort((a, b) => {
28
+ if (sort === "price-low") return a.price - b.price;
29
+ if (sort === "price-high") return b.price - a.price;
30
+ return b.createdAt.localeCompare(a.createdAt);
31
+ });
32
+ }
@@ -10,22 +10,23 @@
10
10
  "test": "pylon test"
11
11
  },
12
12
  "dependencies": {
13
+ "@pylonsync/functions": "^__PYLON_VERSION__",
13
14
  "@pylonsync/react": "^__PYLON_VERSION__",
14
15
  "@pylonsync/sdk": "^__PYLON_VERSION__",
15
- "@pylonsync/functions": "^__PYLON_VERSION__",
16
16
  "@pylonsync/sync": "^__PYLON_VERSION__",
17
- "react": "^19.0.0",
18
- "react-dom": "^19.0.0",
19
- "tailwindcss": "^4.3.0",
17
+ "@radix-ui/react-dialog": "^1.1.4",
18
+ "@radix-ui/react-label": "^2.1.0",
19
+ "@radix-ui/react-slot": "^1.1.0",
20
20
  "@tailwindcss/cli": "^4.3.0",
21
- "tw-animate-css": "^1.2.0",
22
21
  "class-variance-authority": "^0.7.1",
23
22
  "clsx": "^2.1.1",
24
- "tailwind-merge": "^2.5.0",
25
23
  "lucide-react": "^0.460.0",
26
- "@radix-ui/react-dialog": "^1.1.4",
27
- "@radix-ui/react-slot": "^1.1.0",
28
- "@radix-ui/react-label": "^2.1.0"
24
+ "radix-ui": "^1.6.7",
25
+ "react": "^19.0.0",
26
+ "react-dom": "^19.0.0",
27
+ "tailwind-merge": "^2.5.0",
28
+ "tailwindcss": "^4.3.0",
29
+ "tw-animate-css": "^1.2.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@pylonsync/cli": "^__PYLON_VERSION__",