From b4b1c80005f7a5e2bc0751367d6b29a1ea6b3948 Mon Sep 17 00:00:00 2001 From: nonggde Date: Tue, 7 Jul 2026 01:58:44 +0800 Subject: [PATCH 1/2] feat: add merch empty state CTA --- app/src/components/MerchesContent.tsx | 495 +++++++++++++------------- 1 file changed, 253 insertions(+), 242 deletions(-) diff --git a/app/src/components/MerchesContent.tsx b/app/src/components/MerchesContent.tsx index c836d34..86ddc22 100644 --- a/app/src/components/MerchesContent.tsx +++ b/app/src/components/MerchesContent.tsx @@ -1,247 +1,258 @@ -'use client'; - -import { useState } from 'react'; -import { Filter, Search, ShoppingBag, X, Loader2 } from 'lucide-react'; -import { featureFlags } from '@/lib/featureFlags'; -import { MOCK_MERCH_ITEMS, MOCK_MERCH_METRICS } from '@/lib/mockData'; -import MockDataBadge from '@/components/MockDataBadge'; -import useMerchService, { MerchItem, CreateMerchPayload } from '@/services/merchService'; - -interface MerchFormProps { - initial?: Partial; - onSave: (payload: CreateMerchPayload) => void; - onClose: () => void; - isBusy: boolean; -} - -function MerchForm({ initial, onSave, onClose, isBusy }: MerchFormProps) { - const [form, setForm] = useState({ - title: initial?.title ?? '', - detail: initial?.detail ?? '', - date: initial?.date ?? '', - time: initial?.time ?? '', - price: initial?.price ?? '', - image: initial?.image ?? '', - }); - - const set = (key: keyof CreateMerchPayload) => - (e: React.ChangeEvent) => - setForm((prev) => ({ ...prev, [key]: e.target.value })); - - const handleSave = () => { - if (!form.title.trim() || !form.price.trim()) return; - onSave(form); - }; - - return ( -
-
-
-

- {initial ? 'Edit Merch' : 'New Merch'} -

- -
- - {(['title', 'detail', 'date', 'time', 'price', 'image'] as (keyof CreateMerchPayload)[]).map((field) => ( -
- - -
- ))} - -
- - -
-
-
- ); -} - -export default function MerchesContent() { - const { useGetMerches, useCreateMerch, useUpdateMerch, useDeleteMerch } = useMerchService(); - const { data, isLoading } = useGetMerches(); - - const createMutation = useCreateMerch(); - - const [editTarget, setEditTarget] = useState(null); - const [showCreate, setShowCreate] = useState(false); - const [deleteId, setDeleteId] = useState(null); - - const updateMutation = useUpdateMerch(editTarget?.id ?? 0); - const deleteMutation = useDeleteMerch(deleteId ?? 0); - - const metrics = featureFlags.useMockMerches - ? MOCK_MERCH_METRICS - : (data?.metrics ?? []); - - const items = featureFlags.useMockMerches - ? MOCK_MERCH_ITEMS - : (data?.items ?? []); - - const handleCreate = (payload: CreateMerchPayload) => { - createMutation.mutate(payload, { onSuccess: () => setShowCreate(false) }); - }; - - const handleUpdate = (payload: CreateMerchPayload) => { - updateMutation.mutate(payload, { onSuccess: () => setEditTarget(null) }); - }; - - const handleDelete = (id: number) => { - setDeleteId(id); - deleteMutation.mutate(undefined as any, { onSuccess: () => setDeleteId(null) }); - }; - - return ( -
-
-
-

My Merch

-

- My Merch - {featureFlags.useMockMerches && } -

-
- {!featureFlags.useMockMerches && ( - - )} - {featureFlags.useMockMerches && ( - - )} -
- - {isLoading && !featureFlags.useMockMerches ? ( -
- -
- ) : ( - <> -
- {metrics.map((metric) => ( -
-
-
- {metric.label} -

{metric.value}

- {metric.descriptor} -
-
- ))} -
- -
-
- -

Latest Drops

-
-
-
- - -
- -
-
- +'use client'; + +import { useState } from 'react'; +import { Filter, Search, ShoppingBag, X, Loader2 } from 'lucide-react'; +import { featureFlags } from '@/lib/featureFlags'; +import { MOCK_MERCH_ITEMS, MOCK_MERCH_METRICS } from '@/lib/mockData'; +import MockDataBadge from '@/components/MockDataBadge'; +import useMerchService, { MerchItem, CreateMerchPayload } from '@/services/merchService'; + +interface MerchFormProps { + initial?: Partial; + onSave: (payload: CreateMerchPayload) => void; + onClose: () => void; + isBusy: boolean; +} + +function MerchForm({ initial, onSave, onClose, isBusy }: MerchFormProps) { + const [form, setForm] = useState({ + title: initial?.title ?? '', + detail: initial?.detail ?? '', + date: initial?.date ?? '', + time: initial?.time ?? '', + price: initial?.price ?? '', + image: initial?.image ?? '', + }); + + const set = (key: keyof CreateMerchPayload) => + (e: React.ChangeEvent) => + setForm((prev) => ({ ...prev, [key]: e.target.value })); + + const handleSave = () => { + if (!form.title.trim() || !form.price.trim()) return; + onSave(form); + }; + + return ( +
+
+
+

+ {initial ? 'Edit Merch' : 'New Merch'} +

+ +
+ + {(['title', 'detail', 'date', 'time', 'price', 'image'] as (keyof CreateMerchPayload)[]).map((field) => ( +
+ + +
+ ))} + +
+ + +
+
+
+ ); +} + +export default function MerchesContent() { + const { useGetMerches, useCreateMerch, useUpdateMerch, useDeleteMerch } = useMerchService(); + const { data, isLoading } = useGetMerches(); + + const createMutation = useCreateMerch(); + + const [editTarget, setEditTarget] = useState(null); + const [showCreate, setShowCreate] = useState(false); + const [deleteId, setDeleteId] = useState(null); + + const updateMutation = useUpdateMerch(editTarget?.id ?? 0); + const deleteMutation = useDeleteMerch(deleteId ?? 0); + + const metrics = featureFlags.useMockMerches + ? MOCK_MERCH_METRICS + : (data?.metrics ?? []); + + const items = featureFlags.useMockMerches + ? MOCK_MERCH_ITEMS + : (data?.items ?? []); + + const handleCreate = (payload: CreateMerchPayload) => { + createMutation.mutate(payload, { onSuccess: () => setShowCreate(false) }); + }; + + const handleUpdate = (payload: CreateMerchPayload) => { + updateMutation.mutate(payload, { onSuccess: () => setEditTarget(null) }); + }; + + const handleDelete = (id: number) => { + setDeleteId(id); + deleteMutation.mutate(undefined as any, { onSuccess: () => setDeleteId(null) }); + }; + + return ( +
+
+
+

My Merch

+

+ My Merch + {featureFlags.useMockMerches && } +

+
+ {!featureFlags.useMockMerches && ( + + )} + {featureFlags.useMockMerches && ( + + )} +
+ + {isLoading && !featureFlags.useMockMerches ? ( +
+ +
+ ) : ( + <> +
+ {metrics.map((metric) => ( +
+
+
+ {metric.label} +

{metric.value}

+ {metric.descriptor} +
+
+ ))} +
+ +
+
+ +

Latest Drops

+
+
+
+ + +
+ +
+
+ {items.length === 0 ? ( -
+
+
+ +

No merch yet

Add your first merch drop to get started.

+ {!featureFlags.useMockMerches && ( + + )}
) : ( -
- {items.map((item) => ( -
-
- {item.title} -
-
-

{item.title}

-

{item.detail}

-
- {item.date} - {item.time} -
-
-
- {!featureFlags.useMockMerches && ( - <> - - - - )} - {featureFlags.useMockMerches && ( - - )} -
- {item.price} -
-
-
- ))} -
- )} - - )} - - {showCreate && ( - setShowCreate(false)} - isBusy={createMutation.isPending} - /> - )} - - {editTarget && ( - setEditTarget(null)} - isBusy={updateMutation.isPending} - /> - )} -
- ); -} +
+ {items.map((item) => ( +
+
+ {item.title} +
+
+

{item.title}

+

{item.detail}

+
+ {item.date} + {item.time} +
+
+
+ {!featureFlags.useMockMerches && ( + <> + + + + )} + {featureFlags.useMockMerches && ( + + )} +
+ {item.price} +
+
+
+ ))} +
+ )} + + )} + + {showCreate && ( + setShowCreate(false)} + isBusy={createMutation.isPending} + /> + )} + + {editTarget && ( + setEditTarget(null)} + isBusy={updateMutation.isPending} + /> + )} +
+ ); +} From d492e29eb121623a0a9b0bf9baf519fbdfde8fa0 Mon Sep 17 00:00:00 2001 From: nonggde Date: Tue, 7 Jul 2026 01:59:47 +0800 Subject: [PATCH 2/2] chore: normalize merch CTA diff --- app/src/components/MerchesContent.tsx | 482 +++++++++++++------------- 1 file changed, 241 insertions(+), 241 deletions(-) diff --git a/app/src/components/MerchesContent.tsx b/app/src/components/MerchesContent.tsx index 86ddc22..a1ab52f 100644 --- a/app/src/components/MerchesContent.tsx +++ b/app/src/components/MerchesContent.tsx @@ -1,177 +1,177 @@ -'use client'; - -import { useState } from 'react'; -import { Filter, Search, ShoppingBag, X, Loader2 } from 'lucide-react'; -import { featureFlags } from '@/lib/featureFlags'; -import { MOCK_MERCH_ITEMS, MOCK_MERCH_METRICS } from '@/lib/mockData'; -import MockDataBadge from '@/components/MockDataBadge'; -import useMerchService, { MerchItem, CreateMerchPayload } from '@/services/merchService'; - -interface MerchFormProps { - initial?: Partial; - onSave: (payload: CreateMerchPayload) => void; - onClose: () => void; - isBusy: boolean; -} - -function MerchForm({ initial, onSave, onClose, isBusy }: MerchFormProps) { - const [form, setForm] = useState({ - title: initial?.title ?? '', - detail: initial?.detail ?? '', - date: initial?.date ?? '', - time: initial?.time ?? '', - price: initial?.price ?? '', - image: initial?.image ?? '', - }); - - const set = (key: keyof CreateMerchPayload) => - (e: React.ChangeEvent) => - setForm((prev) => ({ ...prev, [key]: e.target.value })); - - const handleSave = () => { - if (!form.title.trim() || !form.price.trim()) return; - onSave(form); - }; - - return ( -
-
-
-

- {initial ? 'Edit Merch' : 'New Merch'} -

- -
- - {(['title', 'detail', 'date', 'time', 'price', 'image'] as (keyof CreateMerchPayload)[]).map((field) => ( -
- - -
- ))} - -
- - -
-
-
- ); -} - -export default function MerchesContent() { - const { useGetMerches, useCreateMerch, useUpdateMerch, useDeleteMerch } = useMerchService(); - const { data, isLoading } = useGetMerches(); - - const createMutation = useCreateMerch(); - - const [editTarget, setEditTarget] = useState(null); - const [showCreate, setShowCreate] = useState(false); - const [deleteId, setDeleteId] = useState(null); - - const updateMutation = useUpdateMerch(editTarget?.id ?? 0); - const deleteMutation = useDeleteMerch(deleteId ?? 0); - - const metrics = featureFlags.useMockMerches - ? MOCK_MERCH_METRICS - : (data?.metrics ?? []); - - const items = featureFlags.useMockMerches - ? MOCK_MERCH_ITEMS - : (data?.items ?? []); - - const handleCreate = (payload: CreateMerchPayload) => { - createMutation.mutate(payload, { onSuccess: () => setShowCreate(false) }); - }; - - const handleUpdate = (payload: CreateMerchPayload) => { - updateMutation.mutate(payload, { onSuccess: () => setEditTarget(null) }); - }; - - const handleDelete = (id: number) => { - setDeleteId(id); - deleteMutation.mutate(undefined as any, { onSuccess: () => setDeleteId(null) }); - }; - - return ( -
-
-
-

My Merch

-

- My Merch - {featureFlags.useMockMerches && } -

-
- {!featureFlags.useMockMerches && ( - - )} - {featureFlags.useMockMerches && ( - - )} -
- - {isLoading && !featureFlags.useMockMerches ? ( -
- -
- ) : ( - <> -
- {metrics.map((metric) => ( -
-
-
- {metric.label} -

{metric.value}

- {metric.descriptor} -
-
- ))} -
- -
-
- -

Latest Drops

-
-
-
- - -
- -
-
- +'use client'; + +import { useState } from 'react'; +import { Filter, Search, ShoppingBag, X, Loader2 } from 'lucide-react'; +import { featureFlags } from '@/lib/featureFlags'; +import { MOCK_MERCH_ITEMS, MOCK_MERCH_METRICS } from '@/lib/mockData'; +import MockDataBadge from '@/components/MockDataBadge'; +import useMerchService, { MerchItem, CreateMerchPayload } from '@/services/merchService'; + +interface MerchFormProps { + initial?: Partial; + onSave: (payload: CreateMerchPayload) => void; + onClose: () => void; + isBusy: boolean; +} + +function MerchForm({ initial, onSave, onClose, isBusy }: MerchFormProps) { + const [form, setForm] = useState({ + title: initial?.title ?? '', + detail: initial?.detail ?? '', + date: initial?.date ?? '', + time: initial?.time ?? '', + price: initial?.price ?? '', + image: initial?.image ?? '', + }); + + const set = (key: keyof CreateMerchPayload) => + (e: React.ChangeEvent) => + setForm((prev) => ({ ...prev, [key]: e.target.value })); + + const handleSave = () => { + if (!form.title.trim() || !form.price.trim()) return; + onSave(form); + }; + + return ( +
+
+
+

+ {initial ? 'Edit Merch' : 'New Merch'} +

+ +
+ + {(['title', 'detail', 'date', 'time', 'price', 'image'] as (keyof CreateMerchPayload)[]).map((field) => ( +
+ + +
+ ))} + +
+ + +
+
+
+ ); +} + +export default function MerchesContent() { + const { useGetMerches, useCreateMerch, useUpdateMerch, useDeleteMerch } = useMerchService(); + const { data, isLoading } = useGetMerches(); + + const createMutation = useCreateMerch(); + + const [editTarget, setEditTarget] = useState(null); + const [showCreate, setShowCreate] = useState(false); + const [deleteId, setDeleteId] = useState(null); + + const updateMutation = useUpdateMerch(editTarget?.id ?? 0); + const deleteMutation = useDeleteMerch(deleteId ?? 0); + + const metrics = featureFlags.useMockMerches + ? MOCK_MERCH_METRICS + : (data?.metrics ?? []); + + const items = featureFlags.useMockMerches + ? MOCK_MERCH_ITEMS + : (data?.items ?? []); + + const handleCreate = (payload: CreateMerchPayload) => { + createMutation.mutate(payload, { onSuccess: () => setShowCreate(false) }); + }; + + const handleUpdate = (payload: CreateMerchPayload) => { + updateMutation.mutate(payload, { onSuccess: () => setEditTarget(null) }); + }; + + const handleDelete = (id: number) => { + setDeleteId(id); + deleteMutation.mutate(undefined as any, { onSuccess: () => setDeleteId(null) }); + }; + + return ( +
+
+
+

My Merch

+

+ My Merch + {featureFlags.useMockMerches && } +

+
+ {!featureFlags.useMockMerches && ( + + )} + {featureFlags.useMockMerches && ( + + )} +
+ + {isLoading && !featureFlags.useMockMerches ? ( +
+ +
+ ) : ( + <> +
+ {metrics.map((metric) => ( +
+
+
+ {metric.label} +

{metric.value}

+ {metric.descriptor} +
+
+ ))} +
+ +
+
+ +

Latest Drops

+
+
+
+ + +
+ +
+
+ {items.length === 0 ? (
@@ -189,70 +189,70 @@ export default function MerchesContent() { )}
) : ( -
- {items.map((item) => ( -
-
- {item.title} -
-
-

{item.title}

-

{item.detail}

-
- {item.date} - {item.time} -
-
-
- {!featureFlags.useMockMerches && ( - <> - - - - )} - {featureFlags.useMockMerches && ( - - )} -
- {item.price} -
-
-
- ))} -
- )} - - )} - - {showCreate && ( - setShowCreate(false)} - isBusy={createMutation.isPending} - /> - )} - - {editTarget && ( - setEditTarget(null)} - isBusy={updateMutation.isPending} - /> - )} -
- ); -} +
+ {items.map((item) => ( +
+
+ {item.title} +
+
+

{item.title}

+

{item.detail}

+
+ {item.date} + {item.time} +
+
+
+ {!featureFlags.useMockMerches && ( + <> + + + + )} + {featureFlags.useMockMerches && ( + + )} +
+ {item.price} +
+
+
+ ))} +
+ )} + + )} + + {showCreate && ( + setShowCreate(false)} + isBusy={createMutation.isPending} + /> + )} + + {editTarget && ( + setEditTarget(null)} + isBusy={updateMutation.isPending} + /> + )} +
+ ); +}