diff --git a/app/(dashboard)/admin/ads/[id]/page.tsx b/app/(dashboard)/admin/ads/[id]/page.tsx new file mode 100644 index 0000000..2c3eadc --- /dev/null +++ b/app/(dashboard)/admin/ads/[id]/page.tsx @@ -0,0 +1,125 @@ +// ********************* +// Role of the page: Admin page for editing existing advertisement +// Name: Edit Ad Page +// Developer: AI Assistant +// Version: 1.0 +// Route: /admin/ads/[id] +// ********************* + +"use client"; + +import React, { useEffect, useState } from "react"; +import { DashboardSidebar, AdsForm } from "@/components"; +import { useRouter, useParams } from "next/navigation"; +import toast from "react-hot-toast"; +import { FaArrowLeft, FaSpinner } from "react-icons/fa"; + +interface Ad { + id: string; + image: string; + name: string; + title: string; + desc: string; + startDate: string; + endDate: string; + isShow: boolean; +} + +const EditAdPage = () => { + const router = useRouter(); + const params = useParams(); + const [ad, setAd] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchAd = async () => { + try { + const response = await fetch(`/api/ads/${params.id}`); + if (!response.ok) throw new Error("Failed to fetch ad"); + const data = await response.json(); + setAd(data); + } catch (error) { + console.error("Error fetching ad:", error); + toast.error("Failed to load advertisement"); + router.push("/admin/ads"); + } finally { + setLoading(false); + } + }; + + if (params.id) { + fetchAd(); + } + }, [params.id, router]); + + const handleSubmit = async (adData: any) => { + try { + const response = await fetch(`/api/ads/${params.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(adData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to update ad"); + } + + toast.success("Advertisement updated successfully!"); + router.push("/admin/ads"); + } catch (error: any) { + console.error("Error updating ad:", error); + toast.error(error.message || "Failed to update advertisement"); + } + }; + + const handleCancel = () => { + router.push("/admin/ads"); + }; + + if (loading) { + return ( +
+ +
+ +
+
+ ); + } + + return ( +
+ +
+ {/* Header */} +
+ +

+ Edit Advertisement +

+

+ Update the details of this advertisement +

+
+ + {/* Form */} +
+ {ad && ( + + )} +
+
+
+ ); +}; + +export default EditAdPage; diff --git a/app/(dashboard)/admin/ads/new/page.tsx b/app/(dashboard)/admin/ads/new/page.tsx new file mode 100644 index 0000000..659cdd3 --- /dev/null +++ b/app/(dashboard)/admin/ads/new/page.tsx @@ -0,0 +1,77 @@ +// ********************* +// Role of the page: Admin page for creating new advertisement +// Name: Create New Ad Page +// Developer: AI Assistant +// Version: 1.0 +// Route: /admin/ads/new +// ********************* + +"use client"; + +import React from "react"; +import { DashboardSidebar, AdsForm } from "@/components"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { FaArrowLeft } from "react-icons/fa"; + +const CreateAdPage = () => { + const router = useRouter(); + + const handleSubmit = async (adData: any) => { + try { + const response = await fetch("/api/ads", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(adData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to create ad"); + } + + toast.success("Advertisement created successfully!"); + router.push("/admin/ads"); + } catch (error: any) { + console.error("Error creating ad:", error); + toast.error(error.message || "Failed to create advertisement"); + } + }; + + const handleCancel = () => { + router.push("/admin/ads"); + }; + + return ( +
+ +
+ {/* Header */} +
+ +

+ Create New Advertisement +

+

+ Fill in the details to create a new promotional ad +

+
+ + {/* Form */} +
+ +
+
+
+ ); +}; + +export default CreateAdPage; diff --git a/app/(dashboard)/admin/ads/page.tsx b/app/(dashboard)/admin/ads/page.tsx new file mode 100644 index 0000000..29d2de6 --- /dev/null +++ b/app/(dashboard)/admin/ads/page.tsx @@ -0,0 +1,211 @@ +// ********************* +// Role of the page: Admin page for managing advertisements +// Name: Ads Management Page +// Developer: AI Assistant +// Version: 1.0 +// Route: /admin/ads +// ********************* + +"use client"; + +import React, { useEffect, useState } from "react"; +import { DashboardSidebar, AdsTable } from "@/components"; +import { FaPlus, FaSpinner } from "react-icons/fa"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; + +interface Ad { + id: string; + image: string; + name: string; + title: string; + desc: string; + startDate: string; + endDate: string; + isShow: boolean; + createdAt: string; + updatedAt: string; +} + +const AdsManagementPage = () => { + const router = useRouter(); + const [ads, setAds] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState< + "all" | "active" | "scheduled" | "expired" + >("all"); + + const fetchAds = async () => { + try { + setLoading(true); + const response = await fetch("/api/ads"); + if (!response.ok) throw new Error("Failed to fetch ads"); + const data = await response.json(); + setAds(data); + } catch (error) { + console.error("Error fetching ads:", error); + toast.error("Failed to load advertisements"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchAds(); + }, []); + + const handleDelete = async (id: string) => { + try { + const response = await fetch(`/api/ads/${id}`, { + method: "DELETE", + }); + + if (!response.ok) throw new Error("Failed to delete ad"); + + toast.success("Ad deleted successfully!"); + fetchAds(); // Refresh list + } catch (error) { + console.error("Error deleting ad:", error); + toast.error("Failed to delete ad"); + } + }; + + const handleEdit = (id: string) => { + router.push(`/admin/ads/${id}`); + }; + + const handleToggleShow = async (id: string, currentStatus: boolean) => { + try { + const response = await fetch(`/api/ads/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ isShow: !currentStatus }), + }); + + if (!response.ok) throw new Error("Failed to update ad"); + + toast.success(`Ad ${!currentStatus ? "shown" : "hidden"} successfully!`); + fetchAds(); // Refresh list + } catch (error) { + console.error("Error toggling ad visibility:", error); + toast.error("Failed to update ad"); + } + }; + + const getFilteredAds = () => { + const now = new Date(); + switch (filter) { + case "active": + return ads.filter( + (ad) => + ad.isShow && + new Date(ad.startDate) <= now && + new Date(ad.endDate) >= now + ); + case "scheduled": + return ads.filter((ad) => new Date(ad.startDate) > now); + case "expired": + return ads.filter((ad) => new Date(ad.endDate) < now); + default: + return ads; + } + }; + + const filteredAds = getFilteredAds(); + + return ( +
+ +
+ {/* Header */} +
+
+
+

+ Advertisement Management +

+

+ Create and manage your promotional advertisements +

+
+ +
+ + {/* Stats Cards */} +
+
+

Total Ads

+

{ads.length}

+
+
+

Active

+

+ { + ads.filter( + (ad) => + ad.isShow && + new Date(ad.startDate) <= new Date() && + new Date(ad.endDate) >= new Date() + ).length + } +

+
+
+

Scheduled

+

+ {ads.filter((ad) => new Date(ad.startDate) > new Date()).length} +

+
+
+

Expired

+

+ {ads.filter((ad) => new Date(ad.endDate) < new Date()).length} +

+
+
+
+ + {/* Filters */} +
+ {["all", "active", "scheduled", "expired"].map((filterOption) => ( + + ))} +
+ + {/* Content */} + {loading ? ( +
+ +
+ ) : ( + + )} +
+
+ ); +}; + +export default AdsManagementPage; diff --git a/app/api/ads/[id]/route.ts b/app/api/ads/[id]/route.ts new file mode 100644 index 0000000..f6ef45b --- /dev/null +++ b/app/api/ads/[id]/route.ts @@ -0,0 +1,112 @@ +// ********************* +// API Routes for Individual Ad +// GET: Fetch single ad by ID +// PUT: Update ad by ID +// DELETE: Delete ad by ID +// ********************* + +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +// GET single ad by ID +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const ad = await prisma.ads.findUnique({ + where: { id: params.id }, + }); + + if (!ad) { + return NextResponse.json({ error: "Ad not found" }, { status: 404 }); + } + + return NextResponse.json(ad, { status: 200 }); + } catch (error) { + console.error("Error fetching ad:", error); + return NextResponse.json({ error: "Failed to fetch ad" }, { status: 500 }); + } +} + +// PUT update ad by ID +export async function PUT( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const { image, name, title, desc, startDate, endDate, isShow } = body; + + // Check if ad exists + const existingAd = await prisma.ads.findUnique({ + where: { id: params.id }, + }); + + if (!existingAd) { + return NextResponse.json({ error: "Ad not found" }, { status: 404 }); + } + + // Validate dates if provided + if (startDate && endDate) { + const start = new Date(startDate); + const end = new Date(endDate); + + if (end <= start) { + return NextResponse.json( + { error: "End date must be after start date" }, + { status: 400 } + ); + } + } + + const updatedAd = await prisma.ads.update({ + where: { id: params.id }, + data: { + ...(image && { image }), + ...(name && { name }), + ...(title && { title }), + ...(desc && { desc }), + ...(startDate && { startDate: new Date(startDate) }), + ...(endDate && { endDate: new Date(endDate) }), + ...(isShow !== undefined && { isShow }), + }, + }); + + return NextResponse.json(updatedAd, { status: 200 }); + } catch (error) { + console.error("Error updating ad:", error); + return NextResponse.json({ error: "Failed to update ad" }, { status: 500 }); + } +} + +// DELETE ad by ID +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + // Check if ad exists + const existingAd = await prisma.ads.findUnique({ + where: { id: params.id }, + }); + + if (!existingAd) { + return NextResponse.json({ error: "Ad not found" }, { status: 404 }); + } + + await prisma.ads.delete({ + where: { id: params.id }, + }); + + return NextResponse.json( + { message: "Ad deleted successfully" }, + { status: 200 } + ); + } catch (error) { + console.error("Error deleting ad:", error); + return NextResponse.json({ error: "Failed to delete ad" }, { status: 500 }); + } +} diff --git a/app/api/ads/route.ts b/app/api/ads/route.ts new file mode 100644 index 0000000..9b9d944 --- /dev/null +++ b/app/api/ads/route.ts @@ -0,0 +1,88 @@ +// ********************* +// API Routes for Ads Management +// GET: Fetch all ads (with optional filters) +// POST: Create new ad +// ********************* + +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +// GET all ads +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const isShow = searchParams.get("isShow"); + const active = searchParams.get("active"); // Filter for active ads (current date between start and end) + + let whereClause: any = {}; + + if (isShow !== null) { + whereClause.isShow = isShow === "true"; + } + + if (active === "true") { + const now = new Date(); + whereClause.startDate = { lte: now }; + whereClause.endDate = { gte: now }; + whereClause.isShow = true; + } + + const ads = await prisma.ads.findMany({ + where: whereClause, + orderBy: { + createdAt: "desc", + }, + }); + + return NextResponse.json(ads, { status: 200 }); + } catch (error) { + console.error("Error fetching ads:", error); + return NextResponse.json({ error: "Failed to fetch ads" }, { status: 500 }); + } +} + +// POST create new ad +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { image, name, title, desc, startDate, endDate, isShow } = body; + + // Validation + if (!image || !name || !title || !desc || !startDate || !endDate) { + return NextResponse.json( + { error: "All fields are required" }, + { status: 400 } + ); + } + + // Validate dates + const start = new Date(startDate); + const end = new Date(endDate); + + if (end <= start) { + return NextResponse.json( + { error: "End date must be after start date" }, + { status: 400 } + ); + } + + const newAd = await prisma.ads.create({ + data: { + image, + name, + title, + desc, + startDate: start, + endDate: end, + isShow: isShow !== undefined ? isShow : true, + }, + }); + + return NextResponse.json(newAd, { status: 201 }); + } catch (error) { + console.error("Error creating ad:", error); + return NextResponse.json({ error: "Failed to create ad" }, { status: 500 }); + } +} diff --git a/app/api/ads/upload/route.ts b/app/api/ads/upload/route.ts new file mode 100644 index 0000000..8d2f534 --- /dev/null +++ b/app/api/ads/upload/route.ts @@ -0,0 +1,85 @@ +// ********************* +// API Route for uploading ad images +// POST /api/ads/upload - Upload image file +// ********************* + +import { NextRequest, NextResponse } from "next/server"; +import { writeFile } from "fs/promises"; +import { join } from "path"; +import { existsSync, mkdirSync } from "fs"; + +export async function POST(request: NextRequest) { + try { + const formData = await request.formData(); + const file = formData.get("file") as File; + + if (!file) { + return NextResponse.json({ error: "No file provided" }, { status: 400 }); + } + + // Validate file type + const allowedTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/webp", + "image/gif", + ]; + if (!allowedTypes.includes(file.type)) { + return NextResponse.json( + { + error: "Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed", + }, + { status: 400 } + ); + } + + // Validate file size (max 5MB) + const maxSize = 5 * 1024 * 1024; // 5MB + if (file.size > maxSize) { + return NextResponse.json( + { error: "File size too large. Maximum size is 5MB" }, + { status: 400 } + ); + } + + // Generate unique filename + const timestamp = Date.now(); + const randomString = Math.random().toString(36).substring(2, 8); + const extension = file.name.split(".").pop(); + const filename = `ad_${timestamp}_${randomString}.${extension}`; + + // Ensure upload directory exists + const uploadDir = join(process.cwd(), "public", "uploads", "ads"); + if (!existsSync(uploadDir)) { + mkdirSync(uploadDir, { recursive: true }); + } + + // Convert file to buffer and save + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + const filepath = join(uploadDir, filename); + + await writeFile(filepath, buffer); + + // Return the public URL + const imageUrl = `/uploads/ads/${filename}`; + + return NextResponse.json( + { + success: true, + imageUrl, + filename, + size: file.size, + type: file.type, + }, + { status: 200 } + ); + } catch (error) { + console.error("Error uploading file:", error); + return NextResponse.json( + { error: "Failed to upload file" }, + { status: 500 } + ); + } +} diff --git a/app/globals.css b/app/globals.css index f8239e2..8483abb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,24 +2,54 @@ @tailwind components; @tailwind utilities; +.range { + --range-shdw: #3b82f6; +} + +.checkbox:checked, +.checkbox[checked="true"], +.checkbox[aria-checked="true"] { + background-repeat: no-repeat; + animation: checkmark var(--animation-input, 0.2s) ease-out; + background-color: #3b82f6; + background-image: linear-gradient(-45deg, transparent 65%, #3b82f6 65.99%), + linear-gradient(45deg, transparent 75%, #3b82f6 75.99%), + linear-gradient(-45deg, #3b82f6 40%, transparent 40.99%), + linear-gradient( + 45deg, + #3b82f6 30%, + white 30.99%, + white 40%, + transparent 40.99% + ), + linear-gradient(-45deg, white 50%, #3b82f6 50.99%); +} + +/* Hero Carousel Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} -.range{ - --range-shdw: #3B82F6; +.animate-fadeInUp { + animation: fadeInUp 0.8s ease-out forwards; } -.checkbox:checked, .checkbox[checked="true"], .checkbox[aria-checked="true"] { - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-out; - background-color: #3B82F6; - background-image: linear-gradient(-45deg, transparent 65%, #3B82F6 65.99%), - linear-gradient(45deg, transparent 75%, #3B82F6 75.99%), - linear-gradient(-45deg, #3B82F6 40%, transparent 40.99%), - linear-gradient( - 45deg, - #3B82F6 30%, - white 30.99%, - white 40%, - transparent 40.99% - ), - linear-gradient(-45deg, white 50%, #3B82F6 50.99%); -} \ No newline at end of file +.animate-fadeIn { + animation: fadeIn 0.6s ease-out forwards; +} diff --git a/app/page.tsx b/app/page.tsx index 6fc649a..0a837d6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,19 @@ -import { CategoryMenu, Hero, Incentives, IntroducingSection, Newsletter, ProductsSection } from "@/components"; +import { + CategoryMenu, + Hero, + Incentives, + IntroducingSection, + Newsletter, + ProductsSection, +} from "@/components"; export default function Home() { return ( <> - - - - + + + + ); } diff --git a/components/AdsBanner.tsx b/components/AdsBanner.tsx new file mode 100644 index 0000000..af4e311 --- /dev/null +++ b/components/AdsBanner.tsx @@ -0,0 +1,152 @@ +// ********************* +// Role of the component: Display active advertisements banner +// Name of the component: AdsBanner.tsx +// Developer: AI Assistant +// Version: 1.0 +// Component call: +// Input parameters: no input parameters +// Output: Carousel/Banner of active advertisements +// ********************* + +"use client"; + +import React, { useEffect, useState } from "react"; +import Image from "next/image"; +import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; + +interface Ad { + id: string; + image: string; + name: string; + title: string; + desc: string; + startDate: string; + endDate: string; + isShow: boolean; +} + +const AdsBanner = () => { + const [ads, setAds] = useState([]); + const [currentIndex, setCurrentIndex] = useState(0); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchActiveAds = async () => { + try { + const response = await fetch("/api/ads?active=true"); + if (!response.ok) throw new Error("Failed to fetch ads"); + const data = await response.json(); + setAds(data); + } catch (error) { + console.error("Error fetching ads:", error); + } finally { + setLoading(false); + } + }; + + fetchActiveAds(); + }, []); + + useEffect(() => { + if (ads.length > 1) { + const interval = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % ads.length); + }, 5000); // Auto-slide every 5 seconds + + return () => clearInterval(interval); + } + }, [ads.length]); + + const handlePrev = () => { + setCurrentIndex((prev) => (prev === 0 ? ads.length - 1 : prev - 1)); + }; + + const handleNext = () => { + setCurrentIndex((prev) => (prev + 1) % ads.length); + }; + + if (loading || ads.length === 0) { + return null; // Don't show anything if no active ads + } + + return ( +
+ {/* Ads Slides */} +
+ {ads.map((ad) => ( +
+ {ad.name} + {/* Overlay Gradient */} +
+ + {/* Content */} +
+
+

+ {ad.title} +

+

+ {ad.desc} +

+
+ + +
+
+
+
+ ))} +
+ + {/* Navigation Arrows */} + {ads.length > 1 && ( + <> + + + + )} + + {/* Dots Indicator */} + {ads.length > 1 && ( +
+ {ads.map((_, index) => ( +
+ )} +
+ ); +}; + +export default AdsBanner; diff --git a/components/AdsForm.tsx b/components/AdsForm.tsx new file mode 100644 index 0000000..118f52e --- /dev/null +++ b/components/AdsForm.tsx @@ -0,0 +1,501 @@ +// ********************* +// Role of the component: Form for creating and editing ads with file upload +// Name of the component: AdsForm.tsx +// Developer: AI Assistant +// Version: 2.0 +// Component call: +// Input parameters: ad (optional for edit), onSubmit function, onCancel function +// Output: Form with file upload, validation and submission +// ********************* + +"use client"; + +import React, { useState, useEffect, useRef } from "react"; +import { + FaImage, + FaSave, + FaTimes, + FaSpinner, + FaUpload, + FaCloudUploadAlt, +} from "react-icons/fa"; +import Image from "next/image"; +import toast from "react-hot-toast"; + +interface Ad { + id?: string; + image: string; + name: string; + title: string; + desc: string; + startDate: string; + endDate: string; + isShow: boolean; +} + +interface AdsFormProps { + ad?: Ad; + onSubmit: (ad: Omit) => Promise; + onCancel: () => void; +} + +const AdsForm: React.FC = ({ ad, onSubmit, onCancel }) => { + const [loading, setLoading] = useState(false); + const [uploading, setUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [errors, setErrors] = useState>({}); + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(ad?.image || ""); + const [dragActive, setDragActive] = useState(false); + const fileInputRef = useRef(null); + + const [formData, setFormData] = useState>({ + image: ad?.image || "", + name: ad?.name || "", + title: ad?.title || "", + desc: ad?.desc || "", + startDate: ad?.startDate + ? new Date(ad.startDate).toISOString().split("T")[0] + : "", + endDate: ad?.endDate + ? new Date(ad.endDate).toISOString().split("T")[0] + : "", + isShow: ad?.isShow !== undefined ? ad.isShow : true, + }); + + const validate = (): boolean => { + const newErrors: Record = {}; + + if (!formData.image && !selectedFile) { + newErrors.image = "Image is required"; + } + if (!formData.name) newErrors.name = "Name is required"; + if (!formData.title) newErrors.title = "Title is required"; + if (!formData.desc) newErrors.desc = "Description is required"; + if (!formData.startDate) newErrors.startDate = "Start date is required"; + if (!formData.endDate) newErrors.endDate = "End date is required"; + + if (formData.startDate && formData.endDate) { + const start = new Date(formData.startDate); + const end = new Date(formData.endDate); + if (end <= start) { + newErrors.endDate = "End date must be after start date"; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + validateAndSetFile(file); + } + }; + + const validateAndSetFile = (file: File) => { + // Validate file type + const allowedTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/webp", + "image/gif", + ]; + if (!allowedTypes.includes(file.type)) { + toast.error( + "Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed" + ); + return; + } + + // Validate file size (max 5MB) + const maxSize = 5 * 1024 * 1024; // 5MB + if (file.size > maxSize) { + toast.error("File size too large. Maximum size is 5MB"); + return; + } + + setSelectedFile(file); + + // Create preview URL + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(file); + + // Clear error + if (errors.image) { + setErrors((prev) => ({ ...prev, image: "" })); + } + }; + + const handleDrag = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive(true); + } else if (e.type === "dragleave") { + setDragActive(false); + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragActive(false); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + validateAndSetFile(e.dataTransfer.files[0]); + } + }; + + const uploadImage = async (): Promise => { + if (!selectedFile) { + return formData.image; // Return existing image URL if no new file + } + + setUploading(true); + setUploadProgress(0); + + try { + const uploadFormData = new FormData(); + uploadFormData.append("file", selectedFile); + + // Simulate progress (since fetch doesn't support real progress tracking) + const progressInterval = setInterval(() => { + setUploadProgress((prev) => { + if (prev >= 90) { + clearInterval(progressInterval); + return 90; + } + return prev + 10; + }); + }, 100); + + const response = await fetch("/api/ads/upload", { + method: "POST", + body: uploadFormData, + }); + + clearInterval(progressInterval); + setUploadProgress(100); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to upload image"); + } + + const data = await response.json(); + return data.imageUrl; + } catch (error: any) { + console.error("Error uploading image:", error); + toast.error(error.message || "Failed to upload image"); + throw error; + } finally { + setUploading(false); + setUploadProgress(0); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validate()) return; + + setLoading(true); + try { + // Upload image first if there's a new file + let imageUrl = formData.image; + if (selectedFile) { + imageUrl = await uploadImage(); + } + + // Submit form with image URL + await onSubmit({ + ...formData, + image: imageUrl, + }); + } catch (error) { + console.error("Error submitting form:", error); + } finally { + setLoading(false); + } + }; + + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, value, type } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: + type === "checkbox" ? (e.target as HTMLInputElement).checked : value, + })); + // Clear error when user starts typing + if (errors[name]) { + setErrors((prev) => ({ ...prev, [name]: "" })); + } + }; + + return ( +
+ {/* Image Preview */} + {previewUrl && ( +
+ Ad preview + {selectedFile && ( +
+ New Image Selected +
+ )} + +
+ )} + + {/* File Upload Area */} +
+ + + {/* Drag and Drop Area */} +
+ + +
+
+ +
+ +
+

+ {dragActive ? "Drop image here" : "Drag & drop your image here"} +

+

or

+ +
+ +

+ Supported: JPG, PNG, WebP, GIF (Max 5MB) +

+
+ + {uploading && ( +
+ +

Uploading...

+
+
+
+

{uploadProgress}%

+
+ )} +
+ + {errors.image && ( +

{errors.image}

+ )} +
+ + {/* Name and Title */} +
+
+ + + {errors.name && ( +

{errors.name}

+ )} +
+ +
+ + + {errors.title && ( +

{errors.title}

+ )} +
+
+ + {/* Description */} +
+ +