From c0a1879c957a57a03172a06d933d6ef1178a7c96 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Sat, 29 Nov 2025 13:50:40 +0100 Subject: [PATCH] feat: enhance coffee management with image upload and preview functionality + change it from "Create Subscriptions" to "Create Coffee" --- public/images/misc/{cow.png => cow1.png} | Bin .../subscriptions/createSubscription/page.tsx | 99 ++++++++++++------ .../admin/subscriptions/edit/[id]/page.tsx | 78 +++++++++++--- .../hooks/useCoffeeManagement.ts | 2 + src/app/admin/subscriptions/page.tsx | 23 ++-- 5 files changed, 145 insertions(+), 57 deletions(-) rename public/images/misc/{cow.png => cow1.png} (100%) diff --git a/public/images/misc/cow.png b/public/images/misc/cow1.png similarity index 100% rename from public/images/misc/cow.png rename to public/images/misc/cow1.png diff --git a/src/app/admin/subscriptions/createSubscription/page.tsx b/src/app/admin/subscriptions/createSubscription/page.tsx index 0c93d48..1e9894b 100644 --- a/src/app/admin/subscriptions/createSubscription/page.tsx +++ b/src/app/admin/subscriptions/createSubscription/page.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import PageLayout from '../../../components/PageLayout'; import useCoffeeManagement from '../hooks/useCoffeeManagement'; import { PhotoIcon } from '@heroicons/react/24/solid'; @@ -18,6 +18,7 @@ export default function CreateSubscriptionPage() { const [price, setPrice] = useState(0); const [state, setState] = useState<'available'|'unavailable'>('available'); const [pictureFile, setPictureFile] = useState(undefined); + const [previewUrl, setPreviewUrl] = useState(null); const [currency, setCurrency] = useState('EUR'); const [isFeatured, setIsFeatured] = useState(false); // Fixed billing defaults (locked: month / 1) @@ -43,6 +44,32 @@ export default function CreateSubscriptionPage() { } }; + // preview object URL management + useEffect(() => { + if (pictureFile) { + const url = URL.createObjectURL(pictureFile); + setPreviewUrl(url); + return () => URL.revokeObjectURL(url); + } else { + setPreviewUrl(null); + } + }, [pictureFile]); + + function handleSelectFile(file?: File) { + if (!file) return; + const allowed = ['image/jpeg','image/png','image/webp']; + if (!allowed.includes(file.type)) { + setError('Invalid image type. Allowed: JPG, PNG, WebP'); + return; + } + if (file.size > 10 * 1024 * 1024) { // 10MB + setError('Image exceeds 10MB limit'); + return; + } + setError(null); + setPictureFile(file); + } + return (
@@ -51,8 +78,8 @@ export default function CreateSubscriptionPage() {
-

Create Subscription

-

Add a new product or subscription plan.

+

Create Coffee

+

Add a new coffee.

setIsFeatured(e.target.checked)} />
- {/* Fixed Billing (Locked) */} -
- -

Fixed monthly billing (interval count = 1). These settings are locked.

-
- - + {/* Subscription Billing (Locked) + Availability */} +
+
+ +

Fixed monthly subscription billing (interval count = 1). These settings are locked.

+
+ + +
+
+
+ +
-
- {/* Availability */} -
- -
@@ -116,32 +144,41 @@ export default function CreateSubscriptionPage() {
document.getElementById('file-upload')?.click()} onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); - if (e.dataTransfer.files?.[0]) setPictureFile(e.dataTransfer.files[0]); + if (e.dataTransfer.files?.[0]) handleSelectFile(e.dataTransfer.files[0]); }} > -
-
@@ -151,7 +188,7 @@ export default function CreateSubscriptionPage() { Cancel
diff --git a/src/app/admin/subscriptions/edit/[id]/page.tsx b/src/app/admin/subscriptions/edit/[id]/page.tsx index 917c983..99a416f 100644 --- a/src/app/admin/subscriptions/edit/[id]/page.tsx +++ b/src/app/admin/subscriptions/edit/[id]/page.tsx @@ -27,6 +27,8 @@ export default function EditSubscriptionPage() { const [isFeatured, setIsFeatured] = useState(false); const [state, setState] = useState(true); const [pictureFile, setPictureFile] = useState(undefined); + const [previewUrl, setPreviewUrl] = useState(null); + const [removeExistingPicture, setRemoveExistingPicture] = useState(false); const fileInputRef = useRef(null); useEffect(() => { @@ -51,6 +53,7 @@ export default function EditSubscriptionPage() { setCurrency(found.currency || 'EUR'); setIsFeatured(!!found.is_featured); setState(!!found.state); + setRemoveExistingPicture(false); } } catch (e: any) { if (active) setError(e?.message ?? 'Failed to load subscription'); @@ -81,6 +84,7 @@ export default function EditSubscriptionPage() { is_featured: isFeatured, state, pictureFile, + removePicture: removeExistingPicture && !pictureFile ? true : false, }); router.push('/admin/subscriptions'); } catch (e: any) { @@ -88,6 +92,32 @@ export default function EditSubscriptionPage() { } } + useEffect(() => { + if (pictureFile) { + const url = URL.createObjectURL(pictureFile); + setPreviewUrl(url); + return () => URL.revokeObjectURL(url); + } else { + setPreviewUrl(null); + } + }, [pictureFile]); + + function handleSelectFile(file?: File) { + if (!file) return; + const allowed = ['image/jpeg','image/png','image/webp']; + if (!allowed.includes(file.type)) { + setError('Invalid image type. Allowed: JPG, PNG, WebP'); + return; + } + if (file.size > 10 * 1024 * 1024) { + setError('Image exceeds 10MB limit'); + return; + } + setError(null); + setPictureFile(file); + setRemoveExistingPicture(false); // selecting new overrides removal flag + } + return (
@@ -95,8 +125,8 @@ export default function EditSubscriptionPage() {
-

Edit Subscription

-

Update details of the subscription product.

+

Edit Coffee

+

Update details of the coffee.

fileInputRef.current?.click()} onDragOver={e => e.preventDefault()} onDrop={e => { e.preventDefault(); - if (e.dataTransfer.files?.[0]) setPictureFile(e.dataTransfer.files[0]); + if (e.dataTransfer.files?.[0]) handleSelectFile(e.dataTransfer.files[0]); }} > -
-
diff --git a/src/app/admin/subscriptions/hooks/useCoffeeManagement.ts b/src/app/admin/subscriptions/hooks/useCoffeeManagement.ts index 6cde324..aa1106e 100644 --- a/src/app/admin/subscriptions/hooks/useCoffeeManagement.ts +++ b/src/app/admin/subscriptions/hooks/useCoffeeManagement.ts @@ -108,6 +108,7 @@ export default function useCoffeeManagement() { is_featured: boolean; state: boolean; pictureFile: File; + removePicture: boolean; }>): Promise => { const fd = new FormData(); if (payload.title !== undefined) fd.append('title', String(payload.title)); @@ -116,6 +117,7 @@ export default function useCoffeeManagement() { if (payload.currency !== undefined) fd.append('currency', payload.currency); if (payload.is_featured !== undefined) fd.append('is_featured', String(payload.is_featured)); if (payload.state !== undefined) fd.append('state', String(payload.state)); + if (payload.removePicture) fd.append('removePicture', 'true'); // Keep fixed defaults fd.append('billing_interval', 'month'); fd.append('interval_count', '1'); diff --git a/src/app/admin/subscriptions/page.tsx b/src/app/admin/subscriptions/page.tsx index ad98df3..971ab74 100644 --- a/src/app/admin/subscriptions/page.tsx +++ b/src/app/admin/subscriptions/page.tsx @@ -1,5 +1,6 @@ "use client"; import React, { useEffect, useState } from 'react'; +import { PhotoIcon } from '@heroicons/react/24/solid'; import Link from 'next/link'; import PageLayout from '../../components/PageLayout'; import useCoffeeManagement, { CoffeeItem } from './hooks/useCoffeeManagement'; @@ -45,15 +46,15 @@ export default function AdminSubscriptionsPage() {
-

Subscription Products

-

Manage all products and subscription plans.

+

Coffees

+

Manage all coffees.

- Create Subscription + Create Coffee
@@ -72,9 +73,13 @@ export default function AdminSubscriptionsPage() {

{item.title}

{availabilityBadge(!!item.state)}
- {item.pictureUrl && ( - {item.title} - )} +
+ {item.pictureUrl ? ( + {item.title} + ) : ( + + )} +

{item.description}

@@ -85,7 +90,7 @@ export default function AdminSubscriptionsPage() {
{item.billing_interval && item.interval_count ? (
- Billing: {item.billing_interval} (x{item.interval_count}) + Subscription billing: {item.billing_interval} (x{item.interval_count})
) : null}
@@ -125,8 +130,8 @@ export default function AdminSubscriptionsPage() {
-

Delete subscription?

-

You are about to delete "{deleteTarget.title}". This action cannot be undone.

+

Delete coffee?

+

You are about to delete the coffee "{deleteTarget.title}". This action cannot be undone.