@@ -4,7 +4,7 @@ import { useState, useRef, useEffect } from "react";
44import Image from "next/image" ;
55import Link from "next/link" ;
66import { useRouter } from "next/navigation" ;
7- import { ArrowUp , ArrowDown , Check , ExternalLink , GripVertical , Pencil , Share2 , Trash2 , X } from "lucide-react" ;
7+ import { ArrowUp , ArrowDown , Check , ExternalLink , Eye , GripVertical , Pencil , Share2 , Trash2 , X } from "lucide-react" ;
88import { toast } from "sonner" ;
99import {
1010 DndContext ,
@@ -26,12 +26,13 @@ import { CSS } from "@dnd-kit/utilities";
2626import { Button } from "@/components/ui/button" ;
2727import { ColorPicker } from "@/components/color-picker" ;
2828import { PhoneFrame } from "@/components/phone-frame" ;
29- import { ProductCard } from "@/components/product-card" ;
3029import { PasteUrlPreview } from "@/components/paste-url-preview" ;
3130import { ShareSheet } from "@/components/share-sheet" ;
3231import { CartCover } from "@/components/cart-cover" ;
3332import { CoverPicker } from "@/components/cover-picker" ;
3433import { ImageUploadButton } from "@/components/image-upload-button" ;
34+ import { EditorSection } from "@/components/editor-section" ;
35+ import { CartPreviewSheet } from "@/components/cart-preview-sheet" ;
3536import {
3637 Dialog ,
3738 DialogClose ,
@@ -54,6 +55,7 @@ import type { Cart, Product } from "@/lib/types";
5455export function CartEditor ( { initialCart } : { initialCart : Cart } ) {
5556 const router = useRouter ( ) ;
5657 const [ cart , setCart ] = useState < Cart > ( initialCart ) ;
58+ const [ previewOpen , setPreviewOpen ] = useState ( false ) ;
5759 const [ deleting , setDeleting ] = useState ( false ) ;
5860
5961 // Debounced save: local state is the source of truth while editing. We
@@ -179,92 +181,62 @@ export function CartEditor({ initialCart }: { initialCart: Cart }) {
179181 } ;
180182
181183 return (
182- < div className = "mx-auto max-w-7xl px-4 sm:px-6 py-8 " >
184+ < div className = "mx-auto max-w-7xl px-4 sm:px-6 py-6 pb-24 " >
183185 { /* HEADER */ }
184- < div className = "flex items-center justify-between mb-6 gap-4" >
185- < div className = "min-w-0" >
186- < Link href = "/dashboard" className = "text-sm text-muted hover:text-ink" > ← Back to carts</ Link >
186+ < div className = "flex items-center justify-between gap-3 mb-4" >
187+ < Link href = "/dashboard" className = "text-sm text-muted hover:text-ink shrink-0" > ← Carts</ Link >
188+ < div className = "flex items-center gap-2" >
189+ < button
190+ type = "button"
191+ onClick = { ( ) => setPreviewOpen ( true ) }
192+ className = "lg:hidden inline-flex items-center gap-1.5 rounded-full border border-ink px-4 py-2 text-sm font-medium hover:bg-paper transition-colors"
193+ >
194+ < Eye size = { 15 } /> Preview
195+ </ button >
196+ < ShareSheet slug = { cart . slug } >
197+ < Button variant = "default" > < Share2 size = { 16 } /> Share</ Button >
198+ </ ShareSheet >
187199 </ div >
188- < ShareSheet slug = { cart . slug } >
189- < Button variant = "default" > < Share2 size = { 16 } /> Share</ Button >
190- </ ShareSheet >
191200 </ div >
192201
193- < div className = "grid lg:grid-cols-[1.4fr_1fr] gap-10" >
194- { /* LEFT — EDITOR */ }
195- < div className = "space-y-8" >
196- { /* Cover image */ }
197- < section >
198- < h2 className = "font-serif text-2xl mb-3" > Cover image</ h2 >
199- < div className = "relative aspect-[16/10] rounded-xl overflow-hidden border border-rule bg-paper mb-4" >
200- < CartCover coverImageUrl = { cart . coverImageUrl } accentHex = { cart . accentHex } title = { cart . title } />
201- </ div >
202- < CoverPicker
203- value = { cart . coverImageUrl }
204- accentHex = { cart . accentHex }
205- title = { cart . title }
206- onChange = { ( url ) => patch ( { coverImageUrl : url } ) }
207- />
208- </ section >
209-
210- { /* Title — styled as a heading; subtle hover indicates editability */ }
211- < section className = "space-y-4" >
212- < div className = "group/title relative" >
213- < span className = "sr-only" > Cart title</ span >
214- < input
215- type = "text"
216- value = { cart . title }
217- onChange = { ( e ) => patch ( { title : e . target . value } ) }
218- placeholder = "Untitled cart"
219- aria-label = "Cart title"
220- className = "w-full bg-transparent font-serif text-3xl sm:text-4xl tracking-tight px-0 py-2 border-0 border-b-2 border-transparent group-hover/title:border-rule focus:border-accent focus:outline-none transition-colors placeholder:text-muted/60"
221- />
222- < p className = "text-[11px] uppercase tracking-widest text-muted/70 opacity-0 group-hover/title:opacity-100 group-focus-within/title:opacity-100 transition-opacity mt-1" >
223- Click to edit · changes save as you type
224- </ p >
225- </ div >
226-
227- < div >
228- < label className = "block text-sm font-medium mb-2 text-muted" > Bio</ label >
229- < textarea
230- value = { cart . bio ?? "" }
231- onChange = { ( e ) => patch ( { bio : e . target . value } ) }
232- rows = { 3 }
233- placeholder = "Tell your followers about this cart"
234- className = "w-full rounded-md border border-rule bg-cream px-3 py-2 leading-relaxed focus:outline-none focus:ring-2 focus:ring-accent"
235- />
236- </ div >
237- </ section >
238-
239- { /* Accent color */ }
240- < section >
241- < h2 className = "font-serif text-2xl mb-3" > Accent color</ h2 >
242- < ColorPicker value = { cart . accentHex } onChange = { ( hex ) => patch ( { accentHex : hex } ) } />
243- </ section >
202+ { /* Editable title */ }
203+ < div className = "group/title relative mb-6" >
204+ < input
205+ type = "text"
206+ value = { cart . title }
207+ onChange = { ( e ) => patch ( { title : e . target . value } ) }
208+ placeholder = "Untitled cart"
209+ aria-label = "Cart title"
210+ className = "w-full bg-transparent font-serif text-3xl sm:text-4xl tracking-tight px-0 py-1 border-0 border-b-2 border-transparent group-hover/title:border-rule focus:border-accent focus:outline-none transition-colors placeholder:text-muted/60"
211+ />
212+ </ div >
244213
245- { /* Add product */ }
214+ < div className = "grid lg:grid-cols-[1.4fr_1fr] gap-10" >
215+ { /* LEFT — editor */ }
216+ < div className = "space-y-6" >
217+ { /* PRODUCTS — the core task, first */ }
246218 < section >
247219 < h2 className = "font-serif text-2xl mb-4" > Add a product</ h2 >
248220 < PasteUrlPreview onResolved = { addProduct } />
249- </ section >
250221
251- { /* Products */ }
252- < section >
253- < h2 className = "font-serif text-2xl mb-3" > Products ({ cart . products . length } )</ h2 >
222+ < div className = "flex items-center justify-between mt-6 mb-3" >
223+ < h2 className = "font-serif text-2xl" > Products ({ cart . products . length } )</ h2 >
224+ < Link
225+ href = { `/c/${ cart . slug } ` }
226+ target = "_blank"
227+ rel = "noopener"
228+ className = "inline-flex items-center gap-1 text-sm text-ink underline-offset-4 hover:underline"
229+ >
230+ View live < ExternalLink size = { 14 } />
231+ </ Link >
232+ </ div >
254233
255234 { cart . products . length === 0 && (
256235 < p className = "text-sm text-muted" > No products yet. Paste a link above to add your first product.</ p >
257236 ) }
258237
259- < DndContext
260- sensors = { sensors }
261- collisionDetection = { closestCenter }
262- onDragEnd = { handleDragEnd }
263- >
264- < SortableContext
265- items = { cart . products . map ( ( p ) => p . id ) }
266- strategy = { verticalListSortingStrategy }
267- >
238+ < DndContext sensors = { sensors } collisionDetection = { closestCenter } onDragEnd = { handleDragEnd } >
239+ < SortableContext items = { cart . products . map ( ( p ) => p . id ) } strategy = { verticalListSortingStrategy } >
268240 < ul className = "space-y-2" >
269241 { cart . products . map ( ( p , i ) => (
270242 < SortableProductRow
@@ -283,20 +255,38 @@ export function CartEditor({ initialCart }: { initialCart: Cart }) {
283255 </ DndContext >
284256 </ section >
285257
286- < div className = "text-sm" >
287- < Link
288- href = { `/c/${ cart . slug } ` }
289- target = "_blank"
290- rel = "noopener"
291- className = "inline-flex items-center gap-1 text-ink underline-offset-4 hover:underline"
292- >
293- View live < ExternalLink size = { 14 } />
294- </ Link >
295- </ div >
258+ { /* COVER & LOOK */ }
259+ < EditorSection title = "Cover & look" defaultOpen = { ! cart . coverImageUrl } >
260+ < div className = "relative aspect-[16/10] rounded-xl overflow-hidden border border-rule bg-paper mb-4" >
261+ < CartCover coverImageUrl = { cart . coverImageUrl } accentHex = { cart . accentHex } title = { cart . title } />
262+ </ div >
263+ < CoverPicker
264+ value = { cart . coverImageUrl }
265+ accentHex = { cart . accentHex }
266+ title = { cart . title }
267+ onChange = { ( url ) => patch ( { coverImageUrl : url } ) }
268+ />
269+ < div className = "pt-2" >
270+ < p className = "text-sm font-medium mb-2" > Accent color</ p >
271+ < ColorPicker value = { cart . accentHex } onChange = { ( hex ) => patch ( { accentHex : hex } ) } />
272+ </ div >
273+ </ EditorSection >
274+
275+ { /* CART DETAILS */ }
276+ < EditorSection title = "Cart details" >
277+ < label className = "block text-sm font-medium mb-2 text-muted" > Bio</ label >
278+ < textarea
279+ value = { cart . bio ?? "" }
280+ onChange = { ( e ) => patch ( { bio : e . target . value } ) }
281+ rows = { 3 }
282+ placeholder = "Tell your followers about this cart"
283+ className = "w-full rounded-md border border-rule bg-cream px-3 py-2 leading-relaxed focus:outline-none focus:ring-2 focus:ring-accent"
284+ />
285+ </ EditorSection >
296286
297- { /* Danger zone */ }
298- < section className = "border-t border-rule pt-6 mt-8 ">
299- < h2 className = "text-sm font-medium mb-1" > Delete this cart</ h2 >
287+ { /* SETTINGS / DANGER */ }
288+ < EditorSection title = "Settings ">
289+ < h3 className = "text-sm font-medium mb-1" > Delete this cart</ h3 >
300290 < p className = "text-sm text-muted mb-3" >
301291 Its share link will stop working and it'll disappear from your dashboard. This can't be undone.
302292 </ p >
@@ -311,16 +301,14 @@ export function CartEditor({ initialCart }: { initialCart: Cart }) {
311301 </ DialogTrigger >
312302 < DialogContent >
313303 < DialogHeader >
314- < DialogTitle > Delete “ { cart . title } ” ?</ DialogTitle >
304+ < DialogTitle > Delete “ { cart . title } ” ?</ DialogTitle >
315305 < DialogDescription >
316306 The link < span className = "font-mono" > /c/{ cart . slug } </ span > will stop working and the cart leaves your dashboard. This can't be undone.
317307 </ DialogDescription >
318308 </ DialogHeader >
319309 < div className = "flex justify-end gap-2 mt-4" >
320310 < DialogClose asChild >
321- < button type = "button" className = "rounded-full px-4 py-2 text-sm text-muted hover:text-ink" >
322- Cancel
323- </ button >
311+ < button type = "button" className = "rounded-full px-4 py-2 text-sm text-muted hover:text-ink" > Cancel</ button >
324312 </ DialogClose >
325313 < button
326314 type = "button"
@@ -333,45 +321,31 @@ export function CartEditor({ initialCart }: { initialCart: Cart }) {
333321 </ div >
334322 </ DialogContent >
335323 </ Dialog >
336- </ section >
324+ </ EditorSection >
337325 </ div >
338326
339- { /* RIGHT — PREVIEW */ }
327+ { /* RIGHT — desktop sticky preview */ }
340328 < div className = "hidden lg:block sticky top-24 self-start" >
341329 < p className = "text-sm text-muted text-center mb-3" > Live preview</ p >
342330 < PhoneFrame >
343- < PreviewCartPage cart = { cart } />
331+ < CartPreviewSheet cart = { cart } />
344332 </ PhoneFrame >
345333 </ div >
346334 </ div >
347- </ div >
348- ) ;
349- }
350335
351- function PreviewCartPage ( { cart } : { cart : Cart } ) {
352- return (
353- < div style = { { [ "--accent" as string ] : cart . accentHex } as React . CSSProperties } >
354- < div className = "relative aspect-[5/4]" >
355- < CartCover coverImageUrl = { cart . coverImageUrl } accentHex = { cart . accentHex } title = { cart . title } />
356- < div className = "absolute inset-0 bg-ink/30" />
357- < div className = "absolute inset-0 bg-gradient-to-t from-ink via-ink/60 to-transparent" />
358- < div className = "absolute bottom-3 left-3 right-3 text-cream [text-shadow:0_1px_10px_rgba(0,0,0,0.85)]" >
359- < p className = "text-xs font-medium" > @{ cart . ownerHandle } </ p >
360- < p className = "font-serif text-xl leading-tight" > { cart . title } </ p >
361- </ div >
362- </ div >
363- < div className = "p-3" >
364- { cart . products . length === 0 ? (
365- < p className = "text-xs text-muted text-center py-6" > No products yet.</ p >
366- ) : (
367- < div className = "grid grid-cols-2 gap-2" >
368- { cart . products . slice ( 0 , 4 ) . map ( ( p ) => < ProductCard key = { p . id } product = { p } /> ) }
369- </ div >
370- ) }
371- { cart . products . length > 4 && (
372- < p className = "text-center text-xs text-muted mt-3" > + { cart . products . length - 4 } more on the live page</ p >
373- ) }
374- </ div >
336+ { /* MOBILE full-screen preview */ }
337+ < Dialog open = { previewOpen } onOpenChange = { setPreviewOpen } >
338+ < DialogContent className = "max-w-full w-screen h-[100dvh] p-0 gap-0 rounded-none overflow-y-auto [&>button:last-child]:hidden" >
339+ < DialogHeader className = "sticky top-0 z-10 flex-row items-center justify-between bg-cream/95 backdrop-blur border-b border-rule px-4 py-3 space-y-0" >
340+ < DialogTitle className = "font-serif text-lg" > Preview</ DialogTitle >
341+ < DialogClose asChild >
342+ < button type = "button" aria-label = "Close preview" className = "text-muted hover:text-ink" > < X size = { 20 } /> </ button >
343+ </ DialogClose >
344+ </ DialogHeader >
345+ < DialogDescription className = "sr-only" > A preview of how your cart looks to followers.</ DialogDescription >
346+ < CartPreviewSheet cart = { cart } fullPage />
347+ </ DialogContent >
348+ </ Dialog >
375349 </ div >
376350 ) ;
377351}
0 commit comments