feat: edit subscription
This commit is contained in:
parent
d54a4024cb
commit
198e41e601
226
src/app/admin/subscriptions/edit/[id]/page.tsx
Normal file
226
src/app/admin/subscriptions/edit/[id]/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user