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 */}
+
+
+ {/* 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}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/contributors/dwivediprashant/client/app/page.tsx b/contributors/dwivediprashant/client/app/page.tsx
index b74376f..819bdae 100644
--- a/contributors/dwivediprashant/client/app/page.tsx
+++ b/contributors/dwivediprashant/client/app/page.tsx
@@ -1,14 +1,102 @@
+"use client";
+
+import { useState, useEffect } from "react";
import AppLayout from "./components/AppLayout";
+import { SubscriptionData } from "./subscriptions/page";
+import Modal from "./components/Modal";
+import SubscriptionForm from "./components/SubscriptionForm";
export default function Dashboard() {
+ const [subscriptionList, setSubscriptionList] = useState(
+ []
+ );
+ const [loadingState, setLoadingState] = useState(true);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ useEffect(() => {
+ fetchSubscriptionData();
+ }, []);
+
+ const fetchSubscriptionData = async () => {
+ try {
+ setLoadingState(true);
+ const response = await fetch("/api/subscriptions");
+ const data = await response.json();
+ setSubscriptionList(data);
+ } catch (error) {
+ console.error("Failed to fetch subscriptions:", error);
+ } finally {
+ setLoadingState(false);
+ }
+ };
+
+ const calculateStats = () => {
+ const activeSubscriptions = subscriptionList.filter(
+ (sub) => sub.serviceStatus === "active"
+ );
+ const trialSubscriptions = subscriptionList.filter(
+ (sub) => sub.serviceStatus === "trial"
+ );
+ const monthlySubscriptions = activeSubscriptions.filter(
+ (sub) => sub.billingInterval === "monthly"
+ );
+ const monthlyTotal = monthlySubscriptions.reduce(
+ (sum, sub) => sum + sub.cost,
+ 0
+ );
+
+ const today = new Date();
+ const sevenDaysFromNow = new Date();
+ sevenDaysFromNow.setDate(today.getDate() + 7);
+ const upcomingRenewals = subscriptionList.filter((sub) => {
+ const renewalDate = new Date(sub.upcomingRenewal);
+ return renewalDate >= today && renewalDate <= sevenDaysFromNow;
+ });
+
+ return {
+ monthlySpend: monthlyTotal,
+ activeCount: activeSubscriptions.length,
+ upcomingRenewalsCount: upcomingRenewals.length,
+ trialCount: trialSubscriptions.length,
+ };
+ };
+
+ const handleFormSuccess = () => {
+ setIsModalOpen(false);
+ fetchSubscriptionData();
+ };
+
+ const stats = calculateStats();
+
return (
{/* Header Section */}
-
-
Dashboard
-
- Welcome back! Here's an overview of your subscriptions.
-
+
+
+
Dashboard
+
+ Welcome back! Here's an overview of your subscriptions.
+
+
+
{/* Quick Stats */}
@@ -22,9 +110,11 @@ export default function Dashboard() {
💰
- $284.50
+
+ ${stats.monthlySpend.toFixed(2)}
+
- +12% from last month
+ From {stats.activeCount} active subscriptions
@@ -37,7 +127,9 @@ export default function Dashboard() {
✓
- 12
+
+ {stats.activeCount}
+
All active
@@ -50,7 +142,9 @@ export default function Dashboard() {
📅
- 3
+
+ {stats.upcomingRenewalsCount}
+
Next 7 days
@@ -63,10 +157,21 @@ export default function Dashboard() {
⏱
- 2
- Ending soon
+
+ {stats.trialCount}
+
+ Active trials
+
+ {/* Modal */}
+ setIsModalOpen(false)}
+ title="Add New Subscription"
+ >
+
+
);
}
diff --git a/contributors/dwivediprashant/client/app/subscriptions/add/page.tsx b/contributors/dwivediprashant/client/app/subscriptions/add/page.tsx
new file mode 100644
index 0000000..c72fa34
--- /dev/null
+++ b/contributors/dwivediprashant/client/app/subscriptions/add/page.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useState } from "react";
+import AppLayout from "../../components/AppLayout";
+import SubscriptionForm from "../../components/SubscriptionForm";
+import { useRouter } from "next/navigation";
+
+export default function AddSubscription() {
+ const router = useRouter();
+ const [showForm, setShowForm] = useState(true);
+
+ const handleSuccess = () => {
+ // Navigate back to subscriptions list
+ router.push("/subscriptions");
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+ Add New Subscription
+
+
+ Track your recurring expenses and never miss a renewal
+
+
+
+
+
+ {/* Form Container */}
+ {showForm &&
}
+
+
+
+ );
+}
diff --git a/contributors/dwivediprashant/client/app/subscriptions/page.tsx b/contributors/dwivediprashant/client/app/subscriptions/page.tsx
index 8e86b90..c25b17d 100644
--- a/contributors/dwivediprashant/client/app/subscriptions/page.tsx
+++ b/contributors/dwivediprashant/client/app/subscriptions/page.tsx
@@ -6,6 +6,8 @@ import SubscriptionCard from "../components/SubscriptionCard";
import FilterControls from "../components/FilterControls";
import EmptyState from "../components/EmptyState";
import Loader from "../utils/Loader";
+import Modal from "../components/Modal";
+import SubscriptionForm from "../components/SubscriptionForm";
export interface SubscriptionData {
identifier: string;
@@ -24,7 +26,7 @@ export interface SubscriptionData {
export interface FilterConfiguration {
statusFilter: string;
cycleFilter: string;
- sortBy: "renewalDate" | "cost";
+ sortBy: "renewalDate" | "cost" | "serviceCategory";
sortOrder: "asc" | "desc";
}
@@ -39,6 +41,7 @@ export default function Subscriptions() {
sortBy: "renewalDate",
sortOrder: "asc",
});
+ const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
fetchSubscriptionData();
@@ -61,6 +64,11 @@ export default function Subscriptions() {
}
};
+ const handleFormSuccess = () => {
+ setIsModalOpen(false);
+ fetchSubscriptionData(); // Refresh the list
+ };
+
const getFilteredAndSortedSubscriptions = () => {
let filtered = subscriptionList.filter((subscription) => {
const statusMatch =
@@ -128,12 +136,35 @@ export default function Subscriptions() {
return (
-
- Subscription Management
-
-
- Monitor and manage all your active subscriptions
-
+
+
+
+ Subscription Management
+
+
+ Monitor and manage all your active subscriptions
+
+
+
+
)}
+
+ {/* Modal */}
+ setIsModalOpen(false)}
+ title="Add New Subscription"
+ >
+
+
);
}
diff --git a/contributors/dwivediprashant/server/package.json b/contributors/dwivediprashant/server/package.json
index c6ca2d1..363506d 100644
--- a/contributors/dwivediprashant/server/package.json
+++ b/contributors/dwivediprashant/server/package.json
@@ -12,10 +12,10 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
+ "cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"joi": "^18.0.2",
- "mongoose": "^9.1.0",
- "uuid": "^13.0.0"
+ "mongoose": "^9.1.0"
}
}
diff --git a/contributors/dwivediprashant/server/src/controllers/subscriptionController.js b/contributors/dwivediprashant/server/src/controllers/subscriptionController.js
index e6be9bf..17fcd99 100644
--- a/contributors/dwivediprashant/server/src/controllers/subscriptionController.js
+++ b/contributors/dwivediprashant/server/src/controllers/subscriptionController.js
@@ -1,28 +1,36 @@
-const Subscription = require("../models/Subscription");
-const { generateUserId } = require("../utils/userIdGenerate");
+const Subscription = require("../models/subscription");
///////////-1)--create subscription---///////////
const createSubscription = async (req, res) => {
try {
- const { userId, name, amount, billingCycle, renewalDate, isTrial, source } =
- req.body;
+ const {
+ name,
+ amount,
+ billingCycle,
+ renewalDate,
+ isTrial,
+ source,
+ category,
+ notes,
+ } = req.body;
// const userId = generateUserId();
- if (!userId || !name || !amount || !billingCycle || !renewalDate) {
+ if (!name || !amount || !billingCycle || !renewalDate || !category) {
return res.status(400).json({
success: false,
message:
- "Missing required fields: name, amount, billingCycle, renewalDate",
+ "Missing required fields: name, amount, billingCycle, renewalDate, category",
});
}
const subscription = new Subscription({
- userId,
name,
amount,
billingCycle,
renewalDate: new Date(renewalDate),
isTrial: isTrial || false,
source: source || "manual",
+ category,
+ notes: notes || "",
});
await subscription.save();
diff --git a/contributors/dwivediprashant/server/src/models/subscription.js b/contributors/dwivediprashant/server/src/models/subscription.js
index ed7731b..6af19cd 100644
--- a/contributors/dwivediprashant/server/src/models/subscription.js
+++ b/contributors/dwivediprashant/server/src/models/subscription.js
@@ -1,11 +1,7 @@
-const mongoose=require("mongoose")
+const mongoose = require("mongoose");
const subscriptionSchema = new mongoose.Schema(
{
- userId: {
- type: String,
- required: true,
- },
name: {
type: String,
required: true,
@@ -16,7 +12,7 @@ const subscriptionSchema = new mongoose.Schema(
},
billingCycle: {
type: String,
- enum: ['monthly', 'yearly'],
+ enum: ["monthly", "yearly"],
required: true,
},
renewalDate: {
@@ -29,13 +25,30 @@ const subscriptionSchema = new mongoose.Schema(
},
source: {
type: String,
- enum: ['manual', 'email'],
- default: 'manual',
+ enum: ["manual", "email"],
+ default: "manual",
+ },
+ category: {
+ type: String,
+ required: true,
+ enum: [
+ "entertainment",
+ "productivity",
+ "utilities",
+ "education",
+ "health",
+ "finance",
+ "other",
+ ],
+ },
+ notes: {
+ type: String,
+ default: "",
},
},
{ timestamps: true }
);
-const Subscription = mongoose.model('Subscription', subscriptionSchema);
+const Subscription = mongoose.model("Subscription", subscriptionSchema);
module.exports = Subscription;
diff --git a/contributors/dwivediprashant/server/src/utils/userIdGenerate.js b/contributors/dwivediprashant/server/src/utils/userIdGenerate.js
index f6e7401..53dcc7c 100644
--- a/contributors/dwivediprashant/server/src/utils/userIdGenerate.js
+++ b/contributors/dwivediprashant/server/src/utils/userIdGenerate.js
@@ -1,7 +1,2 @@
-const { v4: uuidv4 } = require('uuid');
-
-const generateUserId = () => {
- return uuidv4();
-};
-
-module.exports = { generateUserId };
\ No newline at end of file
+// This file is no longer needed - authentication has been removed
+// This file can be safely deleted
diff --git a/contributors/dwivediprashant/server/src/validators/subscriptionValidators.js b/contributors/dwivediprashant/server/src/validators/subscriptionValidators.js
index 7ce642a..416a550 100644
--- a/contributors/dwivediprashant/server/src/validators/subscriptionValidators.js
+++ b/contributors/dwivediprashant/server/src/validators/subscriptionValidators.js
@@ -1,33 +1,33 @@
-const Joi = require('joi');
+const Joi = require("joi");
const createSubscriptionSchema = Joi.object({
- userId: Joi.string(),
+ name: Joi.string().trim().min(1).required(),
- name: Joi.string()
- .trim()
- .min(1)
- .required(),
+ amount: Joi.number().positive().required(),
- amount: Joi.number()
- .positive()
- .required(),
+ billingCycle: Joi.string().valid("monthly", "yearly").required(),
- billingCycle: Joi.string()
- .valid('monthly', 'yearly')
- .required(),
+ renewalDate: Joi.date().iso().required(),
- renewalDate: Joi.date()
- .iso()
+ category: Joi.string()
+ .valid(
+ "entertainment",
+ "productivity",
+ "utilities",
+ "education",
+ "health",
+ "finance",
+ "other"
+ )
.required(),
- isTrial: Joi.boolean()
- .optional(),
+ isTrial: Joi.boolean().optional(),
+
+ source: Joi.string().valid("manual", "email").optional(),
- source: Joi.string()
- .valid('manual', 'email')
- .optional()
+ notes: Joi.string().optional().allow(""),
});
module.exports = {
- createSubscriptionSchema
+ createSubscriptionSchema,
};