sharenet/frontend/src/app/products/page.tsx

224 lines
No EOL
6.4 KiB
TypeScript

/**
* This file is part of Sharenet.
*
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
*
* You may obtain a copy of the license at:
* https://creativecommons.org/licenses/by-nc-sa/4.0/
*
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
*/
'use client';
import { useState, useEffect } from 'react';
import { productApi, Product } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { ResponsiveTable } from '@/components/ui/responsive-table';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export default function ProductsPage() {
const [products, setProducts] = useState<Product[]>([]);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [formData, setFormData] = useState({
name: '',
description: '',
});
const [errors, setErrors] = useState({
name: '',
description: '',
});
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async () => {
try {
const response = await productApi.getAll();
setProducts(response.data);
} catch (error) {
console.error('Error loading products:', error);
}
};
const validateForm = () => {
const newErrors = {
name: '',
description: '',
};
// Product name cannot be empty
if (!formData.name.trim()) {
newErrors.name = 'Product name is required';
}
// Description can be empty, no validation needed
setErrors(newErrors);
return !newErrors.name;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
try {
if (editingProduct) {
await productApi.update(editingProduct.id, formData);
} else {
await productApi.create(formData);
}
setIsDialogOpen(false);
setEditingProduct(null);
setFormData({ name: '', description: '' });
setErrors({ name: '', description: '' });
loadProducts();
} catch (error) {
console.error('Error saving product:', error);
}
};
const handleEdit = (product: Product) => {
setEditingProduct(product);
setFormData({
name: product.name,
description: product.description,
});
setErrors({ name: '', description: '' });
setIsDialogOpen(true);
};
const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this product?')) {
try {
await productApi.delete(id);
loadProducts();
} catch (error) {
console.error('Error deleting product:', error);
}
}
};
const handleInputChange = (field: 'name' | 'description', value: string) => {
setFormData({ ...formData, [field]: value });
// Clear error when user starts typing
if (errors[field]) {
setErrors({ ...errors, [field]: '' });
}
};
const columns = [
{
key: 'name',
header: 'Name',
mobilePriority: true,
},
{
key: 'description',
header: 'Description',
mobilePriority: true,
render: (value: unknown) => String(value || 'No description'),
},
{
key: 'actions',
header: 'Actions',
mobilePriority: true,
render: (_: unknown, product: Record<string, unknown>) => (
<div className="flex flex-col sm:flex-row gap-2 sm:gap-1">
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(product as unknown as Product)}
className="w-full sm:w-auto"
>
Edit
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete((product as unknown as Product).id)}
className="w-full sm:w-auto"
>
Delete
</Button>
</div>
),
},
];
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 className="text-2xl font-bold">Products</h1>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
onClick={() => {
setEditingProduct(null);
setFormData({ name: '', description: '' });
setErrors({ name: '', description: '' });
}}
className="w-full sm:w-auto"
>
Add Product
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{editingProduct ? 'Edit Product' : 'Add Product'}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
className={errors.name ? 'border-red-500' : ''}
/>
{errors.name && (
<p className="text-red-500 text-sm">{errors.name}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
/>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-2">
<Button type="submit" className="flex-1">
{editingProduct ? 'Update' : 'Create'}
</Button>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
className="flex-1"
>
Cancel
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
<ResponsiveTable columns={columns} data={products as unknown as Record<string, unknown>[]} />
</div>
);
}