feat: edit subscription

This commit is contained in:
seaznCode 2025-11-20 17:48:51 +01:00
parent d54a4024cb
commit 198e41e601
2 changed files with 232 additions and 0 deletions

View File

@ -0,0 +1,226 @@
"use client";
import React, { useEffect, useState, useRef } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Link from 'next/link';
import PageLayout from '../../../../components/PageLayout';
import useCoffeeManagement, { CoffeeItem } from '../../hooks/useCoffeeManagement';
import { PhotoIcon } from '@heroicons/react/24/solid';
export default function EditSubscriptionPage() {
const router = useRouter();
// next/navigation app router dynamic param
const params = useParams();
const idParam = params?.id;
const id = typeof idParam === 'string' ? parseInt(idParam, 10) : Array.isArray(idParam) ? parseInt(idParam[0], 10) : NaN;
const { listProducts, updateProduct } = useCoffeeManagement();
const [item, setItem] = useState<CoffeeItem | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Form state
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [price, setPrice] = useState<string>('');
const [currency, setCurrency] = useState('EUR');
const [isFeatured, setIsFeatured] = useState(false);
const [state, setState] = useState(true);
const [pictureFile, setPictureFile] = useState<File | undefined>(undefined);
const fileInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
let active = true;
async function load() {
if (!id || Number.isNaN(id)) {
setError('Invalid subscription id');
setLoading(false);
return;
}
try {
const all = await listProducts();
const found = all.find((p: CoffeeItem) => p.id === id) || null;
if (!active) return;
if (!found) {
setError('Subscription not found');
} else {
setItem(found);
setTitle(found.title || '');
setDescription(found.description || '');
setPrice(found.price != null ? String(found.price) : '');
setCurrency(found.currency || 'EUR');
setIsFeatured(!!found.is_featured);
setState(!!found.state);
}
} catch (e: any) {
if (active) setError(e?.message ?? 'Failed to load subscription');
} finally {
if (active) setLoading(false);
}
}
load();
return () => { active = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!item) return;
setError(null);
try {
const numericPrice = Number(price);
if (!Number.isFinite(numericPrice) || numericPrice < 0) {
setError('Price must be a valid non-negative number');
return;
}
await updateProduct(item.id, {
title: title.trim(),
description: description.trim(),
price: numericPrice,
currency: currency.trim(),
is_featured: isFeatured,
state,
pictureFile,
});
router.push('/admin/subscriptions');
} catch (e: any) {
setError(e?.message ?? 'Update failed');
}
}
return (
<PageLayout>
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Edit Subscription</h1>
<p className="text-lg text-blue-700 mt-2">Update details of the subscription product.</p>
</div>
<Link href="/admin/subscriptions"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>
Back to list
</Link>
</div>
</header>
{loading && (
<div className="rounded-md bg-blue-50 p-4 text-blue-700 text-sm mb-6">Loading subscription</div>
)}
{error && !loading && (
<div className="rounded-md bg-red-50 p-4 text-red-700 text-sm mb-6">{error}</div>
)}
{!loading && item && (
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg">
<form onSubmit={handleSubmit} className="space-y-8">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-blue-900">Title</label>
<input
required
className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900">Price</label>
<input
type="number"
min={0}
step={0.01}
required
className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black"
value={price}
onChange={e => setPrice(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900">Currency</label>
<input
required
maxLength={3}
pattern="[A-Za-z]{3}"
className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black"
value={currency}
onChange={e => setCurrency(e.target.value.toUpperCase().slice(0,3))}
/>
</div>
<div className="flex items-center gap-4 mt-6">
<div className="flex items-center gap-2">
<input id="featured" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-blue-900 focus:ring-blue-900" checked={isFeatured} onChange={e => setIsFeatured(e.target.checked)} />
<label htmlFor="featured" className="text-sm font-medium text-blue-900">Featured</label>
</div>
<div className="flex items-center gap-2">
<input id="enabled" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-blue-900 focus:ring-blue-900" checked={state} onChange={e => setState(e.target.checked)} />
<label htmlFor="enabled" className="text-sm font-medium text-blue-900">Enabled</label>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-blue-900">Description</label>
<textarea
required
rows={4}
className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black"
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-blue-900">Picture (optional)</label>
<div
className="mt-2 flex max-w-xl justify-center rounded-lg border border-dashed border-blue-300 px-6 py-10 bg-blue-50 cursor-pointer"
onClick={() => fileInputRef.current?.click()}
onDragOver={e => e.preventDefault()}
onDrop={e => {
e.preventDefault();
if (e.dataTransfer.files?.[0]) setPictureFile(e.dataTransfer.files[0]);
}}
>
<div className="text-center w-full">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-blue-400" />
<div className="mt-4 text-sm text-blue-700">
<span>Drag and drop a new image</span>
</div>
<p className="text-xs text-blue-600 mt-2">PNG, JPG up to 10MB</p>
{pictureFile && (
<p className="mt-2 text-xs text-blue-900 font-medium">{pictureFile.name}</p>
)}
{!pictureFile && item.pictureUrl && (
<img src={item.pictureUrl} alt={item.title} className="mt-4 h-40 w-full object-cover rounded-xl ring-1 ring-gray-200" />
)}
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={e => setPictureFile(e.target.files?.[0])}
/>
</div>
</div>
<div className="flex items-center justify-end gap-x-4">
<Link href="/admin/subscriptions" className="text-sm font-medium text-blue-900 hover:text-blue-700">
Cancel
</Link>
<button type="submit" className="inline-flex justify-center rounded-lg bg-blue-900 px-5 py-3 text-sm font-semibold text-blue-50 shadow hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-900 transition">
Save Changes
</button>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
</form>
</div>
)}
</main>
</div>
</PageLayout>
);
}

View File

@ -99,6 +99,12 @@ export default function AdminSubscriptionsPage() {
> >
{item.state ? 'Disable' : 'Enable'} {item.state ? 'Disable' : 'Enable'}
</button> </button>
<Link
href={`/admin/subscriptions/edit/${item.id}`}
className="inline-flex items-center rounded-lg bg-indigo-50 px-4 py-2 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-200 hover:bg-indigo-100 shadow transition"
>
Edit
</Link>
<button <button
className="inline-flex items-center rounded-lg bg-red-50 px-4 py-2 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100 shadow transition" className="inline-flex items-center rounded-lg bg-red-50 px-4 py-2 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100 shadow transition"
onClick={() => setDeleteTarget(item)} onClick={() => setDeleteTarget(item)}