From 8968c974d4b643cbebad168c2ce6b3ca3e949aa6 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Sat, 8 Mar 2025 04:44:35 +0000 Subject: [PATCH] uh oh stinky --- app/dashboard/promotions/page.tsx | 29 ++ .../promotions/EditPromotionForm.tsx | 347 ++++++++++++++++++ .../dashboard/promotions/NewPromotionForm.tsx | 340 +++++++++++++++++ .../dashboard/promotions/PromotionsList.tsx | 286 +++++++++++++++ .../promotions/PromotionsPageSkeleton.tsx | 62 ++++ config/sidebar.ts | 28 +- lib/client-service.ts | 69 ++++ lib/types/promotion.ts | 28 ++ 8 files changed, 1180 insertions(+), 9 deletions(-) create mode 100644 app/dashboard/promotions/page.tsx create mode 100644 components/dashboard/promotions/EditPromotionForm.tsx create mode 100644 components/dashboard/promotions/NewPromotionForm.tsx create mode 100644 components/dashboard/promotions/PromotionsList.tsx create mode 100644 components/dashboard/promotions/PromotionsPageSkeleton.tsx create mode 100644 lib/client-service.ts create mode 100644 lib/types/promotion.ts diff --git a/app/dashboard/promotions/page.tsx b/app/dashboard/promotions/page.tsx new file mode 100644 index 0000000..407a1c0 --- /dev/null +++ b/app/dashboard/promotions/page.tsx @@ -0,0 +1,29 @@ +import { Suspense } from "react"; +import Dashboard from "@/components/dashboard/dashboard"; +import { Metadata } from "next"; +import PromotionsList from "@/components/dashboard/promotions/PromotionsList"; +import PromotionsPageSkeleton from "@/components/dashboard/promotions/PromotionsPageSkeleton"; + +export const metadata: Metadata = { + title: "Promotions | Ember Market", + description: "Manage promotion codes for your store on Ember Market" +}; + +export default function PromotionsPage() { + return ( + +
+
+

Promotions

+

+ Create and manage promotional codes and discounts for your store +

+
+ + }> + + +
+
+ ); +} \ No newline at end of file diff --git a/components/dashboard/promotions/EditPromotionForm.tsx b/components/dashboard/promotions/EditPromotionForm.tsx new file mode 100644 index 0000000..d88e678 --- /dev/null +++ b/components/dashboard/promotions/EditPromotionForm.tsx @@ -0,0 +1,347 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { Save, X, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; +import { toast } from '@/components/ui/use-toast'; +import { Promotion, PromotionFormData } from '@/lib/types/promotion'; +import { fetchClient } from '@/lib/client-service'; + +// Form schema validation with Zod (same as NewPromotionForm) +const formSchema = z.object({ + code: z.string() + .min(3, 'Code must be at least 3 characters') + .max(20, 'Code cannot exceed 20 characters') + .regex(/^[A-Za-z0-9]+$/, 'Only letters and numbers are allowed'), + discountType: z.enum(['percentage', 'fixed']), + discountValue: z.coerce.number() + .positive('Discount value must be positive') + .refine(val => val <= 100, { + message: 'Percentage discount cannot exceed 100%', + path: ['discountValue'], + // Only validate this rule if discount type is percentage + params: { type: 'percentage' }, + }), + minOrderAmount: z.coerce.number() + .min(0, 'Minimum order amount cannot be negative'), + maxUsage: z.coerce.number() + .nullable() + .optional(), + isActive: z.boolean().default(true), + startDate: z.string().optional(), + endDate: z.string().nullable().optional(), + description: z.string().max(200, 'Description cannot exceed 200 characters').optional(), +}); + +interface EditPromotionFormProps { + promotion: Promotion; + onSuccess: () => void; + onCancel: () => void; +} + +export default function EditPromotionForm({ promotion, onSuccess, onCancel }: EditPromotionFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + + // Format dates from ISO to YYYY-MM-DD for input elements + const formatDateForInput = (dateString: string | null) => { + if (!dateString) return ''; + return new Date(dateString).toISOString().split('T')[0]; + }; + + // Initialize form with promotion values + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + code: promotion.code, + discountType: promotion.discountType, + discountValue: promotion.discountValue, + minOrderAmount: promotion.minOrderAmount, + maxUsage: promotion.maxUsage, + isActive: promotion.isActive, + description: promotion.description, + startDate: formatDateForInput(promotion.startDate), + endDate: formatDateForInput(promotion.endDate), + }, + }); + + // Form submission handler + async function onSubmit(data: z.infer) { + setIsSubmitting(true); + + try { + await fetchClient(`/promotions/${promotion._id}`, { + method: 'PUT', + body: data, + }); + + toast({ + title: 'Success', + description: 'Promotion updated successfully', + }); + + onSuccess(); + } catch (error) { + console.error('Error updating promotion:', error); + // Error toast is already shown by fetchClient + } finally { + setIsSubmitting(false); + } + } + + return ( +
+ +
+ ( + + Promotion Code + + field.onChange(e.target.value.toUpperCase())} + /> + + + Enter a unique code for your promotion. Only letters and numbers. + + + + )} + /> +
+ +
+ ( + + Discount Type + + + + )} + /> + + ( + + Discount Value + + + + + {form.watch('discountType') === 'percentage' + ? 'Enter a percentage (1-100%)' + : 'Enter an amount in £'} + + + + )} + /> + + ( + + Minimum Order Amount (£) + + + + + Minimum purchase required + + + + )} + /> +
+ +
+ ( + + Maximum Usage Count + + { + const value = e.target.value; + field.onChange(value === '' ? null : parseInt(value, 10)); + }} + /> + + + Leave empty for unlimited usage (currently used: {promotion.usageCount} times) + + + + )} + /> + + ( + + Start Date + + + + + + )} + /> + + ( + + End Date (Optional) + + { + const value = e.target.value; + field.onChange(value === '' ? null : value); + }} + /> + + + Leave empty for no expiration + + + + )} + /> +
+ + ( + +
+ Active Status + + Enable or disable this promotion + +
+ + + +
+ )} + /> + + ( + + Description (Optional) + +