diff --git a/contributors/dwivediprashant/client/app/api/subscriptions/route.ts b/contributors/dwivediprashant/client/app/api/subscriptions/route.ts index 860cde9..ee3ed44 100644 --- a/contributors/dwivediprashant/client/app/api/subscriptions/route.ts +++ b/contributors/dwivediprashant/client/app/api/subscriptions/route.ts @@ -55,6 +55,66 @@ export async function GET() { } } +export async function POST(request: Request) { + try { + const body = await request.json(); + + // Validate required fields + const requiredFields = [ + "name", + "amount", + "billingCycle", + "renewalDate", + "category", + ]; + const missingFields = requiredFields.filter((field) => !body[field]); + + if (missingFields.length > 0) { + return NextResponse.json( + { + success: false, + message: `Missing required fields: ${missingFields.join(", ")}`, + }, + { status: 400 } + ); + } + + // Forward the request to your backend server + const response = await fetch("http://localhost:5000/api/subscriptions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + const result = await response.json(); + + if (!response.ok) { + return NextResponse.json(result, { + status: response.status, + }); + } + + // Return the backend response + return NextResponse.json(result, { + status: 201, + headers: { + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("API Error:", error); + return NextResponse.json( + { + success: false, + message: "Failed to create subscription", + }, + { status: 500 } + ); + } +} + // Helper functions to map backend data to frontend format function getCategoryFromName(serviceName: string): string { const categories: { [key: string]: string } = { diff --git a/contributors/dwivediprashant/client/app/components/Modal.tsx b/contributors/dwivediprashant/client/app/components/Modal.tsx new file mode 100644 index 0000000..dc6eda4 --- /dev/null +++ b/contributors/dwivediprashant/client/app/components/Modal.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { useEffect } from "react"; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: React.ReactNode; +} + +export default function Modal({ + isOpen, + onClose, + title, + children, +}: ModalProps) { + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("keydown", handleEscape); + document.body.style.overflow = "hidden"; + } + + return () => { + document.removeEventListener("keydown", handleEscape); + document.body.style.overflow = "unset"; + }; + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal Container */} +
+
e.stopPropagation()} + > + {/* Header */} +
+

{title}

+ +
+ + {/* Content */} +
{children}
+
+
+
+ ); +} diff --git a/contributors/dwivediprashant/client/app/components/Sidebar.tsx b/contributors/dwivediprashant/client/app/components/Sidebar.tsx index a00c1ad..e6fe1ee 100644 --- a/contributors/dwivediprashant/client/app/components/Sidebar.tsx +++ b/contributors/dwivediprashant/client/app/components/Sidebar.tsx @@ -24,11 +24,6 @@ export default function Sidebar({ activePage }: SidebarProperties) { { name: "Analytics", href: "/analytics" }, ]; - const accountItems = [ - { name: "Settings", href: "/settings" }, - { name: "Profile", href: "/profile" }, - ]; - return ( <> {/* Mobile backdrop */} @@ -96,37 +91,7 @@ export default function Sidebar({ activePage }: SidebarProperties) { {item.name} ))} - -
- {accountItems.map((item) => ( - setSidebarOpen(false)} - > - {item.name} - - ))} -
- - {/* User Section */} -
-
-
- U -
-
-
User
-
user@example.com
-
-
-
{/* Mobile menu button */} diff --git a/contributors/dwivediprashant/client/app/components/SubscriptionForm.tsx b/contributors/dwivediprashant/client/app/components/SubscriptionForm.tsx new file mode 100644 index 0000000..8547ae4 --- /dev/null +++ b/contributors/dwivediprashant/client/app/components/SubscriptionForm.tsx @@ -0,0 +1,452 @@ +"use client"; + +import { useState } from "react"; + +interface FormData { + name: string; + amount: string; + billingCycle: string; + renewalDate: string; + category: string; + notes: string; + source: string; + isTrial: boolean; +} + +interface FormErrors { + name?: string; + amount?: string; + renewalDate?: string; + category?: string; + source?: string; + general?: string; +} + +export default function SubscriptionForm({ + onSuccess, +}: { + onSuccess: () => void; +}) { + const [formData, setFormData] = useState({ + name: "", + amount: "", + billingCycle: "monthly", + renewalDate: "", + category: "other", + isTrial: false, + source: "manual", + notes: "", + }); + + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + + const validateForm = (): boolean => { + const newErrors: FormErrors = {}; + + if (!formData.name.trim()) { + newErrors.name = "Subscription name is required"; + } + + if (!formData.amount || parseFloat(formData.amount) <= 0) { + newErrors.amount = "Amount must be greater than 0"; + } + + if (!formData.renewalDate) { + newErrors.renewalDate = "Renewal date is required"; + } else { + const renewalDate = new Date(formData.renewalDate); + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (renewalDate < today) { + newErrors.renewalDate = "Renewal date cannot be in the past"; + } + } + + if (!formData.category) { + newErrors.category = "Category is required"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleInputChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement + > + ) => { + const { name, value, type } = e.target; + + if (type === "checkbox") { + const checked = (e.target as HTMLInputElement).checked; + setFormData((prev) => ({ ...prev, [name]: checked })); + } else { + setFormData((prev) => ({ ...prev, [name]: value })); + } + + // Clear error for this field when user starts typing + if (errors[name as keyof FormErrors]) { + setErrors((prev) => ({ ...prev, [name]: undefined })); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + setErrors({}); + + try { + const response = await fetch("/api/subscriptions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...formData, + amount: parseFloat(formData.amount), + notes: formData.notes || "", // Always include notes field + }), + }); + + const data = await response.json(); + + if (!response.ok) { + if (data.message) { + setErrors({ general: data.message }); + } else { + setErrors({ + general: "Failed to create subscription. Please try again.", + }); + } + return; + } + + setIsSuccess(true); + setTimeout(() => { + setIsSuccess(false); + onSuccess(); + // Reset form + setFormData({ + name: "", + amount: "", + billingCycle: "monthly", + renewalDate: "", + category: "other", + isTrial: false, + source: "manual", + notes: "", + }); + }, 2000); + } catch (error) { + console.error("Error creating subscription:", error); + setErrors({ + general: "Network error. Please check your connection and try again.", + }); + } finally { + setIsSubmitting(false); + } + }; + + const categories = [ + { value: "entertainment", label: "Entertainment" }, + { value: "productivity", label: "Productivity" }, + { value: "utilities", label: "Utilities" }, + { value: "education", label: "Education" }, + { value: "health", label: "Health & Wellness" }, + { value: "finance", label: "Finance" }, + { value: "other", label: "Other" }, + ]; + + return ( +
+ {isSuccess && ( +
+
+ + + + Subscription created successfully! +
+
+ )} + + {errors.general && ( +
+
+ + + + {errors.general} +
+
+ )} + +
+
+ {/* Name */} +
+ + + {errors.name && ( +

{errors.name}

+ )} +
+ + {/* Amount */} +
+ + + {errors.amount && ( +

{errors.amount}

+ )} +
+ + {/* Billing Cycle */} +
+ + +
+ + {/* Renewal Date */} +
+ + + {errors.renewalDate && ( +

{errors.renewalDate}

+ )} +
+ + {/* Category */} +
+ + + {errors.category && ( +

{errors.category}

+ )} +
+ + {/* Source */} +
+ + +
+
+ + {/* Trial Toggle */} +
+ + +
+ + {/* Notes */} +
+ +