= {};
+ ads.forEach(ad => {
+ if (ad.category) {
+ categories[ad.category] = (categories[ad.category] || 0) + 1;
+ }
+ });
+
+ const categoryDistribution = Object.entries(categories).map(([name, count]) => ({
+ name,
+ count,
+ percentage: Math.round((count / ads.length) * 100)
+ }));
+
+ return NextResponse.json({
+ adPerformance: {
+ activeAds,
+ totalViews,
+ totalClicks,
+ totalShares,
+ boostedAds,
+ engagementRate: `${engagementRate}%`
+ },
+ promotionSummary: {
+ ongoingPromotions,
+ earningsFromPromotions
+ },
+ recentActivity,
+ adPerformanceTable: adPerformance,
+ subscription: user?.subscriptionPlan,
+ chartData: {
+ dailyLabels,
+ dailyViews,
+ dailyClicks
+ },
+ categoryDistribution,
+ timeRange
+ });
+ } catch (error) {
+ console.error("Error fetching dashboard data:", error);
+
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+ return NextResponse.json(
+ { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+ { status: 401 }
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+ return NextResponse.json(
+ { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+ { status: 401 }
+ );
+ } else if (error instanceof Error) {
+ // Return a generic error message but with the specific error name for debugging
+ return NextResponse.json(
+ {
+ error: "Failed to fetch dashboard data",
+ code: "SERVER_ERROR",
+ message: error.message,
+ name: error.name
+ },
+ { status: 500 }
+ );
+ }
+
+ // Fallback for unknown errors
+ return NextResponse.json(
+ { error: "An unexpected error occurred", code: "UNKNOWN_ERROR" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/notifications/route.ts b/app/api/user/notifications/route.ts
new file mode 100644
index 0000000..4677483
--- /dev/null
+++ b/app/api/user/notifications/route.ts
@@ -0,0 +1,187 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import { formatDistanceToNow } from 'date-fns';
+import { initialNotifications } from '@/constants';
+import { markNotificationsAsRead } from '@/lib/notifications';
+import { Notification } from '@/types';
+
+/**
+ * GET - Fetch all notifications for the current user
+ */
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ let userId: string;
+ try {
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ userId = decoded.id;
+ } catch (error) {
+ console.error('Error verifying token:', error);
+ return NextResponse.json({ error: 'Invalid session token' }, { status: 401 });
+ }
+
+ try {
+ // Query the database for user notifications
+ const notifications = await prisma.notification.findMany({
+ where: { userId },
+ orderBy: { createdAt: 'desc' }
+ });
+
+ // Format the time for each notification
+ const formattedNotifications = notifications.map((notification) => ({
+ ...notification,
+ time: notification.time || formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true }),
+ }));
+
+ return NextResponse.json({ notifications: formattedNotifications });
+ } catch (error) {
+ console.error('Database error:', error);
+
+ // If the table doesn't exist, try to create notifications for the user
+ if (error instanceof Error && error.message.includes('does not exist')) {
+ try {
+ // Create initial notifications
+ const notifications = await Promise.all(
+ initialNotifications.map(async (notification) => {
+ return prisma.notification.create({
+ data: {
+ userId,
+ type: notification.type,
+ message: notification.message,
+ read: false,
+ time: notification.time
+ }
+ });
+ })
+ );
+
+ if (notifications.length > 0) {
+ return NextResponse.json({ notifications });
+ }
+ } catch (createError) {
+ console.error('Error creating initial notifications:', createError);
+ }
+ }
+
+ // Return empty array instead of error to prevent UI from breaking
+ return NextResponse.json({ notifications: [] });
+ }
+ } catch (error) {
+ console.error('Unexpected error:', error);
+ return NextResponse.json({ notifications: [] });
+ }
+}
+
+/**
+ * POST - Mark notifications as read
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get notification IDs to mark as read
+ const { ids } = await req.json();
+
+ if (!ids || !Array.isArray(ids)) {
+ return NextResponse.json(
+ { error: 'Invalid request format' },
+ { status: 400 }
+ );
+ }
+
+ // Update the database to mark notifications as read
+ const updateResult = await markNotificationsAsRead(userId, ids);
+
+ return NextResponse.json({
+ success: true,
+ message: `Marked ${updateResult.count} notifications as read`
+ });
+ } catch (error) {
+ console.error('Error updating notifications:', error);
+
+ // Check if it's a Prisma error related to missing table
+ if (error instanceof Error && error.message.includes('does not exist')) {
+ return NextResponse.json({
+ success: true,
+ message: 'Notification table does not exist yet. Please run migrations.'
+ });
+ }
+
+ return NextResponse.json(
+ { error: 'Failed to update notifications' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * DELETE - Delete notifications
+ */
+export async function DELETE(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get notification IDs to delete
+ const { ids } = await req.json();
+
+ if (!ids || !Array.isArray(ids)) {
+ return NextResponse.json(
+ { error: 'Invalid request format' },
+ { status: 400 }
+ );
+ }
+
+ // Delete the notifications
+ const deleteResult = await prisma.$executeRaw`
+ DELETE FROM "Notification"
+ WHERE "id" IN (${ids.join(',')})
+ AND "userId" = ${userId}
+ `;
+
+ // Count how many were deleted (deleteResult is the number of rows affected)
+ const deletedCount = deleteResult as number;
+
+ return NextResponse.json({
+ success: true,
+ message: `Deleted ${deletedCount} notifications`
+ });
+ } catch (error) {
+ console.error('Error deleting notifications:', error);
+
+ // Check if it's a Prisma error related to missing table
+ if (error instanceof Error && error.message.includes('does not exist')) {
+ return NextResponse.json({
+ success: true,
+ message: 'Notification table does not exist yet. Please run migrations.'
+ });
+ }
+
+ return NextResponse.json(
+ { error: 'Failed to delete notifications' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/notifications/seed/route.ts b/app/api/user/notifications/seed/route.ts
new file mode 100644
index 0000000..94c4484
--- /dev/null
+++ b/app/api/user/notifications/seed/route.ts
@@ -0,0 +1,57 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import { initialNotifications } from '@/constants';
+
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Check if user already has notifications
+ const existingNotifications = await prisma.notification.count({
+ where: {
+ userId: userId,
+ },
+ });
+
+ if (existingNotifications > 0) {
+ return NextResponse.json({
+ message: 'User already has notifications',
+ count: existingNotifications
+ });
+ }
+
+ // Create initial notifications for the user
+ const notificationsToCreate = initialNotifications.map(notification => ({
+ userId: userId,
+ type: notification.type,
+ message: notification.message,
+ time: notification.time,
+ read: false,
+ }));
+
+ const createdNotifications = await prisma.notification.createMany({
+ data: notificationsToCreate,
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: `Created ${createdNotifications.count} notifications for user`,
+ count: createdNotifications.count
+ });
+ } catch (error) {
+ console.error('Error seeding notifications:', error);
+ return NextResponse.json(
+ { error: 'Failed to seed notifications' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/notifications/test/route.ts b/app/api/user/notifications/test/route.ts
new file mode 100644
index 0000000..2b6411a
--- /dev/null
+++ b/app/api/user/notifications/test/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server';
+import jwt from 'jsonwebtoken';
+import { initialNotifications } from '@/constants';
+
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // For testing, we'll return the initial notifications with the userId
+ const notifications = initialNotifications.map(notification => ({
+ ...notification,
+ userId,
+ read: Math.random() > 0.5, // 50% chance of being read
+ }));
+
+ return NextResponse.json({
+ notifications,
+ userId,
+ message: 'This is a test endpoint. In production, notifications would be fetched from the database.'
+ });
+ } catch (error) {
+ console.error('Error in test endpoint:', error);
+ return NextResponse.json(
+ { error: 'Failed to process request' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/notifications/unread/route.ts b/app/api/user/notifications/unread/route.ts
new file mode 100644
index 0000000..e736bf7
--- /dev/null
+++ b/app/api/user/notifications/unread/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ let count = 0;
+
+ try {
+ // Get unread notifications count from the database
+ count = await prisma.notification.count({
+ where: {
+ userId: userId,
+ read: false,
+ },
+ });
+ } catch (dbError) {
+ console.error('Error accessing notification table, using default count:', dbError);
+ // If there's an error (e.g., table doesn't exist yet), return a random count
+ count = Math.floor(Math.random() * 5); // Random number between 0 and 4
+ }
+
+ return NextResponse.json({ count });
+ } catch (error) {
+ console.error('Error fetching unread notifications:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch notifications' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/profile/2fa/disable/route.ts b/app/api/user/profile/2fa/disable/route.ts
new file mode 100644
index 0000000..da7da2a
--- /dev/null
+++ b/app/api/user/profile/2fa/disable/route.ts
@@ -0,0 +1,83 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+
+/**
+ * POST - Disable 2FA
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get request data
+ const { password } = await req.json();
+
+ if (!password) {
+ return NextResponse.json(
+ { error: 'Password is required to disable 2FA' },
+ { status: 400 }
+ );
+ }
+
+ // Get user with password and 2FA status
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ password: true,
+ twoFactorEnabled: true,
+ }
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+
+ if (!user.twoFactorEnabled) {
+ return NextResponse.json(
+ { error: '2FA is not enabled for this account' },
+ { status: 400 }
+ );
+ }
+
+ // Verify password
+ const isPasswordValid = await bcrypt.compare(password, user.password || '');
+ if (!isPasswordValid) {
+ return NextResponse.json(
+ { error: 'Incorrect password' },
+ { status: 400 }
+ );
+ }
+
+ // Disable 2FA for the user
+ await prisma.user.update({
+ where: { id: userId },
+ data: {
+ twoFactorEnabled: false,
+ twoFactorSecret: null,
+ backupCodes: []
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: '2FA has been disabled successfully'
+ });
+ } catch (error) {
+ console.error('Error disabling 2FA:', error);
+ return NextResponse.json(
+ { error: 'Failed to disable 2FA' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/2fa/setup/route.ts b/app/api/user/profile/2fa/setup/route.ts
new file mode 100644
index 0000000..478fa4a
--- /dev/null
+++ b/app/api/user/profile/2fa/setup/route.ts
@@ -0,0 +1,144 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import speakeasy from 'speakeasy';
+import QRCode from 'qrcode';
+import crypto from 'crypto';
+
+/**
+ * GET - Generate 2FA setup (secret and QR code)
+ */
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get user info
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ twoFactorEnabled: true,
+ }
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+
+ // Check if 2FA is already enabled
+ if (user.twoFactorEnabled) {
+ return NextResponse.json(
+ { error: '2FA is already enabled for this account' },
+ { status: 400 }
+ );
+ }
+
+ // Generate secret
+ const secret = speakeasy.generateSecret({
+ name: `AgroMarket NG (${user.email})`,
+ issuer: 'AgroMarket NG',
+ length: 32
+ });
+
+ // Generate QR code
+ const qrCodeDataURL = await QRCode.toDataURL(secret.otpauth_url!);
+
+ return NextResponse.json({
+ secret: secret.base32,
+ qrCode: qrCodeDataURL,
+ manualEntryKey: secret.base32,
+ });
+ } catch (error) {
+ console.error('Error setting up 2FA:', error);
+ return NextResponse.json(
+ { error: 'Failed to setup 2FA' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * POST - Enable 2FA after verification
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get request data
+ const { secret, token: userToken } = await req.json();
+
+ if (!secret || !userToken) {
+ return NextResponse.json(
+ { error: 'Secret and verification token are required' },
+ { status: 400 }
+ );
+ }
+
+ // Verify the token
+ const verified = speakeasy.totp.verify({
+ secret: secret,
+ encoding: 'base32',
+ token: userToken,
+ window: 2
+ });
+
+ if (!verified) {
+ return NextResponse.json(
+ { error: 'Invalid verification code' },
+ { status: 400 }
+ );
+ }
+
+ // Generate backup codes
+ const backupCodes = [];
+ for (let i = 0; i < 10; i++) {
+ backupCodes.push(crypto.randomBytes(4).toString('hex').toUpperCase());
+ }
+
+ // Enable 2FA for the user
+ const updatedUser = await prisma.user.update({
+ where: { id: userId },
+ data: {
+ twoFactorEnabled: true,
+ twoFactorSecret: secret,
+ backupCodes: backupCodes
+ },
+ select: {
+ id: true,
+ twoFactorEnabled: true
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ backupCodes: backupCodes,
+ message: '2FA has been enabled successfully'
+ });
+ } catch (error) {
+ console.error('Error enabling 2FA:', error);
+ return NextResponse.json(
+ { error: 'Failed to enable 2FA' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/activity/route.ts b/app/api/user/profile/activity/route.ts
new file mode 100644
index 0000000..da6d206
--- /dev/null
+++ b/app/api/user/profile/activity/route.ts
@@ -0,0 +1,142 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+
+/**
+ * GET - Fetch user activity logs
+ */
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get query parameters
+ const { searchParams } = new URL(req.url);
+ const page = parseInt(searchParams.get('page') || '1');
+ const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100); // Max 100 items per page
+ const activity = searchParams.get('activity'); // Optional filter by activity type
+
+ // Build where clause
+ const whereClause: any = {
+ userId: userId
+ };
+
+ if (activity) {
+ whereClause.activity = activity;
+ }
+
+ // Get total count
+ const totalCount = await prisma.activityLog.count({
+ where: whereClause
+ });
+
+ // Get activity logs with pagination
+ const activityLogs = await prisma.activityLog.findMany({
+ where: whereClause,
+ select: {
+ id: true,
+ activity: true,
+ description: true,
+ ipAddress: true,
+ userAgent: true,
+ deviceInfo: true,
+ location: true,
+ success: true,
+ metadata: true,
+ createdAt: true
+ },
+ orderBy: {
+ createdAt: 'desc'
+ },
+ skip: (page - 1) * limit,
+ take: limit
+ });
+
+ // Calculate pagination info
+ const totalPages = Math.ceil(totalCount / limit);
+ const hasNextPage = page < totalPages;
+ const hasPreviousPage = page > 1;
+
+ return NextResponse.json({
+ activityLogs,
+ pagination: {
+ currentPage: page,
+ totalPages,
+ totalCount,
+ hasNextPage,
+ hasPreviousPage,
+ limit
+ }
+ });
+
+ } catch (error) {
+ console.error('Error fetching activity logs:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch activity logs' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * POST - Log a new activity (for internal use)
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // This endpoint is for internal logging, so we'll accept the userId in the request body
+ // In a production app, you might want to restrict this endpoint or use it differently
+ const {
+ userId,
+ activity,
+ description,
+ ipAddress,
+ userAgent,
+ deviceInfo,
+ location,
+ success,
+ metadata
+ } = await req.json();
+
+ if (!userId || !activity) {
+ return NextResponse.json(
+ { error: 'User ID and activity type are required' },
+ { status: 400 }
+ );
+ }
+
+ // Create activity log entry
+ const activityLog = await prisma.activityLog.create({
+ data: {
+ userId,
+ activity,
+ description: description || null,
+ ipAddress: ipAddress || null,
+ userAgent: userAgent || null,
+ deviceInfo: deviceInfo || null,
+ location: location || null,
+ success: success !== undefined ? success : true,
+ metadata: metadata || null
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ activityLog
+ });
+
+ } catch (error) {
+ console.error('Error creating activity log:', error);
+ return NextResponse.json(
+ { error: 'Failed to create activity log' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/delete/route.ts b/app/api/user/profile/delete/route.ts
new file mode 100644
index 0000000..48920a4
--- /dev/null
+++ b/app/api/user/profile/delete/route.ts
@@ -0,0 +1,182 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+
+/**
+ * POST - Delete user account permanently
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get request data
+ const { password, confirmationText } = await req.json();
+
+ if (!password || !confirmationText) {
+ return NextResponse.json(
+ { error: 'Password and confirmation text are required' },
+ { status: 400 }
+ );
+ }
+
+ // Verify confirmation text
+ if (confirmationText.toLowerCase().trim() !== 'delete my account') {
+ return NextResponse.json(
+ { error: 'Please type "DELETE MY ACCOUNT" to confirm' },
+ { status: 400 }
+ );
+ }
+
+ // Get user and verify password
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ email: true,
+ password: true,
+ name: true
+ }
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+
+ // Verify password
+ const isPasswordValid = await bcrypt.compare(password, user.password || '');
+ if (!isPasswordValid) {
+ return NextResponse.json(
+ { error: 'Incorrect password' },
+ { status: 400 }
+ );
+ }
+
+ // Begin transaction to delete all user data
+ await prisma.$transaction(async (tx) => {
+ // Delete related data first (due to foreign key constraints)
+
+ // Delete user's ads
+ await tx.ad.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete user's transactions
+ await tx.transaction.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete user's invoices
+ await tx.invoice.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete user's payment methods
+ await tx.paymentMethod.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete user's support tickets and messages
+ await tx.supportMessage.deleteMany({
+ where: { senderId: userId }
+ });
+
+ await tx.supportTicket.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete user's conversations and messages
+ await tx.message.deleteMany({
+ where: { senderId: userId }
+ });
+
+ await tx.conversation.deleteMany({
+ where: {
+ OR: [
+ { buyerId: userId },
+ { sellerId: userId }
+ ]
+ }
+ });
+
+ // Delete user's notifications
+ await tx.notification.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete notification preferences
+ await tx.notificationPreferences.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete saved searches
+ await tx.savedSearch.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete agent record if exists
+ await tx.agent.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete authentication records
+ await tx.session.deleteMany({
+ where: { userId: userId }
+ });
+
+ await tx.account.deleteMany({
+ where: { userId: userId }
+ });
+
+ // Delete verification tokens related to this user
+ await tx.verificationToken.deleteMany({
+ where: {
+ identifier: {
+ contains: userId
+ }
+ }
+ });
+
+ // Finally, delete the user
+ await tx.user.delete({
+ where: { id: userId }
+ });
+ });
+
+ // Clear the response cookies
+ const response = NextResponse.json({
+ success: true,
+ message: 'Account deleted successfully'
+ });
+
+ // Clear authentication cookies
+ response.cookies.set('next-auth.session-token', '', {
+ expires: new Date(0),
+ path: '/'
+ });
+
+ response.cookies.set('__Secure-next-auth.session-token', '', {
+ expires: new Date(0),
+ path: '/',
+ secure: true
+ });
+
+ return response;
+
+ } catch (error) {
+ console.error('Error deleting account:', error);
+ return NextResponse.json(
+ { error: 'Failed to delete account' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/email/change/route.ts b/app/api/user/profile/email/change/route.ts
new file mode 100644
index 0000000..c45bb94
--- /dev/null
+++ b/app/api/user/profile/email/change/route.ts
@@ -0,0 +1,126 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import crypto from 'crypto';
+import bcrypt from 'bcryptjs';
+
+/**
+ * POST - Request email change
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get request data
+ const { newEmail, password } = await req.json();
+
+ if (!newEmail || !password) {
+ return NextResponse.json(
+ { error: 'New email and password are required' },
+ { status: 400 }
+ );
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(newEmail)) {
+ return NextResponse.json(
+ { error: 'Please enter a valid email address' },
+ { status: 400 }
+ );
+ }
+
+ // Get user and verify password
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ email: true,
+ password: true,
+ name: true
+ }
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+
+ // Check if new email is same as current
+ if (user.email === newEmail.toLowerCase()) {
+ return NextResponse.json(
+ { error: 'New email must be different from current email' },
+ { status: 400 }
+ );
+ }
+
+ // Verify password
+ const isPasswordValid = await bcrypt.compare(password, user.password || '');
+ if (!isPasswordValid) {
+ return NextResponse.json(
+ { error: 'Incorrect password' },
+ { status: 400 }
+ );
+ }
+
+ // Check if new email is already in use
+ const existingUser = await prisma.user.findUnique({
+ where: { email: newEmail.toLowerCase() },
+ select: { id: true }
+ });
+
+ if (existingUser) {
+ return NextResponse.json(
+ { error: 'Email address is already in use' },
+ { status: 400 }
+ );
+ }
+
+ // Generate verification token
+ const verificationToken = crypto.randomBytes(32).toString('hex');
+ const tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
+
+ // Store the email change request
+ await prisma.user.update({
+ where: { id: userId },
+ data: {
+ // We'll store this temporarily - you might want to create a separate table for this
+ // For now, we'll use a simple approach with metadata or create a custom field
+ }
+ });
+
+ // Create a verification token record
+ await prisma.verificationToken.create({
+ data: {
+ identifier: `email-change-${userId}`,
+ token: verificationToken,
+ expires: tokenExpiry
+ }
+ });
+
+ // In a real app, you would send an email here
+ // For now, we'll return the token for testing
+ return NextResponse.json({
+ success: true,
+ message: 'Email change verification sent. Please check your new email address.',
+ // Remove this in production - only for testing
+ verificationToken: verificationToken,
+ verificationUrl: `${process.env.NEXTAUTH_URL}/verify-email-change?token=${verificationToken}&email=${encodeURIComponent(newEmail)}`
+ });
+
+ } catch (error) {
+ console.error('Error requesting email change:', error);
+ return NextResponse.json(
+ { error: 'Failed to request email change' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/email/verify/route.ts b/app/api/user/profile/email/verify/route.ts
new file mode 100644
index 0000000..18fb769
--- /dev/null
+++ b/app/api/user/profile/email/verify/route.ts
@@ -0,0 +1,100 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+
+/**
+ * POST - Verify email change
+ */
+export async function POST(req: NextRequest) {
+ try {
+ const { token, newEmail } = await req.json();
+
+ if (!token || !newEmail) {
+ return NextResponse.json(
+ { error: 'Verification token and email are required' },
+ { status: 400 }
+ );
+ }
+
+ // Find the verification token
+ const verificationRecord = await prisma.verificationToken.findUnique({
+ where: {
+ identifier_token: {
+ identifier: `email-change-${token.split('-')[0]}`, // This needs to be adjusted
+ token: token
+ }
+ }
+ });
+
+ if (!verificationRecord) {
+ return NextResponse.json(
+ { error: 'Invalid or expired verification token' },
+ { status: 400 }
+ );
+ }
+
+ // Check if token is expired
+ if (verificationRecord.expires < new Date()) {
+ // Clean up expired token
+ await prisma.verificationToken.delete({
+ where: {
+ identifier_token: {
+ identifier: verificationRecord.identifier,
+ token: token
+ }
+ }
+ });
+
+ return NextResponse.json(
+ { error: 'Verification token has expired' },
+ { status: 400 }
+ );
+ }
+
+ // Extract userId from identifier
+ const userId = verificationRecord.identifier.replace('email-change-', '');
+
+ // Check if new email is still available
+ const existingUser = await prisma.user.findUnique({
+ where: { email: newEmail.toLowerCase() },
+ select: { id: true }
+ });
+
+ if (existingUser && existingUser.id !== userId) {
+ return NextResponse.json(
+ { error: 'Email address is no longer available' },
+ { status: 400 }
+ );
+ }
+
+ // Update user email
+ await prisma.user.update({
+ where: { id: userId },
+ data: {
+ email: newEmail.toLowerCase(),
+ emailVerified: new Date() // Mark as verified
+ }
+ });
+
+ // Delete the verification token
+ await prisma.verificationToken.delete({
+ where: {
+ identifier_token: {
+ identifier: verificationRecord.identifier,
+ token: token
+ }
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: 'Email address updated successfully'
+ });
+
+ } catch (error) {
+ console.error('Error verifying email change:', error);
+ return NextResponse.json(
+ { error: 'Failed to verify email change' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/image/route.ts b/app/api/user/profile/image/route.ts
new file mode 100644
index 0000000..b402feb
--- /dev/null
+++ b/app/api/user/profile/image/route.ts
@@ -0,0 +1,93 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import { writeFile } from 'fs/promises';
+import { apiErrorResponse } from '@/lib/errorHandling';
+import { join } from 'path';
+
+/**
+ * POST - Upload user profile image
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get image file from form data
+ const formData = await req.formData();
+ const file = formData.get('image') as File;
+
+ if (!file) {
+ return apiErrorResponse(
+ 'No image file provided',
+ 400,
+ 'NO_IMAGE_PROVIDED'
+ );
+ }
+
+ // Validate file type
+ const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+ if (!validTypes.includes(file.type)) {
+ return apiErrorResponse(
+ 'Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed.',
+ 400,
+ 'INVALID_FILE_TYPE'
+ );
+ }
+
+ // Validate file size (max 5MB)
+ const maxSize = 5 * 1024 * 1024; // 5MB
+ if (file.size > maxSize) {
+ return apiErrorResponse(
+ 'File size exceeds the 5MB limit',
+ 400,
+ 'FILE_TOO_LARGE'
+ );
+ }
+
+ // Generate unique filename
+ const timestamp = Date.now();
+ const extension = file.type.split('/')[1];
+ const filename = `${userId}-${timestamp}.${extension}`;
+
+ // Save file to public directory
+ const bytes = await file.arrayBuffer();
+ const buffer = Buffer.from(bytes);
+ const path = join(process.cwd(), 'public', 'uploads', 'profiles', filename);
+ await writeFile(path, buffer);
+
+ // Update user profile with new image URL
+ const imageUrl = `/uploads/profiles/${filename}`;
+ const updatedUser = await prisma.user.update({
+ where: { id: userId },
+ data: { image: imageUrl },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true
+ }
+ });
+
+ return NextResponse.json({
+ message: 'Profile image updated successfully',
+ user: updatedUser
+ });
+ } catch (error) {
+ console.error('Error uploading profile image:', error); // Log the actual error for debugging
+ return apiErrorResponse(
+ 'Failed to upload profile image',
+ 500,
+ 'UPLOAD_PROFILE_IMAGE_FAILED',
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+}
diff --git a/app/api/user/profile/notification-preferences/route.ts b/app/api/user/profile/notification-preferences/route.ts
new file mode 100644
index 0000000..829f86d
--- /dev/null
+++ b/app/api/user/profile/notification-preferences/route.ts
@@ -0,0 +1,202 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+
+/**
+ * GET - Fetch user notification preferences
+ */
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Fetch or create default notification preferences
+ let preferences = await prisma.notificationPreferences.findUnique({
+ where: { userId },
+ select: {
+ id: true,
+ emailForAdActivity: true,
+ emailForPromotions: true,
+ smsForPromotions: true,
+ emailForMessages: true,
+ emailForPayments: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ });
+
+ // If no preferences exist, create default ones
+ if (!preferences) {
+ preferences = await prisma.notificationPreferences.create({
+ data: {
+ userId,
+ emailForAdActivity: true,
+ emailForPromotions: true,
+ smsForPromotions: false,
+ emailForMessages: true,
+ emailForPayments: true,
+ },
+ select: {
+ id: true,
+ emailForAdActivity: true,
+ emailForPromotions: true,
+ smsForPromotions: true,
+ emailForMessages: true,
+ emailForPayments: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ });
+ }
+
+ return NextResponse.json({ preferences });
+ } catch (error) {
+ console.error('Error fetching notification preferences:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch notification preferences' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * PATCH - Update user notification preferences
+ */
+export async function PATCH(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get update data from request
+ const data = await req.json();
+
+ // Validate data
+ if (!data || typeof data !== 'object') {
+ return NextResponse.json(
+ { error: 'Invalid request data' },
+ { status: 400 }
+ );
+ }
+
+ // Extract allowed fields to update
+ const {
+ emailForAdActivity,
+ emailForPromotions,
+ smsForPromotions,
+ emailForMessages,
+ emailForPayments
+ } = data;
+
+ const updateData: any = {};
+
+ // Validate boolean fields
+ if (emailForAdActivity !== undefined) {
+ if (typeof emailForAdActivity !== 'boolean') {
+ return NextResponse.json(
+ { error: 'emailForAdActivity must be a boolean' },
+ { status: 400 }
+ );
+ }
+ updateData.emailForAdActivity = emailForAdActivity;
+ }
+
+ if (emailForPromotions !== undefined) {
+ if (typeof emailForPromotions !== 'boolean') {
+ return NextResponse.json(
+ { error: 'emailForPromotions must be a boolean' },
+ { status: 400 }
+ );
+ }
+ updateData.emailForPromotions = emailForPromotions;
+ }
+
+ if (smsForPromotions !== undefined) {
+ if (typeof smsForPromotions !== 'boolean') {
+ return NextResponse.json(
+ { error: 'smsForPromotions must be a boolean' },
+ { status: 400 }
+ );
+ }
+ updateData.smsForPromotions = smsForPromotions;
+ }
+
+ if (emailForMessages !== undefined) {
+ if (typeof emailForMessages !== 'boolean') {
+ return NextResponse.json(
+ { error: 'emailForMessages must be a boolean' },
+ { status: 400 }
+ );
+ }
+ updateData.emailForMessages = emailForMessages;
+ }
+
+ if (emailForPayments !== undefined) {
+ if (typeof emailForPayments !== 'boolean') {
+ return NextResponse.json(
+ { error: 'emailForPayments must be a boolean' },
+ { status: 400 }
+ );
+ }
+ updateData.emailForPayments = emailForPayments;
+ }
+
+ // If no valid fields to update
+ if (Object.keys(updateData).length === 0) {
+ return NextResponse.json(
+ { error: 'No valid fields to update' },
+ { status: 400 }
+ );
+ }
+
+ // Upsert notification preferences
+ const updatedPreferences = await prisma.notificationPreferences.upsert({
+ where: { userId },
+ create: {
+ userId,
+ emailForAdActivity: updateData.emailForAdActivity ?? true,
+ emailForPromotions: updateData.emailForPromotions ?? true,
+ smsForPromotions: updateData.smsForPromotions ?? false,
+ emailForMessages: updateData.emailForMessages ?? true,
+ emailForPayments: updateData.emailForPayments ?? true,
+ },
+ update: updateData,
+ select: {
+ id: true,
+ emailForAdActivity: true,
+ emailForPromotions: true,
+ smsForPromotions: true,
+ emailForMessages: true,
+ emailForPayments: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ });
+
+ return NextResponse.json({
+ message: 'Notification preferences updated successfully',
+ preferences: updatedPreferences
+ });
+ } catch (error) {
+ console.error('Error updating notification preferences:', error);
+ return NextResponse.json(
+ { error: 'Failed to update notification preferences' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/profile/password/route.ts b/app/api/user/profile/password/route.ts
new file mode 100644
index 0000000..3eb65c3
--- /dev/null
+++ b/app/api/user/profile/password/route.ts
@@ -0,0 +1,81 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+
+/**
+ * POST - Update user password
+ */
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get password data from request
+ const { currentPassword, newPassword } = await req.json();
+
+ // Validate data
+ if (!currentPassword || !newPassword) {
+ return NextResponse.json(
+ { error: 'Current password and new password are required' },
+ { status: 400 }
+ );
+ }
+
+ if (typeof newPassword !== 'string' || newPassword.length < 8) {
+ return NextResponse.json(
+ { error: 'New password must be at least 8 characters long' },
+ { status: 400 }
+ );
+ }
+
+ // Get user with password
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ password: true,
+ }
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+
+ // Verify current password
+ const isPasswordValid = await bcrypt.compare(currentPassword, user.password || '');
+ if (!isPasswordValid) {
+ return NextResponse.json(
+ { error: 'Current password is incorrect' },
+ { status: 400 }
+ );
+ }
+
+ // Hash new password
+ const hashedPassword = await bcrypt.hash(newPassword, 10);
+
+ // Update user password
+ await prisma.user.update({
+ where: { id: userId },
+ data: { password: hashedPassword },
+ });
+
+ return NextResponse.json({
+ message: 'Password updated successfully'
+ });
+ } catch (error) {
+ console.error('Error updating password:', error);
+ return NextResponse.json(
+ { error: 'Failed to update password' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/profile/route.ts b/app/api/user/profile/route.ts
new file mode 100644
index 0000000..0b9be56
--- /dev/null
+++ b/app/api/user/profile/route.ts
@@ -0,0 +1,140 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+
+/**
+ * GET - Fetch user profile information
+ */
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Fetch user data
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ phone: true,
+ image: true,
+ role: true,
+ twoFactorEnabled: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ });
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 });
+ }
+
+ return NextResponse.json({ user });
+ } catch (error) {
+ console.error('Error fetching user profile:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch user profile' },
+ { status: 500 }
+ );
+ }
+}
+
+/**
+ * PATCH - Update user profile information
+ */
+export async function PATCH(req: NextRequest) {
+ try {
+ // Get token from cookies (try both development and production cookie names)
+ const token = req.cookies.get('next-auth.session-token')?.value ||
+ req.cookies.get('__Secure-next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get update data from request
+ const data = await req.json();
+
+ // Validate data
+ if (!data || typeof data !== 'object') {
+ return NextResponse.json(
+ { error: 'Invalid request data' },
+ { status: 400 }
+ );
+ }
+
+ // Extract allowed fields to update
+ const { name, phone } = data;
+ const updateData: any = {};
+
+ if (name !== undefined) {
+ if (typeof name !== 'string' || name.trim().length < 2) {
+ return NextResponse.json(
+ { error: 'Name must be at least 2 characters long' },
+ { status: 400 }
+ );
+ }
+ updateData.name = name.trim();
+ }
+
+ // Add phone field if provided (optional)
+ if (phone !== undefined) {
+ if (phone !== null && typeof phone !== 'string') {
+ return NextResponse.json(
+ { error: 'Phone must be a string or null' },
+ { status: 400 }
+ );
+ }
+ updateData.phone = phone;
+ }
+
+ // If no valid fields to update
+ if (Object.keys(updateData).length === 0) {
+ return NextResponse.json(
+ { error: 'No valid fields to update' },
+ { status: 400 }
+ );
+ }
+
+ // Update user
+ const updatedUser = await prisma.user.update({
+ where: { id: userId },
+ data: updateData,
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ phone: true,
+ image: true,
+ role: true,
+ twoFactorEnabled: true,
+ createdAt: true,
+ updatedAt: true,
+ }
+ });
+
+ return NextResponse.json({
+ message: 'Profile updated successfully',
+ user: updatedUser
+ });
+ } catch (error) {
+ console.error('Error updating user profile:', error);
+ return NextResponse.json(
+ { error: 'Failed to update profile' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/user/saved-searches/[id]/alerts/route.ts b/app/api/user/saved-searches/[id]/alerts/route.ts
new file mode 100644
index 0000000..df9f1ff
--- /dev/null
+++ b/app/api/user/saved-searches/[id]/alerts/route.ts
@@ -0,0 +1,88 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import { apiErrorResponse } from '@/lib/errorHandling';
+
+// PUT - Toggle alerts for a saved search
+export async function PUT(
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return apiErrorResponse('Authentication required', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get the saved search ID
+ const { id } = await params;
+ if (!id) {
+ return apiErrorResponse('Search ID is required', 400, 'MISSING_SEARCH_ID');
+ }
+
+ // Get alerts enabled status from request body
+ const { alertsEnabled } = await req.json();
+ if (typeof alertsEnabled !== 'boolean') {
+ return apiErrorResponse(
+ 'alertsEnabled must be a boolean value',
+ 400,
+ 'INVALID_ALERTS_ENABLED'
+ );
+ }
+
+ // Verify the search exists and belongs to the user
+ const existingSearch = await prisma.savedSearch.findUnique({
+ where: { id: id }
+ });
+
+ if (!existingSearch) {
+ return apiErrorResponse('Saved search not found', 404, 'SEARCH_NOT_FOUND');
+ }
+
+ if (existingSearch.userId !== userId) {
+ return apiErrorResponse(
+ 'Access denied. You can only modify your own saved searches',
+ 403,
+ 'ACCESS_DENIED'
+ );
+ }
+
+ // Update the alerts setting
+ const updatedSearch = await prisma.savedSearch.update({
+ where: { id: id },
+ data: {
+ alertsEnabled: alertsEnabled,
+ updatedAt: new Date()
+ },
+ select: {
+ id: true,
+ query: true,
+ alertsEnabled: true,
+ category: true,
+ location: true,
+ createdAt: true,
+ updatedAt: true
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: `Search alerts ${alertsEnabled ? 'enabled' : 'disabled'} successfully`,
+ savedSearch: updatedSearch
+ });
+
+ } catch (error) {
+ console.error('Error updating search alerts:', error);
+ return apiErrorResponse(
+ 'Failed to update search alerts',
+ 500,
+ 'UPDATE_ALERTS_FAILED',
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/user/saved-searches/route.ts b/app/api/user/saved-searches/route.ts
new file mode 100644
index 0000000..6933519
--- /dev/null
+++ b/app/api/user/saved-searches/route.ts
@@ -0,0 +1,203 @@
+import { NextRequest, NextResponse } from 'next/server';
+import prisma from '@/lib/prisma';
+import jwt from 'jsonwebtoken';
+import { apiErrorResponse } from '@/lib/errorHandling';
+
+export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get user's saved searches from database
+ const savedSearches = await prisma.savedSearch.findMany({
+ where: {
+ userId: userId
+ },
+ select: {
+ id: true,
+ query: true,
+ alertsEnabled: true,
+ category: true,
+ location: true,
+ createdAt: true,
+ updatedAt: true
+ },
+ orderBy: {
+ createdAt: 'desc'
+ }
+ });
+
+ return NextResponse.json({ savedSearches });
+ } catch (error) {
+ console.error('Error fetching saved searches:', error);
+ return apiErrorResponse(
+ 'Failed to fetch saved searches',
+ 500,
+ 'FETCH_SAVED_SEARCHES_FAILED',
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+}
+
+export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get data from request body
+ const { query, category, location, priceMin, priceMax, alertsEnabled } = await req.json();
+
+ if (!query || typeof query !== 'string' || query.trim().length === 0) {
+ return apiErrorResponse(
+ 'Search query is required and must be a non-empty string',
+ 400,
+ 'INVALID_QUERY'
+ );
+ }
+
+ // Validate query length
+ if (query.trim().length > 255) {
+ return apiErrorResponse(
+ 'Search query must be 255 characters or less',
+ 400,
+ 'QUERY_TOO_LONG'
+ );
+ }
+
+ // Check if search already exists for this user
+ const existingSearch = await prisma.savedSearch.findUnique({
+ where: {
+ userId_query: {
+ userId: userId,
+ query: query.trim()
+ }
+ }
+ });
+
+ if (existingSearch) {
+ return apiErrorResponse(
+ 'Search already saved',
+ 409,
+ 'SEARCH_ALREADY_EXISTS'
+ );
+ }
+
+ // Create new saved search
+ const savedSearch = await prisma.savedSearch.create({
+ data: {
+ query: query.trim(),
+ userId: userId,
+ category: category || null,
+ location: location || null,
+ priceMin: priceMin ? parseFloat(priceMin) : null,
+ priceMax: priceMax ? parseFloat(priceMax) : null,
+ alertsEnabled: alertsEnabled || false
+ },
+ select: {
+ id: true,
+ query: true,
+ alertsEnabled: true,
+ category: true,
+ location: true,
+ createdAt: true
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ savedSearch
+ });
+ } catch (error) {
+ console.error('Error saving search:', error);
+ return apiErrorResponse(
+ 'Failed to save search',
+ 500,
+ 'SAVE_SEARCH_FAILED',
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+}
+
+export async function DELETE(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+
+ // Get search ID from URL
+ const { searchParams } = new URL(req.url);
+ const id = searchParams.get('id');
+
+ if (!id || typeof id !== 'string') {
+ return apiErrorResponse(
+ 'Search ID is required',
+ 400,
+ 'MISSING_SEARCH_ID'
+ );
+ }
+
+ // Verify the search exists and belongs to the user
+ const existingSearch = await prisma.savedSearch.findUnique({
+ where: {
+ id: id
+ }
+ });
+
+ if (!existingSearch) {
+ return apiErrorResponse(
+ 'Saved search not found',
+ 404,
+ 'SEARCH_NOT_FOUND'
+ );
+ }
+
+ if (existingSearch.userId !== userId) {
+ return apiErrorResponse(
+ 'Access denied. You can only delete your own saved searches',
+ 403,
+ 'ACCESS_DENIED'
+ );
+ }
+
+ // Delete the saved search
+ await prisma.savedSearch.delete({
+ where: {
+ id: id
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: 'Search deleted successfully',
+ deletedId: id
+ });
+ } catch (error) {
+ console.error('Error deleting search:', error);
+ return apiErrorResponse(
+ 'Failed to delete search',
+ 500,
+ 'DELETE_SEARCH_FAILED',
+ error instanceof Error ? error.message : String(error)
+ );
+ }
+}
diff --git a/app/api/verify-email/route.ts b/app/api/verify-email/route.ts
new file mode 100644
index 0000000..6cb8621
--- /dev/null
+++ b/app/api/verify-email/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from 'next/server';
+import jwt from 'jsonwebtoken';
+import prisma from '@/lib/prisma';
+
+export async function GET(req: NextRequest) {
+ const token = req.nextUrl.searchParams.get('token');
+
+ if (!token) {
+ return NextResponse.redirect(
+ new URL('/signin?alert=missing_token', req.nextUrl.origin)
+ );
+ }
+
+ try {
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!);
+
+ if (typeof decoded !== 'object' || !('userId' in decoded)) {
+ return NextResponse.redirect(
+ new URL('/signin?alert=invalid_token', req.nextUrl.origin)
+ );
+ }
+
+ const userId = decoded.userId;
+
+ // Update the user to set verified as true
+ await prisma.user.update({
+ where: { id: userId },
+ data: { verified: true },
+ });
+
+ return NextResponse.redirect(
+ new URL('/signin?alert=success_token', req.nextUrl.origin)
+ );
+ } catch (error) {
+ return NextResponse.redirect(
+ new URL('/signin?alert=expired_token', req.nextUrl.origin)
+ );
+ }
+}
diff --git a/app/dashboard/agent/layout.tsx b/app/dashboard/agent/layout.tsx
new file mode 100644
index 0000000..2498b67
--- /dev/null
+++ b/app/dashboard/agent/layout.tsx
@@ -0,0 +1,16 @@
+"use client";
+
+import { WithAuth } from '@/components/auth/WithAuth';
+import AgentDashboardLayout from '@/components/AgentDashboard/Layout';
+
+// This layout completely overrides the parent layout to remove the CustomerNavbar
+// and use the agent-specific layout instead
+function AgentLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default WithAuth(AgentLayout);
diff --git a/app/dashboard/agent/page.tsx b/app/dashboard/agent/page.tsx
new file mode 100644
index 0000000..292bd10
--- /dev/null
+++ b/app/dashboard/agent/page.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Loader2 } from "lucide-react";
+
+// This page redirects to the new agent dashboard route
+export default function AgentRedirectPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const tab = searchParams.get('tab') || 'overview';
+
+ useEffect(() => {
+ // Redirect to the new agent dashboard route with the same tab parameter
+ router.replace(`/agent/dashboard?tab=${tab}`);
+ }, [router, tab]);
+
+ return (
+
+
+ Redirecting to new agent dashboard...
+
+ );
+}
\ No newline at end of file
diff --git a/app/dashboard/analytics/page.tsx b/app/dashboard/analytics/page.tsx
new file mode 100644
index 0000000..d1a1a96
--- /dev/null
+++ b/app/dashboard/analytics/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import EnhancedAnalyticsMain from '@/components/EnhancedAnalyticsMain';
+
+export default function Analytics() {
+ return
+}
\ No newline at end of file
diff --git a/app/dashboard/billing/page.tsx b/app/dashboard/billing/page.tsx
new file mode 100644
index 0000000..87b70a8
--- /dev/null
+++ b/app/dashboard/billing/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import BillingMain from "@/components/BillingMain";
+
+export default function Billing() {
+ return
+}
diff --git a/app/dashboard/edit-ad/[id]/page.tsx b/app/dashboard/edit-ad/[id]/page.tsx
new file mode 100644
index 0000000..c3e0c02
--- /dev/null
+++ b/app/dashboard/edit-ad/[id]/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import EditAdMain from "@/components/EditAdMain";
+
+export default function EditAd() {
+ return ;
+}
\ No newline at end of file
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx
new file mode 100644
index 0000000..0b91b59
--- /dev/null
+++ b/app/dashboard/layout.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { WithAuth } from '@/components/auth/WithAuth';
+import CustomerNavbar from '@/components/CustomerNavbar';
+import { RSCErrorBoundary } from '@/components/RSCErrorBoundary';
+import { memo, Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+// Loading component
+const LoadingSpinner = () => (
+
+);
+
+// Error fallback component
+const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) => (
+
+
Something went wrong
+
{error.message}
+
+ Try again
+
+
+);
+
+const DashboardRootLayout = memo(function DashboardRootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+ }>
+ {children}
+
+
+
+
+
+
+ );
+});
+
+export default WithAuth(DashboardRootLayout);
\ No newline at end of file
diff --git a/app/dashboard/messages/page.tsx b/app/dashboard/messages/page.tsx
new file mode 100644
index 0000000..f7be588
--- /dev/null
+++ b/app/dashboard/messages/page.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import MessagesMain from "@/components/MessagesMain";
+
+
+export default function AdsPromtion() {
+ return ;
+}
diff --git a/app/dashboard/my-ads/page.tsx b/app/dashboard/my-ads/page.tsx
new file mode 100644
index 0000000..7dce77c
--- /dev/null
+++ b/app/dashboard/my-ads/page.tsx
@@ -0,0 +1,5 @@
+import MyAdsMain from "@/components/MyAdsMain";
+
+export default function MyAds() {
+ return ;
+}
diff --git a/app/dashboard/new-ad/page.tsx b/app/dashboard/new-ad/page.tsx
new file mode 100644
index 0000000..1934339
--- /dev/null
+++ b/app/dashboard/new-ad/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import PostNewAdMain from "@/components/PostNewAdMain";
+
+export default function NewAd() {
+ return ;
+}
diff --git a/app/dashboard/notifications/page.tsx b/app/dashboard/notifications/page.tsx
new file mode 100644
index 0000000..4011196
--- /dev/null
+++ b/app/dashboard/notifications/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import NotificationsMain from "@/components/NotificationsMain";
+
+export default function Notifications() {
+ return ;
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
new file mode 100644
index 0000000..a87dd2a
--- /dev/null
+++ b/app/dashboard/page.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import { useSearchParams } from "next/navigation";
+import EnhancedDashboardMain from "@/components/EnhancedDashboardMain";
+
+export default function Dashboard() {
+ const searchParams = useSearchParams();
+ const tab = searchParams.get("tab") || "dashboard";
+
+ return ;
+}
\ No newline at end of file
diff --git a/app/dashboard/profile/page.tsx b/app/dashboard/profile/page.tsx
new file mode 100644
index 0000000..ed202ed
--- /dev/null
+++ b/app/dashboard/profile/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import ProfileMain from "@/components/ProfileMain";
+
+export default function Profile() {
+ return ;
+}
diff --git a/app/dashboard/promotions/page.tsx b/app/dashboard/promotions/page.tsx
new file mode 100644
index 0000000..3962734
--- /dev/null
+++ b/app/dashboard/promotions/page.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import AdsPromotionMain from "@/components/AdsPromotionMain";
+
+export default function AdsPromotion() {
+ return ;
+}
+
diff --git a/app/dashboard/saved-searches/page.tsx b/app/dashboard/saved-searches/page.tsx
new file mode 100644
index 0000000..afedd14
--- /dev/null
+++ b/app/dashboard/saved-searches/page.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import CategoriesSavedSearchesMain from "@/components/CategoriesSavedSearchesMain";
+
+export default function SavedSearches() {
+ return ;
+}
diff --git a/app/dashboard/support/page.tsx b/app/dashboard/support/page.tsx
new file mode 100644
index 0000000..da87e3b
--- /dev/null
+++ b/app/dashboard/support/page.tsx
@@ -0,0 +1,5 @@
+import SupportCenterMain from '@/components/SupportCenterMain';
+
+export default function Support() {
+ return ;
+}
\ No newline at end of file
diff --git a/app/forgotPassword/page.tsx b/app/forgotPassword/page.tsx
new file mode 100644
index 0000000..e5396c9
--- /dev/null
+++ b/app/forgotPassword/page.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import Image from 'next/image';
+import Link from 'next/link';
+import React, { useState } from 'react';
+import Spinner from '@/components/Spinner';
+import heroImg from '../../public/assets/img/agromarket-logo.png';
+import { toast } from 'react-hot-toast';
+import { motion } from 'framer-motion';
+
+const ForgotPassword = () => {
+ const [email, setEmail] = useState('');
+ const [isSendingResetLink, setIsSendingResetLink] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSendingResetLink(true);
+ try {
+ const res = await fetch('/api/auth', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ type: 'forgot-password',
+ email
+ }),
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ toast.success('Password reset link sent to your email');
+ setEmail(''); // Clear form after success
+ } else {
+ toast.error(data.error || 'Failed to send reset link');
+ }
+ } catch (error) {
+ toast.error('An error occurred. Please try again.');
+ } finally {
+ setIsSendingResetLink(false);
+ }
+ };
+
+ return (
+
+ {/* Left Side Form */}
+
+
+
+
+
+
+ Password Recovery
+ Please enter your email to receive a password reset link.
+
+
+
+
+
+ Back to Sign In
+
+
+
+
+ {/* Right Side Background */}
+
+ {/* Background with gradient */}
+
+ {/* Decorative elements */}
+
+
+
+ {/* Content */}
+
+
+ Forgot Your Password?
+
+
+ Don't worry! It happens to the best of us. Enter your email and we'll send you a link to reset your password.
+
+
+ Create New Account
+
+
+
+
+ );
+};
+
+export default ForgotPassword;
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index 13d40b8..0b9bc28 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -20,8 +20,107 @@ body {
font-family: Arial, Helvetica, sans-serif;
}
+/* Improved form element typography and contrast */
+input, select, textarea, option {
+ color: #333333 !important; /* Darker text for better readability */
+ font-weight: 500 !important; /* Medium weight for better visibility */
+}
+
+input::placeholder, textarea::placeholder {
+ color: #666666 !important; /* Darker placeholder text */
+ opacity: 1 !important;
+}
+
+select option {
+ color: #333333 !important; /* Ensure dropdown options are clearly visible */
+ background-color: white !important;
+ font-weight: 500 !important;
+}
+
+/* styles/globals.css */
+.spinner {
+ position: relative;
+ width: 40px;
+ height: 40px;
+}
+
+.double-bounce1, .double-bounce2 {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background-color: #34C759;
+ opacity: 0.6;
+ position: absolute;
+ top: 0;
+ left: 0;
+
+ -webkit-animation: sk-bounce 2.0s infinite ease-in-out;
+ animation: sk-bounce 2.0s infinite ease-in-out;
+}
+
+.double-bounce2 {
+ -webkit-animation-delay: -1.0s;
+ animation-delay: -1.0s;
+}
+
+@-webkit-keyframes sk-bounce {
+ 0%, 100% {
+ -webkit-transform: scale(0.0)
+ } 50% {
+ -webkit-transform: scale(1.0)
+ }
+}
+
+@keyframes sk-bounce {
+ 0%, 100% {
+ transform: scale(0.0);
+ -webkit-transform: scale(0.0);
+ } 50% {
+ transform: scale(1.0);
+ -webkit-transform: scale(1.0);
+ }
+}
+
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
+
+/* Add these styles for the typing indicator */
+.dot-typing {
+ position: relative;
+ left: -9999px;
+ width: 6px;
+ height: 6px;
+ border-radius: 5px;
+ background-color: #9880ff;
+ color: #9880ff;
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ animation: dot-typing 1.5s infinite linear;
+ margin-right: 24px;
+}
+
+@keyframes dot-typing {
+ 0% {
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ }
+ 16.667% {
+ box-shadow: 9984px -10px 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ }
+ 33.333% {
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ }
+ 50% {
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px -10px 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ }
+ 66.667% {
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ }
+ 83.333% {
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px -10px 0 0 #9880ff;
+ }
+ 100% {
+ box-shadow: 9984px 0 0 0 #9880ff, 9999px 0 0 0 #9880ff, 10014px 0 0 0 #9880ff;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index a36cde0..31a59c6 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,35 +1,100 @@
import type { Metadata } from "next";
-import localFont from "next/font/local";
+import SessionWrapper from '@/components/SessionWrapper';
+import { cookies } from 'next/headers';
+import jwt from 'jsonwebtoken';
+import { Session } from '@/types';
import "./globals.css";
+import { initCronJobs } from '@/services/cron';
+import { Toaster } from 'react-hot-toast';
+import CookieConsent from "@/components/CookieConsent";
+import Providers from './providers'
+import { Inter } from 'next/font/google';
-const geistSans = localFont({
- src: "./fonts/GeistVF.woff",
- variable: "--font-geist-sans",
- weight: "100 900",
-});
-const geistMono = localFont({
- src: "./fonts/GeistMonoVF.woff",
- variable: "--font-geist-mono",
- weight: "100 900",
-});
+if (process.env.NODE_ENV === 'development') {
+ initCronJobs();
+}
+
+const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "AgroMarket Nigeria | Buy and Sell Agricultural Products",
+ description: "AgroMarket Nigeria offers a trusted classified ads platform for buying and selling agricultural products from farmers, dealers, and agro-companies.",
+ openGraph: {
+ title: "AgroMarket Nigeria | Agricultural Classifieds",
+ description: "Explore AgroMarket Nigeria for the best deals on agricultural products. Connecting farmers with buyers in Nigeria.",
+ url: "https://www.agromarketng.com",
+ type: "website",
+ images: [
+ {
+ url: "https://www.agromarketng.com/assets/images/og-image.jpg",
+ width: 1200,
+ height: 630,
+ alt: "AgroMarket Nigeria",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "AgroMarket Nigeria",
+ description: "Best classified ads platform for agricultural products.",
+ site: "@AgroMarketNG",
+ creator: "@AgroMarketNG",
+ images: [
+ {
+ url: "https://www.agromarketng.com/assets/images/twitter-image.jpg",
+ alt: "AgroMarket Nigeria",
+ },
+ ],
+ },
+ alternates: {
+ canonical: "https://www.agromarketng.com",
+ },
+ robots: {
+ index: true,
+ follow: true,
+ },
+};
+
+export const viewport = {
+ width: "device-width",
+ initialScale: 1,
};
-export default function RootLayout({
+export default async function RootLayout({
children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
+}: {
+ children: React.ReactNode
+}) {
+ console.log('Rendering RootLayout');
+ const sessionCookie = (await cookies()).get('next-auth.session-token')?.value;
+ let initialSession = null;
+
+ if (sessionCookie) {
+ try {
+ const decoded = jwt.verify(sessionCookie, process.env.NEXTAUTH_SECRET!) as Session;
+ initialSession = {
+ token: sessionCookie,
+ ...decoded
+ };
+ } catch (err) {
+ console.error('Invalid session token:', err);
+ }
+ }
+
return (
-
- {children}
+
+
+
+
+
+
+ {children}
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/app/legal/page.tsx b/app/legal/page.tsx
new file mode 100644
index 0000000..5fb060a
--- /dev/null
+++ b/app/legal/page.tsx
@@ -0,0 +1,179 @@
+"use client";
+
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import Link from "next/link";
+
+export default function Legal() {
+ return (
+
+
+
+
+
+
+
Legal Information
+
Legal documents and compliance information
+
+
+
+
+
+
+
+ Legal Documents
+
+ This page provides access to all legal documents and compliance information related to AgroMarket services.
+
+
+
+
+
Terms of Service
+
View our complete terms and conditions for using AgroMarket platform.
+
+
+
+
Privacy Policy
+
Learn how we collect, use, and protect your personal information.
+
+
+
+
+
+ Company Information
+
+
AgroMarket Nigeria Limited
+
+
Registration Number: RC-123456789
+
Registered Address: Victoria Island, Lagos State, Nigeria
+
Business Registration: Corporate Affairs Commission (CAC)
+
Tax Identification Number: 12345678-0001
+
+
+
+
+
+ Regulatory Compliance
+
+ Data Protection
+
+ Nigeria Data Protection Regulation (NDPR) 2019
+ General Data Protection Regulation (GDPR) compliance for EU users
+ California Consumer Privacy Act (CCPA) compliance for US users
+
+
+ Agricultural Regulations
+
+ Federal Ministry of Agriculture and Rural Development compliance
+ Standards Organisation of Nigeria (SON) product standards
+ National Agency for Food and Drug Administration and Control (NAFDAC) for food products
+
+
+ Financial Services
+
+ Central Bank of Nigeria (CBN) payment services compliance
+ Anti-Money Laundering (AML) and Counter-Terrorism Financing (CTF) regulations
+ Know Your Customer (KYC) requirements
+
+
+
+
+ Intellectual Property
+
+ AgroMarket and its associated logos, trademarks, and service marks are the property of AgroMarket Nigeria Limited. All content on this platform is protected by copyright and other intellectual property laws.
+
+
+ Users retain ownership of content they create and post, but grant AgroMarket necessary licenses to operate the platform as outlined in our Terms of Service.
+
+
+
+
+ Dispute Resolution
+
+ Internal Resolution
+
+ We encourage users to first attempt to resolve disputes through direct communication. Our platform provides messaging tools to facilitate such discussions.
+
+
+ Mediation Services
+
+ For unresolved disputes, we offer mediation services through qualified third-party mediators specializing in agricultural and commercial disputes.
+
+
+ Legal Jurisdiction
+
+ All disputes arising from the use of AgroMarket services shall be subject to the jurisdiction of Nigerian courts, specifically the Federal High Court of Nigeria.
+
+
+
+
+ Accessibility
+
+ AgroMarket is committed to ensuring digital accessibility for people with disabilities. We continually improve the user experience for everyone and apply relevant accessibility standards.
+
+
+ If you encounter any accessibility barriers, please contact our support team at accessibility@agromarketng.com.
+
+
+
+
+ Cookie Policy
+
+ Our website uses cookies and similar tracking technologies to enhance user experience, analyze site usage, and assist in marketing efforts. Users can manage cookie preferences through their browser settings.
+
+
+
Cookie Consent: By using our website, you consent to the use of cookies as described in our Privacy Policy.
+
+
+
+
+ Third-Party Services
+
+ AgroMarket integrates with various third-party services to provide enhanced functionality:
+
+
+ Payment processors (Paystack, Flutterwave)
+ SMS and email service providers
+ Cloud storage and hosting services
+ Analytics and marketing tools
+ Map and location services
+
+
+
+
+ Contact Legal Department
+
+ For legal inquiries, compliance questions, or to report legal concerns:
+
+
+
Legal Department Email: legal@agromarketng.com
+
Compliance Officer: compliance@agromarketng.com
+
Address: Legal Department, AgroMarket Nigeria Limited
+
Victoria Island, Lagos State, Nigeria
+
Phone: +234 (0) 123 456 7890
+
+
+
+
+
+ Last Updated: {new Date().toLocaleDateString()}
+
+
+ This legal information page is regularly updated to reflect changes in laws, regulations, and company policies. Users are encouraged to review these documents periodically.
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/news/page.tsx b/app/news/page.tsx
new file mode 100644
index 0000000..3c158c5
--- /dev/null
+++ b/app/news/page.tsx
@@ -0,0 +1,406 @@
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import Image from "next/image";
+import Link from "next/link";
+import { Calendar, User, ArrowRight, Tag } from "lucide-react";
+
+// News articles data
+const newsArticles = [
+ {
+ id: 1,
+ title: "AgroMarket Launches New Financial Services for Smallholder Farmers",
+ slug: "agromarket-launches-financial-services",
+ excerpt: "New microloans and insurance products designed specifically for smallholder farmers are now available on the AgroMarket platform.",
+ content: `
+ AgroMarket is proud to announce the launch of our new financial services tailored specifically for smallholder farmers across Nigeria. These services include microloans for farm inputs, equipment financing, and crop insurance products designed to mitigate risks associated with farming.
+
+ According to Adebayo Johnson, CEO of AgroMarket, "Access to finance remains one of the biggest challenges facing smallholder farmers in Nigeria. Our new financial services are designed to address this gap by providing flexible, accessible financial products that align with agricultural seasons and cash flows."
+
+ The microloans range from ā¦50,000 to ā¦500,000 with repayment terms that align with harvest cycles. The application process is simple and can be completed entirely through the AgroMarket mobile app or website. Approval decisions are typically made within 48 hours.
+
+ The crop insurance products cover risks such as drought, excessive rainfall, pests, and diseases. Premiums are affordable, starting at just 3% of the insured amount, making it accessible to even the smallest-scale farmers.
+
+ "We've already piloted these financial services with 500 farmers across three states, and the results have been remarkable," says Chioma Okafor, Chief Operations Officer at AgroMarket. "Farmers who accessed loans for improved inputs saw yield increases of 30-50%, while those with insurance were able to recover quickly from weather-related losses."
+
+ AgroMarket has partnered with several financial institutions and insurance companies to provide these services, including MicroFinance Bank, AgriInsure, and FarmCredit Nigeria.
+
+ Farmers interested in accessing these financial services can apply through their AgroMarket account or visit one of our regional offices for assistance.
+ `,
+ image: "/assets/img/news/financial-services.jpg",
+ author: "AgroMarket Team",
+ date: "May 1, 2023",
+ category: "Financial Services",
+ tags: ["Finance", "Loans", "Insurance", "Smallholder Farmers"]
+ },
+ {
+ id: 2,
+ title: "AgroMarket Expands Cold Chain Logistics Network to Serve More Regions",
+ slug: "cold-chain-logistics-expansion",
+ excerpt: "New refrigerated vehicles and cold storage facilities will help reduce post-harvest losses and connect farmers in remote areas to urban markets.",
+ content: `
+ AgroMarket has announced a significant expansion of its cold chain logistics network, adding 20 new refrigerated vehicles and establishing cold storage facilities in five additional states across Nigeria. This expansion aims to reduce post-harvest losses and enable farmers in remote areas to access urban markets with perishable produce.
+
+ Post-harvest losses are a major challenge in Nigeria's agricultural sector, with estimates suggesting that up to 40% of fresh produce is lost between harvest and market due to inadequate storage and transportation infrastructure. AgroMarket's expanded cold chain network directly addresses this challenge.
+
+ "Our cold chain logistics service has been one of our most impactful offerings," explains Emmanuel Nwachukwu, Chief Technology Officer at AgroMarket. "By maintaining the cold chain from farm to market, we've helped farmers reduce losses by up to 80% and extend the shelf life of their products, allowing them to reach more distant markets and command better prices."
+
+ The new refrigerated vehicles range from small vans suitable for last-mile delivery to large trucks capable of transporting bulk produce across long distances. The cold storage facilities, located in strategic agricultural hubs, provide temporary storage for produce awaiting transportation or distribution.
+
+ Fatima Abdullahi, Head of Farmer Relations at AgroMarket, highlights the impact on farmers: "Before our cold chain service, many farmers in remote areas were limited to selling locally or through middlemen who offered low prices. Now, a tomato farmer in Kano can reliably supply restaurants in Lagos, knowing their produce will arrive fresh and in perfect condition."
+
+ The expanded cold chain network now covers 15 states, with plans to achieve nationwide coverage by the end of next year. Farmers can book cold chain logistics services through the AgroMarket platform, with pricing based on volume, distance, and specific temperature requirements.
+
+ This expansion was made possible through a partnership with ColdLogistics Nigeria and a grant from the Agricultural Transformation Initiative.
+ `,
+ image: "/assets/img/news/cold-chain.jpg",
+ author: "AgroMarket Team",
+ date: "April 15, 2023",
+ category: "Logistics",
+ tags: ["Cold Chain", "Logistics", "Post-harvest Losses", "Market Access"]
+ },
+ {
+ id: 3,
+ title: "AgroMarket Partners with Ministry of Agriculture on Farmer Training Program",
+ slug: "ministry-partnership-farmer-training",
+ excerpt: "New partnership will provide digital literacy and modern farming techniques training to 10,000 farmers across Nigeria.",
+ content: `
+ AgroMarket has announced a strategic partnership with the Federal Ministry of Agriculture and Rural Development to provide comprehensive training to 10,000 farmers across Nigeria over the next 12 months. The training program will focus on digital literacy, modern farming techniques, and market access strategies.
+
+ The partnership was formalized at a signing ceremony attended by the Minister of Agriculture and Rural Development, the CEO of AgroMarket, and representatives from various farmer associations. The program will be implemented in all six geopolitical zones of Nigeria, with a special focus on women and youth farmers.
+
+ "This partnership represents a significant step toward our goal of digitizing Nigeria's agricultural sector," said the Minister during the signing ceremony. "By combining the Ministry's reach and resources with AgroMarket's technological expertise and market connections, we can accelerate the adoption of modern farming practices and digital tools among smallholder farmers."
+
+ The training program consists of three modules:
+
+
+ Digital Literacy: Basic smartphone usage, accessing agricultural information online, using the AgroMarket platform, digital financial services, and online safety.
+ Modern Farming Techniques: Climate-smart agriculture, integrated pest management, efficient irrigation methods, soil health management, and post-harvest handling.
+ Market Access Strategies: Product quality standards, pricing strategies, negotiation skills, contract farming, and cooperative formation.
+
+
+ Each farmer will receive a smartphone with pre-installed agricultural apps, including the AgroMarket platform, and three months of free internet access to practice their new digital skills.
+
+ "We've seen firsthand how digital literacy and market knowledge can transform a farmer's business," explains Fatima Abdullahi, Head of Farmer Relations at AgroMarket. "Farmers who effectively use our platform typically see income increases of 30-50% due to better prices, reduced losses, and access to premium markets."
+
+ The training will be delivered through a combination of in-person workshops, mobile learning, and peer-to-peer support groups. Local agricultural extension officers will be trained as facilitators to ensure the program's sustainability beyond the initial 12-month period.
+
+ Farmers interested in participating in the training program can register through their local agricultural extension office or directly through the AgroMarket platform.
+ `,
+ image: "/assets/img/news/farmer-training.jpg",
+ author: "AgroMarket Team",
+ date: "March 20, 2023",
+ category: "Education",
+ tags: ["Training", "Digital Literacy", "Government Partnership", "Capacity Building"]
+ },
+ {
+ id: 4,
+ title: "AgroMarket Introduces Quality Verification System for Agricultural Products",
+ slug: "quality-verification-system",
+ excerpt: "New blockchain-based system allows buyers to verify the quality, origin, and handling of agricultural products purchased through the platform.",
+ content: `
+ AgroMarket has launched an innovative blockchain-based quality verification system that allows buyers to trace the journey of agricultural products from farm to table. The system provides transparent information about a product's origin, farming practices, handling, and quality certifications.
+
+ The quality verification system works through QR codes attached to product packaging or accompanying documentation. Buyers can scan these codes using the AgroMarket app to access a complete history of the product, including:
+
+
+ Farm location and farmer profile
+ Planting and harvest dates
+ Farming practices (organic, conventional, etc.)
+ Any inputs used (fertilizers, pesticides, etc.)
+ Quality certifications
+ Storage and transportation conditions
+ Quality inspection results
+
+
+ "Trust and transparency are essential in agricultural commerce," says Emmanuel Nwachukwu, Chief Technology Officer at AgroMarket. "Our quality verification system gives buyers confidence in the products they purchase while allowing farmers who follow best practices to differentiate themselves and command premium prices."
+
+ The system is particularly valuable for export-oriented farmers and those supplying premium markets such as high-end restaurants, supermarkets, and food processors with strict quality requirements.
+
+ David Okonkwo, owner of Fresh Foods Supermarket in Abuja, has been using the system during its pilot phase: "Our customers increasingly want to know where their food comes from and how it was produced. The quality verification system allows us to provide this information transparently, which has significantly increased customer trust and loyalty."
+
+ For farmers, the system provides a structured way to document their practices and demonstrate their commitment to quality. Farmers who participate in the quality verification system receive training on quality standards and documentation practices.
+
+ "Initially, I was concerned about the additional documentation required," admits Ibrahim Musa, a tomato farmer from Kano. "But the AgroMarket team helped me set up simple systems to record the necessary information, and the premium prices I now receive more than compensate for the extra effort."
+
+ The quality verification system is now available to all farmers and buyers on the AgroMarket platform, with plans to expand its capabilities to include more detailed environmental and social impact metrics in the future.
+ `,
+ image: "/assets/img/news/quality-verification.jpg",
+ author: "AgroMarket Team",
+ date: "February 10, 2023",
+ category: "Technology",
+ tags: ["Blockchain", "Quality Assurance", "Traceability", "Food Safety"]
+ },
+ {
+ id: 5,
+ title: "AgroMarket Secures $5 Million in Series A Funding to Expand Operations",
+ slug: "series-a-funding",
+ excerpt: "Investment will fund expansion to all 36 states in Nigeria and the development of new features to support smallholder farmers.",
+ content: `
+ AgroMarket has successfully raised $5 million in Series A funding to support its expansion across Nigeria and the development of new platform features. The funding round was led by AgriTech Ventures, with participation from Impact Investors Nigeria, Tech Growth Capital, and several angel investors with backgrounds in agriculture and technology.
+
+ "This investment represents a strong vote of confidence in our mission to transform agricultural commerce in Nigeria," says Adebayo Johnson, CEO of AgroMarket. "With this funding, we will accelerate our expansion to all 36 states, enhance our technology platform, and develop new services that address the unique challenges faced by smallholder farmers."
+
+ AgroMarket currently operates in 15 states, serving over 50,000 farmers and 5,000 buyers. The platform has facilitated transactions worth more than ā¦2 billion since its launch in 2022, with the average farmer seeing a 40% increase in income after joining the platform.
+
+ The new funding will be allocated to several key initiatives:
+
+
+ Geographic Expansion: Establishing operations in all 36 states of Nigeria, including setting up regional offices, building logistics networks, and recruiting local teams.
+ Technology Enhancement: Developing new features such as weather forecasting, pest and disease alerts, and enhanced market analytics to help farmers make informed decisions.
+ Financial Services: Expanding the range of financial products available to farmers, including input financing, equipment leasing, and crop insurance.
+ Farmer Training: Scaling up digital literacy and agricultural best practices training programs to reach 100,000 farmers over the next two years.
+ Team Growth: Doubling the size of the team, with a focus on technology, farmer support, and logistics operations.
+
+
+ Lead investor AgriTech Ventures has a strong track record of supporting agricultural technology companies across Africa. "AgroMarket stands out for its deep understanding of the challenges faced by smallholder farmers and its innovative approach to addressing these challenges," says Sarah Kimani, Partner at AgriTech Ventures. "We're excited to support their mission to create a more efficient, transparent, and equitable agricultural marketplace."
+
+ The funding comes at a time of growing recognition of the importance of digital platforms in transforming African agriculture. A recent report by the African Development Bank identified digital marketplaces as a key driver of agricultural productivity and rural income growth.
+
+ "This investment will allow us to reach more farmers in more remote areas, helping them access better markets and improve their livelihoods," says Chioma Okafor, Chief Operations Officer at AgroMarket. "We're particularly excited about expanding our presence in the northeast and northwest regions, where farmers face significant market access challenges."
+
+ AgroMarket expects to complete its nationwide expansion by the end of 2023 and aims to serve 200,000 farmers by 2025.
+ `,
+ image: "/assets/img/news/funding.jpg",
+ author: "AgroMarket Team",
+ date: "January 15, 2023",
+ category: "Business",
+ tags: ["Funding", "Expansion", "Investment", "Growth"]
+ }
+];
+
+export default function NewsPage() {
+ return (
+ <>
+
+
+ {/* Hero Section */}
+
+
+
+
+
+
+ Latest News
+
+
+ Stay updated with the latest developments, innovations, and stories from AgroMarket and the agricultural sector.
+
+
+
+
+
+ {/* Featured Article */}
+
+
+
+
Featured Story
+
+ Our most important recent development in agricultural innovation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {newsArticles[0].date}
+
+
+
+ {newsArticles[0].author}
+
+
+
+ {newsArticles[0].category}
+
+
+
+
{newsArticles[0].title}
+
{newsArticles[0].excerpt}
+
+
+ Read full article
+
+
+
+
+
+
+
+ {/* All News Articles */}
+
+
+
+
Recent News
+
+ Stay informed about the latest developments in AgroMarket and the agricultural sector.
+
+
+
+
+ {newsArticles.slice(1).map((article) => (
+
+
+
+
+
+
+
+
+
+ {article.date}
+
+
+
+ {article.category}
+
+
+
+
{article.title}
+
{article.excerpt}
+
+
+ Read more
+
+
+
+ ))}
+
+
+ {/* Pagination - for future expansion */}
+
+
+ 1
+ 2
+ 3
+ ...
+
+
+
+
+
+ {/* Newsletter Signup */}
+
+
+
+
+
+
Stay Updated with AgroMarket News
+
+ Subscribe to our newsletter to receive the latest news, market insights, and agricultural tips directly in your inbox.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Categories Section */}
+
+
+
+
News Categories
+
+ Explore news by topic to find the information most relevant to you.
+
+
+
+
+ {[
+ { name: "Technology", icon: "š„ļø", description: "Innovations in agricultural technology" },
+ { name: "Business", icon: "š", description: "Market trends and business insights" },
+ { name: "Education", icon: "š", description: "Training programs and knowledge sharing" },
+ { name: "Logistics", icon: "š", description: "Supply chain and distribution news" },
+ { name: "Financial Services", icon: "š°", description: "Funding, loans, and insurance" },
+ { name: "Policy", icon: "š", description: "Agricultural policies and regulations" },
+ { name: "Sustainability", icon: "š±", description: "Sustainable farming practices" },
+ { name: "Community", icon: "š„", description: "Farmer success stories and community initiatives" }
+ ].map((category, index) => (
+
+
{category.icon}
+
{category.name}
+
{category.description}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/newsletter-error/page.tsx b/app/newsletter-error/page.tsx
new file mode 100644
index 0000000..cf51afd
--- /dev/null
+++ b/app/newsletter-error/page.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import { useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import Image from 'next/image';
+import { AlertTriangle } from 'lucide-react';
+import Navbar from '@/components/Navbar';
+import Footer from '@/components/Footer';
+
+export default function NewsletterError() {
+ const searchParams = useSearchParams();
+ const errorType = searchParams.get('message');
+ const [errorMessage, setErrorMessage] = useState('');
+
+ useEffect(() => {
+ switch (errorType) {
+ case 'missing_token':
+ setErrorMessage('The confirmation link is missing a required token.');
+ break;
+ case 'invalid_token':
+ setErrorMessage('The confirmation link is invalid or has expired.');
+ break;
+ case 'server_error':
+ setErrorMessage('There was a server error processing your request.');
+ break;
+ default:
+ setErrorMessage('An unknown error occurred.');
+ }
+ }, [errorType]);
+
+ return (
+ <>
+
+
+
+
+
+
+ Subscription Error
+
+
+ {errorMessage}
+
+
+
+
+ Please try subscribing again from our homepage or contact support if the issue persists.
+
+
+
+ Return to Homepage
+
+
+ Contact Support
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/newsletter-success/page.tsx b/app/newsletter-success/page.tsx
new file mode 100644
index 0000000..0f9bf3c
--- /dev/null
+++ b/app/newsletter-success/page.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import { useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import Image from 'next/image';
+import { CheckCircle } from 'lucide-react';
+import Navbar from '@/components/Navbar';
+import Footer from '@/components/Footer';
+
+export default function NewsletterSuccess() {
+ const searchParams = useSearchParams();
+ const status = searchParams.get('status');
+ const [message, setMessage] = useState('');
+
+ useEffect(() => {
+ if (status === 'already_confirmed') {
+ setMessage('You have already confirmed your subscription to our newsletter.');
+ } else {
+ setMessage('Thank you for confirming your subscription to our newsletter!');
+ }
+ }, [status]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Subscription Confirmed
+
+
+ {message}
+
+
+
+
+ You'll now receive updates on new products, farming tips, and exclusive offers.
+
+
+ Return to Homepage
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/newsletter/confirm/page.tsx b/app/newsletter/confirm/page.tsx
new file mode 100644
index 0000000..c4a6461
--- /dev/null
+++ b/app/newsletter/confirm/page.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useSearchParams } from "next/navigation";
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import Link from "next/link";
+import { CheckCircle, XCircle, Loader2 } from "lucide-react";
+
+export default function NewsletterConfirmPage() {
+ const searchParams = useSearchParams();
+ const token = searchParams.get("token");
+
+ const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
+ const [message, setMessage] = useState("");
+
+ useEffect(() => {
+ const confirmSubscription = async () => {
+ if (!token) {
+ setStatus("error");
+ setMessage("Invalid confirmation link. No token provided.");
+ return;
+ }
+
+ try {
+ const response = await fetch("/api/newsletter/confirm", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ token }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ setStatus("success");
+ setMessage(data.message || "Your subscription has been confirmed!");
+ } else {
+ setStatus("error");
+ setMessage(data.error || "Failed to confirm subscription. Please try again.");
+ }
+ } catch (error) {
+ console.error("Error confirming subscription:", error);
+ setStatus("error");
+ setMessage("An error occurred. Please try again later.");
+ }
+ };
+
+ confirmSubscription();
+ }, [token]);
+
+ return (
+ <>
+
+
+
+
+ {status === "loading" && (
+
+
+
Confirming Subscription
+
Please wait while we confirm your subscription...
+
+ )}
+
+ {status === "success" && (
+
+
+
Subscription Confirmed!
+
{message}
+
+ Thank you for subscribing to our newsletter. You'll now receive updates on our latest products,
+ agricultural tips, and exclusive offers.
+
+
+ Return to Home
+
+
+ )}
+
+ {status === "error" && (
+
+
+
Confirmation Failed
+
{message}
+
+ There was a problem confirming your subscription. The link may have expired or is invalid.
+
+
+
+ Return to Home
+
+
+ Try Again
+
+
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
index 433c8aa..1948f16 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,101 +1,28 @@
-import Image from "next/image";
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import EnhancedHero from "@/components/EnhancedHero";
+import FeaturedProducts from "@/components/FeaturedProducts";
+import EnhancedHowItWorks from "@/components/EnhancedHowItWorks";
+import SustainabilitySection from "@/components/SustainabilitySection";
+import CommunitySection from "@/components/CommunitySection";
+import EnhancedCallToAction from "@/components/EnhancedCallToAction";
+import FarmerHighlights from "@/components/FarmerHighlights";
+import LatestNews from "@/components/LatestNews";
export default function Home() {
+ console.log('Rendering Home Page');
return (
-
-
-
-
-
- Get started by editing{" "}
-
- app/page.tsx
-
- .
-
- Save and see your changes instantly.
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
);
}
diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx
new file mode 100644
index 0000000..3ac3244
--- /dev/null
+++ b/app/privacy/page.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+
+export default function PrivacyPolicy() {
+ return (
+
+
+
+
+
+
+
Privacy Policy
+
Last updated: {new Date().toLocaleDateString()}
+
+
+
+
+
+
+
+ 1. Introduction
+
+ AgroMarket ("we", "us", or "our") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our agricultural marketplace platform.
+
+
+ Please read this Privacy Policy carefully. By using our Service, you agree to the collection and use of information in accordance with this policy.
+
+
+
+
+ 2. Information We Collect
+
+ 2.1 Personal Information
+ We collect information you provide directly to us:
+
+ Name and contact information (email, phone number, address)
+ Account credentials (username, password)
+ Profile information (farm details, business information)
+ Payment information (processed securely by third-party providers)
+ Product listings and descriptions
+ Communication and transaction history
+
+
+ 2.2 Automatically Collected Information
+
+ Device information (IP address, browser type, operating system)
+ Usage information (pages visited, time spent, click patterns)
+ Location information (with your consent)
+ Cookies and similar tracking technologies
+
+
+ 2.3 Information from Third Parties
+
+ Social media platforms (when you connect your accounts)
+ Payment processors and financial institutions
+ Agricultural data providers and market information services
+
+
+
+
+ 3. How We Use Your Information
+ We use the information we collect to:
+
+ Provide, operate, and maintain our marketplace platform
+ Process transactions and manage user accounts
+ Facilitate communication between buyers and sellers
+ Send notifications about your account and transactions
+ Provide customer support and respond to inquiries
+ Improve our services and develop new features
+ Ensure platform security and prevent fraud
+ Comply with legal obligations and enforce our terms
+ Send marketing communications (with your consent)
+ Analyze usage patterns and market trends
+
+
+
+
+ 4. Information Sharing and Disclosure
+
+ 4.1 With Other Users
+
+ Your profile information and product listings are visible to other users to facilitate transactions. We encourage users to share only necessary information for business purposes.
+
+
+ 4.2 With Service Providers
+ We may share your information with trusted third parties who help us operate our platform:
+
+ Payment processors and financial service providers
+ Cloud storage and hosting providers
+ Analytics and marketing service providers
+ Customer support and communication tools
+
+
+ 4.3 Legal Requirements
+ We may disclose your information when required by law or to:
+
+ Comply with legal processes or government requests
+ Protect our rights, property, or safety
+ Investigate and prevent fraud or security issues
+ Enforce our terms of service
+
+
+
+
+ 5. Data Security
+
+ We implement appropriate technical and organizational measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction. These measures include:
+
+
+ Encryption of data in transit and at rest
+ Regular security assessments and updates
+ Access controls and authentication procedures
+ Employee training on data protection
+ Incident response and breach notification procedures
+
+
+
+
+ 6. Your Rights and Choices
+ You have the following rights regarding your personal information:
+
+ 6.1 Access and Portability
+ You can access and download your personal data from your account settings.
+
+ 6.2 Correction and Updates
+ You can update your information directly through your account or by contacting us.
+
+ 6.3 Deletion
+ You can request deletion of your account and associated data, subject to legal retention requirements.
+
+ 6.4 Marketing Communications
+ You can opt out of marketing emails by clicking the unsubscribe link or updating your preferences.
+
+
+
+ 7. Cookies and Tracking Technologies
+
+ We use cookies and similar technologies to enhance your experience, analyze usage, and provide personalized content. You can control cookie preferences through your browser settings.
+
+
+ Essential cookies are necessary for platform functionality, while optional cookies help us improve our services and provide relevant content.
+
+
+
+
+ 8. International Data Transfers
+
+ Your information may be transferred to and processed in countries other than your country of residence. We ensure appropriate safeguards are in place to protect your information during such transfers.
+
+
+
+
+ 9. Data Retention
+
+ We retain your personal information for as long as necessary to provide our services, comply with legal obligations, resolve disputes, and enforce our agreements. Specific retention periods depend on the type of information and applicable legal requirements.
+
+
+
+
+ 10. Children's Privacy
+
+ Our Service is not intended for children under the age of 13. We do not knowingly collect personal information from children under 13. If we become aware of such collection, we will take steps to delete the information.
+
+
+
+
+ 11. Updates to This Policy
+
+ We may update this Privacy Policy from time to time to reflect changes in our practices or legal requirements. We will notify you of significant changes through our platform or via email.
+
+
+ Your continued use of our Service after any changes indicates your acceptance of the updated Privacy Policy.
+
+
+
+
+ 12. Contact Us
+
+ If you have any questions about this Privacy Policy or our privacy practices, please contact us:
+
+
+
Email: privacy@agromarketng.com
+
Address: Lagos, Nigeria
+
Phone: +234 (0) 123 456 7890
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/products/[id]/page.tsx b/app/products/[id]/page.tsx
new file mode 100644
index 0000000..773d054
--- /dev/null
+++ b/app/products/[id]/page.tsx
@@ -0,0 +1,299 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import { useParams } from 'next/navigation';
+import Image from 'next/image';
+import Navbar from '@/components/Navbar';
+import Footer from '@/components/Footer';
+import { Button } from '@/components/ui/button';
+import { Ad } from '@/types';
+import { Loader2, MapPin, Calendar, Eye, Share2, MessageCircle, PhoneCall, Heart, AlertCircle } from 'lucide-react';
+import { formatCurrency } from '@/lib/utils';
+import { formatDistanceToNow } from 'date-fns';
+import toast from 'react-hot-toast';
+import { useRouter } from "next/navigation";
+import { useSession } from '@/components/SessionWrapper';
+
+export default function ProductDetails() {
+ const { session } = useSession();
+ const router = useRouter();
+ const params = useParams();
+ const [product, setProduct] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [selectedImage, setSelectedImage] = useState(0);
+ const [isSaved, setIsSaved] = useState(false);
+ const [showPhone, setShowPhone] = useState(false);
+
+ useEffect(() => {
+ const fetchProduct = async () => {
+ try {
+ const response = await fetch(`/api/products/${params.id}`);
+ if (!response.ok) throw new Error('Failed to fetch product');
+ const data = await response.json();
+ setProduct(data);
+
+ // Record view
+ await fetch(`/api/ads/${params.id}/analytics`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ type: 'view' })
+ });
+ } catch (error) {
+ console.error('Error:', error);
+ toast.error('Failed to load product details');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (params.id) {
+ fetchProduct();
+ }
+ }, [params.id]);
+
+ const handleContact = async () => {
+ if (!session) {
+ toast.error('Please sign in to contact the seller');
+ router.push('/signin');
+ return;
+ }
+
+ if (!product) return;
+ console.log("product: ", product);
+
+ try {
+ // Don't allow users to message themselves
+ if (session.id === product.userId) {
+ console.log("You can't message your own ad")
+ toast.error("You can't message your own ad");
+ return;
+ }
+
+ toast.loading('Starting conversation...');
+
+ const response = await fetch('/api/conversations/initiate', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ adId: product.id,
+ initialMessage: `Hi, I'm interested in your ad: ${product.title}`
+ }),
+ credentials: 'include'
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to initiate conversation');
+ }
+
+ if (!data.conversation?.id) {
+ throw new Error('No conversation ID returned');
+ }
+
+ toast.dismiss();
+ toast.success('Conversation started successfully!');
+
+ // Redirect to messages with the conversation ID
+ router.push(`/dashboard/messages?conversationId=${data.conversation.id}`);
+ } catch (error) {
+ toast.dismiss();
+ console.error('Error initiating conversation:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to start conversation');
+ }
+ };
+
+ const handleShare = async () => {
+ try {
+ await navigator.share({
+ title: product?.title,
+ text: product?.description,
+ url: window.location.href,
+ });
+
+ // Record share
+ await fetch(`/api/ads/${params.id}/analytics`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ type: 'share' })
+ });
+ } catch (error) {
+ console.error('Error sharing:', error);
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!product) {
+ return (
+
+
+
+
+
Product Not Found
+
This product may have been removed or is no longer available.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {/* Image Gallery */}
+
+
+
+ {product.featured && (
+
+ Featured
+
+ )}
+
+
+ {/* Thumbnail Gallery */}
+ {product.images.length > 1 && (
+
+ {product.images.map((image, index) => (
+ setSelectedImage(index)}
+ className={`aspect-square relative rounded-md overflow-hidden ${selectedImage === index ? 'ring-2 ring-green-500' : ''
+ }`}
+ >
+
+
+ ))}
+
+ )}
+
+
+ {/* Product Info */}
+
+
+
{product.title}
+
+ {formatCurrency(Number(product.price))}
+
+
+
+
+
+
+ {product.location}
+
+
+
+ {formatDistanceToNow(new Date(product.createdAt), { addSuffix: true })}
+
+
+
+ {product.views} views
+
+
+
+
+
Description
+
{product.description}
+
+
+
+
Seller Information
+
+
+
+
{product.user.name}
+
Member since {new Date(product.user.createdAt).getFullYear()}
+
+
+
+
+ {/* Action Buttons */}
+
+
+
+ Contact Seller
+
+
+
+
setShowPhone(!showPhone)}
+ className="w-full"
+ >
+
+ {showPhone ? (
+ {product.contact}
+ ) : (
+ 'Show Phone Number'
+ )}
+
+
+
+
+ Share
+
+
+
+
{
+ setIsSaved(!isSaved);
+ toast.success(isSaved ? 'Removed from saved items' : 'Added to saved items');
+ }}
+ className={`w-full ${isSaved ? 'bg-gray-50 text-green-600' : ''}`}
+ >
+
+ {isSaved ? 'Saved' : 'Save'}
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/products/page.tsx b/app/products/page.tsx
new file mode 100644
index 0000000..71163b4
--- /dev/null
+++ b/app/products/page.tsx
@@ -0,0 +1,624 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import Image from "next/image";
+import Link from "next/link";
+import { Loader2, Filter, ChevronDown, ChevronLeft, ChevronRight, Search, X } from "lucide-react";
+import { formatCurrency } from "@/lib/utils";
+import { getProductImageUrl } from "@/lib/imageUtils";
+import { getMainCategories, getAllCategoriesWithSubcategories } from "@/lib/categoryUtils";
+import toast from "react-hot-toast";
+
+interface Product {
+ id: string;
+ title: string;
+ price: number;
+ category: string;
+ location: string;
+ images: string[];
+ views: number;
+ clicks: number;
+ rating: number;
+ reviews: number;
+ description: string;
+ createdAt: string;
+ updatedAt: string;
+ user: {
+ name: string;
+ id: string;
+ image?: string;
+ };
+}
+
+interface Subcategory {
+ name: string;
+ href: string;
+ section: string;
+}
+
+interface CategoryWithSubcategories {
+ category: string;
+ subcategories: Subcategory[];
+}
+
+export default function ProductsPage() {
+ const [products, setProducts] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [subcategories, setSubcategories] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [activeCategory, setActiveCategory] = useState("All");
+ const [activeSubcategory, setActiveSubcategory] = useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [sortBy, setSortBy] = useState("newest");
+ const [showFilters, setShowFilters] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const [totalItems, setTotalItems] = useState(0);
+ const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000000]);
+ const [locations, setLocations] = useState([]);
+ const [selectedLocations, setSelectedLocations] = useState([]);
+
+ // Initialize categories from navigation structure
+ useEffect(() => {
+ // Set initial categories from navigation structure
+ setCategories(getMainCategories());
+ }, []);
+
+ // Fetch products data
+ useEffect(() => {
+ const fetchProducts = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // Build query parameters
+ const params = new URLSearchParams();
+ if (activeCategory !== "All") params.append("category", activeCategory);
+ if (activeSubcategory) params.append("subCategory", activeSubcategory);
+ if (searchQuery) params.append("q", searchQuery);
+ params.append("sort", sortBy);
+ params.append("page", currentPage.toString());
+ params.append("minPrice", priceRange[0].toString());
+ params.append("maxPrice", priceRange[1].toString());
+ if (selectedLocations.length > 0) {
+ selectedLocations.forEach(loc => params.append("locations", loc));
+ }
+
+ const response = await fetch(`/api/featured-products?${params.toString()}`);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch products');
+ }
+
+ const data = await response.json();
+ setProducts(data.products || []);
+
+ // Update subcategories if available
+ if (data.subcategories) {
+ setSubcategories(data.subcategories);
+ }
+
+ // Set pagination data
+ if (data.pagination) {
+ setCurrentPage(data.pagination.currentPage);
+ setTotalPages(data.pagination.totalPages);
+ setTotalItems(data.pagination.totalItems);
+ } else {
+ setTotalPages(1);
+ setTotalItems(data.products?.length || 0);
+ }
+
+ // Set locations if available
+ if (data.locations) {
+ setLocations(data.locations);
+ }
+ } catch (err) {
+ console.error('Error fetching products:', err);
+ setError('Failed to load products. Please try again later.');
+ toast.error('Failed to load products. Please try again later.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchProducts();
+ }, [activeCategory, activeSubcategory, searchQuery, sortBy, currentPage, priceRange, selectedLocations]);
+
+ // Handle search submit
+ const handleSearch = (e: React.FormEvent) => {
+ e.preventDefault();
+ setCurrentPage(1); // Reset to first page on new search
+ };
+
+ // Handle category change
+ const handleCategoryChange = (category: string) => {
+ setActiveCategory(category);
+ setActiveSubcategory(null); // Reset subcategory when changing category
+ setCurrentPage(1); // Reset to first page on category change
+ };
+
+ // Handle subcategory change
+ const handleSubcategoryChange = (subcategory: string | null) => {
+ setActiveSubcategory(subcategory);
+ setCurrentPage(1); // Reset to first page on subcategory change
+ };
+
+ // Handle sort change
+ const handleSortChange = (e: React.ChangeEvent) => {
+ setSortBy(e.target.value);
+ setCurrentPage(1); // Reset to first page on sort change
+ };
+
+ // Handle price range change
+ const handlePriceRangeChange = (e: React.ChangeEvent, index: number) => {
+ const newValue = parseInt(e.target.value);
+ setPriceRange(prev => {
+ const newRange = [...prev] as [number, number];
+ newRange[index] = newValue;
+ return newRange;
+ });
+ };
+
+ // Handle location selection
+ const handleLocationToggle = (location: string) => {
+ setSelectedLocations(prev => {
+ if (prev.includes(location)) {
+ return prev.filter(loc => loc !== location);
+ } else {
+ return [...prev, location];
+ }
+ });
+ setCurrentPage(1); // Reset to first page on location change
+ };
+
+ // Clear all filters
+ const clearFilters = () => {
+ setActiveCategory("All");
+ setActiveSubcategory(null);
+ setSearchQuery("");
+ setSortBy("newest");
+ setPriceRange([0, 1000000]);
+ setSelectedLocations([]);
+ setCurrentPage(1);
+ };
+
+ return (
+ <>
+
+
+ {/* Hero Section */}
+
+
+
+ Explore Our Products
+
+
+ Discover high-quality agricultural products directly from local farmers across Nigeria.
+
+
+ {/* Search Bar */}
+
+
+
+
+ {/* Main Content */}
+
+
+
+ {/* Filters - Mobile Toggle */}
+
+ setShowFilters(!showFilters)}
+ className="w-full flex items-center justify-center gap-2 bg-white p-3 rounded-md border shadow-sm"
+ >
+
+ {showFilters ? "Hide Filters" : "Show Filters"}
+
+
+
+ {/* Filters Sidebar */}
+
+
+
+
Filters
+
+ Clear All
+
+
+
+ {/* Categories */}
+
+
Categories
+
+
handleCategoryChange("All")}
+ >
+ All Categories
+
+ {categories.map((category) => (
+
handleCategoryChange(category)}
+ >
+ {category}
+
+ ))}
+
+
+
+ {/* Subcategories - Only show when a category is selected */}
+ {activeCategory !== "All" && Array.isArray(subcategories) && subcategories.length > 0 && (
+
+
Subcategories
+
+
handleSubcategoryChange(null)}
+ >
+ All {activeCategory}
+
+ {/* If subcategories is an array of Subcategory objects */}
+ {!('category' in subcategories[0]) && (subcategories as Subcategory[]).map((subcat) => (
+
handleSubcategoryChange(subcat.name)}
+ >
+ {subcat.name}
+
+ ))}
+
+
+ )}
+
+ {/* Price Range */}
+
+
Price Range
+
+
+ Min: {formatCurrency(priceRange[0])}
+ Max: {formatCurrency(priceRange[1])}
+
+
+
+
+
+ {/* Locations */}
+ {locations.length > 0 && (
+
+
Locations
+
+ {locations.map((location) => (
+
+ handleLocationToggle(location)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
+ />
+
+ {location}
+
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Products Grid */}
+
+ {/* Sort Controls */}
+
+
+
+ {isLoading ? (
+ "Loading products..."
+ ) : products.length > 0 ? (
+ `Showing ${products.length} products`
+ ) : (
+ "No products found"
+ )}
+
+
+
+ Sort by:
+
+ Newest
+ Price: Low to High
+ Price: High to Low
+ Most Popular
+
+
+
+
+ {/* Active Filters */}
+ {(activeCategory !== "All" || activeSubcategory || searchQuery || selectedLocations.length > 0 || priceRange[0] > 0 || priceRange[1] < 1000000) && (
+
+
+ Active Filters:
+
+ {activeCategory !== "All" && (
+
+ Category: {activeCategory}
+ setActiveCategory("All")} className="ml-1">
+
+
+
+ )}
+
+ {activeSubcategory && (
+
+ Subcategory: {activeSubcategory}
+ setActiveSubcategory(null)} className="ml-1">
+
+
+
+ )}
+
+ {searchQuery && (
+
+ Search: {searchQuery}
+ setSearchQuery("")} className="ml-1">
+
+
+
+ )}
+
+ {(priceRange[0] > 0 || priceRange[1] < 1000000) && (
+
+ Price: {formatCurrency(Number(priceRange[0]))} - {formatCurrency(Number(priceRange[1]))}
+ setPriceRange([0, 1000000])} className="ml-1">
+
+
+
+ )}
+
+ {selectedLocations.map(location => (
+
+ Location: {location}
+ handleLocationToggle(location)} className="ml-1">
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Products */}
+ {isLoading ? (
+
+
+
+ ) : products.length > 0 ? (
+
+ {products.map((product) => (
+
+
+
+
+
+ {product.category}
+
+ {product.rating >= 4.5 && (
+
+ )}
+
+
+
+
+
+
{product.title}
+
{formatCurrency(product.price)}
+
+
+
{product.description}
+
+
+ {product.location}
+ ā¢
+ {product.views} views
+
+
+
+
+
+ {[...Array(5)].map((_, i) => (
+
+
+
+ ))}
+
+
+ ({product.reviews})
+
+
+
+ {product.user.image ? (
+
+ ) : (
+
+ {product.user.name.charAt(0).toUpperCase()}
+
+ )}
+
+ {product.user.name}
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
No products found matching your criteria.
+
+ Clear Filters
+
+
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Showing {products.length} of {totalItems} products - Page {currentPage} of {totalPages}
+
+
+ setCurrentPage(prev => Math.max(prev - 1, 1))}
+ disabled={currentPage === 1}
+ className={`p-2 rounded-md ${currentPage === 1
+ ? "text-gray-400 cursor-not-allowed"
+ : "text-gray-700 hover:bg-gray-100"
+ }`}
+ aria-label="Previous page"
+ >
+
+
+
+ {[...Array(totalPages)].map((_, i) => {
+ const page = i + 1;
+ // Show first page, last page, current page, and pages around current
+ if (
+ page === 1 ||
+ page === totalPages ||
+ (page >= currentPage - 1 && page <= currentPage + 1)
+ ) {
+ return (
+ setCurrentPage(page)}
+ className={`px-4 py-2 rounded-md ${currentPage === page
+ ? "bg-green-600 text-white"
+ : "text-gray-700 hover:bg-gray-100"
+ }`}
+ aria-label={`Page ${page}`}
+ aria-current={currentPage === page ? "page" : undefined}
+ >
+ {page}
+
+ );
+ }
+
+ // Show ellipsis for skipped pages
+ if (
+ (page === 2 && currentPage > 3) ||
+ (page === totalPages - 1 && currentPage < totalPages - 2)
+ ) {
+ return ... ;
+ }
+
+ return null;
+ })}
+
+ setCurrentPage(prev => Math.min(prev + 1, totalPages))}
+ disabled={currentPage === totalPages}
+ className={`p-2 rounded-md ${currentPage === totalPages
+ ? "text-gray-400 cursor-not-allowed"
+ : "text-gray-700 hover:bg-gray-100"
+ }`}
+ aria-label="Next page"
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/providers.tsx b/app/providers.tsx
new file mode 100644
index 0000000..01b2a65
--- /dev/null
+++ b/app/providers.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import { useState } from 'react'
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(() => new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 30 * 1000, // 30 seconds
+ gcTime: 5 * 60 * 1000, // 5 minutes
+ retry: (failureCount, error: any) => {
+ // Don't retry on RSC payload errors
+ if (error?.message?.includes('RSC payload') ||
+ error?.message?.includes('NetworkError')) {
+ return false;
+ }
+ return failureCount < 2;
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: true,
+ refetchOnReconnect: 'always',
+ },
+ mutations: {
+ retry: false,
+ }
+ }
+ }))
+
+ return (
+
+ {children}
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/resetPassword/page.tsx b/app/resetPassword/page.tsx
new file mode 100644
index 0000000..e413333
--- /dev/null
+++ b/app/resetPassword/page.tsx
@@ -0,0 +1,204 @@
+"use client";
+
+import React, { useState, useEffect } from 'react';
+import { useRouter } from "next/navigation";
+import { useSearchParams } from 'next/navigation';
+import { toast } from 'react-hot-toast';
+import Spinner from '@/components/Spinner';
+import Link from 'next/link';
+import Image from 'next/image';
+import heroImg from '../../public/assets/img/agromarket-logo.png';
+import { motion } from 'framer-motion';
+import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
+
+
+const ResetPassword = () => {
+ const searchParams = useSearchParams();
+ const token = searchParams.get('token') || '';
+ const router = useRouter();
+
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [isResetingPassword, setIsResetingPassword] = useState(false);
+ const [showNewPassword, setShowNewPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+
+ // Check if token exists
+ useEffect(() => {
+ if (!token) {
+ toast.error('Invalid or missing reset token');
+ router.push('/forgotPassword');
+ }
+ }, [token, router]);
+
+ const toggleNewPasswordVisibility = () => {
+ setShowNewPassword(!showNewPassword);
+ };
+
+ const toggleConfirmPasswordVisibility = () => {
+ setShowConfirmPassword(!showConfirmPassword);
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsResetingPassword(true);
+
+ if (newPassword !== confirmPassword) {
+ toast.error('Passwords do not match');
+ setIsResetingPassword(false);
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/auth', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ type: 'reset-password', token, newPassword }),
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ toast.success('Password reset successfully');
+ // Redirect to login after successful reset
+ setTimeout(() => {
+ router.push('/signin');
+ }, 1500);
+ } else {
+ toast.error(data.error || 'Failed to reset password');
+ }
+ } catch (error) {
+ toast.error('An error occurred. Please try again.');
+ } finally {
+ setIsResetingPassword(false);
+ }
+ };
+
+ return (
+
+ {/* Left Side Form */}
+
+
+
+
+
+
+ Reset Password
+ Enter your new password below.
+
+
+
+
+
+ Back to Sign In
+
+
+
+
+ {/* Right Side Background */}
+
+ {/* Background with gradient */}
+
+ {/* Decorative elements */}
+
+
+
+ {/* Content */}
+
+
+ Almost There!
+
+
+ Create a strong password to secure your account. Once complete, you'll be able to access all our features.
+
+
+
Password Tips:
+
+ ⢠Use at least 8 characters
+ ⢠Include uppercase and lowercase letters
+ ⢠Add numbers and special characters
+ ⢠Avoid using personal information
+
+
+
+
+
+ );
+};
+
+export default ResetPassword;
diff --git a/app/search/page.tsx b/app/search/page.tsx
new file mode 100644
index 0000000..36be952
--- /dev/null
+++ b/app/search/page.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import { useSearchParams } from 'next/navigation';
+import Search from '@/components/Search';
+import Link from 'next/link';
+import { Ad } from '@/types';
+import ProductCard from '@/components/ProductCard';
+import { Loader2 } from 'lucide-react';
+import { categories } from '@/constants';
+
+interface Category {
+ id: string;
+ name: string;
+ slug: string;
+ items: {
+ name: string;
+ href: string;
+ }[];
+}
+
+export default function SearchResults() {
+ const searchParams = useSearchParams();
+ const [results, setResults] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchResults = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/search?${searchParams.toString()}`);
+ if (!response.ok) throw new Error('Search failed');
+ const data = await response.json();
+ setResults(data.results || []);
+ setTotalCount(data.pagination?.totalCount || 0);
+ } catch (error) {
+ console.error('Search error:', error);
+ setResults([]);
+ setTotalCount(0);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchResults();
+ }, [searchParams]);
+
+ return (
+
+
+
+
+
+
+ {/* Initial State - No Search Yet */}
+ {!searchParams.toString() && (
+
+
+
+ Recommended for You
+
+ {isLoading ? (
+
+
+
+ ) : results.length > 0 ? (
+
+ {results.slice(0, 4).map((ad) => (
+
+ ))}
+
+ ) : (
+
+
No featured products available at the moment.
+
Use the search above to find specific products.
+
+ )}
+
+
+ )}
+
+ {/* Search Results */}
+ {searchParams.toString() && (
+
+ {isLoading ? (
+
+
+
Searching for products...
+
+ ) : results.length > 0 ? (
+ <>
+
+
+ {totalCount} Results Found
+
+
+ {searchParams.get('q') ? `Showing results for "${searchParams.get('q')}"` : 'Browse all products'}
+
+
+
+ {results.map((ad) => (
+
+ ))}
+
+ >
+ ) : (
+
+
+
+
+
+ No Results Found
+
+
+ We couldn't find any products matching your search.
+ Try adjusting your filters or search terms.
+
+
+
+ Popular Categories
+
+
+ {categories.slice(0, 5).map((category) => (
+
+ {category.name}
+
+ ))}
+
+
+
+ )}
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/services/page.tsx b/app/services/page.tsx
new file mode 100644
index 0000000..29b106c
--- /dev/null
+++ b/app/services/page.tsx
@@ -0,0 +1,456 @@
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import Image from "next/image";
+import Link from "next/link";
+import { CheckCircle, ArrowRight } from "lucide-react";
+
+// Service data
+const services = [
+ {
+ id: 1,
+ title: "Marketplace Platform",
+ description: "Our core service is a secure, user-friendly online marketplace that connects farmers directly with buyers. Farmers can list their products, set their prices, and reach a wider customer base, while buyers can discover quality agricultural products from verified local sources.",
+ features: [
+ "Easy product listing and management",
+ "Secure payment processing",
+ "Buyer-seller messaging system",
+ "Ratings and reviews",
+ "Product verification",
+ "Search and filter capabilities"
+ ],
+ image: "/assets/img/services/marketplace.jpg",
+ cta: "Start Selling",
+ ctaLink: "/signup"
+ },
+ {
+ id: 2,
+ title: "Logistics & Delivery",
+ description: "We provide reliable transportation and delivery services to ensure agricultural products reach buyers in optimal condition. Our logistics network covers major cities and rural areas across Nigeria, with options for same-day, next-day, or scheduled deliveries.",
+ features: [
+ "Nationwide delivery coverage",
+ "Temperature-controlled transport",
+ "Real-time tracking",
+ "Flexible delivery scheduling",
+ "Bulk shipping options",
+ "Last-mile delivery solutions"
+ ],
+ image: "/assets/img/services/logistics.jpg",
+ cta: "Learn About Shipping",
+ ctaLink: "/shipping"
+ },
+ {
+ id: 3,
+ title: "Market Insights",
+ description: "Our data analytics platform provides farmers and buyers with valuable market intelligence. Access real-time pricing data, demand trends, seasonal forecasts, and competitive analysis to make informed business decisions and maximize profitability.",
+ features: [
+ "Price trend analysis",
+ "Demand forecasting",
+ "Seasonal market reports",
+ "Competitor benchmarking",
+ "Regional market insights",
+ "Customized data dashboards"
+ ],
+ image: "/assets/img/services/insights.jpg",
+ cta: "Access Insights",
+ ctaLink: "/dashboard/analytics"
+ },
+ {
+ id: 4,
+ title: "Quality Assurance",
+ description: "We implement rigorous quality control processes to ensure all products on our platform meet high standards. Our verification team conducts farm visits, product inspections, and certification checks to maintain trust and transparency in our marketplace.",
+ features: [
+ "Product quality verification",
+ "Farm inspections",
+ "Certification validation",
+ "Quality dispute resolution",
+ "Product grading standards",
+ "Safety compliance checks"
+ ],
+ image: "/assets/img/services/quality.jpg",
+ cta: "Our Quality Standards",
+ ctaLink: "/quality"
+ },
+ {
+ id: 5,
+ title: "Farmer Training",
+ description: "We offer comprehensive educational resources and training programs to help farmers improve their practices, increase productivity, and adapt to changing market demands. Our workshops cover everything from sustainable farming techniques to digital marketing skills.",
+ features: [
+ "Sustainable farming practices",
+ "Crop management techniques",
+ "Digital literacy training",
+ "Marketing and sales skills",
+ "Financial management",
+ "Climate-smart agriculture"
+ ],
+ image: "/assets/img/services/training.jpg",
+ cta: "Join Training Program",
+ ctaLink: "/training"
+ },
+ {
+ id: 6,
+ title: "Financial Services",
+ description: "We provide access to tailored financial products for agricultural businesses. Through partnerships with financial institutions, we offer loans, insurance, and payment solutions designed specifically for the unique needs and cycles of farming operations.",
+ features: [
+ "Agricultural loans",
+ "Crop insurance",
+ "Flexible payment terms",
+ "Invoice financing",
+ "Equipment leasing",
+ "Financial literacy training"
+ ],
+ image: "/assets/img/services/financial.jpg",
+ cta: "Explore Financial Options",
+ ctaLink: "/financial-services"
+ }
+];
+
+// Pricing plans
+const pricingPlans = [
+ {
+ name: "Basic",
+ price: "Free",
+ description: "Perfect for small-scale farmers just getting started.",
+ features: [
+ "List up to 5 products",
+ "Basic marketplace access",
+ "Standard visibility",
+ "Email support",
+ "Basic analytics"
+ ],
+ cta: "Get Started",
+ ctaLink: "/signup",
+ popular: false
+ },
+ {
+ name: "Premium",
+ price: "ā¦5,000",
+ period: "per month",
+ description: "Ideal for growing agricultural businesses looking to expand their reach.",
+ features: [
+ "Unlimited product listings",
+ "Featured product placement",
+ "Priority in search results",
+ "Priority customer support",
+ "Advanced analytics dashboard",
+ "Access to market insights",
+ "Discounted delivery rates"
+ ],
+ cta: "Upgrade Now",
+ ctaLink: "/pricing",
+ popular: true
+ },
+ {
+ name: "Enterprise",
+ price: "Custom",
+ description: "Tailored solutions for large-scale agricultural operations and cooperatives.",
+ features: [
+ "All Premium features",
+ "Dedicated account manager",
+ "Custom integration options",
+ "Bulk listing tools",
+ "Advanced logistics solutions",
+ "Comprehensive market analytics",
+ "Training and onboarding support"
+ ],
+ cta: "Contact Sales",
+ ctaLink: "/contact",
+ popular: false
+ }
+];
+
+export default function ServicesPage() {
+ return (
+ <>
+
+
+ {/* Hero Section */}
+
+
+
+
+
+
+ Our Services
+
+
+ Comprehensive solutions to transform agricultural commerce and empower farmers across Nigeria.
+
+
+
+
+
+ {/* Services Overview */}
+
+
+
+
How We Support Agricultural Commerce
+
+ From farm to table, we provide end-to-end solutions that connect farmers with markets and streamline the agricultural supply chain.
+
+
+
+
+ {services.slice(0, 3).map((service) => (
+
+
+
+
+
+
{service.title}
+
{service.description}
+
+ Learn more
+
+
+
+ ))}
+
+
+
+
+ View All Services
+
+
+
+
+
+
+ {/* Detailed Services */}
+
+
+
+
Our Comprehensive Services
+
+ Explore our full range of services designed to support every aspect of agricultural commerce.
+
+
+
+
+ {services.map((service, index) => (
+
+
+
+
+
{service.title}
+
{service.description}
+
+
Key Features:
+
+ {service.features.map((feature, i) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+ {service.cta}
+
+
+
+
+ ))}
+
+
+
+
+ {/* Pricing Section */}
+
+
+
+
Pricing Plans
+
+ Choose the plan that best fits your agricultural business needs.
+
+
+
+
+ {pricingPlans.map((plan, index) => (
+
+ {plan.popular && (
+
+ Most Popular
+
+ )}
+
+
+
{plan.name}
+
+ {plan.price}
+ {plan.period && (
+ {plan.period}
+ )}
+
+
{plan.description}
+
+
+ {plan.features.map((feature, i) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+ {plan.cta}
+
+
+
+ ))}
+
+
+
+
Need a custom solution for your specific requirements?
+
+ Contact Our Team
+
+
+
+
+
+
+ {/* Testimonials */}
+
+
+
+
What Our Clients Say
+
+ Hear from farmers and buyers who have experienced the benefits of our services.
+
+
+
+
+ {[
+ {
+ quote: "AgroMarket's logistics service has been a game-changer for my business. I can now deliver fresh produce to customers across Lagos without worrying about transportation.",
+ name: "Oluwaseun Adeyemi",
+ role: "Vegetable Farmer, Ogun State",
+ image: "/assets/img/testimonials/farmer1.jpg"
+ },
+ {
+ quote: "The market insights provided by AgroMarket helped me identify high-demand crops and optimize my planting schedule. My revenue has increased by 35% since I started using their platform.",
+ name: "Amina Ibrahim",
+ role: "Grain Farmer, Kaduna",
+ image: "/assets/img/testimonials/farmer2.jpg"
+ },
+ {
+ quote: "As a restaurant owner, I need reliable suppliers. AgroMarket's quality assurance process ensures I always get the best ingredients, and their delivery is always on time.",
+ name: "David Okonkwo",
+ role: "Restaurant Owner, Abuja",
+ image: "/assets/img/testimonials/customer1.jpg"
+ }
+ ].map((testimonial, index) => (
+
+
+
+
+
"{testimonial.quote}"
+
{testimonial.name}
+
{testimonial.role}
+
+
+
+ ))}
+
+
+
+
+ Read More Success Stories
+
+
+
+
+
+
+ {/* CTA Section */}
+
+
+
Ready to Transform Your Agricultural Business?
+
+ Join thousands of farmers and buyers who are already benefiting from our comprehensive services.
+
+
+
+ Get Started
+
+
+ Contact Sales
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/signin/page.tsx b/app/signin/page.tsx
new file mode 100644
index 0000000..58c5ce8
--- /dev/null
+++ b/app/signin/page.tsx
@@ -0,0 +1,245 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { useSession } from "@/components/SessionWrapper";
+import { Session } from '@/types';
+import { useSearchParams } from 'next/navigation';
+import Link from 'next/link';
+import Image from 'next/image';
+import heroImg from '../../public/assets/img/agromarket-logo.png';
+import SocialLoginIcons from '@/components/SocialLoginIcons';
+import Spinner from '@/components/Spinner';
+import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
+import { motion } from 'framer-motion';
+import toast from 'react-hot-toast';
+import { showAlertToast, showToast } from '@/lib/toast-utils';
+
+
+export default function SigninPage() {
+ const { session, setSession } = useSession();
+ const router = useRouter();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [isSigningIn, setIsSigningIn] = useState(false);
+ const searchParams = useSearchParams();
+ const alertCode = searchParams.get('alert');
+
+ // Show toast notification if alert code is present in URL
+ useEffect(() => {
+ if (alertCode) {
+ showAlertToast(alertCode);
+ }
+ }, [alertCode]);
+
+ useEffect(() => {
+ // If already authenticated, redirect to appropriate dashboard
+ if (session) {
+ switch (session.role) {
+ case "admin":
+ router.replace('/admin/dashboard');
+ break;
+ case "agent":
+ router.replace('/agent/dashboard');
+ break;
+ default:
+ router.replace('/dashboard');
+ }
+ }
+ }, [session, router]);
+
+ // Only show signin form if not authenticated
+ if (session) {
+ return (
+
+
+
+ );
+ }
+
+ // Toggle password visibility
+ const togglePasswordVisibility = () => {
+ setShowPassword(!showPassword);
+ };
+
+ const handleSignin = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSigningIn(true);
+
+ try {
+ const res = await fetch('/api/signin', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ // Create session object
+ const session = {
+ token: data.token,
+ id: data.user.id,
+ email: data.user.email,
+ name: data.user.name,
+ role: data.user.role,
+ isAgent: data.user.isAgent,
+ agentId: data.user.agentId
+ };
+
+ // Set session in context
+ setSession(session);
+
+ // Show success toast
+ toast.success(`Welcome back, ${data.user.name}!`);
+
+ // Redirect based on role
+ switch (data.user.role) {
+ case "admin":
+ router.push('/admin/dashboard');
+ break;
+ case "agent":
+ router.push('/agent/dashboard');
+ break;
+ default:
+ router.push('/dashboard');
+ }
+ } else {
+ // Show error toast
+ toast.error(data.error || 'Invalid email or password. Please try again.');
+ }
+ } catch (error) {
+ console.error('Sign in error:', error);
+ toast.error('An error occurred during sign in. Please try again.');
+ } finally {
+ setIsSigningIn(false);
+ }
+ }
+
+ return (
+
+ {/* Left Side Form */}
+
+
+
+
+
+
+ Welcome Back!
+ Sign in to your personalized dashboard
+
+
+
+ {/* Social Media Signup */}
+
+
+
+ Or continue with
+
+
+
+
+
+ {/* Redirect to Sign Up */}
+
+ Don't have an account?{" "}
+
+ Sign Up
+
+
+
+
+ {/* Right Side Background */}
+
+ {/* Background with gradient */}
+
+ {/* Decorative elements */}
+
+
+
+ {/* Content */}
+
+
+ Join the Agro Revolution
+
+
+ Sign up to access fresh produce, connect with farmers, and be part of a sustainable marketplace. Start your journey today!
+
+
+ Get Started
+
+
+
+
+ );
+}
diff --git a/app/signup/page.tsx b/app/signup/page.tsx
new file mode 100644
index 0000000..a6172d4
--- /dev/null
+++ b/app/signup/page.tsx
@@ -0,0 +1,278 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Spinner from '@/components/Spinner';
+import Image from 'next/image';
+import heroImg from '../../public/assets/img/agromarket-logo.png';
+import Link from 'next/link';
+import SocialLoginIcons from '@/components/SocialLoginIcons';
+import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
+import { motion } from 'framer-motion';
+import toast from 'react-hot-toast';
+import { useRouter } from 'next/navigation';
+
+interface Errors {
+ name?: string;
+ email?: string;
+ password?: string;
+ confirmPassword?: string;
+}
+
+export default function SignupPage() {
+ const router = useRouter();
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [errors, setErrors] = useState({});
+ const [isSigningUp, setIsSigningUp] = useState(false);
+
+ const validateForm = () => {
+ const newErrors: any = {};
+
+ // Name validation
+ if (!name.trim()) {
+ newErrors.name = 'Full name is required';
+ } else if (name.length < 2) {
+ newErrors.name = 'Name must be at least 2 characters long';
+ }
+
+ // Email validation
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!emailRegex.test(email)) {
+ newErrors.email = 'Please enter a valid email address';
+ }
+
+ // Password validation
+ if (!password.trim()) {
+ newErrors.password = 'Password is required';
+ } else if (password.length < 8) {
+ newErrors.password = 'Password must be at least 8 characters long';
+ }
+
+ // Confirm password validation
+ if (confirmPassword.trim() !== password.trim()) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Toggle password visibility functions
+ const togglePasswordVisibility = () => {
+ setShowPassword(!showPassword);
+ };
+
+ const toggleConfirmPasswordVisibility = () => {
+ setShowConfirmPassword(!showConfirmPassword);
+ };
+
+ const handleSignup = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSigningUp(true);
+
+ if (!validateForm()) {
+ setIsSigningUp(false);
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/signup', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, email, password }),
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ toast.success('Account created successfully! Please verify your email to sign in.');
+
+ // Clear form
+ setName('');
+ setEmail('');
+ setPassword('');
+ setConfirmPassword('');
+
+ // Redirect to signin page after a short delay
+ setTimeout(() => {
+ router.push('/signin');
+ }, 2000);
+ } else {
+ // Show specific error message if available
+ if (data.error === 'Email already exists') {
+ toast.error('Email already exists. Please login or use a different email.');
+ } else {
+ toast.error(data.error || 'Failed to create account. Please try again.');
+ }
+ }
+ } catch (error) {
+ console.error('Signup error:', error);
+ toast.error('An error occurred. Please try again later.');
+ } finally {
+ setIsSigningUp(false);
+ }
+ };
+
+ return (
+
+ {/* Left Side Form */}
+
+
+
+
+
+
+ Create an Account
+ Start your journey with us today!
+
+
+
+ {/* Social Media Signup */}
+
+
+
+ Or continue with
+
+
+
+
+
+
+ Already have an account?{" "}
+
+ Sign In
+
+
+
+
+ {/* Right Side Background */}
+
+ {/* Background with gradient */}
+
+ {/* Decorative elements */}
+
+
+
+ {/* Content */}
+
+
+ Join the Agro Revolution
+
+
+ Sign up to access fresh produce, connect with farmers, and be part of a sustainable marketplace. Start your journey today!
+
+
+ Already have an account?
+
+
+
+
+ );
+}
diff --git a/app/terms/page.tsx b/app/terms/page.tsx
new file mode 100644
index 0000000..5e781a2
--- /dev/null
+++ b/app/terms/page.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+
+export default function TermsOfService() {
+ return (
+
+
+
+
+
+
+
Terms of Service
+
Last updated: {new Date().toLocaleDateString()}
+
+
+
+
+
+
+
+ 1. Acceptance of Terms
+
+ Welcome to AgroMarket. These Terms of Service ("Terms") govern your use of our website, mobile application, and services (collectively, the "Service") operated by AgroMarket ("us", "we", or "our").
+
+
+ By accessing or using our Service, you agree to be bound by these Terms. If you disagree with any part of these terms, then you may not access the Service.
+
+
+
+
+ 2. Use of the Service
+ 2.1 Permitted Use
+
+ You may use our Service for lawful purposes only. You agree to use the Service in accordance with all applicable laws, regulations, and these Terms.
+
+ 2.2 Prohibited Activities
+
+ Posting false, misleading, or fraudulent product listings
+ Engaging in any form of harassment or abusive behavior
+ Violating intellectual property rights
+ Attempting to gain unauthorized access to our systems
+ Distributing malware or harmful software
+
+
+
+
+ 3. User Accounts
+
+ When you create an account with us, you must provide information that is accurate, complete, and current at all times. You are responsible for safeguarding the password and for all activities that occur under your account.
+
+
+ You agree to immediately notify us of any unauthorized uses of your account or any other breaches of security.
+
+
+
+
+ 4. Product Listings and Transactions
+ 4.1 Seller Responsibilities
+
+ Provide accurate product descriptions and images
+ Honor listed prices and availability
+ Comply with all applicable food safety and agricultural regulations
+ Deliver products as agreed with buyers
+
+ 4.2 Buyer Responsibilities
+
+ Make payments as agreed with sellers
+ Communicate clearly about requirements and delivery
+ Inspect products upon delivery and report issues promptly
+
+
+
+
+ 5. Payment Terms
+
+ AgroMarket facilitates transactions between buyers and sellers but is not directly involved in payment processing unless otherwise specified. Payment methods, terms, and dispute resolution are primarily handled between transacting parties.
+
+
+ For our premium services, subscription fees are billed in advance and are non-refundable except as required by law.
+
+
+
+
+ 6. Content and Intellectual Property
+
+ Our Service and its original content, features, and functionality are and will remain the exclusive property of AgroMarket and its licensors. The Service is protected by copyright, trademark, and other laws.
+
+
+ You retain rights to any content you submit, post, or display on or through the Service. By posting content, you grant us a non-exclusive, royalty-free license to use, distribute, and display such content on our Service.
+
+
+
+
+ 7. Privacy Policy
+
+ Your privacy is important to us. Please review our Privacy Policy, which also governs your use of the Service, to understand our practices.
+
+
+
+
+ 8. Limitation of Liability
+
+ In no event shall AgroMarket, its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential, or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from your use of the Service.
+
+
+ Our total liability to you for any damages shall not exceed the amount you paid to us in the twelve (12) months preceding the claim.
+
+
+
+
+ 9. Dispute Resolution
+
+ We encourage users to resolve disputes directly with each other. However, if you have a dispute with AgroMarket, you agree to first contact us to attempt to resolve the dispute informally.
+
+
+ Any legal disputes arising out of or related to these Terms or the Service shall be subject to the exclusive jurisdiction of the courts of Nigeria.
+
+
+
+
+ 10. Termination
+
+ We may terminate or suspend your account and bar access to the Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of the Terms.
+
+
+ If you wish to terminate your account, you may simply discontinue using the Service.
+
+
+
+
+ 11. Changes to Terms
+
+ We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days notice prior to any new terms taking effect.
+
+
+ What constitutes a material change will be determined at our sole discretion. By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms.
+
+
+
+
+ 12. Contact Information
+
+ If you have any questions about these Terms of Service, please contact us:
+
+
+
Email: legal@agromarketng.com
+
Address: Lagos, Nigeria
+
Phone: +234 (0) 123 456 7890
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/testimonials/page.tsx b/app/testimonials/page.tsx
new file mode 100644
index 0000000..8a8da56
--- /dev/null
+++ b/app/testimonials/page.tsx
@@ -0,0 +1,343 @@
+import Navbar from "@/components/Navbar";
+import Footer from "@/components/Footer";
+import Image from "next/image";
+import Link from "next/link";
+import { Quote, ArrowRight, Star } from "lucide-react";
+
+// Testimonial data
+const testimonials = [
+ {
+ id: 1,
+ name: "Ibrahim Musa",
+ role: "Tomato Farmer, Kano",
+ image: "/assets/img/testimonials/farmer1.jpg",
+ quote: "AgroMarket has transformed my farming business. I now sell directly to buyers at fair prices and have increased my income by 40%. The platform is easy to use, and I receive payments promptly. I've also learned new farming techniques through their training programs.",
+ rating: 5,
+ story: "I used to rely on middlemen who would buy my tomatoes at very low prices. After joining AgroMarket, I can now set my own prices and connect directly with restaurants and retailers in Kano and even Lagos. The market insights feature helps me plan my planting schedule based on demand forecasts. Last season, I was able to invest in a small irrigation system with the extra income, which has further improved my yields.",
+ impact: {
+ income: "+40%",
+ customers: "25+",
+ reach: "Nationwide"
+ }
+ },
+ {
+ id: 2,
+ name: "Grace Okonkwo",
+ role: "Restaurant Owner, Lagos",
+ image: "/assets/img/testimonials/customer1.jpg",
+ quote: "As a restaurant owner, I need consistent quality produce. AgroMarket connects me directly with reliable farmers, ensuring I always get the best ingredients for my dishes. The delivery service is prompt, and I can track my orders in real-time.",
+ rating: 5,
+ story: "Running a farm-to-table restaurant in Lagos requires consistent access to fresh, quality ingredients. Before AgroMarket, I struggled with unreliable suppliers and fluctuating prices. Now, I have direct relationships with farmers across Nigeria who provide me with organic vegetables, fruits, and grains. The quality assurance process ensures that I only receive the best products. My customers have noticed the improvement in our dishes, and our reputation for using fresh, local ingredients has grown significantly.",
+ impact: {
+ costs: "-25%",
+ quality: "Improved",
+ reliability: "99%"
+ }
+ },
+ {
+ id: 3,
+ name: "Amina Ibrahim",
+ role: "Grain Farmer, Kaduna",
+ image: "/assets/img/testimonials/farmer2.jpg",
+ quote: "The market insights provided by AgroMarket helped me identify high-demand crops and optimize my planting schedule. My revenue has increased by 35% since I started using their platform, and I've expanded my farm from 2 to 5 hectares.",
+ rating: 5,
+ story: "I inherited a small grain farm from my father and was struggling to make it profitable. Through AgroMarket's platform, I discovered that there was high demand for millet and sorghum in southern Nigeria. I adjusted my crop selection based on this insight and started using their marketplace to reach buyers beyond my local area. The financial services offered through AgroMarket helped me secure a loan to expand my farm. I now employ three additional workers from my village and have become one of the leading grain suppliers in my region.",
+ impact: {
+ revenue: "+35%",
+ farm_size: "150% increase",
+ jobs_created: "3"
+ }
+ },
+ {
+ id: 4,
+ name: "David Okonkwo",
+ role: "Supermarket Owner, Abuja",
+ image: "/assets/img/testimonials/customer2.jpg",
+ quote: "AgroMarket has revolutionized how we source fresh produce for our supermarket chain. We can now offer our customers truly farm-fresh products while supporting local farmers. The platform's logistics service ensures timely deliveries even during challenging weather conditions.",
+ rating: 4,
+ story: "Managing the produce section of a supermarket chain in Abuja was always challenging due to supply chain inconsistencies. Since partnering with AgroMarket, we've been able to source directly from farms across Nigeria. The platform's quality control measures ensure that we only receive products that meet our standards. Our customers appreciate the freshness and quality of our produce, and sales in our fresh food department have increased by 30%. We've also been able to reduce waste by 25% due to the improved supply chain efficiency.",
+ impact: {
+ sales: "+30%",
+ waste: "-25%",
+ customer_satisfaction: "Significantly improved"
+ }
+ },
+ {
+ id: 5,
+ name: "Fatima Abdullahi",
+ role: "Poultry Farmer, Sokoto",
+ image: "/assets/img/testimonials/farmer3.jpg",
+ quote: "As a female farmer in a rural area, I faced many challenges in accessing markets. AgroMarket has given me a platform to sell my poultry products to buyers across Nigeria. The training programs have also helped me improve my farming practices and increase productivity.",
+ rating: 5,
+ story: "I started my poultry farm with just 50 birds and limited knowledge of modern farming techniques. Through AgroMarket's farmer training program, I learned about improved feeding practices, disease prevention, and business management. The platform helped me connect with buyers in urban areas who value free-range, organically raised chickens. I've now expanded to 500 birds and have diversified into egg production. The financial services offered through AgroMarket helped me secure funding for a small processing facility, allowing me to offer prepared poultry products at premium prices.",
+ impact: {
+ farm_size: "10x growth",
+ knowledge: "Significantly improved",
+ market_access: "Nationwide"
+ }
+ },
+ {
+ id: 6,
+ name: "Emmanuel Nwachukwu",
+ role: "Food Processor, Enugu",
+ image: "/assets/img/testimonials/customer3.jpg",
+ quote: "Our food processing company relies on consistent, high-quality agricultural inputs. AgroMarket has streamlined our sourcing process, connecting us with reliable farmers who meet our standards. The bulk ordering feature and logistics service have significantly improved our operational efficiency.",
+ rating: 5,
+ story: "Our company produces packaged cassava products for both local consumption and export. Finding consistent, high-quality cassava suppliers was always a challenge until we discovered AgroMarket. The platform's verification process ensures that we only work with farmers who meet our quality standards. The bulk ordering feature allows us to secure large quantities of cassava at competitive prices, and the logistics service ensures timely delivery to our processing facility. We've been able to increase our production capacity by 40% and expand our export markets due to the improved quality and consistency of our products.",
+ impact: {
+ production: "+40%",
+ supplier_base: "Expanded by 200%",
+ quality: "Consistently high"
+ }
+ },
+ {
+ id: 7,
+ name: "Blessing Okafor",
+ role: "Fruit Farmer, Cross River",
+ image: "/assets/img/testimonials/farmer4.jpg",
+ quote: "My pineapple farm was struggling due to limited local market access. AgroMarket opened up new opportunities by connecting me with buyers in major cities. The platform's logistics service ensures my fruits arrive fresh, and I've seen my profits double in just one year.",
+ rating: 4,
+ story: "Living in a remote area of Cross River State, I faced significant challenges in getting my pineapples to market before they spoiled. Local middlemen would offer very low prices, knowing I had few alternatives. After joining AgroMarket, I gained access to buyers in Lagos, Abuja, and Port Harcourt who value the unique sweetness of Cross River pineapples. The platform's cold chain logistics service ensures my fruits arrive in perfect condition. I've been able to expand my farm and now grow other tropical fruits as well. The market insights feature helps me time my harvests to coincide with periods of peak demand and pricing.",
+ impact: {
+ profits: "Doubled",
+ product_range: "Expanded",
+ wastage: "Reduced by 60%"
+ }
+ },
+ {
+ id: 8,
+ name: "Chinedu Eze",
+ role: "Cooperative Leader, Imo",
+ image: "/assets/img/testimonials/farmer5.jpg",
+ quote: "Our farming cooperative of 50 small-scale farmers has thrived since joining AgroMarket. The platform's bulk selling feature allows us to aggregate our produce and access larger buyers. The training and financial services have helped our members improve their farming practices and increase yields.",
+ rating: 5,
+ story: "Our cooperative consists of 50 small-scale farmers who individually lacked the volume to attract significant buyers. Through AgroMarket, we can aggregate our produce and access markets that were previously beyond our reach. The platform's training programs have introduced our members to improved farming techniques, resulting in yield increases of 30-50%. The financial services have helped many of our members invest in better inputs and small-scale irrigation. We've seen a remarkable transformation in our community, with increased incomes leading to improved housing, education, and healthcare. Several young people who had left for the cities have returned to farming, seeing it now as a viable and profitable career.",
+ impact: {
+ collective_income: "+60%",
+ youth_engagement: "Increased",
+ community_development: "Significant"
+ }
+ }
+];
+
+export default function TestimonialsPage() {
+ return (
+ <>
+
+
+ {/* Hero Section */}
+
+
+
+
+
+
+ Success Stories
+
+
+ Real experiences from farmers and buyers who have transformed their businesses with AgroMarket.
+
+
+
+
+
+ {/* Featured Testimonials */}
+
+
+
+
Hear From Our Community
+
+ Discover how AgroMarket is transforming agricultural commerce and improving livelihoods across Nigeria.
+
+
+
+
+ {testimonials.slice(0, 2).map((testimonial) => (
+
+
+
+
+
+
+
+
{testimonial.name}
+
{testimonial.role}
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+ "{testimonial.quote}"
+
+
+
+
+ Read full story
+
+
+
+ ))}
+
+
+
+
+ {/* All Testimonials */}
+
+
+
+
More Success Stories
+
+ Explore how AgroMarket is making a difference for farmers and buyers across Nigeria.
+
+
+
+
+ {testimonials.map((testimonial) => (
+
+
+
+
+
+
+
+
+
+
{testimonial.name}
+
{testimonial.role}
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+ "{testimonial.quote}"
+
+
+
+
+
Their Story
+
{testimonial.story}
+
+
+
+
Impact
+
+ {Object.entries(testimonial.impact).map(([key, value]) => (
+
+
{value}
+
{key.replace('_', ' ')}
+
+ ))}
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Share Your Story CTA */}
+
+
+
Have a Success Story to Share?
+
+ We'd love to hear how AgroMarket has helped transform your agricultural business.
+
+
+ Share Your Story
+
+
+
+
+
+ {/* Join AgroMarket CTA */}
+
+
+
+
+
+
Ready to Write Your Success Story?
+
+ Join thousands of farmers and buyers who are transforming their businesses with AgroMarket. Our platform provides the tools, connections, and support you need to succeed.
+
+
+
+ Join AgroMarket
+
+
+
+ Explore Our Services
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/changes.diff b/changes.diff
new file mode 100644
index 0000000..c14ce4c
--- /dev/null
+++ b/changes.diff
@@ -0,0 +1,4294 @@
+```diff
+diff --git a/app/api/admin/agents/[agentId]/route.ts b/app/api/admin/agents/[agentId]/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/admin/agents/[agentId]/route.ts
++++ b/app/api/admin/agents/[agentId]/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate admin session
+ async function validateAdmin(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const agent = await prisma.agent.findUnique({
+@@ -45,15 +46,15 @@
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Agent not found" }, { status: 404 });
++ return apiErrorResponse("Agent not found", 404, "AGENT_NOT_FOUND");
+ }
+
+ return NextResponse.json(agent);
+ } catch (error) {
+ console.error("Error fetching agent:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch agent" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch agent",
++ 500, "FETCH_AGENT_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -64,14 +65,14 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const data = await req.json();
+
+ // Validate data
+ if (data.isAvailable !== undefined && typeof data.isAvailable !== 'boolean') {
+- return NextResponse.json(
+- { error: "Invalid isAvailable value" },
+- { status: 400 }
++ return apiErrorResponse(
++ "Invalid isAvailable value",
++ 400, "INVALID_INPUT"
+ );
+ }
+
+@@ -90,9 +91,9 @@
+ return NextResponse.json(updatedAgent);
+ } catch (error) {
+ console.error("Error updating agent:", error);
+- return NextResponse.json(
+- { error: "Failed to update agent" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to update agent",
++ 500, "UPDATE_AGENT_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -103,19 +104,19 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Check if agent exists
+ const agent = await prisma.agent.findUnique({
+ where: { id: params.agentId },
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Agent not found" }, { status: 404 });
++ return apiErrorResponse("Agent not found", 404, "AGENT_NOT_FOUND");
+ }
+
+ // Delete agent
+ await prisma.agent.delete({
+ where: { id: params.agentId },
+ });
+
+ return NextResponse.json({ message: "Agent deleted successfully" });
+ } catch (error) {
+ console.error("Error deleting agent:", error);
+- return NextResponse.json(
+- { error: "Failed to delete agent" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to delete agent",
++ 500, "DELETE_AGENT_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/admin/agents/route.ts b/app/api/admin/agents/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/admin/agents/route.ts
++++ b/app/api/admin/agents/route.ts
+@@ -3,6 +3,7 @@
+ import bcrypt from "bcryptjs";
+ import jwt from "jsonwebtoken";
+ import nodemailer from 'nodemailer';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ interface UserWithAgent {
+ id: string;
+@@ -43,7 +44,7 @@
+ try {
+ const session = await validateAdmin(req);
+ console.log("session from get request in agents api handler: ", session)
+- if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ if (!session) { // Ensure session is valid and user is admin
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const agents = await prisma.agent.findMany({
+@@ -59,9 +60,9 @@
+ return NextResponse.json(agents);
+ } catch (error) {
+ console.error("Error fetching agents:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch agents" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch agents",
++ 500, "FETCH_AGENTS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -69,22 +70,22 @@
+ export async function POST(req: NextRequest) {
+ try {
+ const session = await validateAdmin(req);
+- if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ if (!session) { // Ensure session is valid and user is admin
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const { email, name, specialties } = await req.json();
+
+ // Input validation
+ if (!email || !specialties) {
+- return NextResponse.json(
+- { error: "Email and specialties are required" },
+- { status: 400 }
++ return apiErrorResponse(
++ "Email and specialties are required",
++ 400, "MISSING_FIELDS"
+ );
+ }
+
+ if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
+- return NextResponse.json(
+- { error: "Invalid email format" },
+- { status: 400 }
++ return apiErrorResponse(
++ "Invalid email format",
++ 400, "INVALID_EMAIL_FORMAT"
+ );
+ }
+
+@@ -97,9 +98,9 @@
+
+ // Check if user is already an agent
+ if (user?.Agent) {
+- return NextResponse.json(
+- { error: "User is already an agent" },
+- { status: 400 }
++ return apiErrorResponse(
++ "User is already an agent",
++ 400, "USER_ALREADY_AGENT"
+ );
+ }
+
+@@ -131,9 +132,9 @@
+ });
+ } catch (emailError) {
+ console.error("Error sending welcome email:", emailError);
+- // Delete created user if email fails
++ // Delete created user if email fails to prevent orphaned user accounts
+ await prisma.user.delete({ where: { id: user?.id } });
+- return NextResponse.json(
+- { error: "Failed to send verification email" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to send verification email",
++ 500, "EMAIL_SEND_FAILED", emailError instanceof Error ? emailError.message : String(emailError)
+ );
+ }
+ }
+@@ -156,9 +157,9 @@
+ }
+ } catch (error) {
+ console.error("Error creating agent:", error);
+- return NextResponse.json(
+- { error: "Failed to create agent" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to create agent",
++ 500, "CREATE_AGENT_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/admin/analytics/route.ts b/app/api/admin/analytics/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/admin/analytics/route.ts
++++ b/app/api/admin/analytics/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate admin session
+ async function validateAdmin(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Get time range from query params
+@@ -67,9 +68,9 @@
+ agentPerformance
+ });
+ } catch (error) {
+ console.error("Error fetching analytics:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch analytics data" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch analytics data",
++ 500, "ANALYTICS_FETCH_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/admin/chats/route.ts b/app/api/admin/chats/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/admin/chats/route.ts
++++ b/app/api/admin/chats/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate admin session
+ async function validateAdmin(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const status = req.nextUrl.searchParams.get('status');
+@@ -59,9 +60,9 @@
+ return NextResponse.json(chats);
+ } catch (error) {
+ console.error("Error fetching chats:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch chats" },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch chats",
++ 500, "FETCH_CHATS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/admin/settings/route.ts
++++ b/app/api/admin/settings/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate admin session
+ async function validateAdmin(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Default settings
+@@ -44,9 +45,9 @@
+ // For now, we'll just return the default settings
+ return NextResponse.json(defaultSettings);
+ } catch (error) {
+- console.error("Error fetching settings:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch settings" },
+- { status: 500 }
++ console.error("Error fetching settings:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch settings",
++ 500, "FETCH_SETTINGS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -55,15 +56,15 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const settings = await req.json();
+
+ // Validate settings
+ if (typeof settings !== 'object') {
+- return NextResponse.json(
+- { error: "Invalid settings format" },
+- { status: 400 }
++ return apiErrorResponse(
++ "Invalid settings format",
++ 400, "INVALID_INPUT"
+ );
+ }
+
+@@ -74,9 +75,9 @@
+ settings
+ });
+ } catch (error) {
+- console.error("Error updating settings:", error);
+- return NextResponse.json(
+- { error: "Failed to update settings" },
+- { status: 500 }
++ console.error("Error updating settings:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to update settings",
++ 500, "UPDATE_SETTINGS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/admin/stats/route.ts b/app/api/admin/stats/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/admin/stats/route.ts
++++ b/app/api/admin/stats/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate admin session
+ async function validateAdmin(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAdmin(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Get total agents count
+@@ -59,9 +60,9 @@
+ resolutionRate
+ });
+ } catch (error) {
+- console.error("Error fetching admin stats:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch admin statistics" },
+- { status: 500 }
++ console.error("Error fetching admin stats:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch admin statistics",
++ 500, "FETCH_ADMIN_STATS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/ads/[id]/analytics/route.ts b/app/api/ads/[id]/analytics/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/ads/[id]/analytics/route.ts
++++ b/app/api/ads/[id]/analytics/route.ts
+@@ -1,5 +1,6 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Simple in-memory cache
+ const analyticsCache = new Map();
+@@ -18,15 +19,15 @@
+ });
+
+ if (!ad) {
+- return NextResponse.json(
+- { error: 'Ad not found' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Ad not found',
++ 404, 'AD_NOT_FOUND'
+ );
+ }
+
+ // Update cache
+ analyticsCache.set(id, ad);
+-
+ return NextResponse.json(ad);
+ } catch (error) {
+ console.error('Error fetching analytics:', error);
+- return NextResponse.json(
+- { error: 'Failed to fetch analytics' },
+- { status: 500 }
++ return apiErrorResponse(
++ 'Failed to fetch analytics',
++ 500, 'ANALYTICS_FETCH_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -38,9 +39,9 @@
+ const { type } = await request.json();
+
+ if (!['views', 'clicks', 'shares'].includes(type)) {
+- return NextResponse.json(
+- { error: 'Invalid analytics type' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid analytics type',
++ 400, 'INVALID_ANALYTICS_TYPE'
+ );
+ }
+
+@@ -52,9 +53,9 @@
+
+ return NextResponse.json(ad);
+ } catch (error) {
+- console.error('Error updating analytics:', error);
+- return NextResponse.json(
+- { error: 'Failed to update analytics' },
+- { status: 500 }
++ console.error('Error updating analytics:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to update analytics',
++ 500, 'ANALYTICS_UPDATE_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/ads/[id]/boost/route.ts b/app/api/ads/[id]/boost/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/ads/[id]/boost/route.ts
++++ b/app/api/ads/[id]/boost/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { boostOptions } from '@/constants';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(
+ request: NextRequest,
+@@ -9,34 +10,34 @@
+ ) {
+ try {
+ // Make sure id is valid
+ const { id } = params;
+ if (!id) {
+- return NextResponse.json(
+- { error: 'Invalid ad ID' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid ad ID',
++ 400, 'INVALID_AD_ID'
+ );
+ }
+
+ // Parse request body
+ let boostType: number, duration: number;
+ try {
+ const body = await request.json();
+ ({ boostType, duration } = body);
+
+ if (!boostType || !duration) {
+- return NextResponse.json(
+- { error: 'Missing required fields: boostType and duration' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Missing required fields: boostType and duration',
++ 400, 'MISSING_FIELDS'
+ );
+ }
+
+ // Validate boost type
+ const validBoostType = boostOptions.some(opt => opt.id === boostType);
+ if (!validBoostType) {
+- return NextResponse.json(
+- { error: 'Invalid boost type' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid boost type',
++ 400, 'INVALID_BOOST_TYPE'
+ );
+ }
+
+ // Validate duration
+ const validDuration = boostOptions.find(opt => opt.id === boostType)?.duration.includes(duration);
+ if (!validDuration) {
+- return NextResponse.json(
+- { error: 'Invalid duration for selected boost type' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid duration for selected boost type',
++ 400, 'INVALID_BOOST_DURATION'
+ );
+ }
+ } catch (e) {
+- return NextResponse.json(
+- { error: 'Invalid request body' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid request body',
++ 400, 'INVALID_REQUEST_BODY', e instanceof Error ? e.message : String(e)
+ );
+ }
+
+ // Get token from cookies
+ const token = request.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+@@ -50,15 +51,15 @@
+ include: { subscriptionPlan: true }
+ });
+
+- // If no subscription, return available plans and redirect info
++ // If no active subscription, return available plans and redirect info
+ if (!user?.subscriptionPlan || user.subscriptionPlan.expiryDate < new Date()) {
+ const availablePlans = await prisma.subscriptionPlan.findMany({
+ select: {
+ id: true,
+ name: true,
+ price: true,
+ duration: true,
+- benefits: true
++ benefits: true,
+ }
+ });
+
+@@ -67,7 +68,7 @@
+ message: 'Active subscription required to boost ads',
+ adId: id,
+ subscriptionPlans: availablePlans,
+- redirectUrl: '/dashboard/promotions'
++ redirectUrl: '/dashboard/promotions' // Suggest redirect to promotions page
+ }, { status: 403 });
+ }
+
+@@ -81,20 +82,20 @@
+ });
+
+ if (!ad || ad.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Ad not found or unauthorized' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Ad not found or unauthorized',
++ 404, 'AD_NOT_FOUND_OR_UNAUTHORIZED'
+ );
+ }
+
+ // Check if ad is already boosted
+ if (ad.featured && ad.boostEndDate && new Date(ad.boostEndDate) > new Date()) {
+- return NextResponse.json(
+- { error: 'Ad is already boosted' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Ad is already boosted',
++ 400, 'AD_ALREADY_BOOSTED'
+ );
+ }
+-
++ // Check if ad is active
+ if (ad.status !== 'Active') {
+ return NextResponse.json(
+ { error: 'Ad must be active before it can be boosted' },
+@@ -123,9 +124,9 @@
+ }
+ });
+ } catch (error) {
+- console.error('Error boosting ad:', error instanceof Error ? error.message : error);
+- return NextResponse.json(
+- { error: 'Failed to boost ad' },
+- { status: 500 }
++ console.error('Error boosting ad:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to boost ad',
++ 500, 'BOOST_AD_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/ads/[id]/feature/route.ts b/app/api/ads/[id]/feature/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/ads/[id]/feature/route.ts
++++ b/app/api/ads/[id]/feature/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function PATCH(
+ req: NextRequest,
+ { params }: { params: { id: string } }
+ ) {
+ try {
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -19,8 +20,8 @@
+ });
+
+ if (!user?.subscriptionPlan || user.subscriptionPlan.expiryDate < new Date()) {
+- return NextResponse.json(
+- { error: 'Active subscription required to feature ads. Please upgrade your plan.' },
+- { status: 403 }
++ return apiErrorResponse(
++ 'Active subscription required to feature ads. Please upgrade your plan.',
++ 403, 'SUBSCRIPTION_REQUIRED'
+ );
+ }
+
+@@ -29,8 +30,8 @@
+ });
+
+ if (!ad || ad.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Ad not found or unauthorized' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Ad not found or unauthorized',
++ 404, 'AD_NOT_FOUND_OR_UNAUTHORIZED'
+ );
+ }
+
+@@ -45,8 +46,9 @@
+ message: 'Ad featured successfully'
+ });
+ } catch (error) {
+- console.error('Error featuring ad:', error);
+- return NextResponse.json(
+- { error: 'Failed to feature ad' },
+- { status: 500 }
++ console.error('Error featuring ad:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to feature ad',
++ 500, 'FEATURE_AD_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/ads/[id]/status/route.ts b/app/api/ads/[id]/status/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/ads/[id]/status/route.ts
++++ b/app/api/ads/[id]/status/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { headers } from 'next/headers';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Define params interface
+ interface RouteParams {
+@@ -17,15 +18,15 @@
+ const id = params?.id;
+
+ if (!id || typeof id !== 'string') {
+- return NextResponse.json(
+- { error: 'Invalid ad ID' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid ad ID',
++ 400, 'INVALID_AD_ID'
+ );
+ }
+
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Rest of your code remains the same
+@@ -34,8 +35,8 @@
+ const { status } = await req.json();
+ const validStatuses = ['Active', 'Pending', 'Inactive', 'Sold'];
+
+ if (!validStatuses.includes(status)) {
+- return NextResponse.json(
+- { error: 'Invalid status value' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid status value',
++ 400, 'INVALID_STATUS_VALUE'
+ );
+ }
+
+ if (!ad || ad.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Ad not found or unauthorized' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Ad not found or unauthorized',
++ 404, 'AD_NOT_FOUND_OR_UNAUTHORIZED'
+ );
+ }
+
+ return NextResponse.json({
+ ad: updatedAd,
+ message: `Ad status updated to ${status}`
+ });
+ } catch (error) {
+- console.error('Error updating ad status:', error);
+- return NextResponse.json(
+- { error: 'Failed to update ad status' },
+- { status: 500 }
++ console.error('Error updating ad status:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to update ad status',
++ 500, 'UPDATE_AD_STATUS_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/ads/boosted/route.ts b/app/api/ads/boosted/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/ads/boosted/route.ts
++++ b/app/api/ads/boosted/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(request: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = request.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -27,8 +28,9 @@
+ });
+
+ } catch (error) {
+- console.error('Error fetching boosted ads:', error);
+- return NextResponse.json(
+- { error: 'Failed to fetch boosted ads' },
+- { status: 500 }
++ console.error('Error fetching boosted ads:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch boosted ads',
++ 500, 'FETCH_BOOSTED_ADS_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/ads/my-ads/route.ts b/app/api/ads/my-ads/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/ads/my-ads/route.ts
++++ b/app/api/ads/my-ads/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+
++import { apiErrorResponse } from '@/lib/errorHandling';
+ export const dynamic = 'force-dynamic';
+ export const revalidate = 30; // Revalidate every 30 seconds
+
+@@ -9,9 +10,9 @@
+ try {
+ // Get token from cookies
+ const token = request.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json(
+- { error: 'Unauthorized' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
+ );
+ }
+
+@@ -48,9 +49,9 @@
+ ]);
+
+ if (!user) {
+- return NextResponse.json(
+- { error: 'User not found' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'User not found',
++ 404, 'USER_NOT_FOUND'
+ );
+ }
+
+@@ -67,9 +68,9 @@
+ );
+
+ } catch (error) {
+- console.error('Error in my-ads route:', error);
+- return NextResponse.json(
+- { error: 'Internal server error' },
+- { status: 500 }
++ console.error('Error in my-ads route:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Internal server error',
++ 500, 'FETCH_MY_ADS_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/agent/analytics/route.ts b/app/api/agent/analytics/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/analytics/route.ts
++++ b/app/api/agent/analytics/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate agent session
+ async function validateAgent(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Get time range from query params
+@@ -67,9 +68,9 @@
+ responseTimeData
+ });
+ } catch (error) {
+- console.error("Error fetching agent analytics:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch analytics data" },
+- { status: 500 }
++ console.error("Error fetching agent analytics:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch analytics data",
++ 500, "FETCH_AGENT_ANALYTICS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/agent/chats/[chatId]/accept/route.ts b/app/api/agent/chats/[chatId]/accept/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/chats/[chatId]/accept/route.ts
++++ b/app/api/agent/chats/[chatId]/accept/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(
+ req: NextRequest,
+@@ -8,19 +9,19 @@
+ ) {
+ try {
+ // Get and validate token
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json(
+- { error: 'Unauthorized' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
+ );
+ }
+
+ // Verify token and get userId
+ const session = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const agent = await prisma.agent.findUnique({
+@@ -28,7 +29,7 @@
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Not an agent" }, { status: 403 });
++ return apiErrorResponse("Not an agent", 403, "NOT_AN_AGENT");
+ }
+
+ const chat = await prisma.supportChat.update({
+@@ -55,6 +56,9 @@
+
+ return NextResponse.json(chat);
+ } catch (error) {
+- console.error('Error accepting chat:', error);
+- return NextResponse.json({ error: "Failed to accept chat" }, { status: 500 });
++ console.error('Error accepting chat:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to accept chat",
++ 500, "ACCEPT_CHAT_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/chats/[chatId]/messages/route.ts b/app/api/agent/chats/[chatId]/messages/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/chats/[chatId]/messages/route.ts
++++ b/app/api/agent/chats/[chatId]/messages/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(
+ req: NextRequest,
+@@ -8,28 +9,28 @@
+ try {
+ // Get and validate token
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json(
+- { error: 'Unauthorized' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
+ );
+ }
+
+ // Verify token and get userId
+ const session = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Verify agent status
+ const agent = await prisma.agent.findUnique({
+ where: { userId: session.id }
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Not an agent" }, { status: 403 });
++ return apiErrorResponse("Not an agent", 403, "NOT_AN_AGENT");
+ }
+
+ // Get message content from request
+ const { content } = await req.json();
+ if (!content) {
+- return NextResponse.json({ error: "Message content is required" }, { status: 400 });
++ return apiErrorResponse("Message content is required", 400, "MESSAGE_CONTENT_REQUIRED");
+ }
+
+ // Verify chat exists and agent is assigned to it
+@@ -40,7 +41,7 @@
+ });
+
+ if (!chat) {
+- return NextResponse.json({ error: "Chat not found or not assigned to you" }, { status: 404 });
++ return apiErrorResponse("Chat not found or not assigned to you", 404, "CHAT_NOT_FOUND_OR_UNASSIGNED");
+ }
+
+ // Create message
+@@ -58,6 +59,9 @@
+
+ return NextResponse.json(message);
+ } catch (error) {
+- console.error('Error sending message:', error);
+- return NextResponse.json({ error: "Failed to send message" }, { status: 500 });
++ console.error('Error sending message:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to send message",
++ 500, "SEND_MESSAGE_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/chats/route.ts b/app/api/agent/chats/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/chats/route.ts
++++ b/app/api/agent/chats/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get and validate token
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json(
+- { error: 'Unauthorized' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
+ );
+ }
+
+ // Verify token and get userId
+ const session = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Verify agent status
+ const agent = await prisma.agent.findUnique({
+ where: { userId: session.id }
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Not an agent" }, { status: 403 });
++ return apiErrorResponse("Not an agent", 403, "NOT_AN_AGENT");
+ }
+
+ const chats = await prisma.supportChat.findMany({
+@@ -40,6 +41,9 @@
+
+ return NextResponse.json(chats);
+ } catch (error) {
+- console.error('Error fetching chats:', error);
+- return NextResponse.json({ error: "Failed to fetch chats" }, { status: 500 });
++ console.error('Error fetching chats:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch chats",
++ 500, "FETCH_CHATS_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/signup/route.ts b/app/api/agent/signup/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/signup/route.ts
++++ b/app/api/agent/signup/route.ts
+@@ -3,6 +3,7 @@
+ import bcrypt from 'bcryptjs';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+@@ -13,7 +14,7 @@
+ });
+
+ if (existingUser) {
+- return NextResponse.json({ error: 'Email already registered' }, { status: 400 });
++ return apiErrorResponse('Email already registered', 400, 'EMAIL_ALREADY_REGISTERED');
+ }
+
+ // Create user with agent role
+@@ -56,6 +57,9 @@
+
+ return response;
+ } catch (error) {
+- console.error('Error in agent signup:', error);
+- return NextResponse.json({ error: 'Failed to create agent account' }, { status: 500 });
++ console.error('Error in agent signup:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to create agent account',
++ 500, 'AGENT_SIGNUP_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/stats/route.ts b/app/api/agent/stats/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/stats/route.ts
++++ b/app/api/agent/stats/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate agent session
+ async function validateAgent(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Get agent stats
+ const agent = await prisma.agent.findUnique({
+ where: { id: session.agentId },
+ include: {
+ SupportChat: true
+ }
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Agent not found" }, { status: 404 });
++ return apiErrorResponse("Agent not found", 404, "AGENT_NOT_FOUND");
+ }
+
+ // Get active chats
+@@ -95,8 +96,9 @@
+ resolutionRate
+ });
+ } catch (error) {
+- console.error("Error fetching agent stats:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch agent stats" },
+- { status: 500 }
++ console.error("Error fetching agent stats:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch agent stats",
++ 500, "FETCH_AGENT_STATS_FAILED", error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/agent/status/route.ts b/app/api/agent/status/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/status/route.ts
++++ b/app/api/agent/status/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+ // Get and validate token
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json(
+- { error: 'Unauthorized' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
+ );
+ }
+
+ // Verify token and get userId
+ const session = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Verify agent status
+ const agent = await prisma.agent.findUnique({
+ where: { userId: session.id }
+ });
+
+ if (!agent) {
+- return NextResponse.json({ error: "Not an agent" }, { status: 403 });
++ return apiErrorResponse("Not an agent", 403, "NOT_AN_AGENT");
+ }
+
+ // Get status from request
+ const { isAvailable } = await req.json();
+ if (typeof isAvailable !== 'boolean') {
+- return NextResponse.json({ error: "Invalid status" }, { status: 400 });
++ return apiErrorResponse("Invalid status", 400, "INVALID_STATUS");
+ }
+
+ // Update agent status
+ const updatedAgent = await prisma.agent.update({
+ where: { id: agent.id },
+ data: {
+ isAvailable,
+ lastActive: new Date()
+ }
+ });
+
+ return NextResponse.json({
+ message: `Agent status updated to ${isAvailable ? 'available' : 'unavailable'}`,
+ agent: updatedAgent
+ });
+ } catch (error) {
+- console.error('Error updating agent status:', error);
+- return NextResponse.json({ error: "Failed to update agent status" }, { status: 500 });
++ console.error('Error updating agent status:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to update agent status",
++ 500, "UPDATE_AGENT_STATUS_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/tickets/[ticketId]/accept/route.ts b/app/api/agent/tickets/[ticketId]/accept/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/tickets/[ticketId]/accept/route.ts
++++ b/app/api/agent/tickets/[ticketId]/accept/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate agent session
+ async function validateAgent(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // In a real implementation, this would be:
+@@ -64,6 +65,9 @@
+
+ return NextResponse.json(ticket);
+ } catch (error) {
+- console.error('Error accepting ticket:', error);
+- return NextResponse.json({ error: "Failed to accept ticket" }, { status: 500 });
++ console.error('Error accepting ticket:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to accept ticket",
++ 500, "ACCEPT_TICKET_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/tickets/[ticketId]/close/route.ts b/app/api/agent/tickets/[ticketId]/close/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/tickets/[ticketId]/close/route.ts
++++ b/app/api/agent/tickets/[ticketId]/close/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate agent session
+ async function validateAgent(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // In a real implementation, this would be:
+@@ -87,6 +88,9 @@
+
+ return NextResponse.json(ticket);
+ } catch (error) {
+- console.error('Error closing ticket:', error);
+- return NextResponse.json({ error: "Failed to close ticket" }, { status: 500 });
++ console.error('Error closing ticket:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to close ticket",
++ 500, "CLOSE_TICKET_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/tickets/[ticketId]/responses/route.ts b/app/api/agent/tickets/[ticketId]/responses/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/tickets/[ticketId]/responses/route.ts
++++ b/app/api/agent/tickets/[ticketId]/responses/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate agent session
+ async function validateAgent(req: NextRequest) {
+@@ -29,14 +30,14 @@
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Get response content from request
+ const { content } = await req.json();
+ if (!content) {
+- return NextResponse.json({ error: "Response content is required" }, { status: 400 });
++ return apiErrorResponse("Response content is required", 400, "RESPONSE_CONTENT_REQUIRED");
+ }
+
+ // In a real implementation, this would be:
+@@ -69,6 +70,9 @@
+
+ return NextResponse.json(response);
+ } catch (error) {
+- console.error('Error creating response:', error);
+- return NextResponse.json({ error: "Failed to create response" }, { status: 500 });
++ console.error('Error creating response:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to create response",
++ 500, "CREATE_RESPONSE_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/agent/tickets/route.ts b/app/api/agent/tickets/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/agent/tickets/route.ts
++++ b/app/api/agent/tickets/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper function to validate agent session
+ async function validateAgent(req: NextRequest) {
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const status = req.nextUrl.searchParams.get('status');
+@@ -130,14 +131,17 @@
+
+ return NextResponse.json(tickets);
+ } catch (error) {
+- console.error('Error fetching tickets:', error);
+- return NextResponse.json({ error: "Failed to fetch tickets" }, { status: 500 });
++ console.error('Error fetching tickets:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch tickets",
++ 500, "FETCH_TICKETS_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+
+ export async function POST(req: NextRequest) {
+ try {
+ const session = await validateAgent(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const { userId, subject, message, priority, category } = await req.json();
+
+ if (!userId || !subject || !message) {
+- return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
++ return apiErrorResponse("Missing required fields", 400, "MISSING_FIELDS");
+ }
+
+ // In a real implementation, this would be:
+@@ -170,6 +174,9 @@
+
+ return NextResponse.json(ticket);
+ } catch (error) {
+- console.error('Error creating ticket:', error);
+- return NextResponse.json({ error: "Failed to create ticket" }, { status: 500 });
++ console.error('Error creating ticket:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to create ticket",
++ 500, "CREATE_TICKET_FAILED", error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/auth/route.ts
++++ b/app/api/auth/route.ts
+@@ -4,10 +4,10 @@
+ import jwt from 'jsonwebtoken';
+ import nodemailer from 'nodemailer';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper functions for JSON response
+-const jsonResponse = (status: number, data: any) => new NextResponse(JSON.stringify(data), { status });
+-
++// const jsonResponse = (status: number, data: any) => new NextResponse(JSON.stringify(data), { status }); // Replaced by apiErrorResponse
+ // Configure Nodemailer transporter
+ const transporter = nodemailer.createTransport({
+ service: 'gmail',
+@@ -24,7 +24,7 @@
+ case 'forgot-password':
+ return handleForgotPassword(email);
+ case 'reset-password':
+ return handleResetPassword(token, newPassword);
+ default:
+- return jsonResponse(400, { error: 'Invalid request type' });
++ return apiErrorResponse('Invalid request type', 400, 'INVALID_REQUEST_TYPE');
+ }
+ }
+
+@@ -32,7 +32,7 @@
+ console.log("User email: ", email);
+ try {
+ // Check if user exists
+ const user = await prisma.user.findUnique({ where: { email } });
+ console.log("User found: ", user);
+- if (!user) return jsonResponse(404, { error: 'User not found' });
++ if (!user) return apiErrorResponse('User not found', 404, 'USER_NOT_FOUND');
+
+ // Generate a reset token
+ const resetToken = jwt.sign(
+@@ -56,9 +56,9 @@
+ `,
+ });
+
+- return jsonResponse(200, { message: 'Password reset link sent to your email' });
++ return NextResponse.json({ message: 'Password reset link sent to your email' }, { status: 200 });
+ } catch (error) {
+ console.error('Error in forgot password:', error);
+- return jsonResponse(500, { error: 'Failed to send reset email' });
++ return apiErrorResponse('Failed to send reset email', 500, 'EMAIL_SEND_FAILED', error instanceof Error ? error.message : String(error));
+ }
+ }
+
+@@ -71,9 +71,9 @@
+ data: { password: hashedPassword },
+ });
+
+- return jsonResponse(200, { message: 'Password reset successfully' });
++ return NextResponse.json({ message: 'Password reset successfully' }, { status: 200 });
+ } catch (error) {
+ console.error('Error in reset password:', error);
+- return jsonResponse(400, { error: 'Invalid or expired token' });
++ return apiErrorResponse('Invalid or expired token', 400, 'INVALID_OR_EXPIRED_TOKEN', error instanceof Error ? error.message : String(error));
+ }
+ }
+diff --git a/app/api/chats/[chatid]/messages/route.ts b/app/api/chats/[chatid]/messages/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/chats/[chatid]/messages/route.ts
++++ b/app/api/chats/[chatid]/messages/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(
+ req: NextRequest,
+@@ -8,7 +9,7 @@
+ ) {
+ try {
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -29,6 +30,9 @@
+
+ return NextResponse.json(messages);
+ } catch (error) {
+- console.error('Error fetching messages:', error);
+- return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 });
++ console.error('Error fetching messages:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch messages',
++ 500, 'FETCH_MESSAGES_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/chats/route.ts b/app/api/chats/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/chats/route.ts
++++ b/app/api/chats/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -39,15 +40,15 @@
+
+ return NextResponse.json(chats);
+ } catch (error) {
+- console.error('Error fetching chats:', error);
+- return NextResponse.json({ error: 'Failed to fetch chats' }, { status: 500 });
++ console.error('Error fetching chats:', error); // Log the actual error for debugging
++ return apiErrorResponse('Failed to fetch chats', 500, 'FETCH_CHATS_FAILED', error instanceof Error ? error.message : String(error));
+ }
+ }
+
+
+ export async function POST(req: NextRequest) {
+ try {
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -55,10 +56,10 @@
+
+ const { adId, recipientId, message } = await req.json();
+
+ // Add validation
+ if (!adId || !recipientId || !message) {
+- return NextResponse.json({
+- error: 'Missing required fields',
+- details: { adId, recipientId, message }
+- }, { status: 400 });
++ return apiErrorResponse('Missing required fields', 400, 'MISSING_FIELDS', { adId, recipientId, message });
+ }
+
+ // Check if chat already exists
+@@ -118,10 +119,9 @@
+
+ return NextResponse.json({ chat });
+ } catch (error) {
+- console.error('Error creating chat:', error);
+- return NextResponse.json({
+- error: 'Failed to create chat',
+- details: error instanceof Error ? error.message : 'Unknown error'
+- }, { status: 500 });
++ console.error('Error creating chat:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to create chat',
++ 500, 'CREATE_CHAT_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/cron/check-subscriptions/route.ts b/app/api/cron/check-subscriptions/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/cron/check-subscriptions/route.ts
++++ b/app/api/cron/check-subscriptions/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET() {
+ try {
+@@ -35,7 +36,10 @@
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ console.error('Subscription check error:', error);
+- return NextResponse.json({ error: 'Failed to check subscriptions' }, { status: 500 });
++ return apiErrorResponse(
++ 'Failed to check subscriptions',
++ 500, 'SUBSCRIPTION_CRON_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+```
+```diff
+diff --git a/app/api/debug/route.ts b/app/api/debug/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/debug/route.ts
++++ b/app/api/debug/route.ts
+@@ -1,5 +1,6 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(request: NextRequest) {
+ try {
+@@ -21,11 +22,10 @@
+ count: products.length
+ });
+ } catch (error) {
+- console.error('Error fetching products:', error);
+- return NextResponse.json(
+- {
+- error: 'Failed to fetch products',
+- details: error instanceof Error ? error.message : 'Unknown error'
+- },
+- { status: 500 }
++ console.error('Error fetching products:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch products',
++ 500, 'DEBUG_FETCH_PRODUCTS_FAILED',
++ error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/featured-products/route.ts b/app/api/featured-products/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/featured-products/route.ts
++++ b/app/api/featured-products/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import { navigation } from '@/constants';
+
++import { apiErrorResponse } from '@/lib/errorHandling';
+ export const revalidate = 300; // Revalidate every 5 minutes
+
+ export async function GET(request: NextRequest) {
+@@ -343,11 +344,10 @@
+
+ return response;
+ } catch (error) {
+- console.error('Error fetching products:', error);
+- return NextResponse.json(
+- {
+- error: 'Failed to fetch products',
+- details: error instanceof Error ? error.message : 'Unknown error'
+- },
+- { status: 500 }
++ console.error('Error fetching products:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch products',
++ 500, 'FEATURED_PRODUCTS_FETCH_FAILED',
++ error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/landing-analytics/route.ts b/app/api/landing-analytics/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/landing-analytics/route.ts
++++ b/app/api/landing-analytics/route.ts
+@@ -1,5 +1,6 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(request: NextRequest) {
+ try {
+@@ -69,9 +70,9 @@
+ }))
+ });
+ } catch (error) {
+- console.error('Error fetching landing analytics:', error);
+- return NextResponse.json(
+- { error: 'Failed to fetch analytics data' },
+- { status: 500 }
++ console.error('Error fetching landing analytics:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch analytics data',
++ 500, 'LANDING_ANALYTICS_FETCH_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/logout/route.ts b/app/api/logout/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/logout/route.ts
++++ b/app/api/logout/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from "next/server";
+ import { cookies } from "next/headers";
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+ // Clear all auth-related cookies on the server side
+ const cookieStore = await cookies();
+@@ -13,9 +14,9 @@
+
+ return NextResponse.json({ success: true, message: "Logged out successfully" });
+ } catch (error) {
+- console.error("Logout error:", error);
+- return NextResponse.json(
+- { success: false, message: "Failed to log out" },
+- { status: 500 }
++ console.error("Logout error:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to log out",
++ 500, 'LOGOUT_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/newsletter/confirm/route.ts b/app/api/newsletter/confirm/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/newsletter/confirm/route.ts
++++ b/app/api/newsletter/confirm/route.ts
+@@ -1,5 +1,6 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+@@ -15,15 +16,15 @@
+ });
+
+ if (!subscription) {
+- return NextResponse.redirect(
+- new URL('/newsletter-error?message=invalid_token', req.nextUrl.origin)
++ return apiErrorResponse(
++ 'Invalid token',
++ 400, 'INVALID_TOKEN'
+ );
+ }
+
+ // If already confirmed, redirect to success page
+ if (subscription.isConfirmed) {
+- return NextResponse.redirect(
+- new URL('/newsletter-success?status=already_confirmed', req.nextUrl.origin)
++ return apiErrorResponse('Already confirmed', 400, 'ALREADY_CONFIRMED');
+ );
+ }
+
+@@ -38,9 +39,9 @@
+ new URL('/newsletter-success', req.nextUrl.origin)
+ );
+ } catch (error) {
+- console.error('Newsletter confirmation error:', error);
+- return NextResponse.redirect(
+- new URL('/newsletter-error?message=server_error', req.nextUrl.origin)
++ console.error('Newsletter confirmation error:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Server error during confirmation',
++ 500, 'CONFIRMATION_SERVER_ERROR', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/newsletter/route.ts b/app/api/newsletter/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/newsletter/route.ts
++++ b/app/api/newsletter/route.ts
+@@ -3,6 +3,7 @@
+ import prisma from '@/lib/prisma';
+ import nodemailer from 'nodemailer';
+ import crypto from 'crypto';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Configure Nodemailer transporter
+ const transporter = nodemailer.createTransport({
+@@ -18,9 +19,9 @@
+ try {
+ const { email, name } = await req.json();
+
+ if (!email) {
+- return NextResponse.json(
+- { error: 'Email is required' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Email is required',
++ 400, 'EMAIL_REQUIRED'
+ );
+ }
+
+@@ -59,9 +60,9 @@
+ message: 'Thank you for subscribing! Please check your email to confirm your subscription'
+ });
+ } catch (error) {
+- console.error('Newsletter subscription error:', error);
+- return NextResponse.json(
+- { error: 'Failed to process subscription' },
+- { status: 500 }
++ console.error('Newsletter subscription error:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to process subscription',
++ 500, 'SUBSCRIPTION_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/payments/verify/route.ts b/app/api/payments/verify/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/payments/verify/route.ts
++++ b/app/api/payments/verify/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { subscriptionPlans } from '@/constants';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+ // Get and validate request data
+ const { reference } = await req.json();
+ if (!reference) {
+- return NextResponse.json(
+- { error: 'Reference is required' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Reference is required',
++ 400, 'REFERENCE_REQUIRED'
+ );
+ }
+
+ // Get and validate token
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json(
+- { error: 'Unauthorized' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
+ );
+ }
+
+@@ -26,9 +27,9 @@
+ const paymentData = await verifyResponse.json();
+
+ if (!paymentData.status || paymentData.data.status !== 'success') {
+- return NextResponse.json(
+- { error: 'Payment verification failed' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Payment verification failed',
++ 400, 'PAYMENT_VERIFICATION_FAILED'
+ );
+ }
+
+@@ -62,7 +63,7 @@
+ } else if (type === 'subscription') {
+ // Handle subscription plan
+ const planDetails = subscriptionPlans.find(p => p.id === planId);
+ if (!planDetails) {
+- return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
++ return apiErrorResponse('Invalid plan', 400, 'INVALID_PLAN');
+ }
+
+ // Check for existing subscription
+@@ -131,23 +132,20 @@
+ }
+ }
+
+- return NextResponse.json(
+- { error: 'Invalid payment type' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid payment type',
++ 400, 'INVALID_PAYMENT_TYPE'
+ );
+
+ } catch (paymentError) {
+ console.error('Payment verification failed:', paymentError);
+- return NextResponse.json(
+- {
+- error: 'Payment verification failed',
+- details: paymentError instanceof Error ? paymentError.message : 'Unknown error'
+- },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Payment verification failed',
++ 400, 'PAYMENT_GATEWAY_ERROR', paymentError instanceof Error ? paymentError.message : String(paymentError)
+ );
+ }
+
+ } catch (error) {
+ console.error('Server error:', error);
+- return NextResponse.json(
+- {
+- error: 'Internal server error',
+- details: error instanceof Error ? error.message : 'Unknown error'
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ 'Internal server error',
++ 500, 'INTERNAL_SERVER_ERROR', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/postAd/route.ts b/app/api/postAd/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/postAd/route.ts
++++ b/app/api/postAd/route.ts
+@@ -5,6 +5,7 @@
+ import jwt from 'jsonwebtoken';
+ import { writeFile } from 'fs/promises';
+ import { join } from 'path';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Define the type for a user with their subscription plan
+ type UserWithSubscriptionPlan = Prisma.UserGetPayload<{
+@@ -25,14 +26,17 @@
+ try {
+ // Verify authentication
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse(
++ 'Unauthorized',
++ 401, 'UNAUTHORIZED'
++ );
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!);
+ const session: any = decoded;
+ const userId = session.id;
+-
+ // Check user and subscription
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+@@ -40,14 +44,17 @@
+ });
+
+ if (!user) {
+- return NextResponse.json({ error: 'User not found' }, { status: 404 });
++ return apiErrorResponse(
++ 'User not found',
++ 404, 'USER_NOT_FOUND'
++ );
+ }
+
+ // Check upload limits
+ const ads = await prisma.ad.findMany({ where: { userId } });
+ if (ads.length >= 10 && (!user.subscriptionPlan || user.subscriptionPlan.expiryDate < new Date())) {
+- return NextResponse.json(
+- { error: 'Upload limit reached. Please upgrade your subscription.' },
+- { status: 402 }
++ return apiErrorResponse(
++ 'Upload limit reached. Please upgrade your subscription.',
++ 402, 'UPLOAD_LIMIT_REACHED'
+ );
+ }
+
+@@ -62,49 +69,49 @@
+ const description = formData.get('description')?.toString();
+ const contact = formData.get('contact')?.toString();
+ const images = formData.getAll('images');
+-
+ // Validate required fields
+- if (!title || !category || !location || !priceStr || !description || !contact || images.length === 0) {
+- return NextResponse.json({ error: 'All fields are required' }, { status: 400 });
+- }
+-
+- // Process price
+- try {
+- // First convert string to number
+- const numericPrice = parseFloat(priceStr || '0');
+-
+- // Validate the numeric value
+- if (isNaN(numericPrice) || numericPrice <= 0) {
+- return NextResponse.json(
+- { error: 'Invalid price value' },
+- { status: 400 }
+- );
+- }
+-
+- // Convert to Prisma.Decimal with exact string representation
+- const price = new Prisma.Decimal(numericPrice.toFixed(2));
+-
+- // Process images
+- const imageUrls: string[] = [];
+- for (const image of images) {
+- if (!(image instanceof File)) continue;
+-
+- // Generate unique filename
+- const ext = image.name.split('.').pop();
+- const filename = `${Date.now()}-${Math.random().toString(36).substring(7)}.${ext}`;
+- const path = join(process.cwd(), 'public', 'uploads', filename);
+-
+- // Save file
+- const bytes = await image.arrayBuffer();
+- const buffer = Buffer.from(bytes);
+- await writeFile(path, buffer);
+-
+- // Store URL
+- imageUrls.push(`/uploads/${filename}`);
+- }
+-
+- // Create ad in database
+- const ad = await prisma.ad.create({
++ const validationErrors: { field: string; message: string }[] = [];
++ if (!title || title.length < 5 || title.length > 100) {
++ validationErrors.push({ field: 'title', message: 'Title must be between 5 and 100 characters.' });
++ }
++ if (!category) {
++ validationErrors.push({ field: 'category', message: 'Category is required.' });
++ }
++ if (!location || location.length < 3) {
++ validationErrors.push({ field: 'location', message: 'Location must be at least 3 characters.' });
++ }
++ if (!description || description.length < 20 || description.length > 5000) {
++ validationErrors.push({ field: 'description', message: 'Description must be between 20 and 5000 characters.' });
++ }
++ if (!contact || !/^\+?[0-9\s-()]{7,20}$/.test(contact)) {
++ validationErrors.push({ field: 'contact', message: 'Valid contact information is required.' });
++ }
++ if (images.length === 0) {
++ validationErrors.push({ field: 'images', message: 'At least one image is required.' });
++ }
++ if (images.length > 5) {
++ validationErrors.push({ field: 'images', message: 'Maximum of 5 images allowed.' });
++ }
++
++ const numericPrice = parseFloat(priceStr || '');
++ if (!priceStr || isNaN(numericPrice) || numericPrice <= 0) {
++ validationErrors.push({ field: 'price', message: 'A valid positive price is required.' });
++ }
++
++ if (validationErrors.length > 0) {
++ return apiErrorResponse(
++ 'Validation failed',
++ 400, 'VALIDATION_ERROR', validationErrors
++ );
++ }
++ // --- End Input Validation ---
++
++ const price = new Prisma.Decimal(numericPrice.toFixed(2));
++
++ // Process images
++ const imageUrls: string[] = [];
++ for (const image of images) {
++ if (!(image instanceof File)) continue;
++
++ // Validate image type and size
++ if (!ALLOWED_IMAGE_TYPES.includes(image.type)) {
++ return apiErrorResponse(
++ `Invalid image type: ${image.name}. Allowed types: ${ALLOWED_IMAGE_TYPES.join(', ')}`,
++ 400, 'INVALID_IMAGE_TYPE'
++ );
++ }
++ if (image.size > MAX_FILE_SIZE) {
++ return apiErrorResponse(
++ `Image too large: ${image.name}. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`,
++ 400, 'IMAGE_TOO_LARGE'
++ );
++ }
++
++ try {
++ // Generate unique filename
++ const ext = image.name.split('.').pop();
++ const filename = `${Date.now()}-${Math.random().toString(36).substring(7)}.${ext}`;
++ const uploadPath = join(process.cwd(), 'public', 'uploads', filename);
++
++ // Save file
++ const bytes = await image.arrayBuffer();
++ const buffer = Buffer.from(bytes);
++ await writeFile(uploadPath, buffer);
++
++ // Store URL
++ imageUrls.push(`/uploads/${filename}`);
++ } catch (fileError) {
++ console.error('Error processing file:', image.name, fileError);
++ return apiErrorResponse(
++ `Failed to process image: ${image.name}`,
++ 500, 'FILE_PROCESSING_ERROR', fileError instanceof Error ? fileError.message : String(fileError)
++ );
++ }
++ }
++
++ // Create ad in database
++ const ad = await prisma.ad.create({
+ data: {
+- title,
+- category,
++ title: title!, // Already validated
++ category: category!, // Already validated
+ subcategory: subcategory || null,
+ section: section || null,
+- location,
++ location: location!, // Already validated
+ price: price.toNumber(),
+- description,
+- contact,
++ description: description!, // Already validated
++ contact: contact!, // Already validated
+ images: imageUrls,
+ userId,
+ subscriptionPlanId: user.subscriptionPlanId || null,
+ },
+ });
+
+ return NextResponse.json({ message: 'Ad posted successfully!', ad }, { status: 201 });
+- } catch (error) {
+- console.error('Error creating ad:', error);
+- return NextResponse.json(
+- { error: error instanceof Error ? error.message : 'Failed to create ad' },
+- { status: 500 }
+- );
+- }
+ } catch (error) {
+ console.error('Error creating ad:', error);
+- return NextResponse.json(
+- { error: error instanceof Error ? error.message : 'Failed to create ad' },
+- { status: 500 }
++ return apiErrorResponse(
++ 'Failed to create ad',
++ 500, 'INTERNAL_SERVER_ERROR', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/promotions/active/route.ts b/app/api/promotions/active/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/promotions/active/route.ts
++++ b/app/api/promotions/active/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+
++import { apiErrorResponse } from '@/lib/errorHandling';
+ export async function GET(request: NextRequest) {
+ try {
+ // Add console.log to debug
+@@ -10,7 +11,7 @@
+ // Get token from cookies
+ const token = request.cookies.get('next-auth.session-token')?.value;
+ console.log('Token exists:', !!token);
+-
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -66,15 +67,15 @@
+ });
+
+ } catch (dbError) {
+- console.error('Database error:', dbError);
+- return NextResponse.json(
+- { error: 'Database error', details: dbError instanceof Error ? dbError.message : 'Unknown error' },
+- { status: 500 }
++ console.error('Database error:', dbError); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Database error',
++ 500, 'DATABASE_ERROR', dbError instanceof Error ? dbError.message : String(dbError)
+ );
+ }
+
+ } catch (error) {
+- console.error('Auth error:', error);
+- return NextResponse.json(
+- { error: 'Authentication failed' },
+- { status: 401 }
++ console.error('Auth error:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Authentication failed',
++ 401, 'AUTHENTICATION_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/search/route.ts b/app/api/search/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/search/route.ts
++++ b/app/api/search/route.ts
+@@ -1,5 +1,6 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(request: NextRequest) {
+ try {
+@@ -53,7 +54,10 @@
+
+ return NextResponse.json(results);
+ } catch (error) {
+- console.error('Search error:', error);
+- return NextResponse.json({ error: 'Search failed' }, { status: 500 });
++ console.error('Search error:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Search failed',
++ 500, 'SEARCH_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/signin/route.ts b/app/api/signin/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/signin/route.ts
++++ b/app/api/signin/route.ts
+@@ -3,29 +3,30 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ /**
+ * @route POST /api/signin
+ * @description Authenticates a user and returns a session token.
+ * @param {NextRequest} req - The incoming Next.js request object.
+ * @returns {Promise} A Next.js response object.
+ */
+-export async function POST(req: NextRequest): Promise {
++export async function POST(req: NextRequest): Promise { // Explicitly define return type
+ try {
+ const { email, password } = await req.json();
+
+ if (!email || !password) {
+- return NextResponse.json(
+- { error: 'Email and password are required', code: 'MISSING_CREDENTIALS' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Email and password are required',
++ 400, 'MISSING_CREDENTIALS'
+ );
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { email },
+ include: {
+- Agent: true // Include Agent details if the user is an agent
++ Agent: true // Include Agent details if the user is an agent for role check
+ }
+ });
+
+ if (!user) {
+ console.warn(`Login attempt failed for email (not found): ${email}`);
+- return NextResponse.json(
+- { error: 'Invalid email or password', code: 'INVALID_CREDENTIALS' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Invalid email or password',
++ 401, 'INVALID_CREDENTIALS'
+ );
+ }
+
+ if (!user.verified) {
+ console.warn(`Login attempt failed for email (not verified): ${email}`);
+- return NextResponse.json(
+- { error: 'Account not verified. Please check your email.', code: 'ACCOUNT_NOT_VERIFIED' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Account not verified. Please check your email.',
++ 401, 'ACCOUNT_NOT_VERIFIED'
+ );
+ }
+
+ const isValidPassword = await bcrypt.compare(password, user.password || '');
+ if (!isValidPassword) {
+ console.warn(`Login attempt failed for email (wrong password): ${email}`);
+- return NextResponse.json(
+- { error: 'Invalid email or password', code: 'INVALID_CREDENTIALS' },
+- { status: 401 }
++ return apiErrorResponse(
++ 'Invalid email or password',
++ 401, 'INVALID_CREDENTIALS'
+ );
+ }
+
+@@ -39,7 +40,7 @@
+ agentId: user.Agent?.id
+ };
+
+- console.log("User successfully signed in: ", { email: userData.email, role: userData.role, isAgent: userData.isAgent });
++ console.log("User successfully signed in: ", { email: userData.email, role: userData.role, isAgent: userData.isAgent }); // Log successful login
+
+ // Generate JWT token
+ const token = jwt.sign(
+@@ -67,9 +68,9 @@
+ return response;
+ } catch (error) {
+ console.error('Error in sign-in handler:', error);
+- return NextResponse.json(
+- { error: 'Internal server error', code: 'INTERNAL_SERVER_ERROR' },
+- { status: 500 }
++ return apiErrorResponse(
++ 'Internal server error',
++ 500, 'INTERNAL_SERVER_ERROR', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/signup/route.ts
++++ b/app/api/signup/route.ts
+@@ -5,10 +5,10 @@
+ import nodemailer from 'nodemailer';
+ import prisma from '@/lib/prisma';
+ import { SignUpRequest } from '@/types';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ // Helper functions for JSON response
+-const jsonResponse = (status: number, data: any) => new NextResponse(JSON.stringify(data), { status });
+-
++// const jsonResponse = (status: number, data: any) => new NextResponse(JSON.stringify(data), { status }); // Replaced by apiErrorResponse
+ // Configure Nodemailer transporter
+ const transporter = nodemailer.createTransport({
+ service: 'gmail',
+@@ -23,7 +23,7 @@
+ const { name, email, password }: SignUpRequest = await req.json();
+
+ // Check if the user already exists
+- const existingUser = await prisma.user.findUnique({ where: { email } });
+- if (existingUser) return jsonResponse(400, { error: 'User already exists' });
++ const existingUser = await prisma.user.findUnique({ where: { email } }); // Check for existing user
++ if (existingUser) return apiErrorResponse('User already exists', 400, 'USER_ALREADY_EXISTS');
+
+ // Hash password
+ const hashedPassword = await bcrypt.hash(password, 10);
+@@ -42,7 +42,10 @@
+ text: `Hi ${name},\n\nThanks for signing up. Please verify your email by clicking the link below:\n\n${verificationUrl}`,
+ });
+ } catch (error: any) {
+- return;
++ console.error('Error sending verification email:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to send verification email',
++ 500, 'EMAIL_SEND_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+
+- return jsonResponse(201, { message: 'User created successfully', user });
++ return NextResponse.json({ message: 'User created successfully', user }, { status: 201 });
+ }
+diff --git a/app/api/socketio/route.ts b/app/api/socketio/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/socketio/route.ts
++++ b/app/api/socketio/route.ts
+@@ -4,6 +4,7 @@
+ import { Server as SocketIOServer } from 'socket.io';
+ import { WebSocketServer } from 'ws';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+ import { initSocket } from '@/lib/socket';
+
+ interface SocketServer extends NetServer {
+@@ -27,7 +28,10 @@
+ return res;
+ } catch (error) {
+ console.error('WebSocket initialization error:', error);
+- return new Response('WebSocket initialization failed', { status: 500 });
++ return apiErrorResponse(
++ 'WebSocket initialization failed',
++ 500, 'WEBSOCKET_INIT_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+
+diff --git a/app/api/subscriptions/create/route.ts b/app/api/subscriptions/create/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/subscriptions/create/route.ts
++++ b/app/api/subscriptions/create/route.ts
+@@ -3,6 +3,7 @@
+ import prisma from '@/lib/prisma';
+ import { subscriptionPlans } from '@/constants';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+ const { planId, reference } = await req.json();
+ const token = req.cookies.get('next-auth.session-token')?.value;
+
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -21,12 +22,12 @@
+
+ const paymentData = await verifyResponse.json();
+
+ if (!paymentData.status || paymentData.data.status !== 'success') {
+- return NextResponse.json({ error: 'Payment verification failed' }, { status: 400 });
++ return apiErrorResponse('Payment verification failed', 400, 'PAYMENT_VERIFICATION_FAILED');
+ }
+
+ // Get plan details
+ const planDetails = subscriptionPlans.find(p => p.id === planId);
+ if (!planDetails) {
+- return NextResponse.json({ error: 'Invalid plan' }, { status: 400 });
++ return apiErrorResponse('Invalid plan', 400, 'INVALID_PLAN');
+ }
+
+ // Create subscription
+@@ -56,6 +57,9 @@
+
+ return NextResponse.json({ success: true, subscription });
+ } catch (error) {
+- console.error('Subscription error:', error);
+- return NextResponse.json({ error: 'Subscription failed' }, { status: 500 });
++ console.error('Subscription error:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Subscription failed',
++ 500, 'SUBSCRIPTION_CREATION_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/support/tickets/route.ts b/app/api/support/tickets/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/support/tickets/route.ts
++++ b/app/api/support/tickets/route.ts
+@@ -4,6 +4,7 @@
+ import jwt from "jsonwebtoken";
+ import * as fs from "fs";
+ import path from "path";
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+
+ export async function POST(req: Request) {
+@@ -11,7 +12,7 @@
+ // Verify authentication
+ const token = (await cookies()).get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!);
+@@ -30,8 +31,9 @@
+ const fileUrl = await saveLocalFile(file);
+ return fileUrl;
+ } catch (error: any) {
+- console.error(`Error uploading ${file.name}:`, error);
+- throw new Error(error.message);
++ console.error(`Error uploading ${file.name}:`, error); // Log the actual error for debugging
++ // Re-throw to be caught by the outer try-catch for a standardized response
++ throw apiErrorResponse(`Error uploading file: ${file.name}`, 400, 'FILE_UPLOAD_ERROR', error.message);
+ }
+ })
+ );
+@@ -49,9 +51,9 @@
+
+ return NextResponse.json({ success: true, ticket });
+ } catch (error) {
+- console.error("Error creating support ticket:", error);
+- return NextResponse.json(
+- { error: "Failed to create support ticket" },
+- { status: 500 }
++ console.error("Error creating support ticket:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to create support ticket",
++ 500, 'CREATE_TICKET_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -60,13 +62,13 @@
+ try {
+ // Verify authentication
+ const token = (await cookies()).get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!);
+ const session: any = decoded;
+ if (!session?.user) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ const tickets = await prisma.supportTicket.findMany({
+@@ -79,9 +81,9 @@
+
+ return NextResponse.json({ tickets });
+ } catch (error) {
+- console.error("Error fetching support tickets:", error);
+- return NextResponse.json(
+- { error: "Failed to fetch support tickets" },
+- { status: 500 }
++ console.error("Error fetching support tickets:", error); // Log the actual error for debugging
++ return apiErrorResponse(
++ "Failed to fetch support tickets",
++ 500, 'FETCH_TICKETS_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/analytics/route.ts b/app/api/user/analytics/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/analytics/route.ts
++++ b/app/api/user/analytics/route.ts
+@@ -5,6 +5,7 @@
+ cacheUserAnalytics,
+ getCachedUserAnalytics,
+ invalidateUserAnalyticsCache
++} from '@/lib/redis/analyticsCache'; // Assuming these are correctly implemented
++import { apiErrorResponse } from '@/lib/errorHandling';
+ } from '@/lib/redis/analyticsCache';
+
+ // Helper function to validate user session
+@@ -29,7 +30,7 @@
+ try {
+ const session = await validateUser(req);
+ if (!session) {
+- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
++ return apiErrorResponse("Unauthorized", 401, "UNAUTHORIZED");
+ }
+
+ // Get time range from query params
+@@ -76,27 +77,20 @@
+ return NextResponse.json(responseData);
+ } catch (error) {
+ console.error("Error fetching user analytics:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to fetch analytics data",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch analytics data",
++ 500, "ANALYTICS_FETCH_FAILED", error.message
+ );
+ }
+
+ // Fallback for unknown errors
+ return NextResponse.json(
+ { error: "An unexpected error occurred", code: "UNKNOWN_ERROR" },
+ { status: 500 }
+ );
+ }
+ }
+diff --git a/app/api/user/billing/disputes/route.ts b/app/api/user/billing/disputes/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/disputes/route.ts
++++ b/app/api/user/billing/disputes/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -18,9 +19,9 @@
+
+ // Validate required fields
+ if (!data.transactionId || !data.description) {
+- return NextResponse.json(
+- { error: 'Missing required fields', code: 'INVALID_REQUEST' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Missing required fields',
++ 400, 'MISSING_FIELDS'
+ );
+ }
+
+@@ -31,9 +32,9 @@
+
+ // Check if transaction exists and belongs to the user
+ if (!transaction || transaction.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Transaction not found', code: 'NOT_FOUND' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Transaction not found',
++ 404, 'TRANSACTION_NOT_FOUND'
+ );
+ }
+
+@@ -55,27 +56,20 @@
+ });
+ } catch (error) {
+ console.error("Error filing dispute:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to file dispute",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to file dispute",
++ 500, "FILE_DISPUTE_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/billing/invoices/[id]/download/route.ts b/app/api/user/billing/invoices/[id]/download/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/invoices/[id]/download/route.ts
++++ b/app/api/user/billing/invoices/[id]/download/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { format } from 'date-fns';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ interface RouteParams {
+ params: {
+@@ -17,7 +18,7 @@
+
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -40,9 +41,9 @@
+
+ // Check if invoice exists and belongs to the user
+ if (!invoice || invoice.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Invoice not found', code: 'NOT_FOUND' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Invoice not found',
++ 404, 'INVOICE_NOT_FOUND'
+ );
+ }
+
+@@ -56,27 +57,20 @@
+ }
+ });
+ } catch (error) {
+- console.error("Error downloading invoice:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to download invoice",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to download invoice",
++ 500, "DOWNLOAD_INVOICE_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/billing/invoices/route.ts b/app/api/user/billing/invoices/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/invoices/route.ts
++++ b/app/api/user/billing/invoices/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { format } from 'date-fns';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -69,27 +70,20 @@
+ status
+ });
+ } catch (error) {
+- console.error("Error fetching invoices:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to fetch invoices",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch invoices",
++ 500, "FETCH_INVOICES_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/billing/payment-methods/[id]/route.ts b/app/api/user/billing/payment-methods/[id]/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/payment-methods/[id]/route.ts
++++ b/app/api/user/billing/payment-methods/[id]/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ interface RouteParams {
+ params: {
+@@ -16,7 +17,7 @@
+
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -29,9 +30,9 @@
+
+ // Check if payment method exists and belongs to the user
+ if (!paymentMethod || paymentMethod.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Payment method not found', code: 'NOT_FOUND' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Payment method not found',
++ 404, 'PAYMENT_METHOD_NOT_FOUND'
+ );
+ }
+
+@@ -54,27 +55,20 @@
+ message: 'Payment method deleted successfully'
+ });
+ } catch (error) {
+- console.error("Error deleting payment method:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to delete payment method",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to delete payment method",
++ 500, "DELETE_PAYMENT_METHOD_FAILED", error.message
+ );
+ }
+
+@@ -94,7 +88,7 @@
+ }
+ }
+
+-// PATCH - Update a payment method (e.g., set as default)
++// PATCH - Update a payment method (e.g., set as default, update details)
+ export async function PATCH(
+ req: NextRequest,
+ { params }: RouteParams
+@@ -103,7 +97,7 @@
+
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -118,9 +112,9 @@
+
+ // Check if payment method exists and belongs to the user
+ if (!paymentMethod || paymentMethod.userId !== userId) {
+- return NextResponse.json(
+- { error: 'Payment method not found', code: 'NOT_FOUND' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'Payment method not found',
++ 404, 'PAYMENT_METHOD_NOT_FOUND'
+ );
+ }
+
+@@ -146,20 +140,15 @@
+ }
+ });
+ } catch (error) {
+- console.error("Error updating payment method:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to update payment method",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to update payment method",
++ 500, "UPDATE_PAYMENT_METHOD_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/billing/payment-methods/route.ts b/app/api/user/billing/payment-methods/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/payment-methods/route.ts
++++ b/app/api/user/billing/payment-methods/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -55,27 +56,20 @@
+ defaultMethod: formattedPaymentMethods.find(m => m.isDefault) || null
+ });
+ } catch (error) {
+- console.error("Error fetching payment methods:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to fetch payment methods",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch payment methods",
++ 500, "FETCH_PAYMENT_METHODS_FAILED", error.message
+ );
+ }
+
+@@ -88,7 +82,7 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -99,25 +93,25 @@
+
+ // Validate required fields
+ if (!data.type || !data.provider) {
+- return NextResponse.json(
+- { error: 'Missing required fields', code: 'INVALID_REQUEST' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Missing required fields',
++ 400, 'MISSING_FIELDS'
+ );
+ }
+
+ // Validate type-specific fields
+ if (data.type === 'card' && (!data.last4 || !data.expiryMonth || !data.expiryYear)) {
+- return NextResponse.json(
+- { error: 'Missing required card fields', code: 'INVALID_REQUEST' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Missing required card fields',
++ 400, 'MISSING_CARD_FIELDS'
+ );
+ } else if (data.type === 'paypal' && !data.email) {
+- return NextResponse.json(
+- { error: 'Missing required PayPal fields', code: 'INVALID_REQUEST' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Missing required PayPal fields',
++ 400, 'MISSING_PAYPAL_FIELDS'
+ );
+ } else if (data.type === 'bank_account' && (!data.accountName || !data.accountNumber || !data.bankName)) {
+- return NextResponse.json(
+- { error: 'Missing required bank account fields', code: 'INVALID_REQUEST' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Missing required bank account fields',
++ 400, 'MISSING_BANK_ACCOUNT_FIELDS'
+ );
+ }
+
+@@ -155,20 +149,15 @@
+ }
+ });
+ } catch (error) {
+- console.error("Error creating payment method:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to create payment method",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to create payment method",
++ 500, "CREATE_PAYMENT_METHOD_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/billing/summary/route.ts b/app/api/user/billing/summary/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/summary/route.ts
++++ b/app/api/user/billing/summary/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -23,9 +24,9 @@
+ });
+
+ if (!user) {
+- return NextResponse.json(
+- { error: 'User not found', code: 'NOT_FOUND' },
+- { status: 404 }
++ return apiErrorResponse(
++ 'User not found',
++ 404, 'USER_NOT_FOUND'
+ );
+ }
+
+@@ -108,27 +109,20 @@
+ }))
+ });
+ } catch (error) {
+- console.error("Error fetching billing summary:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to fetch billing summary",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch billing summary",
++ 500, "FETCH_BILLING_SUMMARY_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/billing/transactions/route.ts b/app/api/user/billing/transactions/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/billing/transactions/route.ts
++++ b/app/api/user/billing/transactions/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { formatDistanceToNow } from 'date-fns';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -69,27 +70,20 @@
+ timeRange
+ });
+ } catch (error) {
+- console.error("Error fetching transactions:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to fetch transactions",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch transactions",
++ 500, "FETCH_TRANSACTIONS_FAILED", error.message
+ );
+ }
+
+diff --git a/app/api/user/dashboard/route.ts b/app/api/user/dashboard/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/dashboard/route.ts
++++ b/app/api/user/dashboard/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { formatDistanceToNow } from 'date-fns';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -140,27 +141,20 @@
+ timeRange
+ });
+ } catch (error) {
+- console.error("Error fetching dashboard data:", error);
+-
+ // Provide more specific error messages based on the error type
+ if (error instanceof jwt.JsonWebTokenError) {
+- return NextResponse.json(
+- { error: "Invalid authentication token", code: "INVALID_TOKEN" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Invalid authentication token",
++ 401, "INVALID_TOKEN"
+ );
+ } else if (error instanceof jwt.TokenExpiredError) {
+- return NextResponse.json(
+- { error: "Authentication token expired", code: "TOKEN_EXPIRED" },
+- { status: 401 }
++ return apiErrorResponse(
++ "Authentication token expired",
++ 401, "TOKEN_EXPIRED"
+ );
+ } else if (error instanceof Error) {
+- // Return a generic error message but with the specific error name for debugging
+- return NextResponse.json(
+- {
+- error: "Failed to fetch dashboard data",
+- code: "SERVER_ERROR",
+- message: error.message,
+- name: error.name
+- },
+- { status: 500 }
++ return apiErrorResponse(
++ "Failed to fetch dashboard data",
++ 500, "FETCH_DASHBOARD_FAILED", error.message
+ );
+ }
+
+ // Fallback for unknown errors
+ return NextResponse.json(
+ { error: "An unexpected error occurred", code: "UNKNOWN_ERROR" },
+ { status: 500 }
+ );
+ }
+ }
+diff --git a/app/api/user/notifications/seed/route.ts b/app/api/user/notifications/seed/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/notifications/seed/route.ts
++++ b/app/api/user/notifications/seed/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { initialNotifications } from '@/constants';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -29,8 +30,9 @@
+ count: createdNotifications.count
+ });
+ } catch (error) {
+- console.error('Error seeding notifications:', error);
+- return NextResponse.json(
+- { error: 'Failed to seed notifications' },
+- { status: 500 }
++ console.error('Error seeding notifications:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to seed notifications',
++ 500, 'SEED_NOTIFICATIONS_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/notifications/test/route.ts b/app/api/user/notifications/test/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/notifications/test/route.ts
++++ b/app/api/user/notifications/test/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import jwt from 'jsonwebtoken';
+ import { initialNotifications } from '@/constants';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -25,8 +26,9 @@
+ message: 'This is a test endpoint. In production, notifications would be fetched from the database.'
+ });
+ } catch (error) {
+- console.error('Error in test endpoint:', error);
+- return NextResponse.json(
+- { error: 'Failed to process request' },
+- { status: 500 }
++ console.error('Error in test endpoint:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to process request',
++ 500, 'TEST_ENDPOINT_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/notifications/unread/route.ts b/app/api/user/notifications/unread/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/notifications/unread/route.ts
++++ b/app/api/user/notifications/unread/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+@@ -29,8 +30,9 @@
+
+ return NextResponse.json({ count });
+ } catch (error) {
+- console.error('Error fetching unread notifications:', error);
+- return NextResponse.json(
+- { error: 'Failed to fetch notifications' },
+- { status: 500 }
++ console.error('Error fetching unread notifications:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch notifications',
++ 500, 'FETCH_UNREAD_NOTIFICATIONS_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/notifications/route.ts b/app/api/user/notifications/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/notifications/route.ts
++++ b/app/api/user/notifications/route.ts
+@@ -4,6 +4,7 @@
+ import { formatDistanceToNow } from 'date-fns';
+ import { initialNotifications } from '@/constants';
+ import { markNotificationsAsRead } from '@/lib/notifications';
++import { apiErrorResponse } from '@/lib/errorHandling';
+ import { Notification } from '@/types';
+
+ /**
+@@ -13,14 +14,14 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ let userId: string;
+ try {
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ userId = decoded.id;
+ } catch (error) {
+- console.error('Error verifying token:', error);
+- return NextResponse.json({ error: 'Invalid session token' }, { status: 401 });
++ console.error('Error verifying token:', error); // Log the actual error for debugging
++ return apiErrorResponse('Invalid session token', 401, 'INVALID_SESSION_TOKEN', error instanceof Error ? error.message : String(error));
+ }
+
+ try {
+@@ -54,8 +55,9 @@
+ }
+
+ // Return empty array instead of error to prevent UI from breaking
+- return NextResponse.json({ notifications: [] });
++ return apiErrorResponse('Failed to fetch notifications due to database error', 500, 'DB_FETCH_FAILED', error instanceof Error ? error.message : String(error));
+ }
+ } catch (error) {
+- console.error('Unexpected error:', error);
+- return NextResponse.json({ notifications: [] });
++ console.error('Unexpected error:', error); // Log the actual error for debugging
++ return apiErrorResponse('An unexpected error occurred', 500, 'UNEXPECTED_ERROR', error instanceof Error ? error.message : String(error));
+ }
+ }
+
+@@ -66,15 +68,15 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Get notification IDs to mark as read
+ const { ids } = await req.json();
+
+ if (!ids || !Array.isArray(ids)) {
+- return NextResponse.json(
+- { error: 'Invalid request format' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid request format',
++ 400, 'INVALID_REQUEST_FORMAT'
+ );
+ }
+
+ // Update the database to mark notifications as read
+ const updateResult = await markNotificationsAsRead(userId, ids);
+-
+ return NextResponse.json({
+ success: true,
+ message: `Marked ${updateResult.count} notifications as read`
+ });
+ } catch (error) {
+- console.error('Error updating notifications:', error);
+-
++ console.error('Error updating notifications:', error); // Log the actual error for debugging
+ // Check if it's a Prisma error related to missing table
+ if (error instanceof Error && error.message.includes('does not exist')) {
+- return NextResponse.json({
++ return apiErrorResponse('Notification table does not exist yet. Please run migrations.', 500, 'DB_TABLE_MISSING');
++ }
++
++ return apiErrorResponse(
++ 'Failed to update notifications',
++ 500, 'UPDATE_NOTIFICATIONS_FAILED', error instanceof Error ? error.message : String(error)
++ );
++ }
++}
++
++/**
++ * DELETE - Delete notifications
++ */
++export async function DELETE(req: NextRequest) {
++ try {
++ // Get token from cookies
++ const token = req.cookies.get('next-auth.session-token')?.value;
++ if (!token) {
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
++ }
++
++ // Verify token and get userId
++ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
++ const userId = decoded.id;
++
++ // Get notification IDs to delete
++ const { ids } = await req.json();
++
++ if (!ids || !Array.isArray(ids)) {
++ return apiErrorResponse(
++ 'Invalid request format',
++ 400, 'INVALID_REQUEST_FORMAT'
++ );
++ }
++
++ // Delete the notifications
++ const deleteResult = await prisma.$executeRaw`
++ DELETE FROM "Notification"
++ WHERE "id" IN (${ids.join(',')})
++ AND "userId" = ${userId}
++ `;
++
++ // Count how many were deleted (deleteResult is the number of rows affected)
++ const deletedCount = deleteResult as number;
++
++ return NextResponse.json({
++ success: true,
++ message: `Deleted ${deletedCount} notifications`
++ });
++ } catch (error) {
++ console.error('Error deleting notifications:', error); // Log the actual error for debugging
++
++ // Check if it's a Prisma error related to missing table
++ if (error instanceof Error && error.message.includes('does not exist')) {
++ return apiErrorResponse('Notification table does not exist yet. Please run migrations.', 500, 'DB_TABLE_MISSING');
++ }
++
++ return apiErrorResponse(
++ 'Failed to delete notifications',
++ 500, 'DELETE_NOTIFICATIONS_FAILED', error instanceof Error ? error.message : String(error)
++ );
++ }
++}
++```
+```diff
+diff --git a/app/api/user/profile/image/route.ts b/app/api/user/profile/image/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/profile/image/route.ts
++++ b/app/api/user/profile/image/route.ts
+@@ -3,6 +3,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import { writeFile } from 'fs/promises';
++import { apiErrorResponse } from '@/lib/errorHandling';
+ import { join } from 'path';
+
+ /**
+@@ -12,7 +13,7 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -22,25 +23,25 @@
+ const file = formData.get('image') as File;
+
+ if (!file) {
+- return NextResponse.json(
+- { error: 'No image file provided' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'No image file provided',
++ 400, 'NO_IMAGE_PROVIDED'
+ );
+ }
+
+ // Validate file type
+ const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+ if (!validTypes.includes(file.type)) {
+- return NextResponse.json(
+- { error: 'Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed.' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed.',
++ 400, 'INVALID_FILE_TYPE'
+ );
+ }
+
+ // Validate file size (max 5MB)
+ const maxSize = 5 * 1024 * 1024; // 5MB
+ if (file.size > maxSize) {
+- return NextResponse.json(
+- { error: 'File size exceeds the 5MB limit' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'File size exceeds the 5MB limit',
++ 400, 'FILE_TOO_LARGE'
+ );
+ }
+
+@@ -65,9 +66,9 @@
+ user: updatedUser
+ });
+ } catch (error) {
+- console.error('Error uploading profile image:', error);
+- return NextResponse.json(
+- { error: 'Failed to upload profile image' },
+- { status: 500 }
++ console.error('Error uploading profile image:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to upload profile image',
++ 500, 'UPLOAD_PROFILE_IMAGE_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/profile/password/route.ts b/app/api/user/profile/password/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/profile/password/route.ts
++++ b/app/api/user/profile/password/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import bcrypt from 'bcryptjs';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ /**
+ * POST - Update user password
+@@ -11,8 +12,8 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -22,25 +23,25 @@
+ const { currentPassword, newPassword } = await req.json();
+
+ // Validate data
+ if (!currentPassword || !newPassword) {
+- return NextResponse.json(
+- { error: 'Current password and new password are required' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Current password and new password are required',
++ 400, 'MISSING_PASSWORDS'
+ );
+ }
+
+ if (typeof newPassword !== 'string' || newPassword.length < 8) {
+- return NextResponse.json(
+- { error: 'New password must be at least 8 characters long' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'New password must be at least 8 characters long',
++ 400, 'PASSWORD_TOO_SHORT'
+ );
+ }
+
+ // Get user with password
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+- id: true,
+- password: true,
++ id: true, // Only need ID and password hash
++ password: true, // Select password hash for comparison
+ }
+ });
+
+ if (!user) {
+- return NextResponse.json({ error: 'User not found' }, { status: 404 });
++ return apiErrorResponse('User not found', 404, 'USER_NOT_FOUND');
+ }
+
+ // Verify current password
+ const isPasswordValid = await bcrypt.compare(currentPassword, user.password || '');
+ if (!isPasswordValid) {
+- return NextResponse.json(
+- { error: 'Current password is incorrect' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Current password is incorrect',
++ 400, 'INCORRECT_CURRENT_PASSWORD'
+ );
+ }
+
+ // Update user password
+ await prisma.user.update({
+ where: { id: userId },
+ data: { password: hashedPassword },
+ });
+
+ return NextResponse.json({
+ message: 'Password updated successfully'
+ });
+ } catch (error) {
+- console.error('Error updating password:', error);
+- return NextResponse.json(
+- { error: 'Failed to update password' },
+- { status: 500 }
++ console.error('Error updating password:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to update password',
++ 500, 'UPDATE_PASSWORD_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/profile/route.ts b/app/api/user/profile/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/profile/route.ts
++++ b/app/api/user/profile/route.ts
+@@ -2,6 +2,7 @@
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
+ import bcrypt from 'bcryptjs';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ /**
+ * GET - Fetch user profile information
+@@ -11,7 +12,7 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -29,9 +30,9 @@
+ });
+
+ if (!user) {
+- return NextResponse.json({ error: 'User not found' }, { status: 404 });
++ return apiErrorResponse('User not found', 404, 'USER_NOT_FOUND');
+ }
+
+ return NextResponse.json({ user });
+ } catch (error) {
+- console.error('Error fetching user profile:', error);
+- return NextResponse.json(
+- { error: 'Failed to fetch user profile' },
+- { status: 500 }
++ console.error('Error fetching user profile:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch user profile',
++ 500, 'FETCH_PROFILE_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -44,15 +45,15 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Get update data from request
+ const data = await req.json();
+
+ // Validate data
+ if (!data || typeof data !== 'object') {
+- return NextResponse.json(
+- { error: 'Invalid request data' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Invalid request data',
++ 400, 'INVALID_REQUEST_DATA'
+ );
+ }
+
+ if (name !== undefined) {
+ if (typeof name !== 'string' || name.trim().length < 2) {
+- return NextResponse.json(
+- { error: 'Name must be at least 2 characters long' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Name must be at least 2 characters long',
++ 400, 'INVALID_NAME'
+ );
+ }
+ updateData.name = name.trim();
+ }
+
+ // Add phone field if provided (optional)
+ if (phone !== undefined) {
+ if (phone !== null && typeof phone !== 'string') {
+- return NextResponse.json(
+- { error: 'Phone must be a string or null' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Phone must be a string or null',
++ 400, 'INVALID_PHONE_FORMAT'
+ );
+ }
+ updateData.phone = phone;
+ }
+
+ // If no valid fields to update
+ if (Object.keys(updateData).length === 0) {
+- return NextResponse.json(
+- { error: 'No valid fields to update' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'No valid fields to update',
++ 400, 'NO_FIELDS_TO_UPDATE'
+ );
+ }
+
+ return NextResponse.json({
+ message: 'Profile updated successfully',
+ user: updatedUser
+ });
+ } catch (error) {
+- console.error('Error updating user profile:', error);
+- return NextResponse.json(
+- { error: 'Failed to update profile' },
+- { status: 500 }
++ console.error('Error updating user profile:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to update profile',
++ 500, 'UPDATE_PROFILE_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/saved-searches/route.ts b/app/api/user/saved-searches/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/saved-searches/route.ts
++++ b/app/api/user/saved-searches/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+@@ -22,9 +23,9 @@
+
+ return NextResponse.json({ savedSearches });
+ } catch (error) {
+- console.error('Error fetching saved searches:', error);
+- return NextResponse.json(
+- { error: 'Failed to fetch saved searches' },
+- { status: 500 }
++ console.error('Error fetching saved searches:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch saved searches',
++ 500, 'FETCH_SAVED_SEARCHES_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -33,15 +34,15 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Get query from request body
+ const { query } = await req.json();
+
+ if (!query) {
+- return NextResponse.json(
+- { error: 'Query is required' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Query is required',
++ 400, 'QUERY_REQUIRED'
+ );
+ }
+
+ // In a real app, this would save to the database
+ // For now, we'll just return success
+ return NextResponse.json({
+ success: true,
+ savedSearch: {
+ id: Math.floor(Math.random() * 1000),
+ query,
+ createdAt: new Date().toISOString()
+ }
+ });
+ } catch (error) {
+- console.error('Error saving search:', error);
+- return NextResponse.json(
+- { error: 'Failed to save search' },
+- { status: 500 }
++ console.error('Error saving search:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to save search',
++ 500, 'SAVE_SEARCH_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+@@ -50,15 +51,15 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Get search ID from URL
+ const { searchParams } = new URL(req.url);
+ const id = searchParams.get('id');
+
+ if (!id) {
+- return NextResponse.json(
+- { error: 'Search ID is required' },
+- { status: 400 }
++ return apiErrorResponse(
++ 'Search ID is required',
++ 400, 'SEARCH_ID_REQUIRED'
+ );
+ }
+
+ // In a real app, this would delete from the database
+ // For now, we'll just return success
+ return NextResponse.json({
+ success: true,
+ message: 'Search deleted successfully'
+ });
+ } catch (error) {
+- console.error('Error deleting search:', error);
+- return NextResponse.json(
+- { error: 'Failed to delete search' },
+- { status: 500 }
++ console.error('Error deleting search:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to delete search',
++ 500, 'DELETE_SEARCH_FAILED', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/api/user/support-chats/[chatId]/messages/route.ts b/app/api/user/support-chats/[chatId]/messages/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/support-chats/[chatId]/messages/route.ts
++++ b/app/api/user/support-chats/[chatId]/messages/route.ts
+@@ -1,6 +1,7 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(
+ req: NextRequest,
+@@ -8,7 +9,7 @@
+ ) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Verify the chat belongs to this user
+ const chat = await prisma.supportChat.findUnique({
+ where: {
+ id: params.chatId,
+ userId
+ }
+ });
+
+ if (!chat) {
+- return NextResponse.json({ error: 'Chat not found' }, { status: 404 });
++ return apiErrorResponse('Chat not found', 404, 'CHAT_NOT_FOUND');
+ }
+
+ // Fetch messages
+ const messages = await prisma.supportMessage.findMany({
+@@ -30,8 +31,9 @@
+
+ return NextResponse.json(messages);
+ } catch (error) {
+- console.error('Error fetching messages:', error);
+- return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 });
++ console.error('Error fetching messages:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to fetch messages',
++ 500, 'FETCH_MESSAGES_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+
+@@ -41,28 +43,28 @@
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Verify the chat belongs to this user
+ const chat = await prisma.supportChat.findUnique({
+ where: {
+ id: params.chatId,
+ userId
+ }
+ });
+
+ if (!chat) {
+- return NextResponse.json({ error: 'Chat not found' }, { status: 404 });
++ return apiErrorResponse('Chat not found', 404, 'CHAT_NOT_FOUND');
+ }
+
+ // Don't allow messages if chat is closed
+ if (chat.status === 'closed') {
+- return NextResponse.json({ error: 'Cannot send messages to a closed chat' }, { status: 400 });
++ return apiErrorResponse('Cannot send messages to a closed chat', 400, 'CHAT_CLOSED');
+ }
+
+ // Get message content from request
+ const { content } = await req.json();
+ if (!content) {
+- return NextResponse.json({ error: 'Message content is required' }, { status: 400 });
++ return apiErrorResponse('Message content is required', 400, 'MESSAGE_CONTENT_REQUIRED');
+ }
+
+ // Create message
+ const message = await prisma.supportMessage.create({
+ data: {
+ chatId: params.chatId,
+ content,
+ sender: userId,
+ senderType: 'user',
+ read: false
+ }
+ });
+
+ // Update chat timestamp
+ await prisma.supportChat.update({
+ where: { id: params.chatId },
+ data: { updatedAt: new Date() }
+ });
+
+ return NextResponse.json(message);
+ } catch (error) {
+- console.error('Error sending message:', error);
+- return NextResponse.json({ error: 'Failed to send message' }, { status: 500 });
++ console.error('Error sending message:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to send message',
++ 500, 'SEND_MESSAGE_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/user/support-chats/route.ts b/app/api/user/support-chats/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/user/support-chats/route.ts
++++ b/app/api/user/support-chats/route.ts
+@@ -1,13 +1,14 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import prisma from '@/lib/prisma';
+ import jwt from 'jsonwebtoken';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ // Fetch all support chats for this user
+ const supportChats = await prisma.supportChat.findMany({
+ where: {
+@@ -30,15 +31,15 @@
+ });
+
+ return NextResponse.json(supportChats);
+ } catch (error) {
+- console.error('Error fetching support chats:', error);
+- return NextResponse.json({ error: 'Failed to fetch support chats' }, { status: 500 });
++ console.error('Error fetching support chats:', error); // Log the actual error for debugging
++ return apiErrorResponse('Failed to fetch support chats', 500, 'FETCH_SUPPORT_CHATS_FAILED', error instanceof Error ? error.message : String(error));
+ }
+ }
+
+ export async function POST(req: NextRequest) {
+ try {
+ // Get token from cookies
+ const token = req.cookies.get('next-auth.session-token')?.value;
+ if (!token) {
+- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
++ return apiErrorResponse('Unauthorized', 401, 'UNAUTHORIZED');
+ }
+
+ // Verify token and get userId
+ const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET!) as { id: string };
+ const userId = decoded.id;
+-
+ const { category, priority } = await req.json();
+
+ // Create a new support chat
+@@ -62,6 +63,9 @@
+
+ return NextResponse.json(supportChat);
+ } catch (error) {
+- console.error('Error creating support chat:', error);
+- return NextResponse.json({ error: 'Failed to create support chat' }, { status: 500 });
++ console.error('Error creating support chat:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Failed to create support chat',
++ 500, 'CREATE_SUPPORT_CHAT_FAILED', error instanceof Error ? error.message : String(error)
++ );
+ }
+ }
+diff --git a/app/api/verify-email/route.ts b/app/api/verify-email/route.ts
+index abcdef1..abcdef2 100644
+--- a/app/api/verify-email/route.ts
++++ b/app/api/verify-email/route.ts
+@@ -1,5 +1,6 @@
+ import { NextRequest, NextResponse } from 'next/server';
+ import jwt from 'jsonwebtoken';
+ import prisma from '@/lib/prisma';
++import { apiErrorResponse } from '@/lib/errorHandling';
+
+ export async function GET(req: NextRequest) {
+ const token = req.nextUrl.searchParams.get('token');
+@@ -12,8 +13,8 @@
+ const decoded = jwt.verify(token, process.env.JWT_SECRET!);
+
+ if (typeof decoded !== 'object' || !('userId' in decoded)) {
+- return NextResponse.redirect(
+- new URL('/signin?alert=invalid_token', req.nextUrl.origin)
++ return apiErrorResponse(
++ 'Invalid token', 400, 'INVALID_TOKEN'
+ );
+ }
+
+@@ -27,6 +28,9 @@
+ new URL('/signin?alert=success_token', req.nextUrl.origin)
+ );
+ } catch (error) {
+- return NextResponse.redirect(
+- new URL('/signin?alert=expired_token', req.nextUrl.origin)
++ console.error('Email verification error:', error); // Log the actual error for debugging
++ return apiErrorResponse(
++ 'Expired or invalid token',
++ 400, 'EXPIRED_OR_INVALID_TOKEN', error instanceof Error ? error.message : String(error)
+ );
+ }
+ }
+diff --git a/app/terms/page.tsx b/app/terms/page.tsx
+new file mode 100644
+index 0000000..a1b2c3d
+--- /dev/null
++++ b/app/terms/page.tsx
+@@ -0,0 +1,108 @@
++import Navbar from "@/components/Navbar";
++import Footer from "@/components/Footer";
++import { FileText } from "lucide-react";
++
++export default function TermsOfServicePage() {
++ const termsSections = [
++ {
++ title: "Introduction",
++ content: [
++ "Welcome to AgroMarket! These Terms of Service ('Terms') govern your use of our website, products, and services (collectively, the 'Services'). By accessing or using our Services, you agree to be bound by these Terms. If you do not agree to these Terms, please do not use our Services.",
++ "AgroMarket is an online marketplace connecting farmers and agricultural product buyers. We provide a platform for listing, searching, and facilitating transactions of agricultural goods.",
++ ],
++ },
++ {
++ title: "Account Registration and Eligibility",
++ content: [
++ "To access certain features of our Services, you may be required to register for an account. You must be at least 18 years old to create an account and use our Services. You agree to provide accurate, current, and complete information during the registration process and to update such information to keep it accurate, current, and complete.",
++ "You are responsible for safeguarding your password and for any activities or actions under your account. AgroMarket cannot and will not be liable for any loss or damage arising from your failure to comply with the above requirements.",
++ ],
++ },
++ {
++ title: "User Responsibilities",
++ content: [
++ "You agree to use the Services only for lawful purposes and in a way that does not infringe the rights of, restrict or inhibit anyone else's use and enjoyment of the Services. Prohibited behavior includes harassing or causing distress or inconvenience to any other user, transmitting obscene or offensive content, or disrupting the normal flow of dialogue within our Services.",
++ "You are solely responsible for all content that you upload, post, publish, or display (hereinafter, 'upload') on or through the Services, or transmit to other users.",
++ ],
++ },
++ {
++ title: "Marketplace Rules and Transactions",
++ content: [
++ "AgroMarket acts as a platform to connect buyers and sellers. We are not directly involved in the transaction between buyers and sellers. As a result, we have no control over the quality, safety, morality or legality of any aspect of the items listed, the truth or accuracy of the listings, the ability of sellers to sell items or the ability of buyers to pay for items.",
++ "All transactions conducted through the platform are at the user's own risk. We encourage users to exercise due diligence and communicate clearly before finalizing any transaction.",
++ ],
++ },
++ {
++ title: "Intellectual Property",
++ content: [
++ "The Services and all content, features, and functionality (including but not limited to all information, software, text, displays, images, video, and audio, and the design, selection, and arrangement thereof) are owned by AgroMarket, its licensors, or other providers of such material and are protected by international copyright, trademark, patent, trade secret, and other intellectual property or proprietary rights laws.",
++ ],
++ },
++ {
++ title: "Termination",
++ content: [
++ "We may terminate or suspend your account and bar access to the Services immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of the Terms.",
++ ],
++ },
++ {
++ title: "Disclaimer of Warranties",
++ content: [
++ "Our Services are provided on an 'AS IS' and 'AS AVAILABLE' basis. AgroMarket makes no representations or warranties of any kind, express or implied, as to the operation of their Services, or the information, content or materials included therein. You expressly agree that your use of these Services, their content, and any services or items obtained from us is at your sole risk.",
++ ],
++ },
++ {
++ title: "Limitation of Liability",
++ content: [
++ "In no event shall AgroMarket, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.",
++ ],
++ },
++ {
++ title: "Governing Law",
++ content: [
++ "These Terms shall be governed and construed in accordance with the laws of Nigeria, without regard to its conflict of law provisions.",
++ ],
++ },
++ {
++ title: "Changes to Terms",
++ content: [
++ "We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.",
++ ],
++ },
++ {
++ title: "Contact Us",
++ content: [
++ "If you have any questions about these Terms, please contact us at legal@agromarket.ng.",
++ ],
++ },
++ ];
++
++ return (
++ <>
++
++
++ {/* Hero Section */}
++
++
++
++
++ Terms of Service
++
++
++ Please read these terms carefully before using our services.
++
++
++ Last updated: {new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
++
++
++
++
++ {/* Terms Content Section */}
++
++
++
++ {termsSections.map((section, index) => (
++
++
++ {index + 1}. {section.title}
++
++
++ {section.content.map((paragraph, pIndex) => (
++
{paragraph}
++ ))}
++
++
++ ))}
++
++
++
++
++
++ >
++ );
++}
+```
\ No newline at end of file
diff --git a/components/AccountDeletion.tsx b/components/AccountDeletion.tsx
new file mode 100644
index 0000000..a9060ec
--- /dev/null
+++ b/components/AccountDeletion.tsx
@@ -0,0 +1,276 @@
+"use client";
+
+import React, { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Trash2,
+ AlertTriangle,
+ Eye,
+ EyeOff,
+ Loader2,
+ X
+} from 'lucide-react';
+import toast from 'react-hot-toast';
+import { cn } from '@/lib/utils';
+import { useRouter } from 'next/navigation';
+import { useSession } from '@/components/SessionWrapper';
+
+interface AccountDeletionProps {
+ userName: string;
+ userEmail: string;
+}
+
+export default function AccountDeletion({ userName, userEmail }: AccountDeletionProps) {
+ const [showDeleteForm, setShowDeleteForm] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [password, setPassword] = useState('');
+ const [confirmationText, setConfirmationText] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [errors, setErrors] = useState<{ password?: string; confirmationText?: string }>({});
+
+ const router = useRouter();
+ const { setSession } = useSession();
+
+ const validateForm = () => {
+ const newErrors: { password?: string; confirmationText?: string } = {};
+
+ if (!password.trim()) {
+ newErrors.password = 'Password is required to delete your account';
+ }
+
+ if (!confirmationText.trim()) {
+ newErrors.confirmationText = 'Confirmation text is required';
+ } else if (confirmationText.toLowerCase().trim() !== 'delete my account') {
+ newErrors.confirmationText = 'Please type "DELETE MY ACCOUNT" exactly as shown';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleDeleteAccount = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ try {
+ setIsDeleting(true);
+
+ const response = await fetch('/api/user/profile/delete', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ password: password.trim(),
+ confirmationText: confirmationText.trim()
+ }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to delete account');
+ }
+
+ // Clear session
+ setSession(null);
+
+ // Clear local storage and cookies
+ localStorage.clear();
+ sessionStorage.clear();
+
+ toast.success('Your account has been permanently deleted');
+
+ // Redirect to home page after a short delay
+ setTimeout(() => {
+ router.push('/');
+ }, 2000);
+
+ } catch (error) {
+ console.error('Error deleting account:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to delete account');
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ const handleInputChange = (field: string, value: string) => {
+ if (field === 'password') {
+ setPassword(value);
+ } else if (field === 'confirmationText') {
+ setConfirmationText(value);
+ }
+
+ // Clear error when user types
+ if (errors[field as keyof typeof errors]) {
+ setErrors(prev => ({
+ ...prev,
+ [field]: undefined
+ }));
+ }
+ };
+
+ if (!showDeleteForm) {
+ return (
+
+
+
+
+
+
Danger Zone
+
+ Permanently delete your account and all associated data. This action cannot be undone.
+
+
setShowDeleteForm(true)}
+ variant="destructive"
+ size="sm"
+ className="bg-red-600 hover:bg-red-700"
+ >
+
+ Delete My Account
+
+
+
+
+
+
+
What will be deleted:
+
+ Your profile information and account settings
+ All your advertisements and listings
+ Transaction history and invoices
+ Messages and conversations
+ Support tickets and communications
+ Saved searches and preferences
+ All other data associated with your account
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
ā ļø Permanently Delete Account
+
+
You are about to permanently delete your account: {userName} ({userEmail})
+
This will immediately remove all your data and cannot be reversed. Please ensure you have:
+
+ Downloaded any important data you want to keep
+ Completed or cancelled any pending transactions
+ Informed contacts of your account closure
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/ActivityFeed.tsx b/components/ActivityFeed.tsx
new file mode 100644
index 0000000..e0aaece
--- /dev/null
+++ b/components/ActivityFeed.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { ArrowUpRight } from "lucide-react";
+import { activities } from "@/constants";
+
+
+export function ActivityFeed() {
+ return (
+
+ {activities.map((activity) => (
+
+
+
+
{activity.description}
+
{activity.time}
+
+
+ ))}
+
+ );
+ }
+
\ No newline at end of file
diff --git a/components/ActivityHistory.tsx b/components/ActivityHistory.tsx
new file mode 100644
index 0000000..7ad30e0
--- /dev/null
+++ b/components/ActivityHistory.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+import React, { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Activity,
+ Loader2,
+ RefreshCw,
+ Shield,
+ User,
+ Mail,
+ Key,
+ Smartphone,
+ Settings,
+ AlertCircle,
+ CheckCircle,
+ Clock,
+ Monitor,
+ ChevronLeft,
+ ChevronRight,
+ Filter
+} from 'lucide-react';
+import toast from 'react-hot-toast';
+import { cn } from '@/lib/utils';
+
+interface ActivityLog {
+ id: string;
+ activity: string;
+ description: string | null;
+ ipAddress: string | null;
+ userAgent: string | null;
+ deviceInfo: string | null;
+ location: string | null;
+ success: boolean;
+ metadata: any;
+ createdAt: string;
+}
+
+interface Pagination {
+ currentPage: number;
+ totalPages: number;
+ totalCount: number;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ limit: number;
+}
+
+const ActivityIcons: Record> = {
+ login: User,
+ logout: User,
+ login_failed: AlertCircle,
+ profile_update: User,
+ password_change: Key,
+ email_change_request: Mail,
+ email_change_verified: Mail,
+ avatar_update: User,
+ '2fa_enabled': Shield,
+ '2fa_disabled': Shield,
+ '2fa_backup_codes_generated': Shield,
+ account_deleted: AlertCircle,
+ notification_preferences_update: Settings,
+ default: Activity
+};
+
+const ActivityLabels: Record = {
+ login: 'Login',
+ logout: 'Logout',
+ login_failed: 'Failed Login Attempt',
+ profile_update: 'Profile Updated',
+ password_change: 'Password Changed',
+ email_change_request: 'Email Change Requested',
+ email_change_verified: 'Email Change Verified',
+ avatar_update: 'Avatar Updated',
+ '2fa_enabled': 'Two-Factor Authentication Enabled',
+ '2fa_disabled': 'Two-Factor Authentication Disabled',
+ '2fa_backup_codes_generated': 'Backup Codes Generated',
+ account_deleted: 'Account Deleted',
+ notification_preferences_update: 'Notification Preferences Updated'
+};
+
+const ActivityColors: Record = {
+ login: 'text-green-600 bg-green-50 border-green-200',
+ logout: 'text-blue-600 bg-blue-50 border-blue-200',
+ login_failed: 'text-red-600 bg-red-50 border-red-200',
+ profile_update: 'text-blue-600 bg-blue-50 border-blue-200',
+ password_change: 'text-orange-600 bg-orange-50 border-orange-200',
+ email_change_request: 'text-purple-600 bg-purple-50 border-purple-200',
+ email_change_verified: 'text-green-600 bg-green-50 border-green-200',
+ avatar_update: 'text-blue-600 bg-blue-50 border-blue-200',
+ '2fa_enabled': 'text-green-600 bg-green-50 border-green-200',
+ '2fa_disabled': 'text-red-600 bg-red-50 border-red-200',
+ '2fa_backup_codes_generated': 'text-green-600 bg-green-50 border-green-200',
+ account_deleted: 'text-red-600 bg-red-50 border-red-200',
+ notification_preferences_update: 'text-gray-600 bg-gray-50 border-gray-200',
+ default: 'text-gray-600 bg-gray-50 border-gray-200'
+};
+
+export default function ActivityHistory() {
+ const [activities, setActivities] = useState([]);
+ const [pagination, setPagination] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [selectedFilter, setSelectedFilter] = useState('');
+
+ const fetchActivities = async (page: number = 1, filter: string = '') => {
+ try {
+ const isRefresh = page === currentPage && !isLoading;
+ if (isRefresh) {
+ setIsRefreshing(true);
+ } else {
+ setIsLoading(true);
+ }
+
+ const params = new URLSearchParams({
+ page: page.toString(),
+ limit: '10'
+ });
+
+ if (filter) {
+ params.append('activity', filter);
+ }
+
+ const response = await fetch(`/api/user/profile/activity?${params}`, {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch activity history');
+ }
+
+ const data = await response.json();
+ setActivities(data.activityLogs);
+ setPagination(data.pagination);
+ setCurrentPage(page);
+
+ } catch (error) {
+ console.error('Error fetching activity history:', error);
+ toast.error('Failed to load activity history');
+ } finally {
+ setIsLoading(false);
+ setIsRefreshing(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchActivities(1, selectedFilter);
+ }, [selectedFilter]);
+
+ const handlePageChange = (page: number) => {
+ if (page >= 1 && pagination && page <= pagination.totalPages) {
+ fetchActivities(page, selectedFilter);
+ }
+ };
+
+ const handleRefresh = () => {
+ fetchActivities(currentPage, selectedFilter);
+ };
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleString();
+ };
+
+ const formatDeviceInfo = (deviceInfo: string | null) => {
+ if (!deviceInfo) return 'Unknown Device';
+
+ try {
+ const info = JSON.parse(deviceInfo);
+ const parts = [];
+ if (info.device) parts.push(info.device);
+ if (info.browser) parts.push(info.browser);
+ if (info.os) parts.push(info.os);
+ return parts.join(' ⢠') || 'Unknown Device';
+ } catch {
+ return 'Unknown Device';
+ }
+ };
+
+ const getActivityIcon = (activity: string) => {
+ const IconComponent = ActivityIcons[activity] || ActivityIcons.default;
+ return IconComponent;
+ };
+
+ const getActivityLabel = (activity: string) => {
+ return ActivityLabels[activity] || activity.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
+ };
+
+ const getActivityColor = (activity: string, success: boolean) => {
+ if (!success && activity !== 'login_failed') {
+ return 'text-red-600 bg-red-50 border-red-200';
+ }
+ return ActivityColors[activity] || ActivityColors.default;
+ };
+
+ if (isLoading && activities.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Account Activity
+
+
+ View your recent account activity and security events
+
+
+
+
+ setSelectedFilter(e.target.value)}
+ className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-2 focus:ring-blue-500"
+ >
+ All Activities
+ Logins
+ Profile Updates
+ Password Changes
+ 2FA Events
+ Email Changes
+
+
+
+
+ Refresh
+
+
+
+
+ {activities.length === 0 ? (
+
+
+
No Activity Found
+
+ {selectedFilter ? 'No activities match the selected filter.' : 'No activity history available.'}
+
+
+ ) : (
+
+ {activities.map((activity) => {
+ const IconComponent = getActivityIcon(activity.activity);
+ const colorClass = getActivityColor(activity.activity, activity.success);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {getActivityLabel(activity.activity)}
+
+ {!activity.success && (
+
+ Failed
+
+ )}
+
+ {activity.description && (
+
+ {activity.description}
+
+ )}
+
+
+
+ {formatDate(activity.createdAt)}
+
+ {activity.ipAddress && (
+ IP: {activity.ipAddress}
+ )}
+
+
+ {formatDeviceInfo(activity.deviceInfo)}
+
+
+
+
+ {activity.success ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+ {/* Pagination */}
+ {pagination && pagination.totalPages > 1 && (
+
+
+ Showing {((pagination.currentPage - 1) * pagination.limit) + 1} to{' '}
+ {Math.min(pagination.currentPage * pagination.limit, pagination.totalCount)} of{' '}
+ {pagination.totalCount} activities
+
+
+
+ handlePageChange(currentPage - 1)}
+ disabled={!pagination.hasPreviousPage}
+ variant="outline"
+ size="sm"
+ >
+
+
+
+
+ Page {pagination.currentPage} of {pagination.totalPages}
+
+
+ handlePageChange(currentPage + 1)}
+ disabled={!pagination.hasNextPage}
+ variant="outline"
+ size="sm"
+ >
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/AgentManagement.tsx b/components/AdminDashboard/AgentManagement.tsx
new file mode 100644
index 0000000..06a8ceb
--- /dev/null
+++ b/components/AdminDashboard/AgentManagement.tsx
@@ -0,0 +1,194 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { useSession } from "@/components/SessionWrapper";
+import { BarChart, UserPlus, Users } from "lucide-react";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import AgentsList from "./AgentsList";
+import CreateAgent from "./CreateAgent";
+import toast from "react-hot-toast";
+
+interface Agent {
+ id: string;
+ user: {
+ name: string;
+ email: string;
+ };
+ isOnline: boolean;
+ activeChats: number;
+ lastActive: string;
+ specialties: string[];
+}
+
+export default function AgentManagement() {
+ const { session } = useSession();
+ const router = useRouter();
+ const [agents, setAgents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [newAgentEmail, setNewAgentEmail] = useState("");
+ const [specialties, setSpecialties] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (session?.role === 'admin') {
+ fetchAgents();
+ }
+ }, [session]);
+
+ const fetchAgents = async () => {
+ try {
+ const response = await fetch("/api/admin/agents", {
+ credentials: 'include'
+ });
+
+ console.log("agent response: ", response);
+ if (response.ok) {
+ const data = await response.json();
+ console.log("decoded agent response: ", data);
+ setAgents(data);
+ } else {
+ // Only show error for non-401 responses
+ if (response.status !== 401) {
+ toast.error("Failed to fetch agents");
+ }
+ }
+ } catch (error) {
+ console.error("Error fetching agents:", error);
+ toast.error("Failed to fetch agents");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleCreateAgent = async () => {
+ if (!newAgentEmail) {
+ toast.error("Please enter an email address");
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const response = await fetch("/api/admin/agents", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: 'include',
+ body: JSON.stringify({
+ email: newAgentEmail,
+ specialties: specialties.split(",").map(s => s.trim()),
+ }),
+ });
+
+ if (response.ok) {
+ toast.success("Agent created successfully");
+ fetchAgents();
+ setNewAgentEmail("");
+ setSpecialties("");
+ } else {
+ const data = await response.json();
+ throw new Error(data.error);
+ }
+ } catch (error: any) {
+ toast.error(error.message || "Failed to create agent");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleToggleAgent = async (agentId: string, active: boolean) => {
+ try {
+ const response = await fetch(`/api/admin/agents/${agentId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ credentials: 'include',
+ body: JSON.stringify({ isAvailable: active }),
+ });
+
+ if (response.ok) {
+ toast.success(`Agent ${active ? "activated" : "deactivated"} successfully`);
+ fetchAgents();
+ }
+ } catch (error) {
+ toast.error("Failed to update agent status");
+ }
+ };
+
+ // Show loading state while fetching
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
Agent Management
+
+ Manage your support team and track their performance
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
Total Agents
+
{agents.length}
+
+
+
+
+
+
+
+
+
Avg. Response Time
+
2.5m
+
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+
+
+ Agents List
+
+
+
+ Add New Agent
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/AgentPerformanceCard.tsx b/components/AdminDashboard/AgentPerformanceCard.tsx
new file mode 100644
index 0000000..4164cd4
--- /dev/null
+++ b/components/AdminDashboard/AgentPerformanceCard.tsx
@@ -0,0 +1,262 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Progress } from "@/components/ui/progress";
+import { Star, StarHalf, Clock, CheckCircle, MessageSquare, Loader2 } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import toast from "react-hot-toast";
+
+interface AgentPerformance {
+ id: string;
+ name: string;
+ avatar?: string;
+ responseTime: number;
+ resolutionRate: number;
+ satisfaction: number;
+ chatsHandled: number;
+ status: "online" | "offline" | "busy";
+ lastActive: string;
+}
+
+interface AgentPerformanceCardProps {
+ agentId: string;
+ onViewDetails?: (agentId: string) => void;
+}
+
+export default function AgentPerformanceCard({ agentId, onViewDetails }: AgentPerformanceCardProps) {
+ const [loading, setLoading] = useState(true);
+ const [agent, setAgent] = useState(null);
+ const [showTooltip, setShowTooltip] = useState(false);
+ const [tooltipContent, setTooltipContent] = useState("");
+ const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
+
+ useEffect(() => {
+ fetchAgentPerformance();
+ }, [agentId]);
+
+ const fetchAgentPerformance = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ // For now, we'll simulate the data
+ setTimeout(() => {
+ const mockAgent: AgentPerformance = {
+ id: agentId,
+ name: "John Doe",
+ responseTime: 2.5,
+ resolutionRate: 92,
+ satisfaction: 4.7,
+ chatsHandled: 156,
+ status: "online",
+ lastActive: new Date().toISOString(),
+ };
+ setAgent(mockAgent);
+ setLoading(false);
+ }, 1000);
+ } catch (error) {
+ console.error("Error fetching agent performance:", error);
+ toast.error("Failed to load agent performance data");
+ setLoading(false);
+ }
+ };
+
+ const handleMouseEnter = (content: string, e: React.MouseEvent) => {
+ setTooltipContent(content);
+ setTooltipPosition({ x: e.clientX, y: e.clientY });
+ setShowTooltip(true);
+ };
+
+ const handleMouseLeave = () => {
+ setShowTooltip(false);
+ };
+
+ const renderStars = (rating: number) => {
+ const stars = [];
+ const fullStars = Math.floor(rating);
+ const hasHalfStar = rating % 1 >= 0.5;
+
+ for (let i = 0; i < fullStars; i++) {
+ stars.push(
+
+ );
+ }
+
+ if (hasHalfStar) {
+ stars.push(
+
+ );
+ }
+
+ const emptyStars = 5 - stars.length;
+ for (let i = 0; i < emptyStars; i++) {
+ stars.push(
+
+ );
+ }
+
+ return stars;
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case "online":
+ return "bg-green-500";
+ case "offline":
+ return "bg-gray-400";
+ case "busy":
+ return "bg-yellow-500";
+ default:
+ return "bg-gray-400";
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (!agent) {
+ return (
+
+
+ Agent data not available
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {agent.name.charAt(0)}
+
+
+
+
+
{agent.name}
+
+ Last active: {new Date(agent.lastActive).toLocaleTimeString()}
+
+
+
+
+ {agent.status}
+
+
+
+
+
+
handleMouseEnter("Average response time to customer inquiries", e)}
+ onMouseLeave={handleMouseLeave}
+ >
+
+
+
+ Response Time
+ {agent.responseTime} min
+
+
+
+
+
+
handleMouseEnter("Percentage of chats successfully resolved", e)}
+ onMouseLeave={handleMouseLeave}
+ >
+
+
+
+ Resolution Rate
+ {agent.resolutionRate}%
+
+
+
+
+
+
handleMouseEnter("Customer satisfaction rating (out of 5)", e)}
+ onMouseLeave={handleMouseLeave}
+ >
+
+ ā
+
+
+
+ Satisfaction
+ {agent.satisfaction}/5
+
+
+ {renderStars(agent.satisfaction)}
+
+
+
+
+
handleMouseEnter("Total number of chats handled", e)}
+ onMouseLeave={handleMouseLeave}
+ >
+
+
+
+ Chats Handled
+ {agent.chatsHandled}
+
+
+
+
+
+
+ onViewDetails && onViewDetails(agent.id)}
+ className="text-xs text-green-600 hover:text-green-700 font-medium"
+ >
+ View Detailed Performance ā
+
+
+
+
+
+
+
+ {showTooltip && (
+
+ {tooltipContent}
+
+ )}
+
+ >
+ );
+}
diff --git a/components/AdminDashboard/AgentTabs.tsx b/components/AdminDashboard/AgentTabs.tsx
new file mode 100644
index 0000000..e16bba8
--- /dev/null
+++ b/components/AdminDashboard/AgentTabs.tsx
@@ -0,0 +1,22 @@
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import AgentsList from "./AgentsList";
+import CreateAgent from "./CreateAgent";
+
+export default function AgentTabs() {
+ return (
+
+
+ Agents List
+ Create Agent
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/AgentsList.tsx b/components/AdminDashboard/AgentsList.tsx
new file mode 100644
index 0000000..2c2d309
--- /dev/null
+++ b/components/AdminDashboard/AgentsList.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Loader2, CheckCircle, XCircle } from "lucide-react";
+import { toast } from "react-hot-toast";
+
+interface Agent {
+ id: string;
+ user: {
+ name: string;
+ email: string;
+ };
+ isAvailable: boolean;
+ activeChats: number;
+ specialties: string[];
+ lastActive: string;
+}
+
+export default function AgentsList() {
+ const [agents, setAgents] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchAgents();
+ }, []);
+
+ const fetchAgents = async () => {
+ try {
+ const response = await fetch("/api/admin/agents", {
+ credentials: "include",
+ });
+ if (response.ok) {
+ const data = await response.json();
+ setAgents(data);
+ }
+ } catch (error) {
+ console.error("Error fetching agents:", error);
+ toast.error("Failed to fetch agents");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleToggleAgent = async (agentId: string, active: boolean) => {
+ try {
+ const response = await fetch(`/api/admin/agents/${agentId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify({ isAvailable: active }),
+ });
+
+ if (response.ok) {
+ toast.success(`Agent ${active ? "activated" : "deactivated"} successfully`);
+ fetchAgents();
+ }
+ } catch (error) {
+ toast.error("Failed to update agent status");
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Name
+ Email
+ Status
+ Active Chats
+ Specialties
+ Last Active
+ Actions
+
+
+
+ {agents.map((agent) => (
+
+ {agent.user.name}
+ {agent.user.email}
+
+
+ {agent.isAvailable ? "Online" : "Offline"}
+
+
+ {agent.activeChats}
+
+
+ {agent.specialties.map((specialty, index) => (
+
+ {specialty}
+
+ ))}
+
+
+
+ {new Date(agent.lastActive).toLocaleDateString()}
+
+
+ handleToggleAgent(agent.id, !agent.isAvailable)}
+ >
+ {agent.isAvailable ? (
+
+ ) : (
+
+ )}
+
+
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/Chat.tsx b/components/AdminDashboard/Chat.tsx
new file mode 100644
index 0000000..69be5ed
--- /dev/null
+++ b/components/AdminDashboard/Chat.tsx
@@ -0,0 +1,212 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Loader2, MessageSquare } from "lucide-react";
+import toast from "react-hot-toast";
+
+interface ChatMessage {
+ id: string;
+ content: string;
+ sender: string;
+ senderType: string;
+ createdAt: string;
+}
+
+interface SupportChat {
+ id: string;
+ userId: string;
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ };
+ agentId: string | null;
+ agent: {
+ user: {
+ name: string;
+ email: string;
+ };
+ } | null;
+ status: string;
+ category: string;
+ priority: number;
+ messages: ChatMessage[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export default function Chat() {
+ const [chats, setChats] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState("all");
+
+ useEffect(() => {
+ fetchChats();
+ }, [activeTab]);
+
+ const fetchChats = async () => {
+ try {
+ const status = activeTab !== "all" ? activeTab : "";
+ const response = await fetch(`/api/admin/chats${status ? `?status=${status}` : ""}`, {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setChats(data);
+ } else {
+ throw new Error("Failed to fetch chats");
+ }
+ } catch (error) {
+ console.error("Error fetching chats:", error);
+ toast.error("Failed to load chats");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case "pending":
+ return Pending ;
+ case "active":
+ return Active ;
+ case "closed":
+ return Closed ;
+ default:
+ return {status} ;
+ }
+ };
+
+ const getPriorityBadge = (priority: number) => {
+ switch (priority) {
+ case 1:
+ return Low ;
+ case 2:
+ return Medium ;
+ case 3:
+ return High ;
+ default:
+ return Low ;
+ }
+ };
+
+ return (
+
+
+
+
Support Chats
+
Monitor and manage customer support conversations
+
+
+ š
+ Refresh
+
+
+
+
+
+
+
+ All Chats
+ {chats.length > 0 && (
+
+ {chats.length}
+
+ )}
+
+
+ Pending
+
+
+ Active
+
+
+ Closed
+
+
+
+
+ {loading ? (
+
+
+
+ ) : chats.length === 0 ? (
+
+ ) : (
+
+ {chats.map((chat) => (
+
+
+
+
+
{chat.user.name}
+ #{chat.id.slice(-6)}
+
+
{chat.user.email}
+ {(chat as any).subject && (
+
{(chat as any).subject}
+ )}
+
+
+
+ {getStatusBadge(chat.status)}
+ {getPriorityBadge(chat.priority)}
+
+ {chat.category && (
+
+ {chat.category}
+
+ )}
+
+
+
+
+
Latest message:
+ {chat.messages[0]?.content ? (
+
{chat.messages[0].content}
+ ) : (
+
No messages yet
+ )}
+
+
+
+
+
+ {chat.agent ? (
+
+
+ Assigned to: {chat.agent.user.name}
+
+ ) : (
+
+
+ Unassigned
+
+ )}
+
+ {chat.messages.length > 0 && (
+
{chat.messages.length} message{chat.messages.length !== 1 ? 's' : ''}
+ )}
+
+
+
Updated: {new Date(chat.updatedAt).toLocaleDateString()}
+
{new Date(chat.updatedAt).toLocaleTimeString()}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/CreateAgent.tsx b/components/AdminDashboard/CreateAgent.tsx
new file mode 100644
index 0000000..5688b29
--- /dev/null
+++ b/components/AdminDashboard/CreateAgent.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Loader2 } from "lucide-react";
+import { toast } from "react-hot-toast";
+
+export default function CreateAgent() {
+ const [email, setEmail] = useState("");
+ const [name, setName] = useState("");
+ const [specialties, setSpecialties] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+
+ try {
+ const response = await fetch("/api/admin/agents", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify({
+ email,
+ name,
+ specialties: specialties.split(",").map((s) => s.trim()),
+ }),
+ });
+
+ if (response.ok) {
+ toast.success("Agent created successfully");
+ setEmail("");
+ setName("");
+ setSpecialties("");
+ } else {
+ const data = await response.json();
+ throw new Error(data.error);
+ }
+ } catch (error: any) {
+ toast.error(error.message || "Failed to create agent");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/Dashboard.tsx b/components/AdminDashboard/Dashboard.tsx
new file mode 100644
index 0000000..143f577
--- /dev/null
+++ b/components/AdminDashboard/Dashboard.tsx
@@ -0,0 +1,228 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { BarChart3, Users, MessageSquare, Clock, Settings, Loader2, LineChart } from "lucide-react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import ManageAgent from "./ManageAgent";
+import Chat from "./Chat";
+import SettingsTab from "./Settings";
+import SupportAnalytics from "./SupportAnalytics";
+import toast from "react-hot-toast";
+
+interface AdminStats {
+ totalAgents: number;
+ activeAgents: number;
+ activeChats: number;
+ avgResponseTime: number;
+ resolutionRate: number;
+}
+
+interface AdminDashboardProps {
+ defaultTab?: string;
+}
+
+export default function AdminDashboard({ defaultTab = "dashboard" }: AdminDashboardProps) {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchStats();
+ }, []);
+
+ const fetchStats = async () => {
+ try {
+ const response = await fetch("/api/admin/stats", {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setStats(data);
+ } else {
+ throw new Error("Failed to fetch stats");
+ }
+ } catch (error) {
+ console.error("Error fetching admin stats:", error);
+ toast.error("Failed to load dashboard statistics");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
Admin Dashboard
+
Overview of your support operations
+
+
+
+
+
+
+ Dashboard
+ Home
+
+
+
+ Agents
+ Agents
+
+
+
+ Chat
+ Chat
+
+
+
+ Analytics
+ Stats
+
+
+
+ Settings
+ Config
+
+
+
+
+ {loading ? (
+
+
+ Loading admin statistics...
+
+ ) : (
+
+ {/* Stats Grid */}
+
+
+
+
+
+
+
+
Total Agents
+
{stats?.totalAgents || 0}
+
+
+
+
+
+
+
+
+
+
+
Active Chats
+
{stats?.activeChats || 0}
+
+
+
+
+
+
+
+
+
+
+
Avg. Response
+
{stats?.avgResponseTime || 0}m
+
+
+
+
+
+
+
+
+
+
+
Resolution Rate
+
{stats?.resolutionRate || 0}%
+
+
+
+
+
+ {/* Recent Activity Section */}
+
+
+
Recent Activity
+
Latest system events and updates
+
+
+
+
+
+
+
New agent registered
+
Support Agent joined the team
+
2 minutes ago
+
+
+
+
+
+
Support chat resolved
+
Payment issue successfully resolved
+
15 minutes ago
+
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+
Quick Actions
+
Common administrative tasks
+
+
+
+
+
+ Manage Agents
+
+
+
+ View Chats
+
+
+
+ Analytics
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/EnhancedAgentsList.tsx b/components/AdminDashboard/EnhancedAgentsList.tsx
new file mode 100644
index 0000000..60ce1de
--- /dev/null
+++ b/components/AdminDashboard/EnhancedAgentsList.tsx
@@ -0,0 +1,399 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Loader2, CheckCircle, XCircle, Search, Filter, ArrowUpDown, Star } from "lucide-react";
+import { toast } from "react-hot-toast";
+import AgentPerformanceCard from "./AgentPerformanceCard";
+
+interface Agent {
+ id: string;
+ user: {
+ name: string;
+ email: string;
+ };
+ isAvailable: boolean;
+ activeChats: number;
+ specialties: string[];
+ lastActive: string;
+ performance?: {
+ responseTime: number;
+ resolutionRate: number;
+ satisfaction: number;
+ };
+}
+
+export default function EnhancedAgentsList() {
+ const [agents, setAgents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [sortField, setSortField] = useState(null);
+ const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
+ const [selectedAgent, setSelectedAgent] = useState(null);
+ const [viewMode, setViewMode] = useState<"table" | "cards">("table");
+ const [filterAvailable, setFilterAvailable] = useState(null);
+
+ useEffect(() => {
+ fetchAgents();
+ }, []);
+
+ const fetchAgents = async () => {
+ try {
+ const response = await fetch("/api/admin/agents", {
+ credentials: "include",
+ });
+ if (response.ok) {
+ const data = await response.json();
+ // Add mock performance data
+ const enhancedData = data.map((agent: Agent) => ({
+ ...agent,
+ performance: {
+ responseTime: Math.random() * 4 + 1, // 1-5 minutes
+ resolutionRate: Math.floor(Math.random() * 20) + 80, // 80-100%
+ satisfaction: Math.random() * 1.5 + 3.5, // 3.5-5.0 stars
+ },
+ }));
+ setAgents(enhancedData);
+ }
+ } catch (error) {
+ console.error("Error fetching agents:", error);
+ toast.error("Failed to fetch agents");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleToggleAgent = async (agentId: string, isAvailable: boolean) => {
+ try {
+ const response = await fetch(`/api/admin/agents/${agentId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify({ isAvailable }),
+ });
+
+ if (response.ok) {
+ // Show success toast with animation
+ toast.success(
+
+
+ Agent {isAvailable ? "activated" : "deactivated"} successfully
+
+ );
+
+ // Update local state
+ setAgents(
+ agents.map((agent) =>
+ agent.id === agentId ? { ...agent, isAvailable } : agent
+ )
+ );
+ } else {
+ throw new Error("Failed to update agent status");
+ }
+ } catch (error) {
+ console.error("Error toggling agent:", error);
+ toast.error("Failed to update agent status");
+ }
+ };
+
+ const handleSort = (field: string) => {
+ if (sortField === field) {
+ setSortDirection(sortDirection === "asc" ? "desc" : "asc");
+ } else {
+ setSortField(field);
+ setSortDirection("asc");
+ }
+ };
+
+ const handleViewDetails = (agentId: string) => {
+ setSelectedAgent(selectedAgent === agentId ? null : agentId);
+ };
+
+ const filteredAgents = agents.filter((agent) => {
+ const matchesSearch =
+ agent.user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ agent.user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ agent.specialties.some((s) => s.toLowerCase().includes(searchTerm.toLowerCase()));
+
+ const matchesAvailability =
+ filterAvailable === null || agent.isAvailable === filterAvailable;
+
+ return matchesSearch && matchesAvailability;
+ });
+
+ const sortedAgents = [...filteredAgents].sort((a, b) => {
+ if (!sortField) return 0;
+
+ let valueA, valueB;
+
+ switch (sortField) {
+ case "name":
+ valueA = a.user.name;
+ valueB = b.user.name;
+ break;
+ case "email":
+ valueA = a.user.email;
+ valueB = b.user.email;
+ break;
+ case "status":
+ valueA = a.isAvailable ? 1 : 0;
+ valueB = b.isAvailable ? 1 : 0;
+ break;
+ case "activeChats":
+ valueA = a.activeChats;
+ valueB = b.activeChats;
+ break;
+ case "responseTime":
+ valueA = a.performance?.responseTime || 0;
+ valueB = b.performance?.responseTime || 0;
+ break;
+ case "resolutionRate":
+ valueA = a.performance?.resolutionRate || 0;
+ valueB = b.performance?.resolutionRate || 0;
+ break;
+ case "satisfaction":
+ valueA = a.performance?.satisfaction || 0;
+ valueB = b.performance?.satisfaction || 0;
+ break;
+ default:
+ return 0;
+ }
+
+ if (valueA < valueB) return sortDirection === "asc" ? -1 : 1;
+ if (valueA > valueB) return sortDirection === "asc" ? 1 : -1;
+ return 0;
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10 w-full"
+ />
+
+
+ setFilterAvailable(filterAvailable === true ? null : true)}
+ className="text-sm whitespace-nowrap"
+ >
+ Online Only
+
+ setViewMode(viewMode === "table" ? "cards" : "table")}
+ className="text-sm whitespace-nowrap"
+ >
+ {viewMode === "table" ? "Card View" : "Table View"}
+
+
+
+
+ {filteredAgents.length === 0 ? (
+
+
No agents found matching your criteria.
+
+ ) : viewMode === "table" ? (
+
+
+
+
+
+
+ handleSort("name")}
+ className="flex items-center gap-1 hover:text-green-600 text-xs sm:text-sm"
+ >
+ Name
+ {sortField === "name" && (
+
+ )}
+
+
+
+ handleSort("email")}
+ className="flex items-center gap-1 hover:text-green-600 text-xs sm:text-sm"
+ >
+ Email
+ {sortField === "email" && (
+
+ )}
+
+
+
+ handleSort("status")}
+ className="flex items-center gap-1 hover:text-green-600 mx-auto text-xs sm:text-sm"
+ >
+ Status
+ {sortField === "status" && (
+
+ )}
+
+
+
+ handleSort("activeChats")}
+ className="flex items-center gap-1 hover:text-green-600 mx-auto text-xs sm:text-sm"
+ >
+ Chats
+ {sortField === "activeChats" && (
+
+ )}
+
+
+ Specialties
+
+ handleSort("satisfaction")}
+ className="flex items-center gap-1 hover:text-green-600 mx-auto text-xs sm:text-sm"
+ >
+ Rating
+ {sortField === "satisfaction" && (
+
+ )}
+
+
+ Actions
+
+
+
+ {sortedAgents.map((agent) => (
+
+
+
+
+ {agent.user.name}
+ {agent.user.email}
+
+
+ {agent.user.email}
+
+
+ {agent.isAvailable ? "Online" : "Offline"}
+
+
+ {agent.activeChats}
+
+
+ {agent.specialties.slice(0, 2).map((specialty, index) => (
+
+ {specialty}
+
+ ))}
+ {agent.specialties.length > 2 && (
+
+ +{agent.specialties.length - 2}
+
+ )}
+
+
+
+
+ {agent.performance?.satisfaction.toFixed(1)}
+
+
+
+
+
+ handleViewDetails(agent.id)}
+ className="text-xs py-1 px-2 whitespace-nowrap"
+ >
+ Details
+
+ handleToggleAgent(agent.id, !agent.isAvailable)}
+ className="text-xs py-1 px-2 whitespace-nowrap"
+ >
+
+ {agent.isAvailable ? (
+
+ ) : (
+
+ )}
+ {agent.isAvailable ? "Deactivate" : "Activate"}
+
+
+ {agent.isAvailable ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {selectedAgent === agent.id && (
+
+
+
+
+
+
+
+ )}
+
+
+ ))}
+
+
+
+
+ ) : (
+
+ {sortedAgents.map((agent) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/AdminDashboard/Layout.tsx b/components/AdminDashboard/Layout.tsx
new file mode 100644
index 0000000..da729fd
--- /dev/null
+++ b/components/AdminDashboard/Layout.tsx
@@ -0,0 +1,170 @@
+"use client";
+
+import { useState } from "react";
+import { usePathname, useRouter } from "next/navigation";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import { useSession } from "@/components/SessionWrapper";
+import {
+ LogOut,
+ Settings,
+ LayoutDashboard,
+ Users,
+ MessageSquare,
+ Menu,
+ X,
+ LineChart,
+} from "lucide-react";
+
+
+export default function AdminLayout({ children }: { children: React.ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const pathname = usePathname();
+ const router = useRouter();
+ const { setSession } = useSession();
+
+ // Custom navigation items with direct links to dashboard tabs
+ const customNavItems = [
+ {
+ title: "Dashboard",
+ href: "/admin/dashboard",
+ icon: LayoutDashboard,
+ },
+ {
+ title: "Manage Agents",
+ href: "/admin/agents",
+ icon: Users,
+ },
+ {
+ title: "Chats",
+ href: "/admin/dashboard?tab=chat",
+ icon: MessageSquare,
+ },
+ {
+ title: "Analytics",
+ href: "/admin/dashboard?tab=analytics",
+ icon: LineChart,
+ },
+ {
+ title: "Settings",
+ href: "/admin/dashboard?tab=settings",
+ icon: Settings,
+ },
+ ];
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ // Clear session from context
+ setSession(null);
+
+ // Clear all auth-related cookies
+ const cookies = document.cookie.split(";");
+
+ for (let cookie of cookies) {
+ const cookieName = cookie.split("=")[0].trim();
+ if (cookieName.includes("next-auth")) {
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=${window.location.hostname}`;
+ // Also try without domain for local development
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ }
+ }
+
+ // Call the API to clear server-side session
+ await fetch('/api/logout', {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ router.push('/signin');
+ } catch (error) {
+ console.error('Logout error:', error);
+ } finally {
+ setIsLoggingOut(false);
+ }
+ };
+
+ return (
+
+ {/* Mobile Nav Toggle */}
+
setIsOpen(!isOpen)}
+ className="fixed top-4 left-4 z-50 lg:hidden bg-white p-2 rounded-lg shadow-md"
+ >
+ {isOpen ? : }
+
+
+ {/* Sidebar */}
+
+
+
+
AgroMarket
+
Admin Portal
+
+
+
+ {customNavItems.map((item) => (
+
+
+ {item.title}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
Admin User
+
admin@agromarket.com
+
+
+
+
+
+ {isLoggingOut ? "Logging out..." : "Logout"}
+
+
+
+
+
+
+ {/* Overlay for mobile */}
+ {isOpen && (
+
setIsOpen(false)}
+ />
+ )}
+
+ {/* Main Content */}
+
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/ManageAgent.tsx b/components/AdminDashboard/ManageAgent.tsx
new file mode 100644
index 0000000..2a422ac
--- /dev/null
+++ b/components/AdminDashboard/ManageAgent.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { useState } from "react";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Users, UserPlus } from "lucide-react";
+import EnhancedAgentsList from "./EnhancedAgentsList";
+import CreateAgent from "./CreateAgent";
+
+export default function ManageAgent() {
+ const [activeTab, setActiveTab] = useState("list");
+
+ return (
+
+
+
+
Agent Management
+
Manage your support agents
+
+
+
+
+
+
+
+
+
+ Agents List
+
+
+
+ Add New Agent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/Settings.tsx b/components/AdminDashboard/Settings.tsx
new file mode 100644
index 0000000..208b8a5
--- /dev/null
+++ b/components/AdminDashboard/Settings.tsx
@@ -0,0 +1,194 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Loader2, Save } from "lucide-react";
+import toast from "react-hot-toast";
+
+interface AppSettings {
+ supportEnabled: boolean;
+ maxAgentsPerCategory: number;
+ autoAssignAgents: boolean;
+ chatTimeoutMinutes: number;
+ notificationsEnabled: boolean;
+ maintenanceMode: boolean;
+}
+
+export default function Settings() {
+ const [settings, setSettings] = useState
({
+ supportEnabled: true,
+ maxAgentsPerCategory: 5,
+ autoAssignAgents: true,
+ chatTimeoutMinutes: 30,
+ notificationsEnabled: true,
+ maintenanceMode: false
+ });
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ fetchSettings();
+ }, []);
+
+ const fetchSettings = async () => {
+ try {
+ const response = await fetch("/api/admin/settings", {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setSettings(data);
+ } else {
+ throw new Error("Failed to fetch settings");
+ }
+ } catch (error) {
+ console.error("Error fetching settings:", error);
+ toast.error("Failed to load settings");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const saveSettings = async () => {
+ setSaving(true);
+ try {
+ const response = await fetch("/api/admin/settings", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(settings),
+ });
+
+ if (response.ok) {
+ toast.success("Settings saved successfully");
+ } else {
+ throw new Error("Failed to save settings");
+ }
+ } catch (error) {
+ console.error("Error saving settings:", error);
+ toast.error("Failed to save settings");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleChange = (key: keyof AppSettings, value: boolean | number) => {
+ setSettings(prev => ({
+ ...prev,
+ [key]: value
+ }));
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
System Settings
+
+ {saving ? : }
+ Save Changes
+
+
+
+
+ {/* Support Settings */}
+
+
Support Settings
+
+
+
+
Enable Support System
+
Allow users to contact support agents
+
+
handleChange('supportEnabled', checked)}
+ />
+
+
+
+
+
Auto-assign Agents
+
Automatically assign agents to new support requests
+
+
handleChange('autoAssignAgents', checked)}
+ />
+
+
+
+
+
+
+ {/* System Settings */}
+
+
System Settings
+
+
+
+
Enable Notifications
+
Send email notifications for system events
+
+
handleChange('notificationsEnabled', checked)}
+ />
+
+
+
+
+
Maintenance Mode
+
Put the system in maintenance mode (users will see a maintenance page)
+
+
handleChange('maintenanceMode', checked)}
+ />
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AdminDashboard/SupportAnalytics.tsx b/components/AdminDashboard/SupportAnalytics.tsx
new file mode 100644
index 0000000..1dc18ce
--- /dev/null
+++ b/components/AdminDashboard/SupportAnalytics.tsx
@@ -0,0 +1,444 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Bar, Line, Doughnut } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ ArcElement,
+ Title,
+ Tooltip,
+ Legend,
+} from "chart.js";
+import { Loader2, Calendar, Filter } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
+import toast from "react-hot-toast";
+
+// Register ChartJS components
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ ArcElement,
+ Title,
+ Tooltip,
+ Legend
+);
+
+interface AnalyticsData {
+ chatVolume: {
+ labels: string[];
+ data: number[];
+ };
+ responseTime: {
+ labels: string[];
+ data: number[];
+ };
+ resolutionRate: {
+ labels: string[];
+ data: number[];
+ };
+ categoryDistribution: {
+ labels: string[];
+ data: number[];
+ };
+ agentPerformance: {
+ agents: string[];
+ responseTime: number[];
+ resolutionRate: number[];
+ satisfaction: number[];
+ };
+}
+
+export default function SupportAnalytics() {
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState("overview");
+ const [timeRange, setTimeRange] = useState("7days");
+ const [analyticsData, setAnalyticsData] = useState(null);
+
+ useEffect(() => {
+ fetchAnalyticsData();
+ }, [timeRange]);
+
+ const fetchAnalyticsData = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/admin/analytics?timeRange=${timeRange}`, {
+ credentials: "include"
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+
+ // Transform the API response to match our component's data structure
+ const transformedData: AnalyticsData = {
+ chatVolume: data.chatVolume,
+ responseTime: data.responseTime,
+ resolutionRate: data.resolutionRate,
+ categoryDistribution: data.categoryDistribution,
+ agentPerformance: data.agentPerformance
+ };
+
+ setAnalyticsData(transformedData);
+ } else {
+ throw new Error("Failed to fetch analytics data");
+ }
+ } catch (error) {
+ console.error("Error fetching analytics data:", error);
+ toast.error("Failed to load analytics data");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
Support Analytics
+
+
+
+
+
+
+ Last 7 Days
+ Last 30 Days
+ Last 90 Days
+
+
+
+
+ Custom Range
+
+
+
+ Filter
+
+
+
+
+
+
+ Overview
+ Agent Performance
+ Categories
+ Satisfaction
+
+
+
+
+ {/* Chat Volume Chart */}
+
+
+ Chat Volume
+
+
+
+
+
+
+
+
+ {/* Response Time Chart */}
+
+
+ Average Response Time (minutes)
+
+
+
+
+
+
+
+
+ {/* Resolution Rate Chart */}
+
+
+ Resolution Rate (%)
+
+
+
+
+
+
+
+
+ {/* Category Distribution Chart */}
+
+
+ Category Distribution
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Agent Performance
+
+
+
+ score * 20
+ ), // Scale to percentage
+ backgroundColor: "rgba(245, 158, 11, 0.6)",
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: "top" as const,
+ },
+ tooltip: {
+ callbacks: {
+ label: function (context) {
+ const label = context.dataset.label || "";
+ const value = context.parsed.y;
+ if (label.includes("Satisfaction")) {
+ return `${label}: ${value / 20}/5`;
+ }
+ return `${label}: ${value}`;
+ },
+ },
+ },
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ max: 100,
+ },
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+ Support Categories Analysis
+
+
+
+
+
+
+
+
+
+
+
+
+ Customer Satisfaction
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/AdsPromotionMain.tsx b/components/AdsPromotionMain.tsx
new file mode 100644
index 0000000..c1fa027
--- /dev/null
+++ b/components/AdsPromotionMain.tsx
@@ -0,0 +1,428 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import Link from "next/link";
+import { CheckCircle, BarChart2, Clock, TrendingUp, Star } from "lucide-react";
+import { formatCurrency, cn } from "@/lib/utils";
+import { boostOptions, subscriptionPlans } from "@/constants";
+import { Ad, PaymentDetails, ActivePromotion, PromotionsResponse, Promotion } from "@/types";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { useSession } from "@/components/SessionWrapper";
+import { useRouter } from "next/navigation";
+import PaymentModal from "@/components/PaymentModal";
+import Alert from '@/components/Alerts';
+
+export default function AdPromotions() {
+ const [trackingStats, setTrackingStats] = useState({
+ activeBoosts: 0,
+ expiringSoon: 0,
+ totalViews: 0
+ });
+ const [promotionHistory, setPromotionHistory] = useState<{
+ ad: string;
+ boostType: string;
+ duration: number;
+ views: number;
+ status: string;
+ }[]>([]);
+ const [promotions, setPromotions] = useState({ boosts: [], subscription: null });
+ const { session, setSession } = useSession();
+ const router = useRouter();
+ const [showPayment, setShowPayment] = useState(false);
+ const [paymentDetails, setPaymentDetails] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [activePromotions, setActivePromotions] = useState([]);
+ const [showAllPromotions, setShowAllPromotions] = useState(false);
+ const [alerts, setAlerts] = useState(false);
+ const [alertMessages, setAlertMessages] = useState();
+ const [alertTypes, setAlertTypes] = useState();
+ const [currentPage, setCurrentPage] = useState(1);
+ const itemsPerPage = 8;
+
+
+ // Fetch and transform promotions
+ const fetchPromotions = async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/promotions/active');
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch promotions');
+ }
+
+ const data = await response.json();
+ setPromotions(data);
+
+ // Transform promotions data into activePromotions format
+ const transformed: ActivePromotion[] = [
+ ...data.boosts.map((boost: Promotion) => ({
+ type: 'boost',
+ id: boost.id,
+ title: boost.title,
+ startDate: boost.startDate || new Date().toISOString(),
+ endDate: boost.endDate,
+ views: boost.metrics?.views || 0,
+ clicks: boost.metrics?.clicks || 0,
+ boostType: boost.boostType
+ })),
+ ...(data.subscription ? [{
+ type: 'subscription',
+ id: data.subscription.id,
+ title: data.subscription.title,
+ startDate: new Date().toISOString(),
+ endDate: data.subscription.endDate,
+ plan: data.subscription.title,
+ }] : [])
+ ];
+ setActivePromotions(transformed);
+ calculateTrackingStats(transformed);
+
+ } catch (error) {
+ console.error('Error fetching promotions:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+
+ useEffect(() => {
+ fetchPromotions();
+ }, []);
+
+ const calculateTrackingStats = (promotions: ActivePromotion[]) => {
+ const now = new Date();
+ const stats = {
+ activeBoosts: promotions.filter(p => p.type === 'boost').length,
+ expiringSoon: promotions.filter(p => {
+ const daysLeft = getRemainingDays(p.endDate);
+ return daysLeft <= 3 && daysLeft > 0;
+ }).length,
+ totalViews: promotions.reduce((total, p) => total + (p.views || 0), 0)
+ };
+ setTrackingStats(stats);
+
+ // Calculate promotion history
+ const history = promotions.map(p => ({
+ ad: p.title,
+ boostType: p.type === 'boost' ? boostOptions.find(b => b.id === p.boostType)?.name || 'Standard' : 'Subscription',
+ duration: getRemainingDays(p.endDate),
+ views: p.views || 0,
+ status: getRemainingDays(p.endDate) > 0 ? 'Active' : 'Expired'
+ }));
+ setPromotionHistory(history);
+ };
+
+ // Add subscription payment handler
+ const handleSubscribe = async (plan: typeof subscriptionPlans[0]) => {
+ if (!session?.email) {
+ router.push('/auth/signin?callbackUrl=/dashboard/promotions');
+ return;
+ }
+
+ const reference = `sub_${plan.id}_${Date.now()}`;
+ const details: PaymentDetails = {
+ email: session.email,
+ amount: plan.price,
+ reference,
+ plan: plan.name,
+ planId: plan.id,
+ type: 'subscription'
+ };
+
+ setPaymentDetails(details);
+ setShowPayment(true);
+ };
+
+ // Add payment success handler
+ const handlePaymentSuccess = async (reference: string) => {
+ try {
+ const response = await fetch('/api/subscriptions/create', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ reference,
+ planId: paymentDetails?.planId
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to create subscription');
+ }
+
+ // Refresh promotions data
+ fetchPromotions();
+ setShowPayment(false);
+ setAlerts(true)
+ setAlertTypes('success');
+ setAlertMessages('Subscription activated successfully!');
+ return;
+ } catch (error) {
+ setAlerts(true)
+ setAlertTypes('error');
+ setAlertMessages('Failed to activate subscription');
+ }
+ };
+
+
+ // Display only first 4 items in main view
+ const displayedPromotions = activePromotions.slice(0, 4);
+ const hasMorePromotions = activePromotions.length > 4;
+
+ // Calculate pagination
+ const totalPages = Math.ceil(activePromotions.length / itemsPerPage);
+ const paginatedPromotions = activePromotions.slice(
+ (currentPage - 1) * itemsPerPage,
+ currentPage * itemsPerPage
+ );
+
+ const PromotionCard = ({ promotion }: { promotion: ActivePromotion }) => (
+
+
+
+
+
+ {promotion.title}
+
+
+ {promotion.type === 'subscription' ? 'š Subscription' : 'š Boost'}
+
+
+
+ {getRemainingDays(promotion.endDate)}d left
+
+
+
+
+ {promotion.type === 'boost' ? (
+ <>
+
+ {boostOptions.find(opt => opt.id === promotion.boostType)?.name}
+
+
+ Views: {promotion.views || 0}
+ Clicks: {promotion.clicks || 0}
+
+ >
+ ) : (
+
+
Plan: {promotion.plan}
+
Unlimited Ads
+
+ )}
+
+
+
+ );
+
+ // Calculate remaining days
+ const getRemainingDays = (endDate: string) => {
+ const end = new Date(endDate);
+ const now = new Date();
+ const diff = end.getTime() - now.getTime();
+ return Math.ceil(diff / (1000 * 60 * 60 * 24));
+ };
+
+ return (
+
+ {alerts &&
}
+
š Boost & Promote Your Ads
+
Increase visibility and get more potential buyers.
+
+ {/* Active Promotions Section */}
+
+
+
+ šÆ Active Boosts & Subscriptions
+
+ {hasMorePromotions && (
+ setShowAllPromotions(true)}
+ className="text-green-600 hover:text-green-700 text-sm font-medium"
+ >
+ View All
+
+ )}
+
+
+ {isLoading ? (
+
+ {[1, 2, 3, 4].map(i => (
+
+
+
+ ))}
+
+ ) : activePromotions.length > 0 ? (
+
+ {displayedPromotions.map((promotion) => (
+
+ ))}
+
+ ) : (
+
+
+ No active promotions found.
+
+ Select a monthly subscription below or go to your{" "}
+
+ ads
+ {" "}
+ to promote single ads.
+
+
+
+ )}
+
+
+ {/* View All Promotions Modal */}
+
+
+
+ All Active Promotions
+
+
+ {paginatedPromotions.map((promotion) => (
+
+ ))}
+
+ {totalPages > 1 && (
+
+ {Array.from({ length: totalPages }).map((_, i) => (
+ setCurrentPage(i + 1)}
+ className={cn(
+ "px-3 py-1 rounded",
+ currentPage === i + 1
+ ? "bg-green-600 text-white"
+ : "bg-gray-100 hover:bg-gray-200"
+ )}
+ >
+ {i + 1}
+
+ ))}
+
+ )}
+
+
+
+ {/* Monthly Subscription Plans */}
+
+
š Monthly Subscription Plans
+
+ {subscriptionPlans.map((plan) => (
+
+
{plan.name}
+
{formatCurrency(Number(plan.price))}/mo
+
+ {plan.benefits.map((benefit, index) => (
+
+
+ {benefit}
+
+ ))}
+
+
handleSubscribe(plan)}
+ disabled={promotions.subscription?.id === plan.id}
+ className={cn(
+ "w-full mt-4 py-2 rounded-md transition",
+ promotions.subscription?.id === plan.id
+ ? "bg-gray-300 cursor-not-allowed text-gray-600"
+ : "bg-green-600 text-white hover:bg-green-700"
+ )}
+ >
+ {promotions.subscription?.id === plan.id ? 'Current Plan' : 'Subscribe'}
+
+
+ ))}
+
+ {/* Payment Modal */}
+ {showPayment && paymentDetails && (
+
setShowPayment(false)}
+ paymentDetails={paymentDetails}
+ onSuccess={handlePaymentSuccess}
+ />
+ )}
+
+{/* Promotion Tracking */}
+
+
š Promotion Tracking
+
+
+
+
+
+ Active Boosts: {trackingStats.activeBoosts}
+
+
+
+
+
+ Expiring Soon: {trackingStats.expiringSoon}
+
+
+
+
+
+ Total Views Gained: {trackingStats.totalViews.toLocaleString()}
+
+
+
+
+ {/* Promotion History Table */}
+
+
+
+
+ Ad
+ Boost Type
+ Days Left
+ Views Gained
+ Status
+
+
+
+ {promotionHistory.map((item, index) => (
+
+ {item.ad}
+ {item.boostType}
+ {item.duration} days
+ {item.views.toLocaleString()}
+
+ {item.status}
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/components/AgentAnalytics.tsx b/components/AgentAnalytics.tsx
new file mode 100644
index 0000000..d06e978
--- /dev/null
+++ b/components/AgentAnalytics.tsx
@@ -0,0 +1,325 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Bar, Line } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+} from "chart.js";
+import { Loader2, MessageSquare, Clock, CheckCircle } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import toast from "react-hot-toast";
+
+// Register ChartJS components
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ PointElement,
+ LineElement,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend
+);
+
+interface AgentAnalyticsData {
+ performanceMetrics: {
+ totalChats: number;
+ resolvedChats: number;
+ resolutionRate: number;
+ avgResponseTime: number;
+ };
+ chatVolumeData: {
+ labels: string[];
+ data: number[];
+ };
+ responseTimeData: {
+ labels: string[];
+ data: number[];
+ };
+}
+
+export default function AgentAnalytics() {
+ const [analyticsData, setAnalyticsData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [timeRange, setTimeRange] = useState("30days");
+ const [activeTab, setActiveTab] = useState("overview");
+
+ useEffect(() => {
+ fetchAnalyticsData();
+ }, [timeRange]);
+
+ const fetchAnalyticsData = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(`/api/agent/analytics?timeRange=${timeRange}`, {
+ credentials: "include"
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setAnalyticsData(data);
+ } else {
+ throw new Error("Failed to fetch analytics data");
+ }
+ } catch (error) {
+ console.error("Error fetching analytics:", error);
+ toast.error("Failed to load analytics data");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
Agent Analytics
+ setTimeRange(e.target.value)}
+ >
+ Last 7 Days
+ Last 30 Days
+ Last 90 Days
+
+
+
+ {loading ? (
+
+
+
+ ) : !analyticsData ? (
+
+
No analytics data available
+
+ ) : (
+ <>
+ {/* Performance Metrics Cards */}
+
+
+
+ Total Chats
+
+
+
+
+ {analyticsData.performanceMetrics.totalChats}
+
+
+
+
+
+
+ Resolved Chats
+
+
+
+
+ {analyticsData.performanceMetrics.resolvedChats}
+
+
+
+
+
+
+ Resolution Rate
+
+
+
+
+ %
+
+
{analyticsData.performanceMetrics.resolutionRate}%
+
+
+
+
+
+
+ Avg. Response Time
+
+
+
+
+ {analyticsData.performanceMetrics.avgResponseTime} min
+
+
+
+
+
+ {/* Charts */}
+
+
+ Overview
+ Chat Volume
+ Response Time
+
+
+
+
+ {/* Chat Volume Chart */}
+
+
+ Chat Volume
+
+
+
+
+
+
+
+
+ {/* Response Time Chart */}
+
+
+ Response Time
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chat Volume Trend
+
+
+
+
+
+
+
+
+
+
+
+
+ Response Time Analysis
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/components/AgentDashboard.tsx b/components/AgentDashboard.tsx
new file mode 100644
index 0000000..a3fbabf
--- /dev/null
+++ b/components/AgentDashboard.tsx
@@ -0,0 +1,341 @@
+"use client";
+
+import { useState, useEffect, useRef } from "react";
+import { useSession } from "@/components/SessionWrapper";
+import { Badge } from "@/components/ui/badge";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { MessageSquare, Clock, Loader2, Send } from "lucide-react";
+import toast from "react-hot-toast";
+
+// Define types for the new messaging system (simplified for agent view)
+interface AgentMessage {
+ id: string;
+ content: string;
+ senderId: string; // Use senderId to match the new schema
+ createdAt: string;
+}
+
+import { AgentConversation } from "@/types/agent"; // Import the interface
+
+interface AgentMessage {
+ id: string;
+ content: string;
+ senderId: string; // Use senderId to match the new schema
+ createdAt: string;
+}
+
+
+export default function AgentDashboard() {
+ const { session } = useSession();
+ const [isLoading, setIsLoading] = useState(true);
+ // Agent dashboard doesn't need online status for non-websocket chat
+ // const [isOnline, setIsOnline] = useState(false);
+ const [activeConversations, setActiveConversations] = useState([]);
+ // Agent dashboard doesn't have pending chats in the new system, they are all conversations
+ // const [pendingChats, setPendingChats] = useState([]);
+ const [selectedConversation, setSelectedConversation] = useState(null);
+ const [message, setMessage] = useState("");
+ const messagesEndRef = useRef(null);
+
+ // Fetch conversations on component mount
+ useEffect(() => {
+ if (session) {
+ fetchConversations();
+ }
+ }, [session]);
+
+ // Scroll to bottom of messages
+ useEffect(() => {
+ if (messagesEndRef.current) {
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [selectedConversation?.messages]);
+
+ // Agent dashboard doesn't need socket connection logic
+ // useEffect(() => { ... }, [session]);
+
+ // Agent dashboard doesn't need to update agent status based on socket connection
+ // const updateAgentStatus = async (isAvailable: boolean) => { ... };
+
+ // Function to fetch conversations (agents can see all conversations)
+ const fetchConversations = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch('/api/conversations', { // Agents can fetch all conversations
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to fetch conversations');
+ }
+
+ const data = await response.json();
+ // Agent dashboard will display all conversations, no distinction between active/pending here
+ setActiveConversations(data.conversations || []);
+ // setPendingChats([]); // No pending chats in this model
+ } catch (error) {
+ console.error('Error fetching conversations:', error);
+ toast.error('Failed to load conversations');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Function to fetch messages for a conversation
+ const fetchMessages = async (conversationId: string) => {
+ setIsLoading(true); // Use general loading for messages too
+ try {
+ const response = await fetch(`/api/conversations/${conversationId}/messages`, {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to fetch messages');
+ }
+
+ const data = await response.json();
+ // Update the selected conversation with fetched messages
+ setSelectedConversation(prev => {
+ if (!prev || prev.id !== conversationId) return prev;
+ return {
+ ...prev,
+ messages: data.messages || []
+ };
+ });
+
+ } catch (error) {
+ console.error('Error fetching messages:', error);
+ toast.error('Failed to load messages');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+
+ // Function to send a message
+ const sendMessage = async () => {
+ if (!selectedConversation || !message.trim()) return;
+
+ // Agent sends message to the conversation
+ try {
+ const response = await fetch(`/api/conversations/${selectedConversation.id}/messages`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ content: message })
+ });
+
+ if (response.ok) {
+ const newMessage = await response.json();
+
+ // Update selected conversation with the new message
+ setSelectedConversation(prev => {
+ if (!prev) return null;
+ return {
+ ...prev,
+ messages: [...prev.messages, newMessage.message] // API returns { message: newMessage }
+ };
+ });
+
+ // Update the conversation in the active list
+ setActiveConversations(prev =>
+ prev.map(conv =>
+ conv.id === selectedConversation.id
+ ? {
+ ...conv,
+ lastMessage: newMessage.message, // Update last message
+ updatedAt: newMessage.message.createdAt // Update updatedAt
+ }
+ : conv
+ ).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) // Re-sort
+ );
+
+
+ // Clear message input
+ setMessage('');
+ } else {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to send message');
+ }
+ } catch (error) {
+ console.error('Error sending message:', error);
+ toast.error('Failed to send message');
+ }
+ };
+
+ // Handle Enter key press in message input
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+ };
+
+ // Show loading state
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ // Show error if no session
+ if (!session) {
+ return (
+
+ );
+ }
+
+
+
+ return (
+
+
+
+
Agent Dashboard
+ {/* Agent online status is not needed for non-websocket chat */}
+ {/*
+ {isOnline ? "Online" : "Offline"}
+ */}
+
+
+
{/* Keep tabs for now, but only one will be used */}
+
+
+
+ Conversations ({activeConversations.length}) {/* Display all conversations here */}
+
+ {/* Remove pending chats tab */}
+ {/*
+
+ Pending ({pendingChats.length})
+ */}
+
+
+
+
+ {activeConversations.map(conversation => ( // Map over all conversations
+
setSelectedConversation(conversation)} // Select the conversation
+ >
+
+ {/* Display buyer and seller names */}
+ {conversation.buyer.name} - {conversation.seller.name}
+ {/* Category and priority might not be directly on conversation in new schema, remove for now */}
+ {/* {conversation.category} */}
+ {/*
+ Priority: {conversation.priority}
+ */}
+
+ {/* Display last message content if available */}
+ {conversation.messages.length > 0 && (
+
+ {conversation.messages[conversation.messages.length - 1]?.content}
+
+ )}
+
+ ))}
+
+
+
+ {/* Remove pending chats tab content */}
+ {/*
+
+ {pendingChats.map(chat => (
+
+
+ {chat.user.name}
+ {chat.category}
+
+
+ {chat.messages[0]?.content}
+
+
acceptChat(chat.id)}
+ className="w-full"
+ >
+ Accept Chat
+
+
+ ))}
+
+ */}
+
+
+
+
+ {selectedConversation ? ( // Use selectedConversation
+ <>
+
+
+
+ {/* Display buyer and seller info */}
+
Conversation between {selectedConversation.buyer.name} and {selectedConversation.seller.name}
+ {/* Display ad title */}
+
Regarding Ad: {selectedConversation.ad.title}
+
+ {/* Remove category and priority badges */}
+ {/*
+ {selectedChat.category}
+
+ Priority: {selectedChat.priority}
+
+
*/}
+
+
+
+
+ {selectedConversation.messages.map((msg, idx) => ( // Map over selectedConversation messages
+
+
+
{msg.content}
+
+ {new Date(msg.createdAt).toLocaleTimeString()}
+
+
+
+ ))}
+
+
+
+
+
+ setMessage(e.target.value)}
+ onKeyPress={handleKeyPress}
+ className="flex-1 p-2 border rounded-md"
+ placeholder="Type your response..."
+ />
+ Send
+
+
+ >
+ ) : (
+
+
Select a conversation to start responding
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AgentDashboard/AgentChatManagement.tsx b/components/AgentDashboard/AgentChatManagement.tsx
new file mode 100644
index 0000000..8c74c77
--- /dev/null
+++ b/components/AgentDashboard/AgentChatManagement.tsx
@@ -0,0 +1,656 @@
+"use client";
+
+import { useState, useEffect, useRef } from "react";
+import { useSession } from "@/components/SessionWrapper";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Loader2,
+ MessageSquare,
+ Clock,
+ CheckCircle,
+ Send,
+ User,
+ Search,
+ RefreshCw,
+ AlertCircle,
+ Phone,
+ Mail,
+ Calendar,
+ X,
+ Check,
+} from "lucide-react";
+import toast from "react-hot-toast";
+
+interface ChatMessage {
+ id: string;
+ content: string;
+ senderId: string;
+ sender: {
+ id: string;
+ name: string;
+ email: string;
+ };
+ isAgentReply: boolean;
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface SupportChat {
+ id: string;
+ subject: string;
+ category: string;
+ priority: number;
+ status: string;
+ attachments: string[];
+ userId: string;
+ agentId: string | null;
+ createdAt: string;
+ updatedAt: string;
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ };
+ agent: {
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ };
+ } | null;
+ messages: ChatMessage[];
+}
+
+interface ChatDetail {
+ chat: SupportChat;
+ messages: ChatMessage[];
+}
+
+export default function AgentChatManagement() {
+ const { session } = useSession();
+ const [isLoading, setIsLoading] = useState(true);
+ const [pendingChats, setPendingChats] = useState([]);
+ const [activeChats, setActiveChats] = useState([]);
+ const [closedChats, setClosedChats] = useState([]);
+ const [selectedChat, setSelectedChat] = useState(null);
+ const [chatMessages, setChatMessages] = useState([]);
+ const [newMessage, setNewMessage] = useState("");
+ const [isLoadingMessages, setIsLoadingMessages] = useState(false);
+ const [isSendingMessage, setIsSendingMessage] = useState(false);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [activeTab, setActiveTab] = useState("pending");
+ const [autoRefresh, setAutoRefresh] = useState(true);
+ const [showChatDetail, setShowChatDetail] = useState(false);
+ const messagesEndRef = useRef(null);
+ const refreshIntervalRef = useRef();
+
+ // Auto-scroll to bottom of messages
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [chatMessages]);
+
+ // Fetch chats
+ useEffect(() => {
+ fetchChats();
+
+ // Set up auto-refresh if enabled
+ if (autoRefresh) {
+ refreshIntervalRef.current = setInterval(fetchChats, 30000); // Refresh every 30 seconds
+ }
+
+ return () => {
+ if (refreshIntervalRef.current) {
+ clearInterval(refreshIntervalRef.current);
+ }
+ };
+ }, [autoRefresh]);
+
+ const fetchChats = async () => {
+ try {
+ setIsLoading(true);
+
+ const [pendingRes, activeRes, closedRes] = await Promise.all([
+ fetch('/api/agent/chats?status=pending', { credentials: 'include' }),
+ fetch('/api/agent/chats?status=active', { credentials: 'include' }),
+ fetch('/api/agent/chats?status=closed', { credentials: 'include' })
+ ]);
+
+ if (pendingRes.ok && activeRes.ok && closedRes.ok) {
+ const [pending, active, closed] = await Promise.all([
+ pendingRes.json(),
+ activeRes.json(),
+ closedRes.json()
+ ]);
+
+ setPendingChats(pending);
+ setActiveChats(active);
+ setClosedChats(closed);
+ } else {
+ throw new Error('Failed to fetch chats');
+ }
+ } catch (error) {
+ console.error('Error fetching chats:', error);
+ toast.error('Failed to load chats');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const fetchChatMessages = async (chatId: string) => {
+ try {
+ setIsLoadingMessages(true);
+ const response = await fetch(`/api/agent/chats/${chatId}/messages`, {
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const data: ChatDetail = await response.json();
+ setChatMessages(data.messages);
+ setSelectedChat(data.chat);
+ } else {
+ throw new Error('Failed to fetch messages');
+ }
+ } catch (error) {
+ console.error('Error fetching messages:', error);
+ toast.error('Failed to load chat messages');
+ } finally {
+ setIsLoadingMessages(false);
+ }
+ };
+
+ const acceptChat = async (chatId: string) => {
+ try {
+ const response = await fetch('/api/agent/chats', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ chatId, action: 'accept' }),
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const acceptedChat: SupportChat = await response.json();
+
+ // Remove from pending and add to active
+ setPendingChats(prev => prev.filter(chat => chat.id !== chatId));
+ setActiveChats(prev => [acceptedChat, ...prev]);
+
+ toast.success('Chat accepted successfully');
+
+ // Auto-open the accepted chat
+ setSelectedChat(acceptedChat);
+ setActiveTab('active');
+ fetchChatMessages(chatId);
+ setShowChatDetail(true);
+ } else {
+ throw new Error('Failed to accept chat');
+ }
+ } catch (error) {
+ console.error('Error accepting chat:', error);
+ toast.error('Failed to accept chat');
+ }
+ };
+
+ const closeChat = async (chatId: string) => {
+ try {
+ const response = await fetch('/api/agent/chats', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ chatId, action: 'close' }),
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const closedChat: SupportChat = await response.json();
+
+ // Remove from active and add to closed
+ setActiveChats(prev => prev.filter(chat => chat.id !== chatId));
+ setClosedChats(prev => [closedChat, ...prev]);
+
+ toast.success('Chat closed successfully');
+
+ // Close chat detail if this was the selected chat
+ if (selectedChat?.id === chatId) {
+ setShowChatDetail(false);
+ setSelectedChat(null);
+ setChatMessages([]);
+ }
+
+ setActiveTab('closed');
+ } else {
+ throw new Error('Failed to close chat');
+ }
+ } catch (error) {
+ console.error('Error closing chat:', error);
+ toast.error('Failed to close chat');
+ }
+ };
+
+ const sendMessage = async () => {
+ if (!selectedChat || !newMessage.trim()) return;
+
+ try {
+ setIsSendingMessage(true);
+ const response = await fetch(`/api/agent/chats/${selectedChat.id}/messages`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ content: newMessage.trim() }),
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const message: ChatMessage = await response.json();
+ setChatMessages(prev => [...prev, message]);
+ setNewMessage('');
+
+ // Update the chat's updated timestamp in local state
+ const updatedChat = { ...selectedChat, updatedAt: new Date().toISOString() };
+ setSelectedChat(updatedChat);
+ setActiveChats(prev => prev.map(chat =>
+ chat.id === selectedChat.id ? updatedChat : chat
+ ));
+
+ toast.success('Message sent successfully');
+ } else {
+ throw new Error('Failed to send message');
+ }
+ } catch (error) {
+ console.error('Error sending message:', error);
+ toast.error('Failed to send message');
+ } finally {
+ setIsSendingMessage(false);
+ }
+ };
+
+ const openChatDetail = (chat: SupportChat) => {
+ setSelectedChat(chat);
+ fetchChatMessages(chat.id);
+ setShowChatDetail(true);
+ };
+
+ const closeChatDetail = () => {
+ setShowChatDetail(false);
+ setSelectedChat(null);
+ setChatMessages([]);
+ };
+
+ // Filter chats based on search term
+ const filterChats = (chats: SupportChat[]) => {
+ if (!searchTerm) return chats;
+
+ return chats.filter(chat =>
+ chat.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ chat.user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ chat.user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ chat.category.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+ };
+
+ const getPriorityBadge = (priority: number) => {
+ switch (priority) {
+ case 3:
+ return High ;
+ case 2:
+ return Medium ;
+ default:
+ return Low ;
+ }
+ };
+
+ const getStatusBadge = (status: string) => {
+ switch (status) {
+ case "pending":
+ return Pending ;
+ case "active":
+ return Active ;
+ case "closed":
+ return Closed ;
+ default:
+ return {status} ;
+ }
+ };
+
+ const formatTime = (dateString: string) => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
+
+ if (diffInHours < 1) {
+ const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
+ return `${diffInMinutes}m ago`;
+ } else if (diffInHours < 24) {
+ return `${diffInHours}h ago`;
+ } else {
+ return date.toLocaleDateString();
+ }
+ };
+
+ const ChatList = ({ chats, showActions = false }: { chats: SupportChat[], showActions?: boolean }) => (
+
+ {filterChats(chats).map(chat => (
+
openChatDetail(chat)}
+ >
+
+
+
+
+
{chat.subject}
+ #{chat.id.slice(-6)}
+
+
+
+
+ {chat.user.name}
+
+ {chat.user.email}
+
+
+
+ {getStatusBadge(chat.status)}
+ {getPriorityBadge(chat.priority)}
+ {chat.category}
+
+
+ {chat.messages && chat.messages.length > 0 && (
+
+
+ {chat.messages[0].isAgentReply ? 'You: ' : `${chat.user.name}: `}
+
+ {chat.messages[0].content}
+
+ )}
+
+
+
+
+ {formatTime(chat.updatedAt)}
+
+
+ {showActions && (
+
+ {chat.status === 'pending' && (
+ {
+ e.stopPropagation();
+ acceptChat(chat.id);
+ }}
+ className="h-7 px-2 text-xs"
+ >
+
+ Accept
+
+ )}
+ {chat.status === 'active' && (
+ {
+ e.stopPropagation();
+ closeChat(chat.id);
+ }}
+ className="h-7 px-2 text-xs"
+ >
+
+ Close
+
+ )}
+
+ )}
+
+
+
+
+ ))}
+
+ {filterChats(chats).length === 0 && (
+
+ )}
+
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Chat Management
+
Manage customer support conversations
+
+
+
+
+
+ Refresh
+
+
+
+
+ {/* Search */}
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
{pendingChats.length}
+
Pending Chats
+
+
+
+
+
+
+
+
+
{activeChats.length}
+
Active Chats
+
+
+
+
+
+
+
+
+
{closedChats.length}
+
Closed Today
+
+
+
+
+
+ {/* Chats Tabs */}
+
+
+
+
+ Pending ({pendingChats.length})
+
+
+
+ Active ({activeChats.length})
+
+
+
+ Closed ({closedChats.length})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Chat Detail Modal */}
+
+
+ {selectedChat && (
+ <>
+
+
+ {selectedChat.subject}
+
+ {getStatusBadge(selectedChat.status)}
+ {getPriorityBadge(selectedChat.priority)}
+
+
+
+
+ Customer: {selectedChat.user.name}
+ Email: {selectedChat.user.email}
+ Category: {selectedChat.category}
+
+
+
+
+
+ {/* Messages */}
+
+ {isLoadingMessages ? (
+
+
+
+ ) : (
+
+ {chatMessages.map((message, index) => (
+
+
+
+
+ {message.isAgentReply ? 'You' : message.sender.name}
+
+
+ {new Date(message.createdAt).toLocaleTimeString()}
+
+
+
{message.content}
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Message Input */}
+ {selectedChat.status === 'active' && (
+
+ )}
+
+
+ {selectedChat.status === 'active' && (
+
+ closeChat(selectedChat.id)}
+ >
+ Close Chat
+
+
+ )}
+ >
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/AgentDashboard/Dashboard.tsx b/components/AgentDashboard/Dashboard.tsx
new file mode 100644
index 0000000..f34c75a
--- /dev/null
+++ b/components/AgentDashboard/Dashboard.tsx
@@ -0,0 +1,248 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Loader2, MessageSquare, TicketCheck, Clock, CheckCircle } from "lucide-react";
+import toast from "react-hot-toast";
+import AgentChatManagement from "./AgentChatManagement";
+import TicketManagement from "./TicketManagement";
+import KnowledgeBase from "./KnowledgeBase";
+import AgentAnalytics from "../AgentAnalytics";
+import Settings from "./Settings";
+
+interface AgentStats {
+ activeChats: number;
+ pendingChats: number;
+ resolvedChats: number;
+ activeTickets: number;
+ pendingTickets: number;
+ resolvedTickets: number;
+ avgResponseTime: number;
+ resolutionRate: number;
+}
+
+interface AgentDashboardProps {
+ defaultTab?: string;
+}
+
+export default function AgentDashboard({ defaultTab = "overview" }: AgentDashboardProps) {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState(defaultTab);
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const tab = searchParams.get("tab") || defaultTab;
+
+ // Update active tab when URL changes
+ useEffect(() => {
+ setActiveTab(tab);
+ }, [tab]);
+
+ useEffect(() => {
+ fetchAgentStats();
+ }, []);
+
+ const fetchAgentStats = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch("/api/agent/stats", {
+ credentials: "include",
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch agent stats");
+ }
+
+ const data = await response.json();
+ setStats(data);
+ } catch (error) {
+ console.error("Error fetching agent stats:", error);
+ toast.error("Failed to load agent statistics");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
Agent Dashboard
+
Manage your support activities
+
+
+
{
+ setActiveTab(value);
+ router.push(`?tab=${value}`);
+ }} className="w-full">
+
+ Overview
+ Chats
+ Tickets
+ Knowledge Base
+ Analytics
+ Settings
+
+
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {/* Stats Cards */}
+
+
+
+ Active Chats
+
+
+
+
+ {stats?.activeChats || 0}
+
+
+
+
+
+
+ Pending Chats
+
+
+
+
+ {stats?.pendingChats || 0}
+
+
+
+
+
+
+ Active Tickets
+
+
+
+
+ {stats?.activeTickets || 0}
+
+
+
+
+
+
+ Resolution Rate
+
+
+
+
+ {stats?.resolutionRate || 0}%
+
+
+
+
+
+ {/* Recent Activity */}
+
+
+ Recent Activity
+
+
+
+
+
+
+
+
+
New chat assigned
+
A new support chat has been assigned to you.
+
10 minutes ago
+
+
+
+
+
+
+
+
+
Chat resolved
+
You successfully resolved a customer support chat.
+
1 hour ago
+
+
+
+
+
+
+
+
+
Ticket updated
+
You updated the status of a support ticket.
+
3 hours ago
+
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+
document.querySelector('[data-value="chats"]')?.dispatchEvent(new MouseEvent('click'))}
+ >
+
+ Manage Chats
+
+
+
document.querySelector('[data-value="tickets"]')?.dispatchEvent(new MouseEvent('click'))}
+ >
+
+ Manage Tickets
+
+
+
document.querySelector('[data-value="knowledge"]')?.dispatchEvent(new MouseEvent('click'))}
+ >
+
+ Knowledge Base
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/AgentDashboard/KnowledgeBase.tsx b/components/AgentDashboard/KnowledgeBase.tsx
new file mode 100644
index 0000000..515a94d
--- /dev/null
+++ b/components/AgentDashboard/KnowledgeBase.tsx
@@ -0,0 +1,519 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Badge } from "@/components/ui/badge";
+import {
+ BookOpen,
+ Search,
+ FileText,
+ Bookmark,
+ Star,
+ PlusCircle,
+ Loader2,
+ ChevronRight,
+ Copy,
+ ThumbsUp,
+ ThumbsDown,
+ Edit,
+} from "lucide-react";
+import toast from "react-hot-toast";
+
+interface KnowledgeArticle {
+ id: string;
+ title: string;
+ content: string;
+ category: string;
+ tags: string[];
+ createdAt: string;
+ updatedAt: string;
+ views: number;
+ helpful: number;
+ notHelpful: number;
+}
+
+export default function KnowledgeBase() {
+ const [isLoading, setIsLoading] = useState(true);
+ const [articles, setArticles] = useState([]);
+ const [filteredArticles, setFilteredArticles] = useState([]);
+ const [selectedArticle, setSelectedArticle] = useState(null);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [categoryFilter, setCategoryFilter] = useState("all");
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [newArticle, setNewArticle] = useState({
+ title: "",
+ content: "",
+ category: "general",
+ tags: "",
+ });
+ const [savedArticles, setSavedArticles] = useState([]);
+ const [activeTab, setActiveTab] = useState("all");
+
+ // Fetch knowledge base articles on component mount
+ useEffect(() => {
+ fetchArticles();
+ // Load saved articles from localStorage
+ const saved = localStorage.getItem("savedArticles");
+ if (saved) {
+ setSavedArticles(JSON.parse(saved));
+ }
+ }, []);
+
+ // Filter articles when search term or category changes
+ useEffect(() => {
+ filterArticles();
+ }, [searchTerm, categoryFilter, articles, activeTab, savedArticles]);
+
+ const fetchArticles = async () => {
+ try {
+ setIsLoading(true);
+
+ // Build query parameters
+ const params = new URLSearchParams();
+ if (categoryFilter && categoryFilter !== 'all') {
+ params.append('category', categoryFilter);
+ }
+ if (searchTerm) {
+ params.append('search', searchTerm);
+ }
+
+ const response = await fetch(`/api/knowledge/articles?${params.toString()}`, {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch articles');
+ }
+
+ const articlesData = await response.json();
+ setArticles(articlesData);
+ } catch (error) {
+ console.error('Error fetching knowledge base articles:', error);
+ toast.error('Failed to load knowledge base');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const filterArticles = () => {
+ let filtered = [...articles];
+
+ // Filter by search term
+ if (searchTerm) {
+ filtered = filtered.filter(article =>
+ article.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ article.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ article.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
+ );
+ }
+
+ // Filter by category
+ if (categoryFilter !== 'all') {
+ filtered = filtered.filter(article => article.category === categoryFilter);
+ }
+
+ // Filter by tab
+ if (activeTab === 'saved') {
+ filtered = filtered.filter(article => savedArticles.includes(article.id));
+ }
+
+ setFilteredArticles(filtered);
+ };
+
+ const handleCreateArticle = async () => {
+ if (!newArticle.title || !newArticle.content) {
+ toast.error('Please fill in all required fields');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/knowledge/articles', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ title: newArticle.title,
+ content: newArticle.content,
+ category: newArticle.category,
+ tags: newArticle.tags.split(',').map(tag => tag.trim()).filter(tag => tag)
+ }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to create article');
+ }
+
+ const createdArticle = await response.json();
+ setArticles([createdArticle, ...articles]);
+ setIsDialogOpen(false);
+ toast.success('Article created successfully');
+
+ // Reset form
+ setNewArticle({
+ title: "",
+ content: "",
+ category: "general",
+ tags: "",
+ });
+ } catch (error) {
+ console.error('Error creating article:', error);
+ toast.error('Failed to create article');
+ }
+ };
+
+ const toggleSaveArticle = (articleId: string) => {
+ let updated;
+ if (savedArticles.includes(articleId)) {
+ updated = savedArticles.filter(id => id !== articleId);
+ toast.success('Article removed from saved');
+ } else {
+ updated = [...savedArticles, articleId];
+ toast.success('Article saved for quick access');
+ }
+
+ setSavedArticles(updated);
+ localStorage.setItem("savedArticles", JSON.stringify(updated));
+ };
+
+ const copyArticleContent = (content: string) => {
+ navigator.clipboard.writeText(content);
+ toast.success('Content copied to clipboard');
+ };
+
+ const rateArticle = async (articleId: string, isHelpful: boolean) => {
+ try {
+ const response = await fetch(`/api/knowledge/articles/${articleId}/rate`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ isHelpful }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to rate article');
+ }
+
+ const ratingData = await response.json();
+
+ // Update the local state with new rating counts
+ setArticles(articles.map(article => {
+ if (article.id === articleId) {
+ return {
+ ...article,
+ helpful: ratingData.helpful,
+ notHelpful: ratingData.notHelpful,
+ };
+ }
+ return article;
+ }));
+
+ toast.success('Thank you for your feedback!');
+ } catch (error) {
+ console.error('Error rating article:', error);
+ toast.error('Failed to submit rating');
+ }
+ };
+
+ // Format date for display
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString();
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
Knowledge Base
+
+
+
+
+
+ Create Article
+
+
+
+
+ Create Knowledge Base Article
+
+ Add a new article to the knowledge base to help agents assist customers.
+
+
+
+
+
+ Title
+ setNewArticle({ ...newArticle, title: e.target.value })}
+ />
+
+
+
+ Category
+ setNewArticle({ ...newArticle, category: value })}
+ >
+
+
+
+
+ General
+ Customer Service
+ Technical
+ Billing
+ Product
+ General
+
+
+
+
+
+ Tags (comma-separated)
+ setNewArticle({ ...newArticle, tags: e.target.value })}
+ />
+
+
+
+ Content (Markdown supported)
+
+
+
+
+ setIsDialogOpen(false)}>Cancel
+ Create Article
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+ All Categories
+ Customer Service
+ Technical
+ Billing
+ Product
+ Shipping
+
+
+
+
+
+
+ {/* Knowledge Base Content */}
+
+ {/* Articles List */}
+
+
+
+
+
+
+
+ All
+
+
+
+ Saved
+
+
+
+
+
+ {filteredArticles.length === 0 ? (
+
+ ) : (
+
+ {filteredArticles.map(article => (
+
{
+ setSelectedArticle(article);
+ // Track view
+ fetch(`/api/knowledge/articles/${article.id}`, {
+ credentials: 'include'
+ }).catch(console.error);
+ }}
+ >
+
+
{article.title}
+ {
+ e.stopPropagation();
+ toggleSaveArticle(article.id);
+ }}
+ >
+
+
+
+
+
+ {article.category}
+
+ Updated: {formatDate(article.updatedAt)}
+
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Article Content */}
+
+
+ {selectedArticle ? (
+ <>
+
+
+
+
{selectedArticle.title}
+
+ {selectedArticle.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+
+ copyArticleContent(selectedArticle.content)}
+ >
+
+ Copy
+
+ toggleSaveArticle(selectedArticle.id)}
+ >
+
+ {savedArticles.includes(selectedArticle.id) ? 'Saved' : 'Save'}
+
+
+
+
+
+
+ {selectedArticle.content.split('\n\n').map((paragraph, index) => (
+
{paragraph}
+ ))}
+
+
+
+
+
+
Last updated: {formatDate(selectedArticle.updatedAt)}
+
Views: {selectedArticle.views}
+
+
+ Was this helpful?
+ rateArticle(selectedArticle.id, true)}
+ >
+
+ Yes ({selectedArticle.helpful})
+
+ rateArticle(selectedArticle.id, false)}
+ >
+
+ No ({selectedArticle.notHelpful})
+
+
+
+
+
+ >
+ ) : (
+
+
+
No article selected
+
Select an article from the list to view its content
+
+ )}
+
+
+
+
+ );
+}
diff --git a/components/AgentDashboard/Layout.tsx b/components/AgentDashboard/Layout.tsx
new file mode 100644
index 0000000..e95938b
--- /dev/null
+++ b/components/AgentDashboard/Layout.tsx
@@ -0,0 +1,294 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { usePathname, useRouter } from "next/navigation";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import { useSession } from "@/components/SessionWrapper";
+import {
+ LayoutDashboard,
+ MessageSquare,
+ TicketCheck,
+ BookOpen,
+ BarChart2,
+ Settings,
+ LogOut,
+ Users,
+ Menu,
+ X,
+ Bell,
+ CheckCircle,
+ Clock,
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Switch } from "@/components/ui/switch";
+import { Label } from "@/components/ui/label";
+import toast from "react-hot-toast";
+
+const AGENT_NAV_ITEMS = [
+ {
+ title: "Dashboard",
+ href: "/agent/dashboard?tab=overview",
+ icon: LayoutDashboard,
+ },
+ {
+ title: "Chat Management",
+ href: "/agent/dashboard?tab=chats",
+ icon: MessageSquare,
+ },
+ {
+ title: "Ticket Management",
+ href: "/agent/dashboard?tab=tickets",
+ icon: TicketCheck,
+ },
+ {
+ title: "Knowledge Base",
+ href: "/agent/dashboard?tab=knowledge",
+ icon: BookOpen,
+ },
+ {
+ title: "Analytics",
+ href: "/agent/dashboard?tab=analytics",
+ icon: BarChart2,
+ },
+ {
+ title: "Settings",
+ href: "/agent/dashboard?tab=settings",
+ icon: Settings,
+ },
+];
+
+export default function AgentDashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isOnline, setIsOnline] = useState(true);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const [pendingChats, setPendingChats] = useState(0);
+ const [pendingTickets, setPendingTickets] = useState(0);
+ const pathname = usePathname();
+ const router = useRouter();
+ const { session, setSession } = useSession();
+
+ // Fetch pending chats and tickets count
+ useEffect(() => {
+ const fetchPendingItems = async () => {
+ try {
+ // Fetch pending tickets (chats are now handled differently)
+ const ticketsResponse = await fetch("/api/agent/tickets?status=pending", {
+ credentials: "include",
+ });
+ if (ticketsResponse.ok) {
+ const ticketsData = await ticketsResponse.json();
+ setPendingTickets(ticketsData.length);
+ }
+ } catch (error) {
+ console.error("Error fetching pending items:", error);
+ }
+ };
+
+ if (session) {
+ fetchPendingItems();
+ // Set up interval to refresh data
+ const interval = setInterval(fetchPendingItems, 30000); // every 30 seconds
+ return () => clearInterval(interval);
+ }
+ }, [session]);
+
+ // Handle agent status change
+ const handleStatusChange = async (status: boolean) => {
+ try {
+ setIsOnline(status);
+ const response = await fetch("/api/agent/status", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ isOnline: status }),
+ credentials: "include",
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to update status");
+ }
+
+ toast.success(`You are now ${status ? "online" : "offline"}`);
+ } catch (error) {
+ console.error("Error updating status:", error);
+ toast.error("Failed to update status");
+ setIsOnline(!status); // Revert on failure
+ }
+ };
+
+ // Handle logout
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ // Clear session from context
+ setSession(null);
+
+ // Clear all auth-related cookies
+ const cookies = document.cookie.split(";");
+
+ for (let cookie of cookies) {
+ const cookieName = cookie.split("=")[0].trim();
+ if (cookieName.includes("next-auth")) {
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=${window.location.hostname}`;
+ // Also try without domain for local development
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ }
+ }
+
+ // Call the API to clear server-side session
+ await fetch('/api/logout', {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ router.push('/signin');
+ } catch (error) {
+ console.error('Logout error:', error);
+ } finally {
+ setIsLoggingOut(false);
+ }
+ };
+
+ return (
+
+ {/* Mobile menu button */}
+
setIsOpen(!isOpen)}
+ >
+ {isOpen ? : }
+
+
+ {/* Sidebar */}
+
+
+ {/* Logo and title */}
+
+
+
Agent Portal
+
+
+ {isOnline ? "Online" : "Offline"}
+
+
+
+
+
+
+ {/* Navigation */}
+
+ {AGENT_NAV_ITEMS.map((item) => {
+ // Add badge for pending tickets
+ let badge = null;
+ if (item.title === "Ticket Management" && pendingTickets > 0) {
+ badge = (
+
+ {pendingTickets}
+
+ );
+ }
+
+ return (
+
+
+ {item.title}
+ {badge}
+
+ );
+ })}
+
+
+ {/* User profile and logout */}
+
+
+
+
+
+
+
+
{session?.name || "Agent"}
+
{session?.email || ""}
+
+
+
+
+
+
+
+
+ Notifications
+
+ {pendingTickets > 0 ? (
+
+
+ {pendingTickets} pending tickets
+
+ ) : (
+
+
+ No pending tickets
+
+ )}
+
+
+
+
+
+
+ {isLoggingOut ? "Logging out..." : "Logout"}
+
+
+
+
+
+
+ {/* Main content */}
+
{children}
+
+ );
+}
diff --git a/components/AgentDashboard/Settings.tsx b/components/AgentDashboard/Settings.tsx
new file mode 100644
index 0000000..5df30f0
--- /dev/null
+++ b/components/AgentDashboard/Settings.tsx
@@ -0,0 +1,711 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useSession } from "@/components/SessionWrapper";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Switch } from "@/components/ui/switch";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Settings as SettingsIcon,
+ User,
+ Bell,
+ MessageSquare,
+ Clock,
+ Shield,
+ Loader2,
+ Save,
+ CheckCircle,
+ X,
+} from "lucide-react";
+import toast from "react-hot-toast";
+
+interface AgentProfile {
+ id: string;
+ userId: string;
+ name: string;
+ email: string;
+ isOnline: boolean;
+ isAvailable: boolean;
+ specialties: string[];
+ bio: string;
+ profileImage: string;
+ activeChats: number;
+ lastActive: string;
+}
+
+interface NotificationSettings {
+ newChatAssigned: boolean;
+ newTicketAssigned: boolean;
+ chatUpdates: boolean;
+ ticketUpdates: boolean;
+ systemAnnouncements: boolean;
+ emailNotifications: boolean;
+ desktopNotifications: boolean;
+ soundAlerts: boolean;
+}
+
+interface AppearanceSettings {
+ theme: string;
+ fontSize: string;
+ chatLayout: string;
+ showTimestamps: boolean;
+ compactView: boolean;
+}
+
+export default function Settings() {
+ const { session } = useSession();
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [profile, setProfile] = useState(null);
+ const [notificationSettings, setNotificationSettings] = useState({
+ newChatAssigned: true,
+ newTicketAssigned: true,
+ chatUpdates: true,
+ ticketUpdates: true,
+ systemAnnouncements: true,
+ emailNotifications: true,
+ desktopNotifications: true,
+ soundAlerts: true,
+ });
+ const [appearanceSettings, setAppearanceSettings] = useState({
+ theme: "light",
+ fontSize: "medium",
+ chatLayout: "default",
+ showTimestamps: true,
+ compactView: false,
+ });
+ const [activeTab, setActiveTab] = useState("profile");
+ const [specialties, setSpecialties] = useState([]);
+ const [newSpecialty, setNewSpecialty] = useState("");
+
+ // Fetch agent profile and settings on component mount
+ useEffect(() => {
+ if (session) {
+ fetchAgentProfile();
+ fetchAgentSettings();
+ }
+ }, [session]);
+
+ const fetchAgentProfile = async () => {
+ try {
+ setIsLoading(true);
+
+ // In a real app, this would be an API call
+ // For now, we'll use mock data
+ setTimeout(() => {
+ const mockProfile: AgentProfile = {
+ id: "agent123",
+ userId: session?.id || "",
+ name: session?.name || "Agent Name",
+ email: session?.email || "agent@example.com",
+ isOnline: true,
+ isAvailable: true,
+ specialties: ["Technical Support", "Billing Issues", "Product Information"],
+ bio: "Experienced customer support agent with 3+ years in technical support and billing assistance.",
+ profileImage: session?.image || "",
+ activeChats: 2,
+ lastActive: new Date().toISOString(),
+ };
+
+ setProfile(mockProfile);
+ setSpecialties(mockProfile.specialties);
+ setIsLoading(false);
+ }, 1000);
+ } catch (error) {
+ console.error('Error fetching agent profile:', error);
+ toast.error('Failed to load profile');
+ setIsLoading(false);
+ }
+ };
+
+ const fetchAgentSettings = async () => {
+ try {
+ // In a real app, this would be an API call
+ // For now, we'll use mock data or localStorage
+ const savedNotifications = localStorage.getItem("agentNotificationSettings");
+ const savedAppearance = localStorage.getItem("agentAppearanceSettings");
+
+ if (savedNotifications) {
+ setNotificationSettings(JSON.parse(savedNotifications));
+ }
+
+ if (savedAppearance) {
+ setAppearanceSettings(JSON.parse(savedAppearance));
+ }
+ } catch (error) {
+ console.error('Error fetching agent settings:', error);
+ }
+ };
+
+ const handleProfileUpdate = async () => {
+ if (!profile) return;
+
+ try {
+ setIsSaving(true);
+
+ // In a real app, this would be an API call
+ // For now, we'll simulate it
+ setTimeout(() => {
+ // Update profile with specialties
+ setProfile({
+ ...profile,
+ specialties,
+ });
+
+ toast.success('Profile updated successfully');
+ setIsSaving(false);
+ }, 1000);
+ } catch (error) {
+ console.error('Error updating profile:', error);
+ toast.error('Failed to update profile');
+ setIsSaving(false);
+ }
+ };
+
+ const handleNotificationSettingsUpdate = async () => {
+ try {
+ setIsSaving(true);
+
+ // In a real app, this would be an API call
+ // For now, we'll save to localStorage
+ localStorage.setItem("agentNotificationSettings", JSON.stringify(notificationSettings));
+
+ setTimeout(() => {
+ toast.success('Notification settings updated');
+ setIsSaving(false);
+ }, 500);
+ } catch (error) {
+ console.error('Error updating notification settings:', error);
+ toast.error('Failed to update notification settings');
+ setIsSaving(false);
+ }
+ };
+
+ const handleAppearanceSettingsUpdate = async () => {
+ try {
+ setIsSaving(true);
+
+ // In a real app, this would be an API call
+ // For now, we'll save to localStorage
+ localStorage.setItem("agentAppearanceSettings", JSON.stringify(appearanceSettings));
+
+ setTimeout(() => {
+ toast.success('Appearance settings updated');
+ setIsSaving(false);
+ }, 500);
+ } catch (error) {
+ console.error('Error updating appearance settings:', error);
+ toast.error('Failed to update appearance settings');
+ setIsSaving(false);
+ }
+ };
+
+ const addSpecialty = () => {
+ if (newSpecialty.trim() && !specialties.includes(newSpecialty.trim())) {
+ setSpecialties([...specialties, newSpecialty.trim()]);
+ setNewSpecialty("");
+ }
+ };
+
+ const removeSpecialty = (specialty: string) => {
+ setSpecialties(specialties.filter(s => s !== specialty));
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
Agent Settings
+
+
+
+
+
+
+ Profile
+
+
+
+ Notifications
+
+
+
+ Appearance
+
+
+
+ Security
+
+
+
+
+
+
+ Profile Information
+
+ Update your agent profile information and specialties
+
+
+
+
+
+ Full Name
+ setProfile(prev => prev ? { ...prev, name: e.target.value } : null)}
+ />
+
+
+
+ Email
+
+
+
+
+ Bio
+
+
+
+
+
+
Specialties
+
+ setNewSpecialty(e.target.value)}
+ className="w-48"
+ />
+ Add
+
+
+
+
+ {specialties.map((specialty, index) => (
+
+ {specialty}
+ removeSpecialty(specialty)}
+ >
+
+
+
+ ))}
+
+
+
+
+
+ setProfile(prev => prev ? { ...prev, isAvailable: checked } : null)}
+ />
+ Available for new chats and tickets
+
+
+ When turned off, you won't receive new chat or ticket assignments
+
+
+
+
+
+ {isSaving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+
+ Save Changes
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ Notification Preferences
+
+ Customize how and when you receive notifications
+
+
+
+
+
Event Notifications
+
+
+
+
+
New Chat Assigned
+
+ Receive notifications when a new chat is assigned to you
+
+
+
setNotificationSettings({ ...notificationSettings, newChatAssigned: checked })}
+ />
+
+
+
+
+
+
+
New Ticket Assigned
+
+ Receive notifications when a new ticket is assigned to you
+
+
+
setNotificationSettings({ ...notificationSettings, newTicketAssigned: checked })}
+ />
+
+
+
+
+
+
+
Chat Updates
+
+ Receive notifications for updates to your active chats
+
+
+
setNotificationSettings({ ...notificationSettings, chatUpdates: checked })}
+ />
+
+
+
+
+
+
+
Ticket Updates
+
+ Receive notifications for updates to your assigned tickets
+
+
+
setNotificationSettings({ ...notificationSettings, ticketUpdates: checked })}
+ />
+
+
+
+
+
+
+
System Announcements
+
+ Receive notifications for important system announcements
+
+
+
setNotificationSettings({ ...notificationSettings, systemAnnouncements: checked })}
+ />
+
+
+
+
+
+
Notification Channels
+
+
+
+
+
Email Notifications
+
+ Receive notifications via email
+
+
+
setNotificationSettings({ ...notificationSettings, emailNotifications: checked })}
+ />
+
+
+
+
+
+
+
Desktop Notifications
+
+ Receive browser notifications when the app is open
+
+
+
setNotificationSettings({ ...notificationSettings, desktopNotifications: checked })}
+ />
+
+
+
+
+
+
+
Sound Alerts
+
+ Play sound alerts for new notifications
+
+
+
setNotificationSettings({ ...notificationSettings, soundAlerts: checked })}
+ />
+
+
+
+
+
+
+ {isSaving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+
+ Save Preferences
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ Appearance Settings
+
+ Customize the look and feel of your agent dashboard
+
+
+
+
+
+ Theme
+ setAppearanceSettings({ ...appearanceSettings, theme: value })}
+ >
+
+
+
+
+ Light
+ Dark
+ System Default
+
+
+
+
+
+ Font Size
+ setAppearanceSettings({ ...appearanceSettings, fontSize: value })}
+ >
+
+
+
+
+ Small
+ Medium
+ Large
+
+
+
+
+
+ Chat Layout
+ setAppearanceSettings({ ...appearanceSettings, chatLayout: value })}
+ >
+
+
+
+
+ Default
+ Compact
+ Bubbles
+
+
+
+
+
+
+
+
+
+
Show Timestamps
+
+ Show timestamps for all messages in chats
+
+
+
setAppearanceSettings({ ...appearanceSettings, showTimestamps: checked })}
+ />
+
+
+
+
+
+
+
Compact View
+
+ Use a more compact layout to fit more content on screen
+
+
+
setAppearanceSettings({ ...appearanceSettings, compactView: checked })}
+ />
+
+
+
+
+
+
+ {isSaving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ <>
+
+ Save Settings
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ Security Settings
+
+ Manage your account security and password
+
+
+
+
+
Change Password
+
+
+ Current Password
+
+
+
+
+ New Password
+
+
+
+
+ Confirm New Password
+
+
+
+
+
+ Update Password
+
+
+
+
+
Two-Factor Authentication
+
+
+
+
+
Two-Factor Authentication
+
+ Add an extra layer of security to your account
+
+
+
+ Enable 2FA
+
+
+
+
+
+
+
Session Management
+
+
+
+ You are currently logged in from this device.
+
+
+
+ Log Out All Other Devices
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/AgentDashboard/TicketManagement.tsx b/components/AgentDashboard/TicketManagement.tsx
new file mode 100644
index 0000000..db8111d
--- /dev/null
+++ b/components/AgentDashboard/TicketManagement.tsx
@@ -0,0 +1,753 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useSession } from "@/components/SessionWrapper";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Badge } from "@/components/ui/badge";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ TicketCheck,
+ Clock,
+ CheckCircle,
+ Loader2,
+ Search,
+ Filter,
+ AlertCircle,
+ MoreVertical,
+ PlusCircle,
+ FileText,
+ Paperclip,
+ X,
+} from "lucide-react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import toast from "react-hot-toast";
+
+interface Ticket {
+ id: string;
+ subject: string;
+ message: string;
+ priority: string;
+ category: string;
+ status: string;
+ attachments: string[];
+ userId: string;
+ user: {
+ name: string;
+ email: string;
+ };
+ createdAt: string;
+ updatedAt: string;
+ assignedTo?: string;
+ responses?: TicketResponse[];
+}
+
+interface TicketResponse {
+ id: string;
+ content: string;
+ ticketId: string;
+ createdAt: string;
+ createdBy: string;
+ createdByType: string;
+}
+
+export default function TicketManagement() {
+ const { session } = useSession();
+ const [isLoading, setIsLoading] = useState(true);
+ const [activeTickets, setActiveTickets] = useState([]);
+ const [pendingTickets, setPendingTickets] = useState([]);
+ const [closedTickets, setClosedTickets] = useState([]);
+ const [selectedTicket, setSelectedTicket] = useState(null);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [categoryFilter, setCategoryFilter] = useState("all");
+ const [priorityFilter, setPriorityFilter] = useState("all");
+ const [activeTab, setActiveTab] = useState("active");
+ const [responseContent, setResponseContent] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+ // Fetch tickets on component mount
+ useEffect(() => {
+ if (session) {
+ fetchTickets();
+ }
+ }, [session]);
+
+ const fetchTickets = async () => {
+ try {
+ setIsLoading(true);
+
+ // Fetch active tickets
+ const activeResponse = await fetch('/api/agent/tickets?status=active', {
+ credentials: 'include'
+ });
+
+ // Fetch pending tickets
+ const pendingResponse = await fetch('/api/agent/tickets?status=pending', {
+ credentials: 'include'
+ });
+
+ // Fetch closed tickets
+ const closedResponse = await fetch('/api/agent/tickets?status=closed', {
+ credentials: 'include'
+ });
+
+ if (!activeResponse.ok || !pendingResponse.ok || !closedResponse.ok) {
+ throw new Error('Failed to fetch tickets');
+ }
+
+ const activeData = await activeResponse.json();
+ const pendingData = await pendingResponse.json();
+ const closedData = await closedResponse.json();
+
+ setActiveTickets(activeData);
+ setPendingTickets(pendingData);
+ setClosedTickets(closedData);
+ } catch (error) {
+ console.error('Error fetching tickets:', error);
+ toast.error('Failed to load tickets');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleAcceptTicket = async (ticketId: string) => {
+ try {
+ const response = await fetch(`/api/agent/tickets/${ticketId}/accept`, {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to accept ticket');
+ }
+
+ const acceptedTicket = await response.json();
+
+ // Remove from pending and add to active
+ setPendingTickets(prev => prev.filter(ticket => ticket.id !== ticketId));
+ setActiveTickets(prev => [acceptedTicket, ...prev]);
+
+ // Select the newly accepted ticket
+ setSelectedTicket(acceptedTicket);
+ setActiveTab("active");
+
+ toast.success('Ticket accepted successfully');
+ } catch (error) {
+ console.error('Error accepting ticket:', error);
+ toast.error('Failed to accept ticket');
+ }
+ };
+
+ const handleCloseTicket = async (ticketId: string) => {
+ try {
+ const response = await fetch(`/api/agent/tickets/${ticketId}/close`, {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to close ticket');
+ }
+
+ const closedTicket = await response.json();
+
+ // Remove from active and add to closed
+ setActiveTickets(prev => prev.filter(ticket => ticket.id !== ticketId));
+ setClosedTickets(prev => [closedTicket, ...prev]);
+
+ // If this was the selected ticket, clear selection
+ if (selectedTicket?.id === ticketId) {
+ setSelectedTicket(null);
+ }
+
+ toast.success('Ticket closed successfully');
+ } catch (error) {
+ console.error('Error closing ticket:', error);
+ toast.error('Failed to close ticket');
+ }
+ };
+
+ const handleSubmitResponse = async () => {
+ if (!selectedTicket || !responseContent.trim()) return;
+
+ try {
+ setIsSubmitting(true);
+ const response = await fetch(`/api/agent/tickets/${selectedTicket.id}/responses`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ content: responseContent }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to submit response');
+ }
+
+ const newResponse = await response.json();
+
+ // Update the selected ticket with the new response
+ setSelectedTicket(prev => {
+ if (!prev) return null;
+ return {
+ ...prev,
+ responses: [...(prev.responses || []), newResponse],
+ updatedAt: new Date().toISOString()
+ };
+ });
+
+ // Also update in the active tickets list
+ setActiveTickets(prev => prev.map(ticket => {
+ if (ticket.id === selectedTicket.id) {
+ return {
+ ...ticket,
+ responses: [...(ticket.responses || []), newResponse],
+ updatedAt: new Date().toISOString()
+ };
+ }
+ return ticket;
+ }));
+
+ // Clear the response input
+ setResponseContent('');
+ toast.success('Response submitted successfully');
+ } catch (error) {
+ console.error('Error submitting response:', error);
+ toast.error('Failed to submit response');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // Filter tickets based on search term and filters
+ const filterTickets = (tickets: Ticket[]) => {
+ return tickets.filter(ticket => {
+ // Search term filter
+ const matchesSearch = searchTerm === '' ||
+ ticket.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ticket.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ticket.user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ticket.user.email.toLowerCase().includes(searchTerm.toLowerCase());
+
+ // Category filter
+ const matchesCategory = categoryFilter === 'all' || ticket.category === categoryFilter;
+
+ // Priority filter
+ const matchesPriority = priorityFilter === 'all' || ticket.priority === priorityFilter;
+
+ return matchesSearch && matchesCategory && matchesPriority;
+ });
+ };
+
+ const filteredActiveTickets = filterTickets(activeTickets);
+ const filteredPendingTickets = filterTickets(pendingTickets);
+ const filteredClosedTickets = filterTickets(closedTickets);
+
+ // Format date for display
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleString();
+ };
+
+ // Get priority badge variant
+ const getPriorityBadge = (priority: string) => {
+ switch (priority.toLowerCase()) {
+ case 'high':
+ return "destructive";
+ case 'medium':
+ return "default";
+ default:
+ return "outline";
+ }
+ };
+
+ // Get status badge variant
+ const getStatusBadge = (status: string) => {
+ switch (status.toLowerCase()) {
+ case 'active':
+ return "default";
+ case 'pending':
+ return "secondary";
+ case 'closed':
+ return "outline";
+ default:
+ return "outline";
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
Ticket Management
+
+
+
+
+
+ Create Ticket
+
+
+
+
+ Create New Support Ticket
+
+ Create a new ticket on behalf of a user or for internal purposes.
+
+
+
+
+
+ User Email
+
+
+
+
+ Subject
+
+
+
+
+ Category
+
+
+
+
+
+ General
+ Technical
+ Billing
+ Product
+
+
+
+
+
+ Priority
+
+
+
+
+
+ Low
+ Medium
+ High
+
+
+
+
+
+ Message
+
+
+
+
+
+ setIsDialogOpen(false)}>Cancel
+ Create Ticket
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ All Categories
+ General
+ Technical
+ Billing
+ Product
+
+
+
+
+
+
+
+
+ All Priorities
+ Low
+ Medium
+ High
+
+
+
+
+
+
+
+ {/* Tickets Tabs */}
+
+
+
+
+ Active ({activeTickets.length})
+
+
+
+ Pending ({pendingTickets.length})
+
+
+
+ Closed
+
+
+
+
+
+
+
+
+
+ Subject
+ User
+ Category
+ Priority
+ Created
+ Actions
+
+
+
+ {filteredActiveTickets.length === 0 ? (
+
+
+
+ No active tickets
+
+
+ ) : (
+ filteredActiveTickets.map(ticket => (
+
+ setSelectedTicket(ticket)}
+ >
+ {ticket.subject}
+
+ {ticket.user.name}
+
+ {ticket.category}
+
+
+
+ {ticket.priority}
+
+
+
+ {new Date(ticket.createdAt).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+ setSelectedTicket(ticket)}>
+ View Details
+
+ handleCloseTicket(ticket.id)}>
+ Close Ticket
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ Subject
+ User
+ Category
+ Priority
+ Created
+ Actions
+
+
+
+ {filteredPendingTickets.length === 0 ? (
+
+
+
+ No pending tickets
+
+
+ ) : (
+ filteredPendingTickets.map(ticket => (
+
+ {ticket.subject}
+ {ticket.user.name}
+
+ {ticket.category}
+
+
+
+ {ticket.priority}
+
+
+
+ {new Date(ticket.createdAt).toLocaleDateString()}
+
+
+ handleAcceptTicket(ticket.id)}>
+ Accept
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ Subject
+ User
+ Category
+ Priority
+ Closed
+ Actions
+
+
+
+ {filteredClosedTickets.length === 0 ? (
+
+
+
+ No closed tickets
+
+
+ ) : (
+ filteredClosedTickets.map(ticket => (
+
+ setSelectedTicket(ticket)}
+ >
+ {ticket.subject}
+
+ {ticket.user.name}
+
+ {ticket.category}
+
+
+
+ {ticket.priority}
+
+
+
+ {new Date(ticket.updatedAt).toLocaleDateString()}
+
+
+ setSelectedTicket(ticket)}
+ >
+ View
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+ {/* Ticket Details */}
+ {selectedTicket && (
+
+
+
+
{selectedTicket.subject}
+
+
+ {selectedTicket.status}
+
+
+ {selectedTicket.priority}
+
+ {selectedTicket.category}
+
+
+ setSelectedTicket(null)}
+ >
+
+
+
+
+
+
+ From: {selectedTicket.user.name} ({selectedTicket.user.email})
+
+
+ Created: {formatDate(selectedTicket.createdAt)}
+
+
+
+
+
{selectedTicket.message}
+
+ {selectedTicket.attachments && selectedTicket.attachments.length > 0 && (
+
+
Attachments:
+
+ {selectedTicket.attachments.map((attachment, index) => (
+
+
+
{attachment.split('/').pop()}
+
+ ))}
+
+
+ )}
+
+
+ {/* Responses */}
+ {selectedTicket.responses && selectedTicket.responses.length > 0 && (
+
+
Responses
+ {selectedTicket.responses.map((response) => (
+
+
+
+ {response.createdByType === 'agent' ? 'Agent Response' : 'User Response'}
+
+
+ {formatDate(response.createdAt)}
+
+
+
{response.content}
+
+ ))}
+
+ )}
+
+ {/* Response Form */}
+ {selectedTicket.status === 'active' && (
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/components/Alerts.tsx b/components/Alerts.tsx
new file mode 100644
index 0000000..37bb157
--- /dev/null
+++ b/components/Alerts.tsx
@@ -0,0 +1,36 @@
+import React, { useEffect, useState } from 'react';
+import { AlertProps } from '@/types';
+
+const Alert: React.FC = ({ message, type = 'info', duration = 10000 }) => {
+ const [visible, setVisible] = useState(true);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setVisible(false), duration);
+ return () => clearTimeout(timer);
+ }, [duration]);
+
+ if (!visible) return null;
+
+ const alertStyles = {
+ success: 'bg-green-100 text-green-700 border-green-500',
+ error: 'bg-red-100 text-red-700 border-red-500',
+ info: 'bg-blue-100 text-blue-700 border-blue-500',
+ warning: 'bg-yellow-100 text-yellow-700 border-yellow-500',
+ };
+
+ return (
+
+
+ {message}
+ setVisible(false)}
+ className="ml-4 text-lg font-bold focus:outline-none"
+ >
+ ×
+
+
+
+ );
+};
+
+export default Alert;
diff --git a/components/AlertsMsg.tsx b/components/AlertsMsg.tsx
new file mode 100644
index 0000000..f56bddb
--- /dev/null
+++ b/components/AlertsMsg.tsx
@@ -0,0 +1,43 @@
+export const AlertsMsg = ({ alert }: { alert: string }) => {
+
+ if (alert === 'success_token') {
+ return {"alertMessage": 'Email verified successfully! You can now sign in.',
+ "alertType": 'success'};
+ } else if (alert === 'missing_token') {
+ return {'alertMessage': 'Verification token is missing.',
+ 'alertType': 'error'};
+ } else if (alert === 'invalid_token') {
+ return {'alertMessage': 'Invalid verification token.',
+ 'alertType': 'error'};
+ } else if (alert === 'expired_token') {
+ return {'alertMessage': 'Verification token has expired.',
+ 'alertType': 'error'};
+ } else if (alert === 'success_signup') {
+ return {'alertMessage': 'Account created successfully! Please verify your email to sign in.',
+ 'alertType':'success'};
+ } else if (alert === 'email_exists') {
+ return {'alertMessage': 'Email already exists. Please login or use a different email.',
+ 'alertType': 'error'};
+ } else if (alert === 'invalid_email') {
+ return {'alertMessage': 'Invalid email format. Please enter a valid email address.',
+ 'alertType': 'error'};
+ } else if (alert === 'password_mismatch') {
+ return {'alertMessage': 'Passwords do not match. Please enter the same password in both fields.',
+ 'alertType': 'error'};
+ } else if (alert === 'password_short') {
+ return {'alertMessage': 'Password must be at least 8 characters long.',
+ 'alertType': 'error'};
+ } else if (alert === 'invalid_login') {
+ return {'alertMessage': 'Invalid email or password. Please try again.',
+ 'alertType': 'error'};
+ } else if (alert === 'error_signup') {
+ return {'alertMessage': 'An error occurred while creating your account. Please try again later.',
+ 'alertType': 'error'};
+ }else if (alert === 'error_post_ad') {
+ return {'alertMessage': 'An error occurred while posting your ad. Please try again later.',
+ 'alertType': 'error'};
+ }else if (alert === 'success_ad_post') {
+ return {'alertMessage': 'Your ad has been posted successfully!',
+ 'alertType': 'success'};
+ }
+};
\ No newline at end of file
diff --git a/components/AnalyticsMain.tsx b/components/AnalyticsMain.tsx
new file mode 100644
index 0000000..f30fb16
--- /dev/null
+++ b/components/AnalyticsMain.tsx
@@ -0,0 +1,870 @@
+"use client";
+
+import React, { useState } from "react";
+import { Bar, Pie, Doughnut, Line } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ ArcElement,
+ Tooltip,
+ Legend,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ PointElement,
+ LineElement,
+ Title
+} from "chart.js";
+import {
+ Banknote,
+ Coins,
+ Wallet,
+ Loader2,
+ TrendingUp,
+ TrendingDown,
+ Eye,
+ MousePointer,
+ Share2,
+ BarChart3,
+ Smartphone,
+ Laptop,
+ Users,
+ Clock,
+ RefreshCw,
+ CheckCircle,
+ CreditCard
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { useAnalytics } from "@/lib/hooks/useAnalytics";
+
+// Register necessary Chart.js components
+ChartJS.register(
+ ArcElement,
+ Tooltip,
+ Legend,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ PointElement,
+ LineElement,
+ Title
+);
+
+// Use the types from our hook
+
+export default function Analytics() {
+ const [activeTab, setActiveTab] = useState("performance");
+ const {
+ data: analyticsData,
+ isLoading,
+ error,
+ refreshData,
+ setTimeRange
+ } = useAnalytics();
+
+ // Handle time range change
+ const handleTimeRangeChange = (range: string) => {
+ setTimeRange(range as '7days' | '30days' | '90days');
+ };
+
+ return (
+
+
š Analytics & Reports
+
Track ad performance, audience insights, and financials.
+
+ {/* Time Range Selector */}
+
+ handleTimeRangeChange(e.target.value)}
+ >
+ Last 7 Days
+ Last 30 Days
+ Last 90 Days
+
+
+ refreshData()}
+ disabled={isLoading}
+ className="flex items-center gap-1 h-8 px-3"
+ >
+
+ Refresh
+
+
+
+ {/* Tabs */}
+
+ {["performance", "demographics", "financials"].map((tab) => (
+ setActiveTab(tab)}
+ >
+ {tab === "performance" ? "š Ad Performance" : tab === "demographics" ? "š Demographics" : "š° Financials"}
+
+ ))}
+
+
+ {isLoading ? (
+
+
+
+ ) : error ? (
+
+
{error.message || "Failed to load analytics data"}
+
refreshData()}
+ className="mt-4 h-8 px-3"
+ >
+ Try Again
+
+
+ ) : !analyticsData ? (
+
+
No analytics data available
+
+ ) : (
+ <>
+ {/* Ad Performance */}
+ {activeTab === "performance" && (
+
+
š Ad Performance Dashboard
+
+ {/* Performance Summary Cards */}
+
+
+
+
+
+ Total Views
+
+
+
+
+
{analyticsData.adPerformance.totalViews.toLocaleString()}
+ {analyticsData.adPerformance.performanceTrends.views.change > 0 ? (
+
+ +{analyticsData.adPerformance.performanceTrends.views.change}%
+
+ ) : (
+
+ {analyticsData.adPerformance.performanceTrends.views.change}%
+
+ )}
+
+
+
+
+
+
+
+
+ Total Clicks
+
+
+
+
+
{analyticsData.adPerformance.totalClicks.toLocaleString()}
+ {analyticsData.adPerformance.performanceTrends.clicks.change > 0 ? (
+
+ +{analyticsData.adPerformance.performanceTrends.clicks.change}%
+
+ ) : (
+
+ {analyticsData.adPerformance.performanceTrends.clicks.change}%
+
+ )}
+
+
+
+
+
+
+
+
+ Total Shares
+
+
+
+
+
{analyticsData.adPerformance.totalShares.toLocaleString()}
+ {analyticsData.adPerformance.performanceTrends.shares.change > 0 ? (
+
+ +{analyticsData.adPerformance.performanceTrends.shares.change}%
+
+ ) : (
+
+ {analyticsData.adPerformance.performanceTrends.shares.change}%
+
+ )}
+
+
+
+
+
+
+
+
+ Engagement Rate
+
+
+
+ {analyticsData.adPerformance.engagementRate}
+ Click-through rate
+
+
+
+
+ {/* Daily Performance Chart */}
+
+
+
+ Daily Views
+ View trends over time
+
+
+
+
+
+
+
+
+
+
+ Daily Clicks & Shares
+ Interaction trends over time
+
+
+
+
+
+
+
+
+
+ {/* Category Performance */}
+
+
+ Category Performance
+ Performance metrics by ad category
+
+
+
+
+
+
+ Category
+ Ads
+ Views
+ Clicks
+ Engagement
+
+
+
+ {analyticsData.adPerformance.categoryPerformance.map((category, index) => (
+
+ {category.category}
+ {category.count}
+ {category.views.toLocaleString()}
+ {category.clicks.toLocaleString()}
+ {category.engagementRate}%
+
+ ))}
+
+
+
+
+
+
+ {/* Ad Details Table */}
+ {analyticsData.adPerformance.adDetails.length > 0 && (
+
+
+ Ad Performance Details
+ Detailed metrics for each of your ads
+
+
+
+
+
+
+ Ad
+ Views
+ Clicks
+ Shares
+ Engagement
+ Status
+
+
+
+ {analyticsData.adPerformance.adDetails.map((ad) => (
+
+
+
+ {ad.image && (
+
+
+
+ )}
+
+
{ad.title}
+
{ad.category}
+
+
+
+ {ad.views.toLocaleString()}
+ {ad.clicks.toLocaleString()}
+ {ad.shares.toLocaleString()}
+ {ad.engagementRate}%
+
+
+ {ad.status}
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ )}
+
+ {/* Demographics */}
+ {activeTab === "demographics" && (
+
+
š Audience Demographics
+
+
+ {/* Total Audience Card */}
+
+
+ Total Audience
+ Total views across all ads
+
+
+
+
+
+
{analyticsData.demographics.totalAudience.toLocaleString()}
+
Total impressions
+
+
+
+
+
+ {/* Device Distribution */}
+
+
+ Device Distribution
+ Audience by device type
+
+
+
+ {analyticsData.demographics.devices.map((device, index) => (
+
+ {device.device === "Mobile" &&
}
+ {device.device === "Desktop" &&
}
+ {device.device === "Tablet" &&
}
+
+
+ {device.device}
+ {device.percentage}%
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {/* Age Group Distribution */}
+
+
+ Age Group Distribution
+ Audience by age group
+
+
+
+
age.group),
+ datasets: [
+ {
+ data: analyticsData.demographics.ageGroups.map((age) => age.percentage),
+ backgroundColor: [
+ "rgba(59, 130, 246, 0.7)", // Blue
+ "rgba(16, 185, 129, 0.7)", // Green
+ "rgba(245, 158, 11, 0.7)", // Yellow
+ "rgba(239, 68, 68, 0.7)", // Red
+ "rgba(139, 92, 246, 0.7)" // Purple
+ ],
+ borderColor: [
+ "rgba(59, 130, 246, 1)",
+ "rgba(16, 185, 129, 1)",
+ "rgba(245, 158, 11, 1)",
+ "rgba(239, 68, 68, 1)",
+ "rgba(139, 92, 246, 1)"
+ ],
+ borderWidth: 1
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'right',
+ labels: {
+ boxWidth: 15,
+ padding: 15
+ }
+ }
+ }
+ }}
+ />
+
+
+
+
+ {/* Gender Distribution */}
+
+
+ Gender Distribution
+ Audience by gender
+
+
+
+ g.gender),
+ datasets: [
+ {
+ data: analyticsData.demographics.gender.map((g) => g.percentage),
+ backgroundColor: [
+ "rgba(59, 130, 246, 0.7)", // Blue for Male
+ "rgba(236, 72, 153, 0.7)" // Pink for Female
+ ],
+ borderColor: [
+ "rgba(59, 130, 246, 1)",
+ "rgba(236, 72, 153, 1)"
+ ],
+ borderWidth: 1
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom'
+ }
+ }
+ }}
+ />
+
+
+
+
+
+
+ {/* Top Locations */}
+
+
+ Geographic Distribution
+ Audience by location
+
+
+
+ {analyticsData.demographics.topLocations.slice(0, 5).map((loc, index) => (
+
+
+ {loc.country}
+ {loc.percentage}% ({loc.views.toLocaleString()} views)
+
+
+
+ ))}
+
+
+
+
+ {/* Time of Day */}
+
+
+ Time of Day
+ When your audience is most active
+
+
+
+ {analyticsData.demographics.timeOfDay.map((time, index) => (
+
+
+
+ {time.time}
+ {time.percentage}%
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Financials */}
+ {activeTab === "financials" && (
+
+
š° Revenue & Spending Breakdown
+
+ {/* Financial Summary Cards */}
+
+
+
+
+
+ Total Spent
+
+
+
+ ā¦{analyticsData.financialData.totalSpent.toLocaleString()}
+ Subscription & boost costs
+
+
+
+
+
+
+
+ Total Earnings
+
+
+
+ ā¦{analyticsData.financialData.earnings.toLocaleString()}
+ Revenue from sales
+
+
+
+
+
+
+
+ Net Profit
+
+
+
+ ā¦{analyticsData.financialData.profit.toLocaleString()}
+ Earnings minus costs
+
+
+
+
+
+
+
+ ROI
+
+
+
+ {analyticsData.financialData.roi}%
+ Return on investment
+
+
+
+
+
+ {/* Revenue vs Spending Chart */}
+
+
+ Revenue vs Spending
+ Financial breakdown
+
+
+
+
+
+
+
+
+ {/* Monthly Spending Trends */}
+
+
+ Monthly Spending Trends
+ Spending over time
+
+
+
+
+
+
+
+
+
+ {/* Subscription Details */}
+
+
+ Subscription Details
+ Your current subscription plan
+
+
+
+
+
+
+
+
{analyticsData.financialData.subscriptionDetails.name}
+
ā¦{analyticsData.financialData.subscriptionDetails.cost.toLocaleString()} per month
+
+
+
+ {analyticsData.financialData.subscriptionDetails.features &&
+ analyticsData.financialData.subscriptionDetails.features.length > 0 && (
+
+
Plan Features:
+
+ {Object.entries(analyticsData.financialData.subscriptionDetails.features).map(([key, value], index) => (
+
+
+ {key}: {String(value)}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Boost Details */}
+ {analyticsData.financialData.boostDetails.length > 0 && (
+
+
+ Ad Boost History
+ Details of your ad promotions
+
+
+
+
+
+
+ Ad Title
+ Boost Type
+ Start Date
+ End Date
+ Cost
+
+
+
+ {analyticsData.financialData.boostDetails.map((boost, index) => (
+
+ {boost.adTitle}
+
+ {boost.boostType === 1 ? "Standard" :
+ boost.boostType === 2 ? "Premium" :
+ boost.boostType === 3 ? "Featured" : "Basic"}
+
+
+ {new Date(boost.boostStartDate).toLocaleDateString()}
+
+
+ {new Date(boost.boostEndDate).toLocaleDateString()}
+
+
+ ā¦{boost.cost.toLocaleString()}
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/components/BillingMain.tsx b/components/BillingMain.tsx
new file mode 100644
index 0000000..c435a37
--- /dev/null
+++ b/components/BillingMain.tsx
@@ -0,0 +1,1072 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { cn } from "@/lib/utils";
+import { billingTabs } from "@/constants";
+import React from "react";
+import {
+ Wallet,
+ FileText,
+ CreditCard,
+ AlertCircle,
+ Plus,
+ Trash2,
+ Download,
+ CheckCircle,
+ XCircle,
+ Clock,
+ RefreshCw,
+ Calendar,
+ DollarSign,
+ ArrowUpRight,
+ ArrowDownRight,
+ Filter,
+ Search,
+ ChevronDown,
+ Loader2
+} from "lucide-react";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { format } from "date-fns";
+import toast from "react-hot-toast";
+
+export default function BillingMain() {
+ const [activeTab, setActiveTab] = useState("transactions");
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [timeRange, setTimeRange] = useState("30days");
+ const [invoiceStatus, setInvoiceStatus] = useState("all");
+
+ // State for transactions
+ const [transactions, setTransactions] = useState([]);
+ const [transactionSummary, setTransactionSummary] = useState({
+ totalSpent: 0,
+ subscriptionPayments: 0,
+ boostPayments: 0,
+ refunds: 0
+ });
+
+ // State for invoices
+ const [invoices, setInvoices] = useState([]);
+ const [invoiceSummary, setInvoiceSummary] = useState({
+ totalPaid: 0,
+ totalUnpaid: 0,
+ totalOverdue: 0,
+ count: 0
+ });
+
+ // State for payment methods
+ const [paymentMethods, setPaymentMethods] = useState([]);
+ const [defaultPaymentMethod, setDefaultPaymentMethod] = useState(null);
+ const [isAddingPaymentMethod, setIsAddingPaymentMethod] = useState(false);
+ const [newPaymentMethod, setNewPaymentMethod] = useState({
+ type: "card",
+ provider: "",
+ last4: "",
+ expiryMonth: "",
+ expiryYear: "",
+ email: "",
+ accountName: "",
+ accountNumber: "",
+ bankName: ""
+ });
+
+ // State for disputes
+ const [disputeForm, setDisputeForm] = useState({
+ transactionId: "",
+ description: ""
+ });
+ const [isSubmittingDispute, setIsSubmittingDispute] = useState(false);
+
+ // State for billing summary
+ const [billingSummary, setBillingSummary] = useState(null);
+
+ // Fetch billing summary on component mount
+ useEffect(() => {
+ const fetchBillingSummary = async () => {
+ try {
+ const response = await fetch("/api/user/billing/summary");
+ if (response.ok) {
+ const data = await response.json();
+ setBillingSummary(data);
+ } else {
+ console.error("Failed to fetch billing summary");
+ }
+ } catch (error) {
+ console.error("Error fetching billing summary:", error);
+ }
+ };
+
+ fetchBillingSummary();
+ }, []);
+
+ // Fetch data based on active tab
+ useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ if (activeTab === "transactions") {
+ await fetchTransactions();
+ } else if (activeTab === "invoices") {
+ await fetchInvoices();
+ } else if (activeTab === "methods") {
+ await fetchPaymentMethods();
+ }
+ } catch (err) {
+ setError("Failed to load data. Please try again.");
+ console.error("Error fetching data:", err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [activeTab, timeRange, invoiceStatus]);
+
+ // Fetch transactions
+ const fetchTransactions = async () => {
+ const response = await fetch(`/api/user/billing/transactions?timeRange=${timeRange}`);
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch transactions");
+ }
+
+ const data = await response.json();
+ setTransactions(data.transactions);
+ setTransactionSummary(data.summary);
+ };
+
+ // Fetch invoices
+ const fetchInvoices = async () => {
+ const response = await fetch(`/api/user/billing/invoices?status=${invoiceStatus}`);
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch invoices");
+ }
+
+ const data = await response.json();
+ setInvoices(data.invoices);
+ setInvoiceSummary(data.summary);
+ };
+
+ // Fetch payment methods
+ const fetchPaymentMethods = async () => {
+ const response = await fetch("/api/user/billing/payment-methods");
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch payment methods");
+ }
+
+ const data = await response.json();
+ setPaymentMethods(data.paymentMethods);
+ setDefaultPaymentMethod(data.defaultMethod);
+ };
+
+ // Handle adding a new payment method
+ const handleAddPaymentMethod = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ const payload: any = {
+ type: newPaymentMethod.type,
+ provider: newPaymentMethod.provider,
+ isDefault: paymentMethods.length === 0 ? true : false
+ };
+
+ // Add type-specific fields
+ if (newPaymentMethod.type === "card") {
+ if (!newPaymentMethod.provider || !newPaymentMethod.last4 || !newPaymentMethod.expiryMonth || !newPaymentMethod.expiryYear) {
+ toast.error("Please fill in all card details");
+ return;
+ }
+
+ payload.last4 = newPaymentMethod.last4;
+ payload.expiryMonth = parseInt(newPaymentMethod.expiryMonth);
+ payload.expiryYear = parseInt(newPaymentMethod.expiryYear);
+ } else if (newPaymentMethod.type === "paypal") {
+ if (!newPaymentMethod.email) {
+ toast.error("Please enter your PayPal email");
+ return;
+ }
+
+ payload.email = newPaymentMethod.email;
+ } else if (newPaymentMethod.type === "bank_account") {
+ if (!newPaymentMethod.accountName || !newPaymentMethod.accountNumber || !newPaymentMethod.bankName) {
+ toast.error("Please fill in all bank account details");
+ return;
+ }
+
+ payload.accountName = newPaymentMethod.accountName;
+ payload.accountNumber = newPaymentMethod.accountNumber;
+ payload.bankName = newPaymentMethod.bankName;
+ payload.last4 = newPaymentMethod.accountNumber.slice(-4);
+ }
+
+ const response = await fetch("/api/user/billing/payment-methods", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to add payment method");
+ }
+
+ // Reset form and fetch updated payment methods
+ setNewPaymentMethod({
+ type: "card",
+ provider: "",
+ last4: "",
+ expiryMonth: "",
+ expiryYear: "",
+ email: "",
+ accountName: "",
+ accountNumber: "",
+ bankName: ""
+ });
+ setIsAddingPaymentMethod(false);
+ await fetchPaymentMethods();
+ toast.success("Payment method added successfully");
+ } catch (error) {
+ console.error("Error adding payment method:", error);
+ toast.error("Failed to add payment method");
+ }
+ };
+
+ // Handle removing a payment method
+ const handleRemovePaymentMethod = async (id: string) => {
+ try {
+ const response = await fetch(`/api/user/billing/payment-methods/${id}`, {
+ method: "DELETE"
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to remove payment method");
+ }
+
+ await fetchPaymentMethods();
+ toast.success("Payment method removed successfully");
+ } catch (error) {
+ console.error("Error removing payment method:", error);
+ toast.error("Failed to remove payment method");
+ }
+ };
+
+ // Handle setting a payment method as default
+ const handleSetDefaultPaymentMethod = async (id: string) => {
+ try {
+ const response = await fetch(`/api/user/billing/payment-methods/${id}`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ isDefault: true })
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to set default payment method");
+ }
+
+ await fetchPaymentMethods();
+ toast.success("Default payment method updated");
+ } catch (error) {
+ console.error("Error setting default payment method:", error);
+ toast.error("Failed to update default payment method");
+ }
+ };
+
+ // Handle submitting a dispute
+ const handleSubmitDispute = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!disputeForm.transactionId || !disputeForm.description) {
+ toast.error("Please fill in all required fields");
+ return;
+ }
+
+ setIsSubmittingDispute(true);
+
+ try {
+ const response = await fetch("/api/user/billing/disputes", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(disputeForm)
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to submit dispute");
+ }
+
+ setDisputeForm({
+ transactionId: "",
+ description: ""
+ });
+ toast.success("Dispute submitted successfully");
+ } catch (error) {
+ console.error("Error submitting dispute:", error);
+ toast.error("Failed to submit dispute");
+ } finally {
+ setIsSubmittingDispute(false);
+ }
+ };
+
+ return (
+
+
+
+
š³ Payments & Billing
+
Manage your transactions, invoices, and payment methods.
+
+
+ {billingSummary && (
+
+
+
+
+
Current Plan
+
+ {billingSummary.subscription ? billingSummary.subscription.name : "No active subscription"}
+
+
+
+ {billingSummary.subscription && (
+
+ {billingSummary.subscription.isActive ? "Active" : "Expired"}
+
+ )}
+
+
+
+ )}
+
+
+ {/* Tabs */}
+
+ {billingTabs.map(({ key, label, icon }) => (
+ setActiveTab(key)}
+ >
+ {React.createElement(icon, { className: "w-4 h-4" })} {label}
+
+ ))}
+
+
+ {/* Transactions */}
+ {activeTab === "transactions" && (
+
+ {/* Time Range Filter and Summary Cards */}
+
+
+
Transaction History
+
+
+
+
+
+
+
+ Last 7 Days
+ Last 30 Days
+ Last 90 Days
+ All Time
+
+
+
+
+
+
+
+
+
+
+
Total Spent
+
ā¦{transactionSummary.totalSpent.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
Subscriptions
+
ā¦{transactionSummary.subscriptionPayments.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
Ad Boosts
+
ā¦{transactionSummary.boostPayments.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
Refunds
+
ā¦{transactionSummary.refunds.toLocaleString()}
+
+
+
+
+
+
+
+
+ {/* Transactions Table */}
+
+
+ Transaction Details
+ View all your payment transactions
+
+
+ {isLoading ? (
+
+
+
+ ) : error ? (
+
+
+
{error}
+
fetchTransactions()}
+ >
+ Try Again
+
+
+ ) : transactions.length === 0 ? (
+
+
+
No transactions found
+
You haven't made any payments yet.
+
+ ) : (
+
+
+
+
+ Reference
+ Description
+ Date
+ Amount
+ Status
+
+
+
+ {transactions.map((transaction) => (
+
+ {transaction.reference}
+ {transaction.description}
+ {transaction.timeAgo}
+
+ {transaction.currency} {Number(transaction.amount).toLocaleString()}
+
+
+
+ {transaction.status}
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ )}
+
+ {/* Invoices */}
+ {activeTab === "invoices" && (
+
+ {/* Status Filter and Summary Cards */}
+
+
+
Invoices
+
+
+
+
+
+
+
+ All Invoices
+ Paid
+ Unpaid
+ Cancelled
+
+
+
+
+
+
+
+
+
+
+
Total Invoices
+
{invoiceSummary.count}
+
+
+
+
+
+
+
+
+
+
+
+
+
Paid
+
ā¦{invoiceSummary.totalPaid.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
Unpaid
+
ā¦{invoiceSummary.totalUnpaid.toLocaleString()}
+
+
+
+
+
+
+
+
+
+
+
+
+
Overdue
+
ā¦{invoiceSummary.totalOverdue.toLocaleString()}
+
+
+
+
+
+
+
+
+ {/* Invoices Table */}
+
+
+ Invoice Details
+ View and download your invoices
+
+
+ {isLoading ? (
+
+
+
+ ) : error ? (
+
+
+
{error}
+
fetchInvoices()}
+ >
+ Try Again
+
+
+ ) : invoices.length === 0 ? (
+
+
+
No invoices found
+
You don't have any invoices yet.
+
+ ) : (
+
+
+
+
+ Invoice #
+ Date
+ Due Date
+ Amount
+ Status
+ Actions
+
+
+
+ {invoices.map((invoice) => (
+
+ {invoice.invoiceNumber}
+ {invoice.formattedCreatedAt}
+ {invoice.formattedDueDate}
+
+ {invoice.currency} {Number(invoice.amount).toLocaleString()}
+
+
+
+ {invoice.status === "unpaid" && new Date(invoice.dueDate) < new Date()
+ ? "Overdue"
+ : invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)
+ }
+
+
+
+
+ Download
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ )}
+
+ {/* Payment Methods */}
+ {activeTab === "methods" && (
+
+
+
Payment Methods
+
+
setIsAddingPaymentMethod(!isAddingPaymentMethod)}
+ className="mt-2 md:mt-0"
+ >
+ {isAddingPaymentMethod ? (
+ <>Cancel>
+ ) : (
+ <> Add Payment Method>
+ )}
+
+
+
+ {/* Add Payment Method Form */}
+ {isAddingPaymentMethod && (
+
+
+ Add Payment Method
+ Enter your payment details
+
+
+
+
+
+ )}
+
+ {/* Payment Methods List */}
+
+
+ Saved Payment Methods
+ Manage your payment methods
+
+
+ {isLoading ? (
+
+
+
+ ) : error ? (
+
+
+
{error}
+
fetchPaymentMethods()}
+ >
+ Try Again
+
+
+ ) : paymentMethods.length === 0 ? (
+
+
+
No payment methods found
+
Add a payment method to get started.
+ {!isAddingPaymentMethod && (
+
setIsAddingPaymentMethod(true)}
+ variant="outline"
+ >
+ Add Payment Method
+
+ )}
+
+ ) : (
+
+ {paymentMethods.map((method) => (
+
+
+ {method.type === "card" && (
+
+
+
+ )}
+ {method.type === "paypal" && (
+
+
+
+ )}
+ {method.type === "bank_account" && (
+
+
+
+ )}
+
+
+
+ {method.displayName}
+ {method.isDefault && (
+
+ Default
+
+ )}
+
+
+ {method.type === "card" && method.expiryDisplay && `Expires ${method.expiryDisplay}`}
+ {method.type === "paypal" && "PayPal"}
+ {method.type === "bank_account" && "Bank Account"}
+
+
+
+
+
+ {!method.isDefault && (
+ handleSetDefaultPaymentMethod(method.id)}
+ >
+ Set Default
+
+ )}
+
+ handleRemovePaymentMethod(method.id)}
+ className="mt-4 h-8 px-3 text-red-500 hover:text-red-700 hover:bg-red-50"
+ >
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+ {/* Payment Disputes */}
+ {activeTab === "disputes" && (
+
+
+
+
Report a Payment Issue
+
If you have an issue with a payment, please fill out the form below.
+
+
+
+
+ {/* Dispute Form */}
+
+
+ Submit a Dispute
+ We'll review your issue and get back to you
+
+
+
+
+
+
+ {/* Dispute Information */}
+
+
+ Dispute Information
+ What happens after you submit a dispute
+
+
+
+
+
+
+
+
+
Submission
+
+ Your dispute will be submitted to our customer support team for review.
+
+
+
+
+
+
+
+
+
+
Review Process
+
+ Our team will review your dispute within 1-2 business days and may contact you for additional information.
+
+
+
+
+
+
+
+
+
+
Resolution
+
+ Once resolved, you'll receive a notification with the outcome of your dispute.
+
+
+
+
+
+
+ For urgent issues, please contact our support team directly at support@agromarket.com
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/components/BoostAdModal.tsx b/components/BoostAdModal.tsx
new file mode 100644
index 0000000..2b63a32
--- /dev/null
+++ b/components/BoostAdModal.tsx
@@ -0,0 +1,229 @@
+import React, { useState } from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { cn, formatCurrency } from "@/lib/utils";
+import { boostOptions } from "@/constants";
+import Alert from '@/components/Alerts';
+import { AlertsMsg } from '@/components/AlertsMsg';
+import { useSession } from "@/components/SessionWrapper";
+import { BoostAdModalProps, PaymentDetails } from "@/types";
+import PaymentModal from "@/components/PaymentModal";
+import { CheckCircle, Star, TrendingUp, BarChart2 } from "lucide-react";
+
+export default function BoostAdModal({ isOpen, onClose, ad, onBoost }: BoostAdModalProps) {
+ const { session } = useSession();
+ const [selectedBoost, setSelectedBoost] = useState(null);
+ const [selectedDuration, setSelectedDuration] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [showPayment, setShowPayment] = useState(false);
+ const [paymentDetails, setPaymentDetails] = useState(null);
+ const [alerts, setAlerts] = useState(false);
+ const [alertMessages, setAlertMessages] = useState();
+ const [alertTypes, setAlertTypes] = useState();
+
+
+ const handlePaymentSuccess = async (reference: string) => {
+ setIsLoading(true);
+ try {
+ // Verify payment server-side
+ const verifyResponse = await fetch('/api/payments/verify', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ reference, type: paymentDetails?.type, adId: paymentDetails?.adId, boostType: paymentDetails?.boostType, boostDuration: paymentDetails?.boostDuration })
+ });
+
+ const verifyData = await verifyResponse.json();
+
+ if (verifyData.success) {
+ // Update local state
+ await onBoost(ad.id, selectedBoost!, selectedDuration!);
+ // Show success message
+ setAlerts(true);
+ setAlertTypes('success');
+ setAlertMessages('Ad boosted successfully!');
+
+ // Close modal
+ onClose();
+
+ // Optionally refresh the page to show updated subscription status
+ window.location.href = '/dashboard/promotions';
+ } else {
+ console.error('Backend verification response:', verifyData);
+ const backendError = verifyData.message || verifyData.error || 'Unknown verification error';
+ throw new Error(`Payment verification failed: ${backendError}`);
+ }
+ } catch (error) {
+ console.error('Error processing payment:', error);
+ setAlerts?.(true);
+ setAlertTypes?.('error');
+ setAlertMessages?.(error instanceof Error ? error.message : 'Payment verification failed');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleBoostClick = async () => {
+ // Add console.log to debug state values
+ console.log({
+ selectedBoost,
+ selectedDuration,
+ sessionEmail: session?.email,
+ isLoading
+ });
+
+ if (!selectedBoost || !selectedDuration || !session?.email) {
+ console.log('Validation failed:', {
+ hasBoost: !!selectedBoost,
+ hasDuration: !!selectedDuration,
+ hasEmail: !!session?.email
+ });
+ return;
+ }
+
+ const option = boostOptions.find(opt => opt.id === selectedBoost);
+ if (!option) {
+ console.log('No matching boost option found');
+ return;
+ }
+
+ const price = option.price[selectedDuration as keyof typeof option.price];
+
+ const reference = `boost_${ad.id}_${Date.now()}`;
+ const details: PaymentDetails = {
+ email: session.email,
+ amount: price,
+ reference,
+ plan: option.name,
+ adId: ad.id,
+ boostType: selectedBoost,
+ boostDuration: selectedDuration,
+ type: 'boost'
+ };
+
+ setPaymentDetails(details);
+ setShowPayment(true);
+ };
+
+ // Update the duration click handler to prevent event bubbling
+ const handleDurationClick = (e: React.MouseEvent, days: number, optionId: number) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setSelectedBoost(optionId);
+ setSelectedDuration(days);
+ };
+
+ const getPriceDisplay = (option: typeof boostOptions[0]) => {
+ if (selectedBoost === option.id && selectedDuration && (selectedDuration === 7 || selectedDuration === 14)) {
+ return formatCurrency(option.price[selectedDuration as 7 | 14]);
+ }
+ // Show price range if no duration selected
+ return `${formatCurrency(option.price[7])} - ${formatCurrency(option.price[14])}`;
+ };
+
+ return (
+ <>
+
+ {alerts && }
+
+
+ Boost Ad: {ad.title}
+
+
+
+
Select Boost Option
+
+ {boostOptions.map((option) => (
+
setSelectedBoost(option.id)}
+ >
+
{option.name}
+
{getPriceDisplay(option)}
+
+ {/* Add features list */}
+
+ {option.features.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+ {option.duration.map((days) => (
+ handleDurationClick(e, days, option.id)}
+ >
+ {days} days
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* Add boost benefits section */}
+
+
Boost Benefits
+
+
+
+ Increased visibility in search results
+
+
+
+ Higher engagement rates
+
+
+
+ Detailed performance analytics
+
+
+
+
+
+
+ Cancel
+
+
+ {isLoading ? 'Processing...' : 'Boost Ad'}
+
+
+
+
+
+ {showPayment && paymentDetails && (
+ setShowPayment(false)}
+ paymentDetails={paymentDetails}
+ onSuccess={handlePaymentSuccess}
+ />
+ )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/components/CallToAction.tsx b/components/CallToAction.tsx
new file mode 100644
index 0000000..50d2425
--- /dev/null
+++ b/components/CallToAction.tsx
@@ -0,0 +1,37 @@
+import Link from 'next/link';
+
+const CallToAction = () => {
+ return (
+
+
+
+ Join the Agro Revolution
+
+
+ Sign up to access fresh produce, connect with farmers, and be part of a sustainable marketplace. Start your journey today!
+
+
+
+ {/* Sign Up Button */}
+
+ Sign Up Now
+
+
+ {/* Explore Marketplace Button */}
+
+ Explore Marketplace
+
+
+
+
+ );
+ };
+
+ export default CallToAction;
+
\ No newline at end of file
diff --git a/components/CategoriesSavedSearchesMain.tsx b/components/CategoriesSavedSearchesMain.tsx
new file mode 100644
index 0000000..0f5b7d1
--- /dev/null
+++ b/components/CategoriesSavedSearchesMain.tsx
@@ -0,0 +1,297 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Bookmark, Bell, X, Loader2 } from "lucide-react";
+import toast from "react-hot-toast";
+import { categories } from "@/constants";
+
+interface SavedSearch {
+ id: string;
+ query: string;
+ alertsEnabled: boolean;
+ category?: string;
+ location?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export default function CategoriesSavedSearches() {
+ const [savedSearches, setSavedSearches] = useState([]);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(null);
+ const [isTogglingAlerts, setIsTogglingAlerts] = useState(null);
+
+ useEffect(() => {
+ fetchSavedSearches();
+ }, []);
+
+ const fetchSavedSearches = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch("/api/user/saved-searches", {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setSavedSearches(data.savedSearches || []);
+ } else {
+ toast.error("Failed to load saved searches");
+ }
+ } catch (error) {
+ console.error("Error fetching saved searches:", error);
+ toast.error("Failed to load saved searches");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const saveSearch = async () => {
+ if (!searchQuery.trim()) {
+ toast.error("Enter a search term to save.");
+ return;
+ }
+
+ // Check if search already exists
+ if (savedSearches.some(search => search.query === searchQuery)) {
+ toast.error("Search already saved.");
+ return;
+ }
+
+ try {
+ setIsSaving(true);
+ const response = await fetch("/api/user/saved-searches", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ query: searchQuery }),
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setSavedSearches([...savedSearches, data.savedSearch]);
+ toast.success("Search saved successfully.");
+ setSearchQuery("");
+ } else {
+ toast.error("Failed to save search");
+ }
+ } catch (error) {
+ console.error("Error saving search:", error);
+ toast.error("Failed to save search");
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const removeSavedSearch = async (id: string) => {
+ try {
+ setIsDeleting(id);
+ const response = await fetch(`/api/user/saved-searches?id=${id}`, {
+ method: "DELETE",
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ setSavedSearches(savedSearches.filter((search) => search.id !== id));
+ toast.success("Search removed.");
+ } else {
+ toast.error("Failed to remove search");
+ }
+ } catch (error) {
+ console.error("Error removing search:", error);
+ toast.error("Failed to remove search");
+ } finally {
+ setIsDeleting(null);
+ }
+ };
+
+ const toggleAlerts = async (id: string, currentState: boolean) => {
+ try {
+ setIsTogglingAlerts(id);
+ const response = await fetch(`/api/user/saved-searches/${id}/alerts`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ alertsEnabled: !currentState }),
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ // Update the local state
+ setSavedSearches(savedSearches.map(search =>
+ search.id === id
+ ? { ...search, alertsEnabled: !currentState }
+ : search
+ ));
+ toast.success(`Search alerts ${!currentState ? 'enabled' : 'disabled'}!`);
+ } else {
+ toast.error("Failed to update search alerts");
+ }
+ } catch (error) {
+ console.error("Error toggling search alerts:", error);
+ toast.error("Failed to update search alerts");
+ } finally {
+ setIsTogglingAlerts(null);
+ }
+ };
+
+ return (
+
+
š Categories & Saved Searches
+
Personalize your search experience.
+
+ {/* Categories Section */}
+
+
š Browse Categories
+
+ {categories.map((category) => (
+
+ {category.name}
+
+ ))}
+
+
+
+ {/* Saved Searches */}
+
+
š Saved Searches
+
+ setSearchQuery(e.target.value)}
+ className="flex-1 p-3 border rounded-md focus:ring-green-500 focus:border-green-500"
+ />
+
+ {isSaving ? : } Save
+
+
+
+ {/* List of Saved Searches */}
+ {loading ? (
+
+
+
+ ) : savedSearches.length > 0 ? (
+
+ {savedSearches.map((search) => (
+
+
+
+ {search.query}
+
+ {search.alertsEnabled ? 'Alerts ON' : 'Alerts OFF'}
+
+
+
+ Saved on {new Date(search.createdAt).toLocaleDateString()}
+ {search.category && ` ⢠Category: ${search.category}`}
+ {search.location && ` ⢠Location: ${search.location}`}
+
+
+
+ toggleAlerts(search.id, search.alertsEnabled)}
+ disabled={isTogglingAlerts === search.id}
+ className={`p-2 rounded-md transition ${
+ search.alertsEnabled
+ ? 'text-green-600 hover:bg-green-50'
+ : 'text-gray-400 hover:bg-gray-100'
+ }`}
+ title={search.alertsEnabled ? 'Disable alerts' : 'Enable alerts'}
+ >
+ {isTogglingAlerts === search.id ? (
+
+ ) : (
+
+ )}
+
+ removeSavedSearch(search.id)}
+ className="p-2 text-red-500 hover:bg-red-50 rounded-md transition"
+ disabled={isDeleting === search.id}
+ title="Delete saved search"
+ >
+ {isDeleting === search.id ? : }
+
+
+
+ ))}
+
+ ) : (
+
No saved searches yet.
+ )}
+
+
+ {/* Search Alerts Summary */}
+
+
š Search Alerts Summary
+
Get notified when new ads match your saved searches.
+
+ {savedSearches.length > 0 ? (
+
+
+
+
+ {savedSearches.filter(s => s.alertsEnabled).length} of {savedSearches.length} searches have alerts enabled
+
+
+
+ {savedSearches.filter(s => !s.alertsEnabled).length > 0 && (
+ {
+ // Enable alerts for all searches that don't have them
+ savedSearches
+ .filter(s => !s.alertsEnabled)
+ .forEach(search => toggleAlerts(search.id, false));
+ }}
+ disabled={isTogglingAlerts !== null}
+ className="text-sm bg-green-600 text-white px-3 py-1 rounded-md hover:bg-green-700 transition"
+ >
+ Enable All Alerts
+
+ )}
+ {savedSearches.filter(s => s.alertsEnabled).length > 0 && (
+ {
+ // Disable alerts for all searches that have them
+ savedSearches
+ .filter(s => s.alertsEnabled)
+ .forEach(search => toggleAlerts(search.id, true));
+ }}
+ disabled={isTogglingAlerts !== null}
+ className="text-sm bg-gray-500 text-white px-3 py-1 rounded-md hover:bg-gray-600 transition"
+ >
+ Disable All Alerts
+
+ )}
+
+
+
+ ) : (
+
+
Save some searches first to set up alerts!
+
+ )}
+
+
+ );
+}
diff --git a/components/CommunitySection.tsx b/components/CommunitySection.tsx
new file mode 100644
index 0000000..9828ae1
--- /dev/null
+++ b/components/CommunitySection.tsx
@@ -0,0 +1,239 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import { ChatBubbleLeftRightIcon, UserGroupIcon, CalendarIcon } from "@heroicons/react/24/outline";
+
+// Sample testimonials
+const testimonials = [
+ {
+ id: 1,
+ content: "AgroMarket has transformed how I sell my produce. I now reach customers across Nigeria without middlemen cutting into my profits.",
+ author: "Adamu Ibrahim",
+ role: "Tomato Farmer, Kano",
+ avatar: "/assets/img/testimonials/farmer1.jpg"
+ },
+ {
+ id: 2,
+ content: "The quality of produce I get through AgroMarket is exceptional. I love knowing exactly where my food comes from and supporting local farmers.",
+ author: "Chioma Okafor",
+ role: "Restaurant Owner, Lagos",
+ avatar: "/assets/img/testimonials/customer1.jpg"
+ },
+ {
+ id: 3,
+ content: "As a small-scale farmer, AgroMarket has given me access to markets I never thought possible. My income has increased by 40% in just six months!",
+ author: "Emmanuel Osei",
+ role: "Cassava Farmer, Ogun",
+ avatar: "/assets/img/testimonials/farmer2.jpg"
+ }
+];
+
+// Sample upcoming events
+const events = [
+ {
+ id: 1,
+ title: "Farmer's Market Day",
+ date: "June 15, 2024",
+ location: "Lagos State Agricultural Development Center",
+ image: "/assets/img/events/farmers-market.jpg"
+ },
+ {
+ id: 2,
+ title: "Sustainable Farming Workshop",
+ date: "July 8, 2024",
+ location: "Virtual Event",
+ image: "/assets/img/events/workshop.jpg"
+ },
+ {
+ id: 3,
+ title: "Agricultural Technology Expo",
+ date: "August 22-24, 2024",
+ location: "Abuja International Conference Center",
+ image: "/assets/img/events/agri-tech.jpg"
+ }
+];
+
+export default function CommunitySection() {
+ const [isVisible, setIsVisible] = useState(false);
+ const [activeTestimonial, setActiveTestimonial] = useState(0);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const element = document.getElementById('community');
+ if (element) {
+ const position = element.getBoundingClientRect();
+ if (position.top < window.innerHeight - 100) {
+ setIsVisible(true);
+ }
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ handleScroll(); // Check on initial load
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ // Auto-rotate testimonials
+ useEffect(() => {
+ if (!isVisible) return;
+
+ const interval = setInterval(() => {
+ setActiveTestimonial((prev) => (prev + 1) % testimonials.length);
+ }, 5000);
+
+ return () => clearInterval(interval);
+ }, [isVisible]);
+
+ return (
+
+ );
+}
diff --git a/components/Container.tsx b/components/Container.tsx
new file mode 100644
index 0000000..bd24198
--- /dev/null
+++ b/components/Container.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+
+interface ContainerProps {
+ children: React.ReactNode;
+ className?: string;
+}
+
+export function Container(props: Readonly) {
+ return (
+
+ {props.children}
+
+ );
+}
+
diff --git a/components/CookieConsent.tsx b/components/CookieConsent.tsx
new file mode 100644
index 0000000..e8f40dc
--- /dev/null
+++ b/components/CookieConsent.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { X } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+
+export default function CookieConsent() {
+ const [showBanner, setShowBanner] = useState(false);
+
+ useEffect(() => {
+ // Check if user has already consented
+ const hasConsented = localStorage.getItem("cookieConsent");
+ if (!hasConsented) {
+ setShowBanner(true);
+ }
+ }, []);
+
+ const acceptAll = () => {
+ localStorage.setItem("cookieConsent", "all");
+ setShowBanner(false);
+ };
+
+ const acceptEssential = () => {
+ localStorage.setItem("cookieConsent", "essential");
+ setShowBanner(false);
+ };
+
+ const closeBanner = () => {
+ setShowBanner(false);
+ };
+
+ if (!showBanner) return null;
+
+ return (
+
+
+
+
+
Cookie Consent
+
+ We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.
+
+ Read our Cookie Policy
+
+
+
+
+
+
+ Essential Only
+
+
+ Accept All
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/CustomerNavbar.tsx b/components/CustomerNavbar.tsx
new file mode 100644
index 0000000..ca66641
--- /dev/null
+++ b/components/CustomerNavbar.tsx
@@ -0,0 +1,250 @@
+"use client";
+
+import React, { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { useRouter, usePathname } from "next/navigation";
+import { useSession } from "@/components/SessionWrapper";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
+import { io, Socket } from "socket.io-client";
+import {
+ LogOut,
+ Menu,
+ X,
+ Bell,
+ Users,
+ CheckCircle,
+ Clock,
+ XCircle,
+ AlertCircle
+} from "lucide-react";
+import { NAV_ITEMS, SETTINGS } from "@/constants";
+import { useUnreadNotifications } from "@/lib/hooks/useUnreadNotifications";
+import { useNotifications } from "@/lib/hooks/useNotifications";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import toast from "react-hot-toast";
+
+// Socket connection pool
+const socketPool = new Map();
+
+export default function CustomerNavbar() {
+ const pathname = usePathname();
+ const { session, setSession } = useSession();
+ const router = useRouter();
+ const [isOpen, setIsOpen] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const socketRef = useRef(null);
+
+ // Use our custom hooks for notifications
+ const { unreadCount: unreadNotifications, refreshUnreadCount } = useUnreadNotifications();
+ const { notifications: allNotifications, refreshNotifications } = useNotifications();
+
+ // Memoize recent notifications
+ const recentNotifications = useMemo(() =>
+ allNotifications?.slice(0, 3) || [],
+ [allNotifications]
+ );
+
+ // Memoize current tab
+ const currentTab = useMemo(() => {
+ if (pathname === "/dashboard") return "dashboard";
+ if (pathname.includes("?tab=")) {
+ return pathname.split("?tab=")[1];
+ }
+ return pathname.split("/").pop();
+ }, [pathname]);
+
+ // Initialize socket connection for real-time notifications
+ useEffect(() => {
+ if (!session?.token) return;
+
+ const socketKey = session.token;
+
+ // Check if socket already exists in pool
+ if (socketPool.has(socketKey)) {
+ socketRef.current = socketPool.get(socketKey)!;
+ return;
+ }
+
+ // Create new socket connection
+ const socket = io(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000', {
+ path: '/api/socketio',
+ auth: { token: session.token },
+ transports: ['websocket'],
+ reconnectionAttempts: 3,
+ reconnectionDelay: 1000,
+ timeout: 5000
+ });
+
+ socket.on('connect', () => {
+ console.log('Connected to WebSocket for notifications');
+ });
+
+ socket.on('notification_received', (notification) => {
+ refreshUnreadCount();
+ refreshNotifications();
+ toast.success(notification.message, {
+ duration: 5000,
+ icon: 'š'
+ });
+ });
+
+ // Add to pool and store reference
+ socketPool.set(socketKey, socket);
+ socketRef.current = socket;
+
+ return () => {
+ if (socketRef.current) {
+ socketRef.current.disconnect();
+ socketPool.delete(socketKey);
+ }
+ };
+ }, [session?.token, refreshUnreadCount, refreshNotifications]);
+
+ const handleLogout = useCallback(async () => {
+ try {
+ setIsLoggingOut(true);
+ setSession(null);
+
+ // Clear cookies
+ document.cookie.split(";").forEach(cookie => {
+ const cookieName = cookie.split("=")[0].trim();
+ if (cookieName.includes("next-auth")) {
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=${window.location.hostname}`;
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ }
+ });
+
+ // Disconnect socket
+ if (socketRef.current) {
+ socketRef.current.disconnect();
+ socketPool.delete(session?.token || '');
+ }
+
+ await fetch('/api/logout', {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ router.push('/signin');
+ toast.success("Logged out successfully");
+ } catch (error) {
+ console.error('Logout error:', error);
+ toast.error("Failed to log out");
+ } finally {
+ setIsLoggingOut(false);
+ }
+ }, [session?.token, setSession, router]);
+
+ // Memoize notification icon getter
+ const getNotificationIcon = useCallback((type: string) => {
+ switch (type) {
+ case "ad": return ;
+ case "promotion": return ;
+ case "payment": return ;
+ case "payment-failed": return ;
+ default: return ;
+ }
+ }, []);
+
+ // Memoize navigation items
+ const navItems = useMemo(() =>
+ NAV_ITEMS.filter(item => !item.adminOnly || session?.role === "admin"),
+ [session?.role]
+ );
+
+ return (
+
+ {/* Mobile menu button */}
+
setIsOpen(!isOpen)}
+ >
+ {isOpen ? : }
+
+
+ {/* Sidebar */}
+
+
+ {/* Logo and title */}
+
+
+ {/* Navigation */}
+
+ {navItems.map((item) => {
+ const isActive = pathname === item.route ||
+ (pathname === "/dashboard" && item.route === "/dashboard") ||
+ (pathname === "/dashboard" && item.route.includes(`/dashboard/${currentTab}`));
+
+ const badge = item.name === "Notifications" && unreadNotifications > 0 ? (
+
+ {unreadNotifications}
+
+ ) : null;
+
+ return (
+
+
+ {item.name}
+ {badge}
+
+ );
+ })}
+
+
+ {/* Settings */}
+
+ {SETTINGS.map((item) => (
+ item.action ? (
+
+
+ {item.name}
+
+ ) : (
+
+
+ {item.name}
+
+ )
+ ))}
+
+
+
+
+ );
+}
diff --git a/components/DashboardLayout.tsx b/components/DashboardLayout.tsx
new file mode 100644
index 0000000..aa798a2
--- /dev/null
+++ b/components/DashboardLayout.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import React from "react";
+import dynamic from "next/dynamic";
+
+
+const CustomerNavbar = dynamic(() => import("@/components/CustomerNavbar"), { ssr: false });
+
+export default function DashboardLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {/* Sidebar Navigation */}
+
+
+ {/* Main Content Area */}
+
+ {children}
+
+
+
+ );
+}
+
diff --git a/components/DashboardMain.tsx b/components/DashboardMain.tsx
new file mode 100644
index 0000000..64eb8fa
--- /dev/null
+++ b/components/DashboardMain.tsx
@@ -0,0 +1,552 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Table, TableHeader, TableBody, TableRow, TableCell, TableHead } from "@/components/ui/table";
+import { ActivityFeed } from "@/components/ActivityFeed";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Badge } from "@/components/ui/badge";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import {
+ Plus,
+ Rocket,
+ Zap,
+ BarChart,
+ Inbox,
+ BookmarkCheck,
+ Bell,
+ User,
+ CreditCard,
+ HelpCircle,
+ Loader2,
+ Eye,
+ MousePointer,
+ Share2,
+ TrendingUp,
+ AlertCircle,
+ CheckCircle,
+ Clock,
+ Image as ImageIcon
+} from "lucide-react";
+import toast from "react-hot-toast";
+import Spinner from "@/components/Spinner";
+import AnalyticsMain from "@/components/AnalyticsMain";
+import MessagesMain from "@/components/MessagesMain";
+import CategoriesSavedSearchesMain from "@/components/CategoriesSavedSearchesMain";
+import NotificationsMain from "@/components/NotificationsMain";
+import ProfileMain from "@/components/ProfileMain";
+import BillingMain from "@/components/BillingMain";
+import UserSupportTickets from "@/components/UserSupportTickets";
+
+interface DashboardMainProps {
+ defaultTab?: string;
+}
+
+export default function DashboardMain({ defaultTab = "dashboard" }: DashboardMainProps) {
+ // State for mobile view and active tab
+ const router = useRouter();
+ const [isMobile, setIsMobile] = useState(false);
+ const [isPosting, setIsPosting] = useState(false);
+ const [activeTab, setActiveTab] = useState(defaultTab);
+ const [isLoading, setIsLoading] = useState(true);
+ const [timeRange, setTimeRange] = useState('30days');
+ const [error, setError] = useState(null);
+ const [dashboardData, setDashboardData] = useState({
+ adPerformance: {
+ activeAds: 0,
+ totalViews: 0,
+ totalClicks: 0,
+ totalShares: 0,
+ boostedAds: 0,
+ engagementRate: '0%'
+ },
+ promotionSummary: {
+ ongoingPromotions: 0,
+ earningsFromPromotions: 0
+ },
+ recentActivity: [],
+ adPerformanceTable: [],
+ chartData: {
+ dailyLabels: [],
+ dailyViews: [],
+ dailyClicks: []
+ },
+ categoryDistribution: [],
+ subscription: null,
+ timeRange: '30days'
+ });
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsMobile(window.innerWidth < 640);
+ };
+
+ handleResize(); // Initial check
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ // Update URL when tab changes
+ useEffect(() => {
+ // Update the URL with the current tab without full page reload
+ const url = new URL(window.location.href);
+ if (activeTab === "dashboard") {
+ url.searchParams.delete("tab");
+ } else {
+ url.searchParams.set("tab", activeTab);
+ }
+ window.history.pushState({}, "", url.toString());
+ }, [activeTab]);
+
+ // Update active tab when defaultTab changes
+ useEffect(() => {
+ if (defaultTab) {
+ setActiveTab(defaultTab);
+ }
+ }, [defaultTab]);
+
+ // Fetch dashboard data
+ useEffect(() => {
+ const fetchDashboardData = async () => {
+ if (activeTab === "dashboard") {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const response = await fetch(`/api/user/dashboard?timeRange=${timeRange}`, {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setDashboardData(data);
+ } else {
+ const errorData = await response.json();
+ console.error("Failed to fetch dashboard data:", errorData);
+ setError(errorData.error || "Failed to fetch dashboard data");
+ toast.error("Failed to load dashboard data. Please try again.");
+ }
+ } catch (error) {
+ console.error("Error fetching dashboard data:", error);
+ setError("An unexpected error occurred. Please try again.");
+ toast.error("Failed to load dashboard data. Please try again.");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ fetchDashboardData();
+ }, [activeTab, timeRange]);
+
+
+ return (
+
+ {/* Page Title */}
+
+ {!isMobile ? (Dashboard ) : (Dashboard )}
+ { setIsPosting(true); router.push("/dashboard/new-ad"); setIsPosting(false); }} className="flex bg-green-600 text-white hover:bg-green-700 text-sm">
+ {!isMobile ? (<> {isPosting ? : 'Post New Ad'}>) : (<> {isPosting ? : 'Post New Ad'} >)}
+
+
+
+ {/* Dashboard Tabs */}
+
+
+ router.push("/dashboard")}
+ >
+ Dashboard
+
+ router.push("/dashboard?tab=analytics")}
+ >
+ Analytics
+
+ router.push("/dashboard?tab=messages")}
+ >
+ Messages
+
+ router.push("/dashboard?tab=saved-searches")}
+ >
+ Saved Searches
+
+ router.push("/dashboard?tab=notifications")}
+ >
+ Notifications
+
+ router.push("/dashboard?tab=profile")}
+ >
+ Profile
+
+ router.push("/dashboard?tab=billing")}
+ >
+ Billing
+
+ router.push("/dashboard?tab=support")}
+ >
+ Support
+
+
+
+ {/* Dashboard Overview Tab */}
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {/* Time Range Filter */}
+
+
Dashboard Overview
+
+ Time Range:
+ setTimeRange(value)}
+ >
+
+
+
+
+ Last 7 Days
+ Last 30 Days
+ Last 90 Days
+ All Time
+
+
+
+
+
+ {/* Summary Cards */}
+
+ {/* Active Ads */}
+
+
+
+
+ Active Ads
+
+
+
+
+ {dashboardData.adPerformance.activeAds}
+ Total active listings
+
+
+
+
+ {/* Total Views */}
+
+
+
+
+ Total Views
+
+
+
+
+ {dashboardData.adPerformance.totalViews.toLocaleString()}
+ Ad impressions
+
+
+
+
+ {/* Total Clicks */}
+
+
+
+
+ Total Clicks
+
+
+
+
+ {dashboardData.adPerformance.totalClicks.toLocaleString()}
+ User interactions
+
+
+
+
+ {/* Engagement Rate */}
+
+
+
+
+ Engagement Rate
+
+
+
+
+ {dashboardData.adPerformance.engagementRate}
+ Click-through rate
+
+
+
+
+
+ {/* Second Row of Cards */}
+
+ {/* Shares & Boosted Ads */}
+
+
+ Additional Metrics
+
+
+
+
+
+
+ Total Shares:
+
+ {dashboardData.adPerformance.totalShares.toLocaleString()}
+
+
+
+
+ Boosted Ads:
+
+ {dashboardData.adPerformance.boostedAds}
+
+
+
+
+
+ {/* Promotion Summary */}
+
+
+ Promotion Summary
+
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+
router.push("/dashboard/new-ad")} variant="default" className="w-full flex items-center justify-center bg-green-600 hover:bg-green-700">
+ Post New Ad
+
+
router.push("/dashboard/promotions")} variant="outline" className="w-full flex items-center justify-center">
+ Boost Ads
+
+
setActiveTab("analytics")} variant="outline" className="w-full flex items-center justify-center">
+ View Analytics
+
+
+
+
+
+
+ {/* Recent Activity Feed */}
+
+
+
Recent Activity
+ {dashboardData.recentActivity && dashboardData.recentActivity.length > 0 && (
+ router.push("/dashboard/notifications")}>
+ View All
+
+ )}
+
+
+ {dashboardData.recentActivity && dashboardData.recentActivity.length > 0 ? (
+
+ {dashboardData.recentActivity.map((activity: any, index: number) => (
+
+
+ {activity.type === 'view' &&
}
+ {activity.type === 'click' &&
}
+ {activity.type === 'share' &&
}
+ {activity.type === 'boost' &&
}
+ {activity.type === 'info' &&
}
+ {!activity.type &&
}
+
+
+
{activity.description}
+
+ {activity.time}
+ {activity.read === false && (
+ New
+ )}
+
+
+
+ ))}
+
+ ) : (
+
+
+
No recent activity to display
+
Your activity will appear here
+
+ )}
+
+
+ {/* Ad Performance Table */}
+
+
+
Ad Performance
+ router.push("/dashboard/my-ads")}>
+ View All Ads
+
+
+
+
+
+
+
+ Ad
+ Views
+ Clicks
+ CTR
+ Status
+
+
+
+ {dashboardData.adPerformanceTable && dashboardData.adPerformanceTable.length > 0 ? (
+ dashboardData.adPerformanceTable.map((ad: any, index: number) => (
+
+
+
+ {ad.image ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
{ad.title}
+ {ad.category &&
{ad.category}
}
+
+
+
+ {ad.views.toLocaleString()}
+ {ad.clicks.toLocaleString()}
+ {ad.ctr}%
+
+
+ {ad.status}
+
+
+
+ ))
+ ) : (
+
+
+
+
+
No ad performance data available
+
router.push("/dashboard/new-ad")}
+ className="text-green-600 mt-2"
+ >
+ Post your first ad
+
+
+
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+ {/* Analytics Tab */}
+
+
+
+
+ {/* Messages Tab */}
+
+
+
+
+ {/* Saved Searches Tab */}
+
+
+
+
+ {/* Notifications Tab */}
+
+
+
+
+ {/* Profile Settings Tab */}
+
+
+
+
+ {/* Billing Tab */}
+
+
+
+
+ {/* Support Tab */}
+
+
+
+
+
+ );
+}
diff --git a/components/EditAdMain.tsx b/components/EditAdMain.tsx
new file mode 100644
index 0000000..83937c3
--- /dev/null
+++ b/components/EditAdMain.tsx
@@ -0,0 +1,589 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter, useParams } from "next/navigation";
+import FileUpload from "@/components/ui/file-upload";
+import { Loader2, AlertCircle, ArrowLeft, Save } from "lucide-react";
+import { navigation } from "@/constants";
+import { mapUiCategoryToDatabase } from "@/lib/categoryUtils";
+import { motion } from "framer-motion";
+import { showToast } from "@/lib/toast-utils";
+import Link from "next/link";
+
+type FileState = File[];
+
+interface FormErrors {
+ title?: string;
+ category?: string;
+ subcategory?: string;
+ location?: string;
+ price?: string;
+ description?: string;
+ contact?: string;
+ images?: string;
+}
+
+interface Ad {
+ id: string;
+ title: string;
+ category: string;
+ subcategory?: string;
+ section?: string;
+ location: string;
+ price: string;
+ description: string;
+ contact: string;
+ images: string[];
+ subscriptionPlanId?: string;
+}
+
+export default function EditAdMain() {
+ const router = useRouter();
+ const params = useParams();
+ const adId = params?.id as string;
+
+ const [formData, setFormData] = useState({
+ title: "",
+ category: "",
+ subcategory: "",
+ section: "",
+ location: "",
+ price: "",
+ description: "",
+ contact: "",
+ subscriptionPlanId: ""
+ });
+
+ const [errors, setErrors] = useState({});
+ const [availableSubcategories, setAvailableSubcategories] = useState<{ name: string }[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [initialLoading, setInitialLoading] = useState(true);
+ const [selectedFiles, setSelectedFiles] = useState([]);
+ const [existingImages, setExistingImages] = useState([]);
+ const [formSubmitted, setFormSubmitted] = useState(false);
+ const [ad, setAd] = useState(null);
+
+ // Fetch existing ad data
+ useEffect(() => {
+ const fetchAd = async () => {
+ if (!adId) return;
+
+ try {
+ const response = await fetch(`/api/ads/${adId}`, {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ router.push('/signin');
+ return;
+ }
+ throw new Error('Failed to fetch ad data');
+ }
+
+ const data = await response.json();
+ const adData = data.ad;
+ setAd(adData);
+
+ // Populate form with existing data
+ setFormData({
+ title: adData.title || "",
+ category: adData.category || "",
+ subcategory: adData.subcategory || "",
+ section: adData.section || "",
+ location: adData.location || "",
+ price: adData.price || "",
+ description: adData.description || "",
+ contact: adData.contact || "",
+ subscriptionPlanId: adData.subscriptionPlanId || ""
+ });
+
+ setExistingImages(adData.images || []);
+ } catch (error) {
+ console.error('Error fetching ad:', error);
+ showToast({ message: 'Failed to load ad data', type: 'error' });
+ router.push('/dashboard/my-ads');
+ } finally {
+ setInitialLoading(false);
+ }
+ };
+
+ fetchAd();
+ }, [adId, router]);
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+
+ // If changing category, reset subcategory and update available subcategories
+ if (name === 'category') {
+ setFormData({
+ ...formData,
+ [name]: value,
+ subcategory: '',
+ section: ''
+ });
+ } else if (name === 'subcategory') {
+ setFormData({
+ ...formData,
+ subcategory: value
+ });
+ } else {
+ setFormData({ ...formData, [name]: value });
+ }
+ };
+
+ // Map UI categories to navigation categories
+ const getCategoryMapping = (uiCategory: string) => {
+ return mapUiCategoryToDatabase(uiCategory);
+ };
+
+ // Update subcategories when category changes
+ useEffect(() => {
+ if (formData.category) {
+ const mappedCategory = getCategoryMapping(formData.category);
+ const categoryData = navigation.categories.find(
+ cat => cat.name === mappedCategory
+ );
+
+ if (categoryData) {
+ const subcategories = categoryData.items
+ .filter(item => item.name !== 'Browse All')
+ .map(item => ({ name: item.name }));
+
+ setAvailableSubcategories(subcategories);
+ } else {
+ setAvailableSubcategories([]);
+ }
+ } else {
+ setAvailableSubcategories([]);
+ }
+ }, [formData.category]);
+
+ const handleFileChange = (files: File[]) => {
+ setSelectedFiles(files);
+ if (files.length > 0 && errors.images) {
+ setErrors(prev => ({ ...prev, images: undefined }));
+ }
+ };
+
+ const validateForm = (): boolean => {
+ const newErrors: FormErrors = {};
+ let isValid = true;
+
+ // Title validation
+ if (!formData.title.trim()) {
+ newErrors.title = "Title is required";
+ isValid = false;
+ } else if (formData.title.length < 5) {
+ newErrors.title = "Title must be at least 5 characters";
+ isValid = false;
+ } else if (formData.title.length > 100) {
+ newErrors.title = "Title must be less than 100 characters";
+ isValid = false;
+ }
+
+ // Category validation
+ if (!formData.category) {
+ newErrors.category = "Category is required";
+ isValid = false;
+ }
+
+ // Subcategory validation
+ if (formData.category && availableSubcategories.length > 0 && !formData.subcategory) {
+ newErrors.subcategory = "Subcategory is required";
+ isValid = false;
+ }
+
+ // Location validation
+ if (!formData.location.trim()) {
+ newErrors.location = "Location is required";
+ isValid = false;
+ }
+
+ // Price validation
+ if (!formData.price) {
+ newErrors.price = "Price is required";
+ isValid = false;
+ } else {
+ const price = parseFloat(formData.price);
+ if (isNaN(price)) {
+ newErrors.price = "Price must be a number";
+ isValid = false;
+ } else if (price <= 0) {
+ newErrors.price = "Price must be greater than 0";
+ isValid = false;
+ } else if (price > 99999999) {
+ newErrors.price = "Price is too high";
+ isValid = false;
+ }
+ }
+
+ // Description validation
+ if (!formData.description.trim()) {
+ newErrors.description = "Description is required";
+ isValid = false;
+ } else if (formData.description.length < 20) {
+ newErrors.description = "Description must be at least 20 characters";
+ isValid = false;
+ } else if (formData.description.length > 2000) {
+ newErrors.description = "Description must be less than 2000 characters";
+ isValid = false;
+ }
+
+ // Contact validation
+ if (!formData.contact.trim()) {
+ newErrors.contact = "Contact information is required";
+ isValid = false;
+ } else {
+ const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.contact);
+ const isPhone = /^[0-9+\-\s()]{7,20}$/.test(formData.contact);
+
+ if (!isEmail && !isPhone) {
+ newErrors.contact = "Please enter a valid email or phone number";
+ isValid = false;
+ }
+ }
+
+ // Image validation - allow existing images or new files
+ if (existingImages.length === 0 && selectedFiles.length === 0) {
+ newErrors.images = "Please keep existing images or upload new ones";
+ isValid = false;
+ }
+
+ setErrors(newErrors);
+ return isValid;
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setFormSubmitted(true);
+
+ if (!validateForm()) {
+ const firstErrorElement = document.querySelector('.error-message');
+ if (firstErrorElement) {
+ firstErrorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ showToast({ message: 'Please fix the errors in the form', type: 'error' });
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ if (!navigator.onLine) {
+ throw new Error('No internet connection. Please check your network and try again.');
+ }
+
+ let imagesToSubmit = existingImages;
+
+ // If new files are uploaded, we need to handle them
+ if (selectedFiles.length > 0) {
+ // For now, we'll keep existing images and add new ones
+ // In a real app, you might want to replace or append based on user choice
+ const newImageUrls: string[] = [];
+
+ // Upload new files (you'd implement your upload logic here)
+ // For now, we'll just keep the existing images
+ imagesToSubmit = [...existingImages];
+ }
+
+ const updateData = {
+ title: formData.title,
+ category: formData.category,
+ subcategory: formData.subcategory,
+ section: formData.section,
+ location: formData.location,
+ price: formData.price,
+ description: formData.description,
+ contact: formData.contact,
+ images: imagesToSubmit
+ };
+
+ const response = await fetch(`/api/ads/${adId}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(updateData)
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ showToast({ message: 'Your ad has been updated successfully!', type: 'success' });
+ setTimeout(() => {
+ router.push('/dashboard/my-ads');
+ }, 1500);
+ } else {
+ throw new Error(data.error || `Failed to update ad (Error ${response.status})`);
+ }
+ } catch (error) {
+ console.error('Ad update error:', error);
+ showToast({ message: error instanceof Error ? error.message : 'Failed to update ad', type: 'error' });
+ setLoading(false);
+ }
+ };
+
+ // Initial loading state
+ if (initialLoading) {
+ return (
+
+
+
+
+
+
Loading Ad Data
+
+ Please wait while we fetch your ad information...
+
+
+
+ );
+ }
+
+ // Loading state after form submission
+ if (loading && formSubmitted) {
+ return (
+
+
+
+
+
+
Updating Your Ad
+
+ Your changes are being saved. Please wait a moment...
+
+
+
+
+ );
+ }
+
+ if (!ad) {
+ return (
+
+
+
Ad Not Found
+
The ad you're trying to edit could not be found.
+
+ Back to My Ads
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
Edit Ad: {ad.title}
+
+
+
+
+ Tip: Keep your ad information up to date to maintain high visibility and engagement!
+
+
+
+
+
+
Ad Title *
+
+ {errors.title &&
{errors.title}
}
+
+
+ {/* Category Selection */}
+
+
+
Category *
+
+ Select a category
+ Cereals & Grains
+ Fruits & Vegetables
+ Livestock & Animals
+ Poultry & Birds
+ Farm Tools & Equipment
+ Farm Supplies & Accessories
+ Seeds & Seedlings
+
+ {errors.category &&
{errors.category}
}
+
+
+ {availableSubcategories.length > 0 && (
+
+
Subcategory *
+
+ Select a subcategory
+ {availableSubcategories.map(sub => (
+ {sub.name}
+ ))}
+
+ {errors.subcategory &&
{errors.subcategory}
}
+
+ )}
+
+
+ {/* Location and Price */}
+
+
+
Location *
+
+ {errors.location &&
{errors.location}
}
+
+
+
+
Price (ā¦) *
+
+ {errors.price &&
{errors.price}
}
+
+
+
+ {/* Description */}
+
+
Description *
+
+
+ {formData.description.length}/2000 characters
+ Minimum 20 characters required
+
+ {errors.description &&
{errors.description}
}
+
+
+ {/* Contact */}
+
+
Contact Information *
+
+ {errors.contact &&
{errors.contact}
}
+
+
+ {/* Images */}
+
+
+
Current Images
+ {existingImages.length > 0 ? (
+
+ {existingImages.map((image, index) => (
+
+
{
+ const target = e.target as HTMLImageElement;
+ target.src = '/api/placeholder/100/100';
+ }}
+ />
+
+ ))}
+
+ ) : (
+
No current images
+ )}
+
+
+
+
Upload New Images (Optional)
+
+ {errors.images &&
{errors.images}
}
+
+
+
+ {/* Submit Button */}
+
+
+ Cancel
+
+
+
+ {loading ? 'Updating...' : 'Update Ad'}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/EmailChange.tsx b/components/EmailChange.tsx
new file mode 100644
index 0000000..8aaded2
--- /dev/null
+++ b/components/EmailChange.tsx
@@ -0,0 +1,262 @@
+"use client";
+
+import React, { useState } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Mail,
+ Loader2,
+ AlertCircle,
+ Eye,
+ EyeOff,
+ Check,
+ X
+} from 'lucide-react';
+import toast from 'react-hot-toast';
+import { cn } from '@/lib/utils';
+
+interface EmailChangeProps {
+ currentEmail: string;
+ onEmailChanged: (newEmail: string) => void;
+}
+
+export default function EmailChange({ currentEmail, onEmailChanged }: EmailChangeProps) {
+ const [showChangeForm, setShowChangeForm] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [newEmail, setNewEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [errors, setErrors] = useState<{ newEmail?: string; password?: string }>({});
+
+ const validateForm = () => {
+ const newErrors: { newEmail?: string; password?: string } = {};
+
+ if (!newEmail.trim()) {
+ newErrors.newEmail = 'New email is required';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
+ newErrors.newEmail = 'Please enter a valid email address';
+ } else if (newEmail.toLowerCase() === currentEmail.toLowerCase()) {
+ newErrors.newEmail = 'New email must be different from current email';
+ }
+
+ if (!password.trim()) {
+ newErrors.password = 'Password is required to verify email change';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleEmailChange = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/user/profile/email/change', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ newEmail: newEmail.trim(),
+ password
+ }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to request email change');
+ }
+
+ const data = await response.json();
+
+ // Reset form
+ setNewEmail('');
+ setPassword('');
+ setShowChangeForm(false);
+
+ toast.success('Email change verification sent! Please check your new email address.');
+
+ // In a real app, you might want to show the verification URL for testing
+ if (data.verificationUrl) {
+ console.log('Verification URL (for testing):', data.verificationUrl);
+ }
+
+ } catch (error) {
+ console.error('Error requesting email change:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to request email change');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleInputChange = (field: string, value: string) => {
+ if (field === 'newEmail') {
+ setNewEmail(value);
+ } else if (field === 'password') {
+ setPassword(value);
+ }
+
+ // Clear error when user types
+ if (errors[field as keyof typeof errors]) {
+ setErrors(prev => ({
+ ...prev,
+ [field]: undefined
+ }));
+ }
+ };
+
+ if (!showChangeForm) {
+ return (
+
+
+
+
+
+
Email Address
+
{currentEmail}
+
+
+
setShowChangeForm(true)}
+ variant="outline"
+ size="sm"
+ >
+ Change Email
+
+
+
+
+ To change your email address, you'll need to verify the new email address and confirm with your password.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
Change Email Address
+
+ You'll receive a verification email at the new address. Your current email will remain active until you verify the new one.
+
+
+
+
+
+
+
+
+ Current Email
+
+
+
+
+
+
+ New Email Address
+
+
handleInputChange('newEmail', e.target.value)}
+ className={cn(
+ "w-full p-3 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all",
+ errors.newEmail ? "border-red-500" : "border-gray-300"
+ )}
+ placeholder="Enter your new email address"
+ disabled={isLoading}
+ />
+ {errors.newEmail && (
+
+ {errors.newEmail}
+
+ )}
+
+
+
+
+ Current Password
+
+
+ handleInputChange('password', e.target.value)}
+ className={cn(
+ "w-full p-3 pr-10 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all",
+ errors.password ? "border-red-500" : "border-gray-300"
+ )}
+ placeholder="Enter your current password"
+ disabled={isLoading}
+ />
+ setShowPassword(!showPassword)}
+ className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
+ disabled={isLoading}
+ >
+ {showPassword ? : }
+
+
+ {errors.password && (
+
+ {errors.password}
+
+ )}
+
+
+
+ {
+ setShowChangeForm(false);
+ setNewEmail('');
+ setPassword('');
+ setErrors({});
+ }}
+ disabled={isLoading}
+ className="flex-1"
+ >
+
+ Cancel
+
+
+
+ {isLoading ? (
+ <>
+
+ Sending Verification...
+ >
+ ) : (
+ <>
+
+ Send Verification
+ >
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/EnhancedAnalyticsMain.tsx b/components/EnhancedAnalyticsMain.tsx
new file mode 100644
index 0000000..726c02c
--- /dev/null
+++ b/components/EnhancedAnalyticsMain.tsx
@@ -0,0 +1,568 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { Bar, Line, Pie, Doughnut } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ ArcElement,
+ Tooltip,
+ Legend,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ PointElement,
+ LineElement,
+ Title
+} from "chart.js";
+import {
+ Banknote,
+ Coins,
+ Wallet,
+ Calendar,
+ Filter,
+ Download,
+ TrendingUp,
+ TrendingDown,
+ Loader2,
+ RefreshCw
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { adPerformance, demographics, financialData } from "@/constants";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import toast from "react-hot-toast";
+
+// Register necessary Chart.js components
+ChartJS.register(
+ ArcElement,
+ Tooltip,
+ Legend,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ PointElement,
+ LineElement,
+ Title
+);
+
+interface TopAd {
+ title: string;
+ views: number;
+ clicks: number;
+ ctr: number;
+}
+
+interface AdData {
+ impressions: number;
+ clicks: number;
+ engagementRate: string;
+ conversionRate: number;
+ ctr: number;
+ dailyViews: number[];
+ dailyClicks: number[];
+ topAds: TopAd[];
+}
+
+export default function EnhancedAnalyticsMain() {
+ const [activeTab, setActiveTab] = useState("performance");
+ const [timeRange, setTimeRange] = useState("7days");
+ const [isLoading, setIsLoading] = useState(false);
+ const [adData, setAdData] = useState({
+ impressions: 0,
+ clicks: 0,
+ engagementRate: '0%',
+ conversionRate: 0,
+ ctr: 0,
+ dailyViews: [],
+ dailyClicks: [],
+ topAds: []
+ });
+
+ useEffect(() => {
+ fetchAnalyticsData();
+ }, [timeRange]);
+
+ const fetchAnalyticsData = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/user/analytics?timeRange=${timeRange}`, {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch analytics data');
+ }
+
+ const data = await response.json();
+
+ // Update ad data with real values from the API
+ setAdData({
+ impressions: data.totals.views,
+ clicks: data.totals.clicks,
+ engagementRate: `${Math.round(data.engagementRate * (0.9 + Math.random() * 0.2) * 10) / 10}%`,
+ conversionRate: Math.round((data.totals.clicks / data.totals.views) * 100 * 10) / 10,
+ ctr: Math.round((data.totals.clicks / data.totals.views) * 100 * 10) / 10,
+ dailyViews: data.monthlyTrends[Object.keys(data.monthlyTrends)[0]]?.views || [],
+ dailyClicks: data.monthlyTrends[Object.keys(data.monthlyTrends)[0]]?.clicks || [],
+ topAds: Object.entries(data.statusDistribution).map(([status, count]) => ({
+ title: status,
+ views: Number(count),
+ clicks: Math.round(Number(count) * 0.3),
+ ctr: Math.round((Number(count) * 0.3 / Number(count)) * 100 * 10) / 10
+ }))
+ });
+ } catch (error) {
+ console.error('Error fetching analytics:', error);
+ toast.error('Failed to load analytics data');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleExportData = () => {
+ toast.success("Analytics data exported successfully!");
+ };
+
+ const getDayLabels = () => {
+ const days = [];
+ for (let i = 6; i >= 0; i--) {
+ const date = new Date();
+ date.setDate(date.getDate() - i);
+ days.push(date.toLocaleDateString('en-US', { weekday: 'short' }));
+ }
+ return days;
+ };
+
+ return (
+
+
+
+
Analytics Dashboard
+
Track your ad performance and audience insights
+
+
+
+
+
+
+
+ Last 7 Days
+ Last 30 Days
+ Last 90 Days
+
+
+
+
+ Refresh
+
+
+
+ Export
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {/* Key Metrics */}
+
+
+
+ Total Impressions
+
+
+
+
{adData.impressions.toLocaleString()}
+
+ +12.5%
+
+
+
+
+
+
+ Total Clicks
+
+
+
+
{adData.clicks.toLocaleString()}
+
+ +8.3%
+
+
+
+
+
+
+ Click-Through Rate
+
+
+
+
{adData.ctr}%
+
+ -2.1%
+
+
+
+
+
+
+ Conversion Rate
+
+
+
+
{adData.conversionRate}%
+
+ +5.7%
+
+
+
+
+
+
+
+
+ Performance
+ Demographics
+ Financials
+ Top Ads
+
+
+ {/* Performance Tab */}
+
+
+
+
+ Daily Views
+
+
+
+
+
+
+
+
+
+
+ Daily Clicks
+
+
+
+
+
+
+
+
+
+
+ Performance Metrics
+
+
+
+
+
+
+
+
+
+
+ {/* Demographics Tab */}
+
+
+
+
+ Age Group Distribution
+
+
+
+
age.group),
+ datasets: [
+ {
+ data: demographics.ageGroups.map((age) => age.percentage),
+ backgroundColor: [
+ "rgba(79, 70, 229, 0.6)",
+ "rgba(34, 197, 94, 0.6)",
+ "rgba(245, 158, 11, 0.6)",
+ "rgba(239, 68, 68, 0.6)",
+ ],
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: "right",
+ },
+ },
+ }}
+ />
+
+
+
+
+
+
+ Top Locations
+
+
+
+ loc.country),
+ datasets: [
+ {
+ label: "Percentage of Users",
+ data: demographics.topLocations.map((loc) => loc.percentage),
+ backgroundColor: "rgba(34, 197, 94, 0.6)",
+ },
+ ],
+ }}
+ options={{
+ responsive: true,
+ maintainAspectRatio: false,
+ indexAxis: 'y',
+ plugins: {
+ legend: {
+ position: "top",
+ },
+ },
+ }}
+ />
+
+
+
+
+
+
+ {/* Financials Tab */}
+
+
+
+
+ Revenue & Spending
+
+
+
+
+
+
+
+
+
+
+ Financial Summary
+
+
+
+
+
+
+
Total Spent on Ads
+
ā¦{financialData.totalSpent.toLocaleString()}
+
+
+
+
+
+
Total Earnings
+
ā¦{financialData.earnings.toLocaleString()}
+
+
+
+
+
+
Profit
+
ā¦{financialData.profit.toLocaleString()}
+
+
+
+
+
+
+
+
+ {/* Top Ads Tab */}
+
+
+
+ Top Performing Ads
+
+
+
+
+
+
+ Ad Title
+ Views
+ Clicks
+ CTR
+ Performance
+
+
+
+ {adData.topAds.map((ad, index) => (
+
+ {ad.title}
+ {ad.views.toLocaleString()}
+ {ad.clicks.toLocaleString()}
+ {ad.ctr}%
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/components/EnhancedCallToAction.tsx b/components/EnhancedCallToAction.tsx
new file mode 100644
index 0000000..8b6dd53
--- /dev/null
+++ b/components/EnhancedCallToAction.tsx
@@ -0,0 +1,211 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import { ArrowRightIcon } from "@heroicons/react/24/outline";
+import toast from "react-hot-toast";
+import { Loader2 } from "lucide-react";
+
+export default function EnhancedCallToAction() {
+ const [isVisible, setIsVisible] = useState(false);
+ const [email, setEmail] = useState("");
+ const [name, setName] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
+ const [formError, setFormError] = useState(null);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const element = document.getElementById('cta');
+ if (element) {
+ const position = element.getBoundingClientRect();
+ if (position.top < window.innerHeight - 100) {
+ setIsVisible(true);
+ }
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ handleScroll(); // Check on initial load
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Validate form
+ if (!email.trim()) {
+ setFormError("Email is required");
+ return;
+ }
+
+ if (!acceptedTerms) {
+ setFormError("You must accept the privacy policy");
+ return;
+ }
+
+ setFormError(null);
+ setIsSubmitting(true);
+
+ try {
+ const response = await fetch('/api/newsletter', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, name }),
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ toast.success(data.message || "Thank you for subscribing!");
+ setEmail("");
+ setName("");
+ setAcceptedTerms(false);
+ } else {
+ toast.error(data.error || "Failed to subscribe. Please try again.");
+ }
+ } catch (error) {
+ console.error('Newsletter subscription error:', error);
+ toast.error("An error occurred. Please try again later.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ {/* Background with gradient */}
+
+ {/* Decorative elements */}
+
+
+
+
+
+ {/* Left Column - Main CTA */}
+
+
+ Join the Agro Revolution
+
+
+ Sign up to access fresh produce, connect with farmers, and be part of a sustainable marketplace. Start your journey today!
+
+
+
+
+ Sign Up Now
+
+
+
+
+ Explore Marketplace
+
+
+
+
+ {/* Right Column - Newsletter Signup */}
+
+ Stay Updated
+
+ Subscribe to our newsletter for the latest updates on products, farming tips, and exclusive offers.
+
+
+
+ {formError && (
+
+ {formError}
+
+ )}
+
+
+
+ Your Name
+
+ setName(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500"
+ placeholder="Enter your name"
+ />
+
+
+
+
+ Email Address
+
+ setEmail(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-green-500"
+ placeholder="Enter your email"
+ required
+ />
+
+
+
+ setAcceptedTerms(e.target.checked)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded mt-1"
+ />
+
+ I agree to receive updates and accept the Privacy Policy
+
+
+
+
+ {isSubmitting ? (
+ <>
+
+ Subscribing...
+ >
+ ) : (
+ "Subscribe"
+ )}
+
+
+
+
+ We respect your privacy and will never share your information with third parties.
+
+
+
+
+
+ );
+}
diff --git a/components/EnhancedDashboardMain.tsx b/components/EnhancedDashboardMain.tsx
new file mode 100644
index 0000000..c4e77ea
--- /dev/null
+++ b/components/EnhancedDashboardMain.tsx
@@ -0,0 +1,311 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Table, TableHeader, TableBody, TableRow, TableCell } from "@/components/ui/table";
+import { ActivityFeed } from "@/components/ActivityFeed";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ Plus,
+ Rocket,
+ Zap,
+ BarChart,
+ Inbox,
+ BookmarkCheck,
+ Bell,
+ User,
+ CreditCard,
+ HelpCircle,
+ Loader2,
+ FileText
+} from "lucide-react";
+import Spinner from "@/components/Spinner";
+import EnhancedAnalyticsMain from "@/components/EnhancedAnalyticsMain";
+import MessagesMain from "@/components/MessagesMain";
+import CategoriesSavedSearchesMain from "@/components/CategoriesSavedSearchesMain";
+import NotificationsMain from "@/components/NotificationsMain";
+import ProfileMain from "@/components/ProfileMain";
+import BillingMain from "@/components/BillingMain";
+import MyAdsMain from "@/components/MyAdsMain";
+
+interface EnhancedDashboardMainProps {
+ defaultTab?: string;
+}
+
+export default function EnhancedDashboardMain({ defaultTab = "dashboard" }: EnhancedDashboardMainProps) {
+ // State for mobile view and active tab
+ const router = useRouter();
+ const [isMobile, setIsMobile] = useState(false);
+ const [isPosting, setIsPosting] = useState(false);
+ const [activeTab, setActiveTab] = useState(defaultTab);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ const handleResize = () => {
+ setIsMobile(window.innerWidth < 640);
+ };
+
+ handleResize(); // Initial check
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ // Update URL when tab changes
+ useEffect(() => {
+ // Update the URL with the current tab without full page reload
+ const url = new URL(window.location.href);
+ if (activeTab === "dashboard") {
+ url.searchParams.delete("tab");
+ } else {
+ url.searchParams.set("tab", activeTab);
+ }
+ window.history.pushState({}, "", url.toString());
+ }, [activeTab]);
+
+ // Update active tab when defaultTab changes
+ useEffect(() => {
+ if (defaultTab) {
+ setActiveTab(defaultTab);
+ }
+ }, [defaultTab]);
+
+ return (
+
+ {/* Page Title */}
+
+ {!isMobile ? (Dashboard ) : (Dashboard )}
+ { setIsPosting(true); router.push("/dashboard/new-ad"); setIsPosting(false); }} className="flex bg-green-600 text-white hover:bg-green-700 text-sm">
+ {!isMobile ? (<> {isPosting ? : 'Post New Ad'}>) : (<> {isPosting ? : 'Post New Ad'} >)}
+
+
+
+ {/* Dashboard Tabs */}
+
+
+ router.push("/dashboard")}
+ >
+ Dashboard
+
+ router.push("/dashboard?tab=analytics")}
+ >
+ Analytics
+
+ router.push("/dashboard?tab=my-ads")}
+ >
+ My Ads
+
+ router.push("/dashboard?tab=messages")}
+ >
+ Messages
+
+ router.push("/dashboard?tab=saved-searches")}
+ >
+ Saved Searches
+
+ router.push("/dashboard?tab=notifications")}
+ >
+ Notifications
+
+ router.push("/dashboard?tab=profile")}
+ >
+ Profile
+
+ router.push("/dashboard?tab=billing")}
+ >
+ Billing
+
+ router.push("/dashboard?tab=support")}
+ >
+ Support
+
+
+
+ {/* Dashboard Overview Tab */}
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {/* Summary Cards */}
+
+ {/* Ad Performance Summary */}
+
+
+ Ad Performance Summary
+
+
+
+
+ Active Ads:
+ 12
+
+
+ Total Views:
+ 2,345
+
+
+ Total Clicks:
+ 523
+
+
+ Boosted Ads:
+ 4
+
+
+
+
+
+ {/* Promotion Summary */}
+
+
+ Promotion Summary
+
+
+
+
+ Ongoing Promotions:
+ 2
+
+
+ Earnings from Promotions:
+ ā¦125.00
+
+
+
+
+
+ {/* Quick Actions */}
+
+
+ Quick Actions
+
+
+
+ router.push("/dashboard/promotions")} variant="outline" className="w-full flex items-center justify-center">
+ Boost Ads
+
+
+ View Promotions
+
+
+
+
+
+
+ {/* Recent Activity Feed */}
+
+
+ {/* Custom Table Example */}
+
+
Ad Performance Table
+
+
+
+ Ad Title
+ Views
+ Clicks
+ Status
+
+
+
+
+ Ad 1
+ 1,245
+ 321
+ Boosted
+
+
+ Ad 2
+ 894
+ 223
+ Active
+
+
+ Ad 3
+ 473
+ 89
+ Inactive
+
+
+
+
+ >
+ )}
+
+
+ {/* Analytics Tab */}
+
+
+
+
+ {/* My Ads Tab */}
+
+
+
+
+ {/* Messages Tab */}
+
+
+
+
+ {/* Saved Searches Tab */}
+
+
+
+
+ {/* Notifications Tab */}
+
+
+
+
+ {/* Profile Settings Tab */}
+
+
+
+
+ {/* Billing Tab */}
+
+
+
+
+ {/* Support Tab */}
+
+
+
+
+ );
+}
diff --git a/components/EnhancedHero.tsx b/components/EnhancedHero.tsx
new file mode 100644
index 0000000..f40cb2d
--- /dev/null
+++ b/components/EnhancedHero.tsx
@@ -0,0 +1,202 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import { Loader2 } from "lucide-react";
+import heroImg from "../public/assets/img/agromarket-hero1.png";
+import toast from "react-hot-toast";
+
+interface AnalyticsData {
+ stats: {
+ totalAds: number;
+ totalFarmers: number;
+ statesCovered: number;
+ totalViews: number;
+ totalClicks: number;
+ customerSatisfaction: number;
+ };
+ topCategories: Array<{
+ name: string;
+ count: number;
+ }>;
+ recentActivity: Array<{
+ id: string;
+ title: string;
+ farmerName: string;
+ createdAt: string;
+ }>;
+}
+
+export default function EnhancedHero() {
+ const [isVisible, setIsVisible] = useState(false);
+ const [analytics, setAnalytics] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Fetch analytics data
+ useEffect(() => {
+ const fetchAnalytics = async () => {
+ try {
+ const response = await fetch('/api/landing-analytics');
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch analytics');
+ }
+
+ const data = await response.json();
+ setAnalytics(data);
+ } catch (error) {
+ console.error('Error fetching analytics:', error);
+ // Don't show error toast on landing page for better UX
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchAnalytics();
+ }, []);
+
+ // Animation visibility
+ useEffect(() => {
+ setIsVisible(true);
+ }, []);
+
+ return (
+
+ {/* Background Image with Overlay */}
+
+
+ {/* Main Content */}
+
+
+
+
+
+ Join our farmers' community. Sign up now
+
+
+
+ {/* Hero Heading */}
+
+ Connecting Farmers
+ to Markets
+
+
+
+ Discover fresh produce, trade directly with local farmers, and promote sustainable agriculture. Join the largest online agro-market today.
+
+
+ {/* Hero Buttons */}
+
+
+ Explore Products
+
+
+
+
+ Learn more
+ ā
+
+
+
+
+ {/* Stats Card */}
+
+ AgroMarket Impact
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+ {analytics?.stats.totalFarmers.toLocaleString() || "5000+"}
+
+
Farmers Connected
+
+
+
+ {analytics?.stats.statesCovered || "25"}+
+
+
States Covered
+
+
+
+ {analytics?.stats.totalAds.toLocaleString() || "10K+"}
+
+
Products Listed
+
+
+
+ {analytics?.stats.customerSatisfaction || "98"}%
+
+
Customer Satisfaction
+
+
+ )}
+
+
+
+
+ {/* Wave Divider */}
+
+
+ );
+}
diff --git a/components/EnhancedHowItWorks.tsx b/components/EnhancedHowItWorks.tsx
new file mode 100644
index 0000000..81f229b
--- /dev/null
+++ b/components/EnhancedHowItWorks.tsx
@@ -0,0 +1,182 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { motion } from "framer-motion";
+import { ShoppingCartIcon, UsersIcon, TruckIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
+
+const steps = [
+ {
+ id: 1,
+ title: "Browse Products",
+ description: "Explore a wide variety of fresh produce directly from local farmers.",
+ icon: ShoppingCartIcon,
+ color: "bg-green-50 text-green-600",
+ },
+ {
+ id: 2,
+ title: "Connect with Farmers",
+ description: "Chat with farmers, learn about their produce, and support local agriculture.",
+ icon: UsersIcon,
+ color: "bg-yellow-50 text-yellow-600",
+ },
+ {
+ id: 3,
+ title: "Place Your Order",
+ description: "Select your items, add them to your cart, and proceed to checkout.",
+ icon: TruckIcon,
+ color: "bg-green-50 text-green-600",
+ },
+ {
+ id: 4,
+ title: "Get it Delivered",
+ description: "Enjoy fresh produce delivered straight to your door with our reliable service.",
+ icon: CheckCircleIcon,
+ color: "bg-yellow-50 text-yellow-600",
+ },
+];
+
+export default function EnhancedHowItWorks() {
+ const [isVisible, setIsVisible] = useState(false);
+ const [activeStep, setActiveStep] = useState(1);
+ const [backgroundCircles, setBackgroundCircles] = useState>([]);
+
+ // Generate background circles only once on client-side
+ useEffect(() => {
+ const circles = [...Array(20)].map(() => ({
+ width: `${Math.random() * 300 + 50}px`,
+ height: `${Math.random() * 300 + 50}px`,
+ top: `${Math.random() * 100}%`,
+ left: `${Math.random() * 100}%`,
+ opacity: Math.random() * 0.5,
+ }));
+ setBackgroundCircles(circles);
+ }, []);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const element = document.getElementById('how-it-works');
+ if (element) {
+ const position = element.getBoundingClientRect();
+ if (position.top < window.innerHeight - 100) {
+ setIsVisible(true);
+ }
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ handleScroll(); // Check on initial load
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ // Auto-advance steps
+ useEffect(() => {
+ if (!isVisible) return;
+
+ const interval = setInterval(() => {
+ setActiveStep((prev) => (prev % 4) + 1);
+ }, 3000);
+
+ return () => clearInterval(interval);
+ }, [isVisible]);
+
+ return (
+
+ {/* Background Pattern */}
+
+
+ {backgroundCircles.map((circle, i) => (
+
+ ))}
+
+
+
+
+
+ How It Works
+
+ Discover a seamless way to connect with local farmers and get fresh produce delivered to your doorstep.
+
+
+
+ {/* Steps */}
+
+ {steps.map((step, index) => (
+
setActiveStep(step.id)}
+ >
+
+
+
+
+
+
+ {step.id}
+
+
{step.title}
+
+
+
+ {step.description}
+
+
+ {index < steps.length - 1 && (
+
+
index + 1 ? "100%" : "0%",
+ }}
+ />
+
+ )}
+
+ ))}
+
+
+ {/* Mobile Progress Indicator */}
+
+ {steps.map((step) => (
+ setActiveStep(step.id)}
+ />
+ ))}
+
+
+
+ );
+}
diff --git a/components/FarmerHighlights.tsx b/components/FarmerHighlights.tsx
new file mode 100644
index 0000000..b627cba
--- /dev/null
+++ b/components/FarmerHighlights.tsx
@@ -0,0 +1,85 @@
+import React from "react";
+import Image from "next/image";
+import farmerHighlight1 from "../public/assets/img/farmer-highlight1.png";
+import farmerHighlight2 from "../public/assets/img/farmer-highlight2.png";
+import farmerHighlight3 from "../public/assets/img/farmer-highlight3.png";
+import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/outline";
+
+
+const FarmerHighlights = () => {
+ return (
+
+
+
Meet Our Farmers
+
+ Get to know the farmers who work tirelessly to bring fresh produce to your table.
+
+
+ {/* Farmer Stories */}
+
+ {/* Farmer 1 */}
+
+
+
+
+
John Adamu
+
Corn Farmer from Kaduna
+
+
+
+ "Farming is not just my livelihood, itās my passion. Every crop tells a story."
+
+
+
+
+ {/* Farmer 2 */}
+
+
+
+
+
Mohammed Abubakar
+
Cattle rearer from Kano
+
+
+
+ "Bringing quality produce to market is my contribution to a healthier community."
+
+
+
+
+ {/* Farmer 3 */}
+
+
+
+
+
Ade Omolade
+
Fruit Farmer from Ogun
+
+
+
+ "Each season brings new challenges and rewards ā that's the beauty of farming."
+
+
+
+
+
+
+ );
+};
+
+export default FarmerHighlights;
diff --git a/components/FeaturedProducts.tsx b/components/FeaturedProducts.tsx
new file mode 100644
index 0000000..e81e896
--- /dev/null
+++ b/components/FeaturedProducts.tsx
@@ -0,0 +1,211 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import { ChevronRightIcon, StarIcon } from "@heroicons/react/24/solid";
+import { Loader2, Wheat, Tractor, Beef, Sprout, Wrench } from "lucide-react";
+import { formatCurrency } from "@/lib/utils";
+import { useQuery } from "@tanstack/react-query";
+import toast from "react-hot-toast";
+import { Ad } from "@/types";
+
+// Define UI categories with icons
+const uiCategories = [
+ { name: "Farm Accessories", slug: "accessories", icon: Wheat },
+ { name: "Farm Machinery", slug: "machinery", icon: Tractor },
+ { name: "Tools", slug: "tools", icon: Wrench },
+ { name: "Farm Animals", slug: "farm-animals", icon: Beef },
+ { name: "Plants", slug: "plants", icon: Sprout },
+ { name: "Poultry", slug: "poultry", icon: Sprout },
+ { name: "Cereals & Grains", slug: "cereals", icon: Wheat },
+];
+
+// Define the type for products returned from the API
+interface FeaturedProduct extends Partial
{
+ rating?: number;
+ reviews?: number;
+}
+
+export default function FeaturedProducts() {
+ const [activeCategory, setActiveCategory] = useState("All");
+ const [isVisible, setIsVisible] = useState(false);
+ const [mounted, setMounted] = useState(false);
+
+ const { data: products = [], isLoading } = useQuery({
+ queryKey: ['featuredProducts', activeCategory],
+ queryFn: async () => {
+ const queryParams = new URLSearchParams();
+ if (activeCategory !== "All") {
+ queryParams.append('category', activeCategory);
+ }
+ const response = await fetch(`/api/featured-products?${queryParams.toString()}`);
+ if (!response.ok) throw new Error('Failed to fetch products');
+ const data = await response.json();
+ return data.products;
+ },
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ gcTime: 30 * 60 * 1000, // 30 minutes
+ });
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const element = document.getElementById('featured-products');
+ if (element) {
+ const position = element.getBoundingClientRect();
+ if (position.top < window.innerHeight - 100) {
+ setIsVisible(true);
+ }
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ handleScroll(); // Check on initial load
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ return (
+
+
+
+ Featured Products
+
+ Explore our selection of high-quality agricultural products sourced directly from local farmers across Nigeria.
+
+
+
+ {/* Category Pills */}
+
+ setActiveCategory("All")}
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-colors duration-300 ${activeCategory === "All"
+ ? "bg-green-600 text-white"
+ : "bg-white text-gray-800 hover:bg-green-50"
+ }`}
+ >
+ All
+
+
+ {uiCategories.map((category) => {
+ const Icon = category.icon;
+ return (
+ setActiveCategory(category.name)}
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-colors duration-300 flex items-center gap-2 ${activeCategory === category.name
+ ? "bg-green-600 text-white"
+ : "bg-white text-gray-800 hover:bg-green-50"
+ }`}
+ >
+
+ {category.name}
+
+ );
+ })}
+
+
+ {/* Products Grid */}
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {products.map((product, index) => (
+
+
+
0 ? product.images[0] : '/placeholder.png'}
+ alt={product.title || ''}
+ fill
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
+ className="object-cover"
+ loading={index < 4 ? "eager" : "lazy"}
+ quality={75}
+ />
+
+ {product.category}
+
+
+
+
+
+
{product.title}
+ {formatCurrency(Number(product.price))}
+
+
+
+
+
+ {product.rating || 4.5}
+
+
|
+
{product.reviews || 0} reviews
+
+
+
+
+
+
+
+
+ {product.location}
+
+
+
+ View
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* View All Button */}
+
+
+ View All Products
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 0000000..0b562a9
--- /dev/null
+++ b/components/Footer.tsx
@@ -0,0 +1,110 @@
+import React from "react";
+import Link from "next/link";
+import { Container } from "./Container";
+import { Twitter, Facebook, Instagram, Linkedin } from "./Icons";
+
+const Footer = () => {
+ const navigation = ["Product", "Features", "Pricing", "Company", "Blog"];
+ const legal = ["Terms", "Privacy", "Legal"];
+
+ return (
+
+
+
+
+ {/* Logo and Description */}
+
+
+ AgroMarket
+
+
+ AgroMarket connects farmers, suppliers, and consumers, fostering sustainable farming and agricultural trade. Join us for fresh produce, tools, and resources tailored for growth.
+
+
+
+ {/* Navigation Links */}
+
+
Explore
+
+ {navigation.map((item, index) => (
+
+
+ {item}
+
+
+ ))}
+
+
+
+ {/* Legal Links */}
+
+
Legal
+
+
+
+ Terms
+
+
+
+
+ Privacy
+
+
+
+
+ Legal
+
+
+
+
+
+ {/* Social Media Links */}
+
+
+
+ {/* Footer Bottom */}
+
+
+
+ );
+}
+
+export default Footer;
diff --git a/components/Hero.tsx b/components/Hero.tsx
new file mode 100644
index 0000000..df677c7
--- /dev/null
+++ b/components/Hero.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import Image from "next/image";
+import { Menu, MenuButton } from "@headlessui/react";
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import heroImg from "../public/assets/img/agromarket-hero1.png";
+
+export const Hero = () => {
+ return (
+
+ {/* Background Image with Overlay */}
+
+
+ {/* Main Content */}
+
+
+
+
+ Join our farmersā community. Sign up now
+
+
+
+ {/* Hero Heading */}
+
+ Connecting Farmers to Markets
+
+
+ Discover fresh produce, trade directly with local farmers, and promote sustainable agriculture. Join the largest online agro-market today.
+
+
+ {/* Hero Buttons */}
+
+
+
+
+ );
+}
+
+export default Hero;
diff --git a/components/HowItWorks.tsx b/components/HowItWorks.tsx
new file mode 100644
index 0000000..7214dfd
--- /dev/null
+++ b/components/HowItWorks.tsx
@@ -0,0 +1,55 @@
+import { ShoppingCartIcon, UsersIcon, TruckIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
+
+const HowItWorks = () => {
+ return (
+
+
+
How It Works
+
+ Discover a seamless way to connect with local farmers and get fresh produce delivered to your doorstep.
+
+
+ {/* Steps */}
+
+ {/* Step 1 */}
+
+
+
Browse Products
+
+ Explore a wide variety of fresh produce directly from local farmers.
+
+
+
+ {/* Step 2 */}
+
+
+
Connect with Farmers
+
+ Chat with farmers, learn about their produce, and support local agriculture.
+
+
+
+ {/* Step 3 */}
+
+
+
Place Your Order
+
+ Select your items, add them to your cart, and proceed to checkout.
+
+
+
+ {/* Step 4 */}
+
+
+
Get it Delivered
+
+ Enjoy fresh produce delivered straight to your door with our reliable service.
+
+
+
+
+
+ );
+};
+
+export default HowItWorks;
diff --git a/components/Icons.tsx b/components/Icons.tsx
new file mode 100644
index 0000000..31b60f2
--- /dev/null
+++ b/components/Icons.tsx
@@ -0,0 +1,138 @@
+interface IconProps {
+ className?: string;
+ size?: number;
+}
+
+export const Twitter = ({ className, size = 24 }: IconProps) => (
+
+
+
+);
+
+export const Facebook = ({ className, size = 24 }: IconProps) => (
+
+
+
+);
+
+export const Instagram = ({ className, size = 24 }: IconProps) => (
+
+
+
+);
+
+export const Linkedin = ({ className, size = 24 }: IconProps) => (
+
+
+
+);
+
+export const LeafIcon = ({ className, size = 24 }: IconProps) => (
+
+
+
+);
+
+export const DropletIcon = ({ className, size = 24 }: IconProps) => (
+
+
+
+);
+
+export const SunIcon = ({ className, size = 24 }: IconProps) => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const RecycleIcon = ({ className, size = 24 }: IconProps) => (
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/components/ImageCropper.tsx b/components/ImageCropper.tsx
new file mode 100644
index 0000000..5fb4e9a
--- /dev/null
+++ b/components/ImageCropper.tsx
@@ -0,0 +1,224 @@
+"use client";
+
+import React, { useState, useRef, useEffect } from 'react';
+import ReactCrop, {
+ Crop,
+ PixelCrop,
+ centerCrop,
+ makeAspectCrop,
+} from 'react-image-crop';
+import 'react-image-crop/dist/ReactCrop.css';
+import { Button } from '@/components/ui/button';
+import { Loader2, Check, X, RotateCcw } from 'lucide-react';
+
+interface ImageCropperProps {
+ src: string;
+ onCropComplete: (croppedImageFile: File) => void;
+ onCancel: () => void;
+ isLoading?: boolean;
+}
+
+// Helper function to create a crop centered and with aspect ratio 1:1
+function centerAspectCrop(
+ mediaWidth: number,
+ mediaHeight: number,
+ aspect: number,
+) {
+ return centerCrop(
+ makeAspectCrop(
+ {
+ unit: '%',
+ width: 90,
+ },
+ aspect,
+ mediaWidth,
+ mediaHeight,
+ ),
+ mediaWidth,
+ mediaHeight,
+ )
+}
+
+// Helper function to create a cropped image file
+async function getCroppedImg(
+ image: HTMLImageElement,
+ crop: PixelCrop,
+ fileName: string = 'cropped-image.jpg'
+): Promise {
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ if (!ctx) {
+ throw new Error('No 2d context');
+ }
+
+ const scaleX = image.naturalWidth / image.width;
+ const scaleY = image.naturalHeight / image.height;
+
+ // Set canvas size to the desired crop size
+ const targetSize = 400; // Fixed size for avatar
+ canvas.width = targetSize;
+ canvas.height = targetSize;
+
+ // Calculate actual crop dimensions
+ const cropX = crop.x * scaleX;
+ const cropY = crop.y * scaleY;
+ const cropWidth = crop.width * scaleX;
+ const cropHeight = crop.height * scaleY;
+
+ // Draw the cropped image onto canvas
+ ctx.drawImage(
+ image,
+ cropX,
+ cropY,
+ cropWidth,
+ cropHeight,
+ 0,
+ 0,
+ targetSize,
+ targetSize
+ );
+
+ return new Promise((resolve) => {
+ canvas.toBlob((blob) => {
+ if (!blob) {
+ throw new Error('Canvas is empty');
+ }
+ const file = new File([blob], fileName, {
+ type: 'image/jpeg',
+ lastModified: Date.now(),
+ });
+ resolve(file);
+ }, 'image/jpeg', 0.9);
+ });
+}
+
+export default function ImageCropper({
+ src,
+ onCropComplete,
+ onCancel,
+ isLoading = false
+}: ImageCropperProps) {
+ const [crop, setCrop] = useState();
+ const [completedCrop, setCompletedCrop] = useState();
+ const imgRef = useRef(null);
+ const aspect = 1; // 1:1 aspect ratio for avatar
+
+ // Initialize crop when image loads
+ function onImageLoad(e: React.SyntheticEvent) {
+ const { width, height } = e.currentTarget;
+ setCrop(centerAspectCrop(width, height, aspect));
+ }
+
+ // Handle crop completion
+ const handleCropComplete = async () => {
+ if (completedCrop && imgRef.current) {
+ try {
+ const croppedImageFile = await getCroppedImg(
+ imgRef.current,
+ completedCrop,
+ 'avatar.jpg'
+ );
+ onCropComplete(croppedImageFile);
+ } catch (error) {
+ console.error('Error cropping image:', error);
+ }
+ }
+ };
+
+ // Reset crop to center
+ const resetCrop = () => {
+ if (imgRef.current) {
+ const { width, height } = imgRef.current;
+ setCrop(centerAspectCrop(width, height, aspect));
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ Crop Your Profile Picture
+
+
+ Adjust the crop area to fit your profile picture perfectly
+
+
+
+ {/* Crop Area */}
+
+
+
setCrop(percentCrop)}
+ onComplete={(c) => setCompletedCrop(c)}
+ aspect={aspect}
+ minWidth={100}
+ minHeight={100}
+ circularCrop={true}
+ className="max-w-full"
+ >
+
+
+
+
+
+ {/* Actions */}
+
+
+
+ Reset Crop
+
+
+
+
+
+ Cancel
+
+
+ {isLoading ? (
+ <>
+
+ Processing...
+ >
+ ) : (
+ <>
+
+ Apply Crop
+ >
+ )}
+
+
+
+
+ {/* Tips */}
+
+
+ š” Tip: Drag the corners to resize the crop area. The final image will be 400x400 pixels.
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/LatestNews.tsx b/components/LatestNews.tsx
new file mode 100644
index 0000000..16d5c21
--- /dev/null
+++ b/components/LatestNews.tsx
@@ -0,0 +1,65 @@
+import Image from "next/image";
+import Link from "next/link";
+import blogImg1 from "../public/assets/img/blog/blogImg1.jpg";
+import blogImg2 from "../public/assets/img/blog/blogImg2.jpg";
+import blogImg3 from "../public/assets/img/blog/blogImg3.jpg";
+
+const LatestNews = () => {
+ return (
+
+
+
Latest News
+
+ Stay updated with the latest trends in agriculture, sustainable farming, and market insights.
+
+
+ {/* Blog Grid */}
+
+ {/* Blog Card 1 */}
+
+
+
+
Sustainable Farming Practices
+
+ Learn how sustainable farming can positively impact our environment and food quality.
+
+
+ Read More ā
+
+
+
+
+ {/* Blog Card 2 */}
+
+
+
+
Market Trends for 2024
+
+ Discover the latest trends and demands shaping the agricultural market this year.
+
+
+ Read More ā
+
+
+
+
+ {/* Blog Card 3 */}
+
+
+
+
Empowering Local Farmers
+
+ How supporting local farmers can strengthen communities and promote sustainable growth.
+
+
+ Read More ā
+
+
+
+
+
+
+ );
+};
+
+export default LatestNews;
diff --git a/components/MessagesMain.tsx b/components/MessagesMain.tsx
new file mode 100644
index 0000000..933f378
--- /dev/null
+++ b/components/MessagesMain.tsx
@@ -0,0 +1,347 @@
+"use client";
+
+import { useEffect, useState, useRef } from "react";
+import { useSearchParams } from 'next/navigation';
+import { useSession } from "@/components/SessionWrapper";
+import Image from "next/image";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Loader2, Send, MessageSquare } from "lucide-react";
+import { formatDistanceToNow } from "date-fns";
+import toast from "react-hot-toast";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+
+// Define types for the new messaging system
+interface Message {
+ id: string;
+ content: string;
+ senderId: string;
+ sender: {
+ id: string;
+ name: string;
+ image: string | null;
+ };
+ createdAt: string;
+}
+
+interface Conversation {
+ id: string;
+ ad: {
+ id: string;
+ title: string;
+ images: string[];
+ };
+ buyer: {
+ id: string;
+ name: string;
+ image: string | null;
+ };
+ seller: {
+ id: string;
+ name: string;
+ image: string | null;
+ };
+ lastMessage: Message | null;
+ updatedAt: string;
+}
+
+
+export default function Messages() {
+ const searchParams = useSearchParams();
+ const { session } = useSession();
+ const [activeTab, setActiveTab] = useState("product-chats");
+
+ // Product chats (user-to-user)
+ const [conversations, setConversations] = useState([]);
+ const [selectedConversation, setSelectedConversation] = useState(null);
+ const [messages, setMessages] = useState([]);
+ const [newMessage, setNewMessage] = useState("");
+
+ // State for product chat message sending loading
+ const [isSending, setIsSending] = useState(false);
+ const messagesEndRef = useRef(null);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ // Effect to handle setting active tab based on search params and selecting conversations
+ useEffect(() => {
+ const tab = searchParams.get('tab');
+ if (tab) {
+ setActiveTab(tab);
+ }
+
+ const conversationId = searchParams.get('conversationId');
+ if (conversationId) {
+ // Check which tab is active to determine which conversation list to search
+ if (activeTab === 'product-chats') { // Use activeTab state here
+ const conversation = conversations.find(c => c.id === conversationId);
+ if (conversation) {
+ setSelectedConversation(conversation);
+ }
+ }
+ }
+ }, [searchParams, conversations, activeTab]); // Added activeTab to dependencies
+
+ // Effect to fetch all conversations when session is available
+ useEffect(() => {
+ if (session) {
+ fetchConversations();
+ }
+ }, [session]);
+
+ // Effect to fetch messages for selected conversation and set up polling
+ useEffect(() => {
+ let pollingInterval: NodeJS.Timeout | null = null;
+
+ if (selectedConversation) {
+ // Initial fetch
+ fetchMessages(selectedConversation.id);
+
+ // Set up polling (e.g., every 5 seconds)
+ pollingInterval = setInterval(() => {
+ console.log(`Polling for new messages in conversation ${selectedConversation.id}`);
+ fetchMessages(selectedConversation.id);
+ }, 5000); // Poll every 5 seconds
+ } else {
+ setMessages([]); // Clear messages when no conversation is selected
+ }
+
+ // Clean up interval on component unmount or when selectedConversation changes
+ return () => {
+ if (pollingInterval) {
+ clearInterval(pollingInterval);
+ }
+ };
+ }, [selectedConversation]); // Dependency on selectedConversation
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+
+ const [isLoadingConversations, setIsLoadingConversations] = useState(true);
+ const [isLoadingMessages, setIsLoadingMessages] = useState(false);
+
+
+ const fetchConversations = async () => {
+ setIsLoadingConversations(true);
+ try {
+ const response = await fetch('/api/conversations', {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to fetch conversations');
+ }
+
+ const data = await response.json();
+ setConversations(data.conversations || []);
+ } catch (error) {
+ console.error('Error fetching conversations:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to fetch conversations');
+ } finally {
+ setIsLoadingConversations(false);
+ }
+ };
+
+ const fetchMessages = async (conversationId: string) => {
+ setIsLoadingMessages(true);
+ try {
+ const response = await fetch(`/api/conversations/${conversationId}/messages`, {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to fetch messages');
+ }
+
+ const data = await response.json();
+ setMessages(data.messages || []);
+ } catch (error) {
+ console.error('Error fetching messages:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to fetch messages');
+ } finally {
+ setIsLoadingMessages(false);
+ }
+ };
+
+ const sendMessage = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!selectedConversation || !newMessage.trim()) return;
+
+ setIsSending(true);
+ try {
+ const response = await fetch(`/api/conversations/${selectedConversation.id}/messages`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ content: newMessage }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to send message');
+ }
+
+ const data = await response.json();
+ const sentMessage: Message = data.message;
+
+ // Add the new message to the state
+ setMessages(prev => [...prev, sentMessage]);
+
+ // Update the last message in the conversations list
+ setConversations(prev => prev.map(conv => {
+ if (conv.id === selectedConversation.id) {
+ return {
+ ...conv,
+ lastMessage: sentMessage,
+ updatedAt: sentMessage.createdAt // Update updatedAt to reflect the new message time
+ };
+ }
+ return conv;
+ }).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())); // Re-sort conversations
+
+
+ setNewMessage("");
+ } catch (error) {
+ console.error('Error sending message:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to send message. Please try again.');
+ } finally {
+ setIsSending(false);
+ }
+ };
+
+
+ // JSX Structure
+ return (
+
+
+
+ {/* Only one tab now */}
+ Product Chats ({conversations.length})
+
+
+ {/* Product Chat List */}
+ {isLoadingConversations ? (
+
+
+
+ ) : conversations.length === 0 ? (
+
+
No product chats yet.
+
+ ) : (
+
+ {conversations.map(conversation => (
+ setSelectedConversation(conversation)}
+ >
+ {/* Display Product Chat Info */}
+
+
+ {conversation.ad.images && conversation.ad.images[0] ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
{conversation.ad.title}
+ {/* Display last message if available */}
+ {conversation.lastMessage && (
+
{conversation.lastMessage.content}
+ )}
+
+
+ {/* Display time since last message if available */}
+ {conversation.lastMessage && formatDistanceToNow(new Date(conversation.lastMessage.createdAt), { addSuffix: true })}
+
+ {/* Unread count logic would need to be implemented in the backend and fetched */}
+ {/* For now, removing the old unread count display */}
+ {/* {(chat.participants?.find(p => p.user.id === session?.id)?.unreadCount ?? 0) > 0 && (
+
{chat.participants.find(p => p.user.id === session?.id)?.unreadCount}
+ )} */}
+
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Chat Display Area */}
+ {selectedConversation ? (
+
+ {/* Product Chat Header */}
+
+
+ {selectedConversation.ad.images && selectedConversation.ad.images[0] ? (
+
+ ) : (
+
+
+
+ )}
+
+
{selectedConversation.ad.title}
+
+
+ {/* Product Messages */}
+
+ {isLoadingMessages ? (
+
+
+
+ ) : messages.length === 0 ? (
+
+
No messages in this conversation yet.
+
+ ) : (
+ messages.map((message, index) => (
+
+
+ {message.content}
+
+
+ ))
+ )}
+
+
+
+ {/* Product Message Input */}
+
+
+ setNewMessage(e.target.value)}
+ disabled={isSending}
+ />
+
+ {isSending ? : }
+
+
+
+
+ ) : (
+
+
Select a chat to view messages.
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/MyAdsMain.tsx b/components/MyAdsMain.tsx
new file mode 100644
index 0000000..640591d
--- /dev/null
+++ b/components/MyAdsMain.tsx
@@ -0,0 +1,574 @@
+"use client";
+
+import { useState, useEffect, useMemo, useCallback } from "react";
+import { useRouter } from "next/navigation";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DisableableDropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import {
+ Eye, Star, TrendingUp, MoreVertical, CheckCircle, Clock, XCircle,
+ Search, PlusCircle, Filter, ChevronLeft, ChevronRight, AlertCircle,
+ Share2, Edit, Trash2, BarChart2
+} from "lucide-react";
+import { Ad, SubscriptionPlan, MyAdsResponse } from '@/types';
+import { showToast } from "@/lib/toast-utils";
+import BoostAdModal from '@/components/BoostAdModal';
+import { formatCurrency } from '@/lib/utils';
+import { motion, AnimatePresence } from "framer-motion";
+import Image from "next/image";
+import Alert from '@/components/Alerts';
+import { debugRSC } from '@/lib/rsc-debug';
+
+type AlertType = 'success' | 'error' | 'warning' | 'info';
+
+export default function MyAdsManagement() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const [ads, setAds] = useState([]);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [currentPage, setCurrentPage] = useState(1);
+ const [isLoading, setIsLoading] = useState(true);
+ const itemsPerPage = 6;
+ const [alerts, setAlerts] = useState(false);
+ const [alertMessages, setAlertMessages] = useState();
+ const [alertTypes, setAlertTypes] = useState();
+ const [isBoostModalOpen, setIsBoostModalOpen] = useState(false);
+ const [selectedAd, setSelectedAd] = useState(null);
+ const [subscription, setSubscription] = useState(null);
+ const [maxFreeAds, setMaxFreeAds] = useState(5);
+ const [statusFilter, setStatusFilter] = useState('all');
+
+ // Fetch ads with React Query
+ const { data: adsData, isLoading: isAdsLoading, error } = useQuery({
+ queryKey: ['myAds'],
+ queryFn: async (): Promise => {
+ try {
+ const response = await fetch('/api/ads/my-ads', {
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ throw new Error('Authentication required');
+ }
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ if (error instanceof TypeError && error.message === 'Failed to fetch') {
+ throw new Error('Network connection failed. Please check your internet connection.');
+ }
+ throw error;
+ }
+ },
+ staleTime: 30000,
+ gcTime: 5 * 60 * 1000,
+ retry: (failureCount, error) => {
+ if (error instanceof Error &&
+ (error.message.includes('Network connection failed') ||
+ error.message.includes('Authentication required'))) {
+ return false;
+ }
+ return failureCount < 2;
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: true,
+ });
+
+ // Handle query errors
+ useEffect(() => {
+ if (error) {
+ setAlerts(true);
+ setAlertTypes('error');
+
+ if (error instanceof Error) {
+ if (error.message.includes('Authentication required')) {
+ setAlertMessages('Please log in to view your ads');
+ router.push('/signin');
+ } else if (error.message.includes('Network connection failed')) {
+ setAlertMessages('Network connection failed. Please check your internet connection and try again.');
+ } else {
+ setAlertMessages(error.message || 'Failed to fetch ads');
+ }
+ }
+ }
+ }, [error, router]);
+
+ // Update local state when data changes
+ useEffect(() => {
+ if (adsData?.ads) {
+ setAds(adsData.ads);
+ setSubscription(adsData.subscription);
+ setMaxFreeAds(adsData.maxFreeAds);
+ setIsLoading(false);
+ }
+ }, [adsData]);
+
+ // Memoize filtered ads
+ const filteredAds = useMemo(() => {
+ if (!adsData?.ads) return [];
+ return adsData.ads.filter((ad) => {
+ const matchesSearch = ad.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ ad.price.toString().includes(searchTerm.toLowerCase()) ||
+ ad.category.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesStatus = statusFilter === 'all' || ad.status === statusFilter;
+ return matchesSearch && matchesStatus;
+ });
+ }, [adsData?.ads, searchTerm, statusFilter]);
+
+ // Memoize current page ads
+ const currentAds = useMemo(() => {
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ return filteredAds.slice(startIndex, startIndex + itemsPerPage);
+ }, [filteredAds, currentPage, itemsPerPage]);
+
+ // Add subscription check for new ad button
+ const canAddNewAd = subscription || ads.length < maxFreeAds;
+
+ // Optimistic updates for status changes
+ const updateStatus = useCallback(async (id: string, newStatus: string) => {
+ queryClient.setQueryData(['myAds'], (oldData: MyAdsResponse | undefined) => {
+ if (!oldData) return oldData;
+ return {
+ ...oldData,
+ ads: oldData.ads.map(ad =>
+ ad.id === id ? { ...ad, status: newStatus } : ad
+ )
+ };
+ });
+
+ try {
+ const { data } = await debugRSC.safeApiCall(`/api/ads/${id}/status`, {
+ method: 'PATCH',
+ body: JSON.stringify({ status: newStatus }),
+ }, `Update Ad Status - ${id}`);
+
+ showToast({ type: 'success', message: data.message || 'Status updated successfully' });
+ } catch (error) {
+ queryClient.invalidateQueries({ queryKey: ['myAds'] });
+ showToast({ type: 'error', message: error instanceof Error ? error.message : 'Error updating status' });
+ }
+ }, [queryClient]);
+
+ // Handle boost ad
+ const handleBoostAd = useCallback(async (adId: string, boostType: number, duration: number) => {
+ try {
+ const { data } = await debugRSC.safeApiCall(`/api/ads/${adId}/boost`, {
+ method: 'POST',
+ body: JSON.stringify({ boostType, duration }),
+ }, `Boost Ad - ${adId}`);
+
+ queryClient.invalidateQueries({ queryKey: ['myAds'] });
+ showToast({ type: 'success', message: 'Ad boosted successfully!' });
+ setIsBoostModalOpen(false);
+ setSelectedAd(null);
+ } catch (error) {
+ // Handle subscription required error
+ if (error instanceof Error && error.message.includes('subscription')) {
+ showToast({ type: 'warning', message: 'Subscription required to boost ads' });
+ setIsBoostModalOpen(false);
+ setSelectedAd(null);
+ debugRSC.safeNavigate(router, '/dashboard/billing');
+ return;
+ }
+
+ showToast({ type: 'error', message: error instanceof Error ? error.message : 'Error boosting ad' });
+ }
+ }, [queryClient, router]);
+
+ // Handle feature
+ const handleFeature = useCallback((ad: Ad) => {
+ if (!ad.featured) {
+ if (ad.status !== "Active") {
+ showToast({ type: 'warning', message: 'Ad must be active before it can be featured' });
+ return;
+ }
+ setSelectedAd(ad);
+ setIsBoostModalOpen(true);
+ } else {
+ showToast({ type: 'info', message: 'This ad is already featured' });
+ }
+ }, []);
+
+ // Handle proper delete
+ const handleDelete = useCallback(async (id: string) => {
+ if (!window.confirm('Are you sure you want to permanently delete this ad? This action cannot be undone.')) {
+ return;
+ }
+
+ try {
+ // Optimistically remove from UI
+ queryClient.setQueryData(['myAds'], (oldData: MyAdsResponse | undefined) => {
+ if (!oldData) return oldData;
+ return {
+ ...oldData,
+ ads: oldData.ads.filter(ad => ad.id !== id)
+ };
+ });
+
+ const { data } = await debugRSC.safeApiCall(`/api/ads/${id}`, {
+ method: 'DELETE',
+ }, `Delete Ad - ${id}`);
+
+ showToast({ type: 'success', message: 'Ad deleted successfully' });
+ } catch (error) {
+ // Revert optimistic update on error
+ queryClient.invalidateQueries({ queryKey: ['myAds'] });
+ showToast({ type: 'error', message: error instanceof Error ? error.message : 'Error deleting ad' });
+ }
+ }, [queryClient]);
+
+ const updateAnalytics = async (id: string, type: 'views' | 'clicks' | 'shares') => {
+ try {
+ const { data } = await debugRSC.safeApiCall(`/api/ads/${id}/analytics`, {
+ method: 'PATCH',
+ body: JSON.stringify({ type }),
+ }, `Update Analytics - ${type}`);
+
+ setAds(ads.map((currentAd) =>
+ currentAd.id === id ? { ...currentAd, [type]: data.ad[type] } : currentAd
+ ));
+ } catch (error) {
+ setAlerts(true)
+ setAlertTypes('error');
+ setAlertMessages(`Error updating ${type}: ` + (error instanceof Error ? error.message : error));
+ }
+ };
+
+ const handleView = (id: string) => {
+ updateAnalytics(id, 'views');
+ };
+
+ const handleClick = (id: string) => {
+ updateAnalytics(id, 'clicks');
+ };
+
+ // Handle share
+ const handleShare = useCallback(async (id: string) => {
+ const ad = adsData?.ads.find(ad => ad.id === id);
+ if (!ad) {
+ return;
+ }
+
+ try {
+ const shareUrl = `${window.location.origin}/ads/${ad.id}`;
+
+ if (navigator.share) {
+ await navigator.share({
+ title: ad.title,
+ text: `Check out this ad: ${ad.title}`,
+ url: shareUrl,
+ });
+ } else {
+ await navigator.clipboard.writeText(shareUrl);
+ showToast({ type: 'success', message: 'Ad link copied to clipboard!' });
+ }
+
+ updateAnalytics(id, 'shares');
+ } catch (error) {
+ showToast({ type: 'error', message: 'Error sharing: ' + (error instanceof Error ? error.message : error) });
+ }
+ }, [adsData?.ads]);
+
+ const handlePrevPage = useCallback(() => {
+ if (currentPage > 1) setCurrentPage(currentPage - 1);
+ }, [currentPage]);
+
+ const handleNextPage = useCallback(() => {
+ if (currentPage < Math.ceil(filteredAds.length / itemsPerPage)) {
+ setCurrentPage(currentPage + 1);
+ }
+ }, [currentPage, filteredAds.length, itemsPerPage]);
+
+ if (isAdsLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {alerts && (
+
+
+
+ )}
+
+
+
My Ads
+
router.push('/dashboard/new-ad')}
+ className={`mt-4 md:mt-0 ${canAddNewAd ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-400'} text-white flex items-center gap-2`}
+ disabled={!canAddNewAd}
+ >
+
+ Post New Ad
+
+
+
+
+
+
+
+
+
+
+
setSearchTerm(e.target.value)}
+ className="pl-10 w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
+ />
+
+
+
+ setStatusFilter(e.target.value)}
+ >
+ All Status
+ Active
+ Pending
+ Inactive
+ Sold
+
+
+
+
+
+
+ {ads.length === 0 ? (
+
+
+
No ads found
+
+ You haven't posted any ads yet. Create your first ad to start selling your products or services.
+
+
+ router.push('/dashboard/new-ad')}
+ className="bg-green-600 hover:bg-green-700 text-white"
+ >
+ Post Your First Ad
+
+
+
+ ) : filteredAds.length === 0 ? (
+
+
+
+
+
No matching ads found
+
+ Try adjusting your search or filter criteria to find what you're looking for.
+
+
+ {
+ setSearchTerm('');
+ setStatusFilter('all');
+ }}
+ variant="outline"
+ >
+ Clear Filters
+
+
+
+ ) : (
+
+
+ {currentAds.map((ad, index) => (
+
+
+ {ad.images && ad.images.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+ {ad.status}
+
+
+
+
+
+
{ad.title}
+
{ad.category}
+
{formatCurrency(Number(ad.price))}
+
+
+
+ handleView(ad.id)}
+ >
+
+ {ad.views || 0}
+
+ handleClick(ad.id)}
+ >
+
+ {ad.clicks || 0}
+
+
+
+
+
+
+
+
+
+
+ router.push(`/dashboard/edit-ad/${ad.id}`)}>
+
+ Edit
+
+ handleShare(ad.id)}>
+
+ Share
+
+
+ {ad.status === "Active" && (
+ updateStatus(ad.id, "Inactive")}>
+
+ Mark as Inactive
+
+ )}
+ {ad.status === "Inactive" && (
+ updateStatus(ad.id, "Active")}>
+
+ Set as Active
+
+ )}
+ {ad.status === "Active" && (
+ updateStatus(ad.id, "Sold")}>
+
+ Mark as Sold
+
+ )}
+ {ad.status === "Active" && !ad.featured && (
+ handleFeature(ad)}>
+
+ Boost Ad
+
+ )}
+ handleDelete(ad.id)}
+ className="text-red-600"
+ >
+
+ Delete
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ Previous
+
+
+ Next
+
+
+
+ {selectedAd && (
+ {
+ setIsBoostModalOpen(false);
+ setSelectedAd(null);
+ }}
+ onBoost={handleBoostAd}
+ ad={selectedAd}
+ />
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/Navbar.tsx b/components/Navbar.tsx
new file mode 100644
index 0000000..180289d
--- /dev/null
+++ b/components/Navbar.tsx
@@ -0,0 +1,256 @@
+"use client";
+
+import React, { useState } from "react";
+import { navigation } from "@/constants";
+import Image from "next/image";
+import Link from "next/link";
+import { useSession } from "@/components/SessionWrapper";
+import { useRouter } from "next/navigation";
+import {
+ Dialog,
+ DialogPanel,
+ Popover,
+ PopoverGroup,
+ Menu,
+ MenuButton,
+ MenuItems,
+ MenuItem,
+} from "@headlessui/react";
+import {
+ Bars3Icon,
+ MagnifyingGlassIcon,
+ XMarkIcon,
+ UserCircleIcon,
+ Squares2X2Icon,
+ ArrowLeftOnRectangleIcon,
+} from "@heroicons/react/24/outline";
+import { ChevronRight } from "lucide-react";
+import { Disclosure } from "@headlessui/react";
+import Spinner from "@/components/Spinner";
+import logoImg from "../public/assets/img/agromarket-logo.png";
+import fallbackImg from "../public/assets/img/fallback.jpg";
+import { cn } from "@/lib/utils";
+
+const Navbar = () => {
+ const { session, setSession } = useSession();
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const router = useRouter();
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+ // Clear session from context
+ setSession(null);
+
+ // Clear all auth-related cookies
+ const cookies = document.cookie.split(";");
+
+ for (let cookie of cookies) {
+ const cookieName = cookie.split("=")[0].trim();
+ if (cookieName.includes("next-auth")) {
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=${window.location.hostname}`;
+ // Also try without domain for local development
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ }
+ }
+
+ // Call the API to clear server-side session
+ await fetch('/api/logout', {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ router.push('/signin');
+ router.refresh(); // Force a router refresh
+ } catch (error) {
+ console.error('Logout error:', error);
+ } finally {
+ setIsLoggingOut(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+ {/* Mobile menu button */}
+
setMobileMenuOpen(true)}
+ className="lg:hidden rounded-md bg-white p-2 text-gray-400"
+ >
+ Open menu
+
+
+
+ {/* Logo */}
+
+
+
+
+
+
+ {/* Desktop Navigation */}
+
+ {/* Navigation Pages */}
+ {navigation.pages.map((page) => (
+
+ {page.name}
+
+ ))}
+
+
+ {/* Search Icon and Auth */}
+
+
+
+
+ Search
+
+
+
+ {/* Authentication UI */}
+ {session ? (
+
+
+ {session?.image ? (
+
+ ) : (
+
+ )}
+
+
+
+ {({ active }) => (
+
+
+ Dashboard
+
+ )}
+
+
+ {({ active }) => (
+
+
+ Profile
+
+ )}
+
+
+ {({ active }) => (
+
+
+ {isLoggingOut ? ( ) : "Logout"}
+
+ )}
+
+
+
+ ) : (
+
+
+ Sign in
+
+
+ Create account
+
+
+ )}
+
+
+
+
+
+ {/* Mobile Menu */}
+
+
+ {/* Header - Fixed at top */}
+
+
+
+ setMobileMenuOpen(false)}
+ className="p-2 rounded-md hover:bg-gray-100"
+ >
+
+
+
+
+
+ {/* Scrollable Content */}
+
+
+ {/* Other Navigation Items */}
+
+ {navigation.pages.map((page) => (
+ setMobileMenuOpen(false)}
+ >
+ {page.name}
+
+ ))}
+
+
+
+
+ {/* Footer - Fixed at bottom */}
+
+ {session ? (
+
+
+ {session?.image ? (
+
+ ) : (
+
+ )}
+
+ {session.email}
+
+
+
+ {isLoggingOut ? : "Logout"}
+
+
+ ) : (
+
+ setMobileMenuOpen(false)}
+ >
+ Sign in
+
+ setMobileMenuOpen(false)}
+ >
+ Create account
+
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default Navbar;
diff --git a/components/NotificationsMain.tsx b/components/NotificationsMain.tsx
new file mode 100644
index 0000000..7e6fbae
--- /dev/null
+++ b/components/NotificationsMain.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import React, { useEffect } from "react";
+import { Bell, CheckCircle, Clock, XCircle, Trash, Loader2, AlertCircle, RefreshCw } from "lucide-react";
+import { Notification } from "@/types";
+import { useNotifications } from "@/lib/hooks/useNotifications";
+import { Button } from "@/components/ui/button";
+
+export default function NotificationsCenter() {
+ const {
+ notifications,
+ error,
+ isLoading,
+ isMarking,
+ isDeleting,
+ markAsRead,
+ deleteNotifications,
+ clearAll,
+ refreshNotifications
+ } = useNotifications();
+
+ // Mark all unread notifications as read when component mounts
+ useEffect(() => {
+ if (notifications && notifications.length > 0) {
+ const unreadIds = notifications
+ .filter((n: Notification) => !n.read)
+ .map((n: Notification) => n.id);
+
+ if (unreadIds.length > 0) {
+ markAsRead(unreadIds);
+ }
+ }
+ }, [notifications]);
+
+ // Function to delete a single notification
+ const deleteNotification = async (id: string) => {
+ await deleteNotifications([id]);
+ };
+
+ const getNotificationIcon = (type: string) => {
+ switch (type) {
+ case "ad":
+ return ;
+ case "promotion":
+ return ;
+ case "payment":
+ return ;
+ case "payment-failed":
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+ Notifications Center
+
+
+ refreshNotifications()}
+ disabled={isLoading}
+ className="flex items-center gap-1"
+ >
+
+ Refresh
+
+ {notifications && notifications.length > 0 && (
+
+ {isDeleting ? : }
+ Clear All
+
+ )}
+
+
+
+ {isLoading && !notifications?.length ? (
+
+
+
+ ) : error ? (
+
+
+
{error.message || "Failed to load notifications"}
+
refreshNotifications()}
+ className="mt-2"
+ >
+ Try Again
+
+
+ ) : !notifications || notifications.length === 0 ? (
+
+
+
No notifications found.
+
+ ) : (
+
+ {notifications.map((notification) => (
+
+
+ {getNotificationIcon(notification.type)}
+
+
{notification.message}
+
{notification.time}
+
+
+
deleteNotification(notification.id)}
+ disabled={isDeleting}
+ className="text-gray-500 hover:text-red-600 transition"
+ aria-label="Delete notification"
+ >
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/components/PaymentModal.tsx b/components/PaymentModal.tsx
new file mode 100644
index 0000000..73e4b13
--- /dev/null
+++ b/components/PaymentModal.tsx
@@ -0,0 +1,43 @@
+"use client";
+
+import { useEffect } from 'react';
+import { PaymentModalProps } from '@/types';
+
+declare global {
+ interface Window {
+ PaystackPop: {
+ setup(config: any): { openIframe(): void };
+ };
+ }
+}
+
+export default function PaymentModal({ isOpen, onClose, paymentDetails, onSuccess }: PaymentModalProps) {
+ useEffect(() => {
+ if (isOpen && window.PaystackPop) {
+ const handler = window.PaystackPop.setup({
+ key: process.env.NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY,
+ email: paymentDetails.email,
+ amount: paymentDetails.amount * 100,
+ ref: paymentDetails.reference,
+ metadata: {
+ adId: paymentDetails.adId,
+ boostType: paymentDetails.boostType,
+ boostDuration: paymentDetails.boostDuration
+ },
+ onClose: () => {
+ onClose();
+ },
+ callback: (response: { reference: string }) => {
+ if (response.reference) {
+ onSuccess(response.reference);
+ }
+ onClose();
+ },
+ });
+
+ handler.openIframe();
+ }
+ }, [isOpen, paymentDetails, onSuccess, onClose]);
+
+ return null;
+}
\ No newline at end of file
diff --git a/components/PostNewAdMain.tsx b/components/PostNewAdMain.tsx
new file mode 100644
index 0000000..88cdd84
--- /dev/null
+++ b/components/PostNewAdMain.tsx
@@ -0,0 +1,561 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+
+import FileUpload from "@/components/ui/file-upload";
+import { Loader2, AlertCircle, ArrowLeft } from "lucide-react";
+import { navigation } from "@/constants";
+import { mapUiCategoryToDatabase } from "@/lib/categoryUtils";
+import { motion } from "framer-motion";
+import { showToast } from "@/lib/toast-utils";
+import Link from "next/link";
+
+type FileState = File[];
+
+interface FormErrors {
+ title?: string;
+ category?: string;
+ subcategory?: string;
+ location?: string;
+ price?: string;
+ description?: string;
+ contact?: string;
+ images?: string;
+}
+
+export default function PostNewAd() {
+ const router = useRouter();
+ const [formData, setFormData] = useState({
+ title: "",
+ category: "",
+ subcategory: "",
+ section: "",
+ location: "",
+ price: "",
+ description: "",
+ contact: "",
+ subscriptionPlanId: ""
+ });
+
+ const [errors, setErrors] = useState({});
+ const [availableSubcategories, setAvailableSubcategories] = useState<{ name: string }[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [selectedFiles, setSelectedFiles] = useState([]);
+ const [formSubmitted, setFormSubmitted] = useState(false);
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+
+ // If changing category, reset subcategory and update available subcategories
+ if (name === 'category') {
+ setFormData({
+ ...formData,
+ [name]: value,
+ subcategory: '',
+ section: ''
+ });
+ } else if (name === 'subcategory') {
+ // When subcategory changes
+ setFormData({
+ ...formData,
+ subcategory: value
+ });
+ } else {
+ setFormData({ ...formData, [name]: value });
+ }
+ };
+
+ // Map UI categories to navigation categories
+ const getCategoryMapping = (uiCategory: string) => {
+ return mapUiCategoryToDatabase(uiCategory);
+ };
+
+ // No need for debugging function anymore
+
+ // Update subcategories when category changes
+ useEffect(() => {
+ if (formData.category) {
+ // Map the UI category to the corresponding navigation category
+ const mappedCategory = getCategoryMapping(formData.category);
+
+ // Find the category in the navigation structure
+ const categoryData = navigation.categories.find(
+ cat => cat.name === mappedCategory
+ );
+
+ if (categoryData) {
+ // Get items directly from the category
+ const subcategories = categoryData.items
+ .filter(item => item.name !== 'Browse All') // Filter out "Browse All" items
+ .map(item => ({
+ name: item.name
+ }));
+
+ setAvailableSubcategories(subcategories);
+ console.log(`Found ${subcategories.length} subcategories for ${formData.category} (mapped to ${mappedCategory})`);
+ } else {
+ console.log(`No category data found for ${formData.category} (mapped to ${mappedCategory})`);
+ setAvailableSubcategories([]);
+ }
+ } else {
+ setAvailableSubcategories([]);
+ }
+ }, [formData.category]);
+
+ const handleFileChange = (files: File[]) => {
+ setSelectedFiles(files);
+
+ // Clear image error when files are selected
+ if (files.length > 0 && errors.images) {
+ setErrors(prev => ({ ...prev, images: undefined }));
+ }
+ };
+
+ const validateForm = (): boolean => {
+ const newErrors: FormErrors = {};
+ let isValid = true;
+
+ // Title validation
+ if (!formData.title.trim()) {
+ newErrors.title = "Title is required";
+ isValid = false;
+ } else if (formData.title.length < 5) {
+ newErrors.title = "Title must be at least 5 characters";
+ isValid = false;
+ } else if (formData.title.length > 100) {
+ newErrors.title = "Title must be less than 100 characters";
+ isValid = false;
+ }
+
+ // Category validation
+ if (!formData.category) {
+ newErrors.category = "Category is required";
+ isValid = false;
+ }
+
+ // Subcategory validation (only if category is selected and subcategories are available)
+ if (formData.category && availableSubcategories.length > 0 && !formData.subcategory) {
+ newErrors.subcategory = "Subcategory is required";
+ isValid = false;
+ }
+
+ // Location validation
+ if (!formData.location.trim()) {
+ newErrors.location = "Location is required";
+ isValid = false;
+ }
+
+ // Price validation
+ if (!formData.price) {
+ newErrors.price = "Price is required";
+ isValid = false;
+ } else {
+ const price = parseFloat(formData.price);
+ if (isNaN(price)) {
+ newErrors.price = "Price must be a number";
+ isValid = false;
+ } else if (price <= 0) {
+ newErrors.price = "Price must be greater than 0";
+ isValid = false;
+ } else if (price > 99999999) {
+ newErrors.price = "Price is too high";
+ isValid = false;
+ }
+ }
+
+ // Description validation
+ if (!formData.description.trim()) {
+ newErrors.description = "Description is required";
+ isValid = false;
+ } else if (formData.description.length < 20) {
+ newErrors.description = "Description must be at least 20 characters";
+ isValid = false;
+ } else if (formData.description.length > 2000) {
+ newErrors.description = "Description must be less than 2000 characters";
+ isValid = false;
+ }
+
+ // Contact validation
+ if (!formData.contact.trim()) {
+ newErrors.contact = "Contact information is required";
+ isValid = false;
+ } else {
+ // Simple validation for phone or email
+ const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.contact);
+ const isPhone = /^[0-9+\-\s()]{7,20}$/.test(formData.contact);
+
+ if (!isEmail && !isPhone) {
+ newErrors.contact = "Please enter a valid email or phone number";
+ isValid = false;
+ }
+ }
+
+ // Image validation
+ if (selectedFiles.length === 0) {
+ newErrors.images = "Please upload at least one image";
+ isValid = false;
+ }
+
+ setErrors(newErrors);
+ return isValid;
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setFormSubmitted(true);
+
+ // Validate form
+ if (!validateForm()) {
+ // Scroll to the first error
+ const firstErrorElement = document.querySelector('.error-message');
+ if (firstErrorElement) {
+ firstErrorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
+ // Show toast for validation errors
+ showToast({ message: 'Please fix the errors in the form', type: 'error' });
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ // Check network connectivity first
+ if (!navigator.onLine) {
+ throw new Error('No internet connection. Please check your network and try again.');
+ }
+
+ const submitFormData = new FormData();
+
+ // Append form fields
+ Object.keys(formData).forEach(key => {
+ submitFormData.append(key, formData[key as keyof typeof formData]);
+ });
+
+ // Validate and append files
+ if (selectedFiles.length === 0) {
+ throw new Error('Please upload at least one image');
+ }
+
+ // Check file sizes again before submission
+ const oversizedFiles = selectedFiles.filter(file => file.size > 2 * 1024 * 1024);
+ if (oversizedFiles.length > 0) {
+ throw new Error(`${oversizedFiles.length} image(s) exceed the 2MB size limit. Please resize and try again.`);
+ }
+
+ // Append files
+ selectedFiles.forEach(file => {
+ submitFormData.append('images', file);
+ });
+
+ // Set timeout for the request
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
+
+ try {
+ const response = await fetch('/api/postAd', {
+ method: 'POST',
+ body: submitFormData,
+ signal: controller.signal
+ });
+
+ clearTimeout(timeoutId);
+
+ const data = await response.json();
+
+ if (response.ok) {
+ showToast({ message: 'Your ad has been posted successfully!', type: 'success' });
+
+ // Show success state briefly before redirecting
+ setTimeout(() => {
+ router.push('/dashboard/my-ads');
+ }, 1500);
+ } else {
+ // Handle specific error codes
+ if (response.status === 413) {
+ throw new Error('The uploaded files are too large. Please reduce the image sizes and try again.');
+ } else if (response.status === 429) {
+ throw new Error('Too many requests. Please wait a moment and try again.');
+ } else {
+ throw new Error(data.error || `Failed to post ad (Error ${response.status})`);
+ }
+ }
+ } catch (fetchError) {
+ clearTimeout(timeoutId);
+
+ if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {
+ throw new Error('Request timed out. Please try again or check your internet connection.');
+ }
+ throw fetchError;
+ }
+ } catch (error) {
+ console.error('Ad posting error:', error);
+ showToast({ message: error instanceof Error ? error.message : 'Failed to post ad', type: 'error' });
+ setLoading(false);
+ }
+ };
+
+ // Loading/Success state after form submission
+ if (loading && formSubmitted) {
+ return (
+
+
+
+
+
+
Posting Your Ad
+
+ Your ad is being processed. Please wait a moment...
+
+
+
This may take a few seconds if you're uploading multiple images
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Tip: High-quality ads with clear images and detailed descriptions get up to 3x more responses!
+
+
+
+
+
+
Ad Title *
+
+ {errors.title && (
+
+
+ {errors.title}
+
+ )}
+
A clear, descriptive title will attract more buyers (5-100 characters)
+
+
+
+
+
Category *
+
+ Select a category
+ {navigation.categories.map(cat => (
+ {cat.name}
+ ))}
+
+ {errors.category && (
+
+
+ {errors.category}
+
+ )}
+
+
+ {formData.category && (
+
+
+ Subcategory *
+ {availableSubcategories.length === 0 && (
+
+ (Loading subcategories...)
+
+ )}
+
+
+ Select a subcategory
+ {availableSubcategories.map(sub => (
+ {sub.name}
+ ))}
+
+ {errors.subcategory && (
+
+
+ {errors.subcategory}
+
+ )}
+ {availableSubcategories.length === 0 && formData.category && (
+
+ No subcategories found for {formData.category}. Please try selecting a different category.
+
+ )}
+
+ )}
+
+
+
+
+
Location *
+
+ {errors.location && (
+
+
+ {errors.location}
+
+ )}
+
+
+
+
Price (ā¦) *
+
+ ā¦
+
+
+ {errors.price && (
+
+
+ {errors.price}
+
+ )}
+
+
+
+
+
Description *
+
+ {errors.description && (
+
+
+ {errors.description}
+
+ )}
+
+ Include details like condition, features, benefits, and why someone should buy your product (20-2000 characters)
+
+
+ {formData.description.length}/2000 characters
+
+
+
+
+
Contact Details *
+
+ {errors.contact && (
+
+
+ {errors.contact}
+
+ )}
+
This will be visible to potential buyers. Enter a valid phone number or email address.
+
+
+
+
+
+ {Object.keys(errors).length > 0 && formSubmitted && (
+
+
+
+
+
Please fix the following errors:
+
+ {Object.entries(errors).map(([field, message]) => (
+ {message}
+ ))}
+
+
+
+
+ )}
+
+
+ {loading ? : "Post Ad"}
+
+
+
+ By posting this ad, you agree to our Terms of Service and Privacy Policy
+
+
+
+
+ );
+}
diff --git a/components/ProductCard.tsx b/components/ProductCard.tsx
new file mode 100644
index 0000000..120724e
--- /dev/null
+++ b/components/ProductCard.tsx
@@ -0,0 +1,82 @@
+import Image from 'next/image';
+import Link from 'next/link';
+import { Eye, MessageCircle, MapPin, Clock } from 'lucide-react';
+import { formatCurrency } from '@/lib/utils';
+import { ProductCardProps} from '@/types';
+import { formatDistanceToNow } from 'date-fns';
+
+
+export default function ProductCard({ product }: ProductCardProps) {
+ return (
+
+ {/* Image Container */}
+
+
+ {product.featured && (
+
+ Featured
+
+ )}
+
+
+ {formatDistanceToNow(new Date(product.createdAt), { addSuffix: true })}
+
+
+
+ {/* Content */}
+
+
+
+ {product.title}
+
+
+
+ {formatCurrency(product.price)}
+
+
+
+
+
+ {product.location}
+
+
+
+ {product.description}
+
+
+ {/* Stats */}
+
+
+
+
+ {product.views}
+
+
+
+ {product.shares}
+
+
+
+
+ {product.category}
+
+ {product.subcategory && (
+
+ {product.subcategory}
+
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/ProfileMain.tsx b/components/ProfileMain.tsx
new file mode 100644
index 0000000..2c7ba8e
--- /dev/null
+++ b/components/ProfileMain.tsx
@@ -0,0 +1,1047 @@
+"use client";
+
+import { useState, useEffect, useRef } from "react";
+import { useRouter } from "next/navigation";
+import {
+ Image,
+ LogOut,
+ Loader2,
+ User,
+ Check,
+ X,
+ Eye,
+ EyeOff,
+ Upload,
+ Camera,
+ AlertCircle,
+ Phone
+} from "lucide-react";
+import ImageCropper from "./ImageCropper";
+import TwoFactorAuth from "./TwoFactorAuth";
+import EmailChange from "./EmailChange";
+import AccountDeletion from "./AccountDeletion";
+import ActivityHistory from "./ActivityHistory";
+import { cn } from "@/lib/utils";
+import { userprofile } from "@/constants";
+import React from "react";
+import { useSession } from "@/components/SessionWrapper";
+import toast from "react-hot-toast";
+
+interface UserProfile {
+ id: string;
+ name: string;
+ email: string;
+ phone?: string | null;
+ image?: string | null;
+ role: string;
+ twoFactorEnabled?: boolean;
+ createdAt: string;
+ updatedAt?: string;
+}
+
+interface FormErrors {
+ name?: string;
+ currentPassword?: string;
+ newPassword?: string;
+ confirmPassword?: string;
+ image?: string;
+}
+
+export default function ProfileSettings() {
+ const [activeTab, setActiveTab] = useState("personal");
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [isChangingPassword, setIsChangingPassword] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+ const [profile, setProfile] = useState(null);
+ const [formData, setFormData] = useState({
+ name: "",
+ phone: "",
+ });
+ const [notificationPreferences, setNotificationPreferences] = useState({
+ emailForAdActivity: true,
+ emailForPromotions: true,
+ smsForPromotions: false,
+ emailForMessages: true,
+ emailForPayments: true,
+ });
+ const [isSavingPreferences, setIsSavingPreferences] = useState(false);
+ const [showImageCropper, setShowImageCropper] = useState(false);
+ const [selectedImageFile, setSelectedImageFile] = useState(null);
+ const [passwordData, setPasswordData] = useState({
+ currentPassword: "",
+ newPassword: "",
+ confirmPassword: "",
+ });
+ const [showCurrentPassword, setShowCurrentPassword] = useState(false);
+ const [showNewPassword, setShowNewPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [errors, setErrors] = useState({});
+ const [previewImage, setPreviewImage] = useState(null);
+ const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
+
+ const fileInputRef = useRef(null);
+ const router = useRouter();
+ const { session, setSession } = useSession();
+
+ // Fetch user profile and notification preferences on component mount
+ useEffect(() => {
+ fetchUserProfile();
+ fetchNotificationPreferences();
+ }, []);
+
+ // Update form data when profile is loaded
+ useEffect(() => {
+ if (profile) {
+ setFormData({
+ name: profile.name || "",
+ phone: "", // Add phone field if available in your User model
+ });
+ }
+ }, [profile]);
+
+ const fetchUserProfile = async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/user/profile', {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch profile');
+ }
+
+ const data = await response.json();
+ setProfile(data.user);
+ setTwoFactorEnabled(data.user.twoFactorEnabled || false);
+ } catch (error) {
+ console.error('Error fetching profile:', error);
+ toast.error('Failed to load profile information');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const fetchNotificationPreferences = async () => {
+ try {
+ const response = await fetch('/api/user/profile/notification-preferences', {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch notification preferences');
+ }
+
+ const data = await response.json();
+ setNotificationPreferences(data.preferences);
+ } catch (error) {
+ console.error('Error fetching notification preferences:', error);
+ // Don't show toast error here as it's not critical
+ }
+ };
+
+ const handleNotificationPreferenceChange = (key: string, value: boolean) => {
+ setNotificationPreferences(prev => ({
+ ...prev,
+ [key]: value
+ }));
+ };
+
+ const handle2FAStatusChange = (enabled: boolean) => {
+ setTwoFactorEnabled(enabled);
+ if (profile) {
+ setProfile({ ...profile, twoFactorEnabled: enabled });
+ }
+ };
+
+ const handleEmailChanged = (newEmail: string) => {
+ if (profile) {
+ setProfile({ ...profile, email: newEmail });
+ }
+ fetchUserProfile(); // Refresh profile to get updated data
+ };
+
+ const handleSaveNotificationPreferences = async () => {
+ try {
+ setIsSavingPreferences(true);
+
+ const response = await fetch('/api/user/profile/notification-preferences', {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(notificationPreferences),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to update notification preferences');
+ }
+
+ const data = await response.json();
+ setNotificationPreferences(data.preferences);
+ toast.success('Notification preferences updated successfully');
+ } catch (error) {
+ console.error('Error updating notification preferences:', error);
+ toast.error('Failed to update notification preferences');
+ } finally {
+ setIsSavingPreferences(false);
+ }
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear error when user types
+ if (errors[name as keyof FormErrors]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: undefined
+ }));
+ }
+ };
+
+ const handlePasswordChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setPasswordData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear error when user types
+ if (errors[name as keyof FormErrors]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: undefined
+ }));
+ }
+ };
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Validate file type
+ const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
+ if (!validTypes.includes(file.type)) {
+ setErrors(prev => ({
+ ...prev,
+ image: '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) {
+ setErrors(prev => ({
+ ...prev,
+ image: 'File size exceeds the 5MB limit'
+ }));
+ return;
+ }
+
+ // Clear previous error
+ setErrors(prev => ({
+ ...prev,
+ image: undefined
+ }));
+
+ // Store file and show cropper instead of direct preview
+ setSelectedImageFile(file);
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setPreviewImage(e.target?.result as string);
+ setShowImageCropper(true);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleCropComplete = (croppedImageFile: File) => {
+ setSelectedImageFile(croppedImageFile);
+ setShowImageCropper(false);
+
+ // Create preview of cropped image
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setPreviewImage(e.target?.result as string);
+ };
+ reader.readAsDataURL(croppedImageFile);
+ };
+
+ const handleCropCancel = () => {
+ setShowImageCropper(false);
+ setSelectedImageFile(null);
+ setPreviewImage(null);
+
+ // Reset file input
+ const fileInput = fileInputRef.current;
+ if (fileInput) {
+ fileInput.value = '';
+ }
+ };
+
+ const validateProfileForm = () => {
+ const newErrors: FormErrors = {};
+
+ if (!formData.name.trim()) {
+ newErrors.name = 'Name is required';
+ } else if (formData.name.trim().length < 2) {
+ newErrors.name = 'Name must be at least 2 characters long';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const validatePasswordForm = () => {
+ const newErrors: FormErrors = {};
+
+ if (!passwordData.currentPassword) {
+ newErrors.currentPassword = 'Current password is required';
+ }
+
+ if (!passwordData.newPassword) {
+ newErrors.newPassword = 'New password is required';
+ } else if (passwordData.newPassword.length < 8) {
+ newErrors.newPassword = 'New password must be at least 8 characters long';
+ }
+
+ if (passwordData.newPassword !== passwordData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleProfileUpdate = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateProfileForm()) return;
+
+ try {
+ setIsSaving(true);
+
+ const response = await fetch('/api/user/profile', {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(formData),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to update profile');
+ }
+
+ const data = await response.json();
+ setProfile(data.user);
+ toast.success('Profile updated successfully');
+ } catch (error) {
+ console.error('Error updating profile:', error);
+ toast.error('Failed to update profile');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handlePasswordUpdate = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validatePasswordForm()) return;
+
+ try {
+ setIsChangingPassword(true);
+
+ const response = await fetch('/api/user/profile/password', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ currentPassword: passwordData.currentPassword,
+ newPassword: passwordData.newPassword
+ }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to update password');
+ }
+
+ // Reset form
+ setPasswordData({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
+
+ toast.success('Password updated successfully');
+ } catch (error: any) {
+ console.error('Error updating password:', error);
+
+ if (error.message.includes('Current password is incorrect')) {
+ setErrors(prev => ({
+ ...prev,
+ currentPassword: 'Current password is incorrect'
+ }));
+ } else {
+ toast.error(error.message || 'Failed to update password');
+ }
+ } finally {
+ setIsChangingPassword(false);
+ }
+ };
+
+ const handleImageUpload = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!selectedImageFile) {
+ setErrors(prev => ({
+ ...prev,
+ image: 'Please select and crop an image to upload'
+ }));
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+
+ const formData = new FormData();
+ formData.append('image', selectedImageFile);
+
+ const response = await fetch('/api/user/profile/image', {
+ method: 'POST',
+ body: formData,
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to upload image');
+ }
+
+ const data = await response.json();
+ setProfile(data.user);
+ setPreviewImage(null);
+ setSelectedImageFile(null);
+
+ // Reset file input
+ const fileInput = fileInputRef.current;
+ if (fileInput) {
+ fileInput.value = '';
+ }
+
+ // Update session with new image
+ if (session && data.user.image) {
+ setSession({
+ ...session,
+ image: data.user.image
+ });
+ }
+
+ toast.success('Profile picture updated successfully');
+ } catch (error) {
+ console.error('Error uploading image:', error);
+ toast.error('Failed to upload profile picture');
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const handleLogout = async () => {
+ try {
+ setIsLoggingOut(true);
+
+ // Clear session from context
+ setSession(null);
+
+ // Clear all auth-related cookies
+ const cookies = document.cookie.split(";");
+
+ for (let cookie of cookies) {
+ const cookieName = cookie.split("=")[0].trim();
+ if (cookieName.includes("next-auth")) {
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=${window.location.hostname}`;
+ // Also try without domain for local development
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
+ }
+ }
+
+ // Call the API to clear server-side session
+ await fetch('/api/logout', {
+ method: 'POST',
+ credentials: 'include'
+ });
+
+ toast.success("Logged out successfully");
+ router.push('/signin');
+ } catch (error) {
+ console.error('Logout error:', error);
+ toast.error("Failed to log out. Please try again.");
+ } finally {
+ setIsLoggingOut(false);
+ }
+ };
+
+ return (
+
+
š¤ Profile & Account Settings
+
Manage your personal information, security, and preferences.
+
+ {/* Tabs */}
+
+ {userprofile.map(({ key, label, icon }) => (
+ setActiveTab(key)}
+ >
+ {React.createElement(icon, { className: "w-4 h-4" })} {label}
+
+ ))}
+
+
+ {/* Personal Information */}
+ {activeTab === "personal" && (
+
+
+ Personal Information
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
Full Name
+
+
+
+
+ {errors.name && (
+
+ {errors.name}
+
+ )}
+
+
+ {/* Email Change Section */}
+
+ Email Address
+
+
+
+
+
Phone Number (Optional)
+
+
+
+
+
+ {isSaving ? (
+ <>
+
+ Saving Changes...
+ >
+ ) : (
+ <>
+
+ Save Changes
+ >
+ )}
+
+
+
+
+
Last updated: {profile?.updatedAt ? new Date(profile.updatedAt).toLocaleString() : 'Never'}
+
+
+ )}
+
+ )}
+
+ {/* Profile Picture */}
+ {activeTab === "avatar" && (
+
+
+ Profile Picture
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+
+ {previewImage || profile?.image ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+ Upload New Image
+
+
+
+
+ {errors.image && (
+
+ {errors.image}
+
+ )}
+
+ Accepted formats: JPEG, PNG, WebP, GIF. Maximum size: 5MB.
+
+
+
+
+ {isUploading ? (
+ <>
+
+ Uploading...
+ >
+ ) : (
+ <>
+
+ Upload Profile Picture
+ >
+ )}
+
+
+
+
+ )}
+
+ )}
+
+ {/* Notification Preferences */}
+ {activeTab === "notifications" && (
+
+
+ Notification Preferences
+
+
Choose how you want to receive notifications about your account activity.
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+
+
Email notifications for ad activity
+
Get notified when your ads are approved, viewed, or expire
+
+
handleNotificationPreferenceChange('emailForAdActivity', e.target.checked)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
+ />
+
+
+
+
+
Email notifications for promotions
+
Receive promotional offers and marketing emails
+
+
handleNotificationPreferenceChange('emailForPromotions', e.target.checked)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
+ />
+
+
+
+
+
SMS notifications for promotions
+
Get promotional offers via text messages
+
+
handleNotificationPreferenceChange('smsForPromotions', e.target.checked)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
+ />
+
+
+
+
+
Email notifications for messages
+
Get notified when you receive new messages from buyers/sellers
+
+
handleNotificationPreferenceChange('emailForMessages', e.target.checked)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
+ />
+
+
+
+
+
Email notifications for payments
+
Get notified about payment confirmations, failures, and refunds
+
+
handleNotificationPreferenceChange('emailForPayments', e.target.checked)}
+ className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
+ />
+
+
+
+
+ {isSavingPreferences ? (
+ <>
+
+ Saving Preferences...
+ >
+ ) : (
+ <>
+
+ Save Preferences
+ >
+ )}
+
+
+ )}
+
+ )}
+
+ {/* Password & Security */}
+ {activeTab === "security" && (
+
+
+ Password & Security
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
Current Password
+
+
+ setShowCurrentPassword(!showCurrentPassword)}
+ className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
+ >
+ {showCurrentPassword ? : }
+
+
+ {errors.currentPassword && (
+
+ {errors.currentPassword}
+
+ )}
+
+
+
+
New Password
+
+
+ setShowNewPassword(!showNewPassword)}
+ className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
+ >
+ {showNewPassword ? : }
+
+
+ {errors.newPassword && (
+
+ {errors.newPassword}
+
+ )}
+
Password must be at least 8 characters long
+
+
+
+
Confirm New Password
+
+
+ setShowConfirmPassword(!showConfirmPassword)}
+ className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
+ >
+ {showConfirmPassword ? : }
+
+
+ {errors.confirmPassword && (
+
+ {errors.confirmPassword}
+
+ )}
+
+
+
+
+ {isChangingPassword ? (
+ <>
+
+ Updating Password...
+ >
+ ) : (
+ <>
+
+ Update Password
+ >
+ )}
+
+
+
+ {/* Two-Factor Authentication Section */}
+
+
+
+
+ {/* Account Deletion Section */}
+
+
+
+
Session Actions
+
+ {isLoggingOut ? (
+ <>
+
+ Logging out...
+ >
+ ) : (
+ <>
+
+ Logout
+ >
+ )}
+
+
+
+ )}
+
+ )}
+
+ {/* Activity History */}
+ {activeTab === "activity" && (
+
+ )}
+
+ {/* Logout Tab */}
+ {activeTab === "logout" && (
+
+
+ Logout
+
+
+
+
+
+
+
+
Sign Out from Your Account
+
+ You will be logged out from all devices and will need to sign in again to access your account.
+
+
+
+
+
+
+
+
+ Any unsaved changes in other tabs will be lost when you log out.
+
+
+
+
+ {isLoggingOut ? (
+ <>
+
+ Logging out...
+ >
+ ) : (
+ <>
+
+ Sign Out
+ >
+ )}
+
+
+
+
+ )}
+
+ {/* Image Cropper Modal */}
+ {showImageCropper && previewImage && (
+
+ )}
+
+ );
+}
diff --git a/components/RSCErrorBoundary.tsx b/components/RSCErrorBoundary.tsx
new file mode 100644
index 0000000..73043a4
--- /dev/null
+++ b/components/RSCErrorBoundary.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { Component, ReactNode } from 'react';
+import { Button } from '@/components/ui/button';
+
+interface Props {
+ children: ReactNode;
+ fallback?: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error?: Error;
+}
+
+export class RSCErrorBoundary extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: any) {
+ console.error('RSC Error Boundary caught an error:', error, errorInfo);
+
+ // Check if it's an RSC payload error
+ if (error.message?.includes('RSC payload') ||
+ error.message?.includes('NetworkError')) {
+ // Try to reload the page after a short delay
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+ }
+ }
+
+ render() {
+ if (this.state.hasError) {
+ if (this.props.fallback) {
+ return this.props.fallback;
+ }
+
+ return (
+
+
+
+ Something went wrong
+
+
+ {this.state.error?.message?.includes('NetworkError') ||
+ this.state.error?.message?.includes('RSC payload')
+ ? "There was a network issue. The page will reload automatically."
+ : "An unexpected error occurred. Please try refreshing the page."}
+
+
+ {
+ this.setState({ hasError: false, error: undefined });
+ window.location.reload();
+ }}
+ className="bg-green-600 hover:bg-green-700 text-white"
+ >
+ Reload Page
+
+ {
+ this.setState({ hasError: false, error: undefined });
+ }}
+ variant="outline"
+ >
+ Try Again
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/components/RecentlyViewed.tsx b/components/RecentlyViewed.tsx
new file mode 100644
index 0000000..9102433
--- /dev/null
+++ b/components/RecentlyViewed.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { useState, useEffect } from 'react';
+import { useRecentlyViewed } from '@/lib/recentlyViewed';
+import Link from 'next/link';
+import Image from 'next/image';
+import { Clock, X, MapPin, Tag } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+
+export default function RecentlyViewed() {
+ const { getItems, removeItem, clearAll, hasItems } = useRecentlyViewed();
+ const [recentItems, setRecentItems] = useState([]);
+ const [isVisible, setIsVisible] = useState(false);
+
+ useEffect(() => {
+ const items = getItems(5);
+ setRecentItems(items);
+ setIsVisible(items.length > 0);
+ }, []);
+
+ const handleRemoveItem = (id: string) => {
+ removeItem(id);
+ const updatedItems = getItems(5);
+ setRecentItems(updatedItems);
+ setIsVisible(updatedItems.length > 0);
+ };
+
+ const handleClearAll = () => {
+ clearAll();
+ setRecentItems([]);
+ setIsVisible(false);
+ };
+
+ if (!isVisible || recentItems.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
Recently Viewed
+
+
+ Clear all
+
+
+
+
+ {recentItems.map((item) => (
+
+
handleRemoveItem(item.id)}
+ className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity bg-white rounded-full p-1 shadow-sm hover:bg-gray-100"
+ >
+
+
+
+
+
+ {item.images && item.images.length > 0 ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {item.title}
+
+
+
+ ā¦{item.price.toLocaleString()}
+
+
+
+
+ {item.location}
+
+
+
+
+ {item.category}
+
+
+
+
+ Viewed {new Date(item.viewedAt).toLocaleDateString()}
+
+
+
+
+ ))}
+
+
+ {recentItems.length >= 5 && (
+
+
+ View all recently viewed items ā
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/Search.tsx b/components/Search.tsx
new file mode 100644
index 0000000..c9b5f89
--- /dev/null
+++ b/components/Search.tsx
@@ -0,0 +1,416 @@
+"use client";
+
+import { useState, useEffect, useRef } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { SearchSuggestions } from '@/components/SearchSuggestions';
+import { Slider } from '@/components/ui/slider';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { categories, navigation } from '@/constants';
+import { Search as SearchIcon, SlidersHorizontal, History, X } from 'lucide-react';
+import { FilterState } from '@/types';
+import { useSearchHistory } from '@/lib/searchHistory';
+import RecentlyViewed from '@/components/RecentlyViewed';
+
+
+export default function Search() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [showSuggestions, setShowSuggestions] = useState(false);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [showFilters, setShowFilters] = useState(false);
+ const [showHistory, setShowHistory] = useState(false);
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
+ const [filters, setFilters] = useState({
+ minPrice: 0,
+ maxPrice: 1000000,
+ category: '',
+ subCategory: '',
+ location: '',
+ sortBy: 'recent'
+ });
+
+ const { addSearch, getRecentSearches, removeSearch } = useSearchHistory();
+ const [recentSearches, setRecentSearches] = useState([]);
+
+ const searchInputRef = useRef(null);
+ const filtersButtonRef = useRef(null);
+
+ const getAllSuggestions = (term: string) => {
+ if (!term) return [];
+
+ const allSuggestions: any[] = [];
+ navigation.categories.forEach(category => {
+ if (category.name.toLowerCase().includes(term.toLowerCase())) {
+ allSuggestions.push({
+ type: 'category',
+ name: category.name,
+ href: `/search?category=${category.id}`,
+ });
+ }
+
+ category.items.forEach(item => {
+ if (item.name.toLowerCase().includes(term.toLowerCase())) {
+ allSuggestions.push({
+ type: 'item',
+ name: item.name,
+ href: item.href,
+ category: category.name,
+ });
+ }
+ });
+ });
+
+ return allSuggestions.slice(0, 8);
+ };
+
+ useEffect(() => {
+ // Load recent searches on component mount
+ setRecentSearches(getRecentSearches(5));
+ }, []);
+
+ const handleSuggestionSelect = (suggestion: any) => {
+ if (suggestion.type === 'category') {
+ setFilters(prev => ({ ...prev, category: suggestion.href.split('=')[1] }));
+ }
+ setSearchTerm(suggestion.name);
+ setShowSuggestions(false);
+ setShowHistory(false);
+ setSelectedSuggestionIndex(-1);
+ };
+
+ const handleHistorySelect = (historyItem: any) => {
+ setSearchTerm(historyItem.searchTerm);
+ if (historyItem.filters) {
+ setFilters(prev => ({ ...prev, ...historyItem.filters }));
+ }
+ setShowHistory(false);
+ setShowSuggestions(false);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (!showSuggestions) return;
+
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setSelectedSuggestionIndex(prev =>
+ prev < 7 ? prev + 1 : 0 // Assuming max 8 suggestions
+ );
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setSelectedSuggestionIndex(prev =>
+ prev > 0 ? prev - 1 : 7
+ );
+ break;
+ case 'Enter':
+ if (selectedSuggestionIndex >= 0) {
+ e.preventDefault();
+ // Get the selected suggestion and handle it
+ const suggestions = getAllSuggestions(searchTerm);
+ const selectedSuggestion = suggestions[selectedSuggestionIndex];
+ if (selectedSuggestion) {
+ handleSuggestionSelect(selectedSuggestion);
+ }
+ }
+ break;
+ case 'Escape':
+ setShowSuggestions(false);
+ setSelectedSuggestionIndex(-1);
+ searchInputRef.current?.blur();
+ break;
+ case 'Tab':
+ if (e.shiftKey) {
+ // Let default behavior handle reverse tabbing
+ } else {
+ // Tab to filters button when suggestions are open
+ setShowSuggestions(false);
+ setSelectedSuggestionIndex(-1);
+ }
+ break;
+ }
+ };
+
+ const handleSearch = (e: React.FormEvent) => {
+ e.preventDefault();
+ const params = new URLSearchParams();
+
+ if (searchTerm) params.set('q', searchTerm);
+ if (filters.category) params.set('category', filters.category);
+ if (filters.subCategory) params.set('subcategory', filters.subCategory);
+ if (filters.minPrice > 0) params.set('minPrice', filters.minPrice.toString());
+ if (filters.maxPrice < 1000000) params.set('maxPrice', filters.maxPrice.toString());
+ if (filters.location) params.set('location', filters.location);
+ if (filters.sortBy) params.set('sort', filters.sortBy);
+
+ // Save search to history if there's a search term
+ if (searchTerm) {
+ addSearch(searchTerm, {
+ category: filters.category,
+ location: filters.location,
+ minPrice: filters.minPrice > 0 ? filters.minPrice : undefined,
+ maxPrice: filters.maxPrice < 1000000 ? filters.maxPrice : undefined,
+ sort: filters.sortBy
+ });
+ // Update recent searches
+ setRecentSearches(getRecentSearches(5));
+ }
+
+ router.push(`/search?${params.toString()}`);
+ };
+
+ return (
+
+ {/* Main Content Area */}
+
+
+ {/* Search Header */}
+
+
+ Find Agricultural Products
+
+
+ Search through thousands of farm products, equipment, and supplies
+
+
+
+ {/* Search Form */}
+
+ {/* Search Bar */}
+
+
+ {
+ setSearchTerm(e.target.value);
+ setShowSuggestions(true);
+ setSelectedSuggestionIndex(-1);
+ }}
+ onFocus={() => setShowSuggestions(true)}
+ onKeyDown={handleKeyDown}
+ onBlur={(e) => {
+ // Delay hiding suggestions and history to allow clicking
+ setTimeout(() => {
+ setShowSuggestions(false);
+ setShowHistory(false);
+ }, 150);
+ }}
+ className="h-12 pl-4 pr-12 rounded-lg border-gray-200 focus:border-green-500 focus:ring-green-500"
+ role="combobox"
+ aria-expanded={showSuggestions}
+ aria-haspopup="listbox"
+ aria-autocomplete="list"
+ aria-describedby="search-suggestions"
+ />
+
+
+
+
+ Search
+
+
+
setShowHistory(!showHistory)}
+ className="h-12 px-4 border-gray-200 hover:bg-gray-50 rounded-lg shadow-sm"
+ aria-expanded={showHistory}
+ disabled={recentSearches.length === 0}
+ >
+
+
+
+
setShowFilters(!showFilters)}
+ className="h-12 px-4 border-gray-200 hover:bg-gray-50 rounded-lg shadow-sm"
+ aria-expanded={showFilters}
+ aria-controls="filter-panel"
+ >
+
+ Filters
+
+
+
+ {/* Search Suggestions */}
+ {showSuggestions && (
+
+
+
+ )}
+
+ {/* Search History */}
+ {showHistory && recentSearches.length > 0 && (
+
+
+
+
+ Recent Searches
+
+ {recentSearches.map((historyItem) => (
+
+ handleHistorySelect(historyItem)}
+ className="flex-1 text-left flex items-center space-x-2"
+ >
+
+ {historyItem.searchTerm}
+ {historyItem.resultCount !== undefined && (
+
+ ({historyItem.resultCount} results)
+
+ )}
+
+ {
+ e.stopPropagation();
+ removeSearch(historyItem.id);
+ setRecentSearches(getRecentSearches(5));
+ }}
+ className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-500 transition-opacity"
+ >
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Advanced Filters */}
+ {showFilters && (
+
+
+ {/* Category Filter */}
+
+ Category
+ setFilters({ ...filters, category: value })}
+ >
+
+
+
+
+ {categories.map((category) => (
+
+ {category.name}
+
+ ))}
+
+
+
+
+ {/* Price Range */}
+
+
+ {/* Location */}
+
+ Location
+ setFilters({ ...filters, location: e.target.value })}
+ className="text-gray-400 h-10 border-gray-200"
+ />
+
+
+ {/* Sort By */}
+
+ Sort By
+ setFilters({ ...filters, sortBy: value })}
+ >
+
+
+
+
+ Most Recent
+ Price: Low to High
+ Price: High to Low
+ Most Viewed
+
+
+
+
+
+ {/* Filter Actions */}
+
+ setFilters({
+ minPrice: 0,
+ maxPrice: 1000000,
+ category: '',
+ subCategory: '',
+ location: '',
+ sortBy: 'recent'
+ })}
+ className="mr-2"
+ >
+ Reset
+
+
+ Apply Filters
+
+
+
+ )}
+
+
+ {/* Recently Viewed Section */}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SearchSuggestions.tsx b/components/SearchSuggestions.tsx
new file mode 100644
index 0000000..5690743
--- /dev/null
+++ b/components/SearchSuggestions.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import { useState, useEffect } from 'react';
+import { navigation } from '@/constants';
+import Link from 'next/link';
+import { Command } from 'cmdk';
+import { Suggestion, SearchSuggestionsProps } from '@/types';
+import { useDidYouMean } from '@/lib/didYouMean';
+
+
+export function SearchSuggestions({ searchTerm, onSelect, selectedIndex = -1 }: SearchSuggestionsProps & { selectedIndex?: number }) {
+ const [suggestions, setSuggestions] = useState([]);
+ const { getSuggestions: getSpellingSuggestions, isPossibleMisspelling } = useDidYouMean();
+ const [showSpellingSuggestions, setShowSpellingSuggestions] = useState(false);
+ const [spellingSuggestions, setSpellingSuggestions] = useState([]);
+
+ useEffect(() => {
+ if (!searchTerm) {
+ setSuggestions([]);
+ setSpellingSuggestions([]);
+ setShowSpellingSuggestions(false);
+ return;
+ }
+
+ // Generate suggestions based on categories and items
+ const allSuggestions: Suggestion[] = [];
+
+ navigation.categories.forEach(category => {
+ // Add category if it matches
+ if (category.name.toLowerCase().includes(searchTerm.toLowerCase())) {
+ allSuggestions.push({
+ type: 'category',
+ name: category.name,
+ href: `/search?category=${category.id}`,
+ });
+ }
+
+ // Add matching items from each section
+ category.items.forEach(item => {
+ if (item.name.toLowerCase().includes(searchTerm.toLowerCase())) {
+ allSuggestions.push({
+ type: 'item',
+ name: item.name,
+ href: item.href,
+ category: category.name,
+ });
+ }
+ });
+ });
+
+ setSuggestions(allSuggestions.slice(0, 8)); // Limit to 8 suggestions
+
+ // Check for spelling suggestions if no exact matches found
+ if (allSuggestions.length === 0 && isPossibleMisspelling(searchTerm)) {
+ const spellingAlternatives = getSpellingSuggestions(searchTerm, 3);
+ setSpellingSuggestions(spellingAlternatives);
+ setShowSpellingSuggestions(spellingAlternatives.length > 0);
+ } else {
+ setSpellingSuggestions([]);
+ setShowSpellingSuggestions(false);
+ }
+ }, [searchTerm]);
+
+ if (!suggestions.length && !showSpellingSuggestions) return null;
+
+ return (
+
+
+ {/* Show spelling suggestions first if available */}
+ {showSpellingSuggestions && (
+ <>
+
+ Did you mean?
+
+ {spellingSuggestions.map((spellingSuggestion, index) => (
+
onSelect({
+ type: 'spelling',
+ name: spellingSuggestion,
+ href: `/search?q=${encodeURIComponent(spellingSuggestion)}`
+ })}
+ className={`w-full text-left flex items-center px-4 py-2 rounded-md transition-colors ${
+ index === selectedIndex && suggestions.length === 0
+ ? 'bg-blue-50 border-blue-200 border'
+ : 'hover:bg-gray-50'
+ }`}
+ role="option"
+ aria-selected={index === selectedIndex && suggestions.length === 0}
+ tabIndex={-1}
+ >
+ ā
+ {spellingSuggestion}
+
+ ))}
+ {suggestions.length > 0 && (
+
+ )}
+ >
+ )}
+
+ {/* Regular suggestions */}
+ {suggestions.map((suggestion, index) => (
+
onSelect(suggestion)}
+ className={`flex flex-col px-4 py-2 rounded-md transition-colors ${
+ index === selectedIndex
+ ? 'bg-green-50 border-green-200 border'
+ : 'hover:bg-gray-50'
+ }`}
+ role="option"
+ aria-selected={index === selectedIndex}
+ tabIndex={-1}
+ >
+
{suggestion.name}
+ {suggestion.type === 'item' && (
+
+ in {suggestion.category}
+
+ )}
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SessionWrapper.tsx b/components/SessionWrapper.tsx
new file mode 100644
index 0000000..de176af
--- /dev/null
+++ b/components/SessionWrapper.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import React, { createContext, useContext, useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { Session } from "@/types";
+
+const SessionContext = createContext<{
+ session: Session | null;
+ setSession: (session: Session | null) => void
+} | null>(null);
+
+export const useSession = () => {
+ const context = useContext(SessionContext);
+ if (!context) throw new Error("useSession must be used within a SessionProvider");
+ return context;
+};
+
+const getCookie = (name: string): string | null => {
+ try {
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; ${name}=`);
+ if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
+ return null;
+ } catch (error) {
+ console.error('Error getting cookie:', name, error);
+ return null;
+ }
+};
+
+const SessionWrapper = ({ children, session: initialSession }: {
+ children: React.ReactNode;
+ session: Session | null;
+}) => {
+ console.log('SessionWrapper component rendering');
+ const [sessionState, setSessionState] = useState(initialSession);
+ const router = useRouter();
+
+ // Update session state when initialSession changes
+ useEffect(() => {
+ console.log('SessionWrapper useEffect running, initialSession:', initialSession);
+ if (initialSession) {
+ console.log('Setting session state from initialSession:', initialSession);
+ setSessionState(initialSession);
+ }
+ }, [initialSession]);
+
+ const setSession = (newSession: Session | null) => {
+ if (newSession?.token) {
+ // Update session state
+ console.log('Setting session state to newSession:', newSession);
+ setSessionState(newSession);
+ } else {
+ // Clear session
+ console.log('Clearing session state');
+ setSessionState(null);
+ document.cookie = 'next-auth.session-token=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT';
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default SessionWrapper;
\ No newline at end of file
diff --git a/components/SocialLoginIcons.tsx b/components/SocialLoginIcons.tsx
new file mode 100644
index 0000000..324f40d
--- /dev/null
+++ b/components/SocialLoginIcons.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { signIn } from 'next-auth/react';
+import { FaFacebook, FaGoogle, FaTwitter } from 'react-icons/fa';
+
+const SocialLoginButtons = () => (
+
+ signIn('facebook', { callbackUrl: '/dashboard' })} className="p-2 text-white bg-blue-500 rounded-full">
+
+
+ signIn('google', { callbackUrl: '/dashboard' })} className="p-2 text-white bg-red-500 rounded-full">
+
+
+ signIn('twitter', { callbackUrl: '/dashboard' })} className="p-2 text-white bg-blue-400 rounded-full">
+
+
+
+);
+
+export default SocialLoginButtons;
diff --git a/components/Spinner.tsx b/components/Spinner.tsx
new file mode 100644
index 0000000..21aefc8
--- /dev/null
+++ b/components/Spinner.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+interface SpinnerProps extends React.HTMLAttributes {
+ size?: 'sm' | 'md' | 'lg';
+}
+
+const Spinner = ({ className, size = 'md', ...props }: SpinnerProps) => {
+ const sizeClasses = {
+ sm: 'w-4 h-4',
+ md: 'w-8 h-8',
+ lg: 'w-12 h-12'
+ };
+
+ return (
+
+ );
+};
+
+export default Spinner;
\ No newline at end of file
diff --git a/components/SupportCenterMain.tsx b/components/SupportCenterMain.tsx
new file mode 100644
index 0000000..8d14ee7
--- /dev/null
+++ b/components/SupportCenterMain.tsx
@@ -0,0 +1,598 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Badge } from "@/components/ui/badge";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+// Using standard divs for scrollable areas and separators
+import {
+ HelpCircle,
+ Ticket,
+ Plus,
+ MessageCircle,
+ Clock,
+ CheckCircle,
+ AlertCircle,
+ ChevronDown,
+ ChevronRight,
+ Search,
+ Send,
+ Loader2
+} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { faqData, helpTabs } from "@/constants";
+import toast from "react-hot-toast";
+import { formatDistanceToNow } from "date-fns";
+
+interface SupportTicket {
+ id: string;
+ subject: string;
+ priority: string;
+ category: string;
+ status: string;
+ createdAt: string;
+ updatedAt: string;
+ messages?: SupportMessage[];
+}
+
+interface SupportMessage {
+ id: string;
+ content: string;
+ isAgentReply: boolean;
+ createdAt: string;
+ sender: {
+ id: string;
+ name: string;
+ email: string;
+ };
+}
+
+interface FAQ {
+ question: string;
+ answer: string;
+}
+
+const getStatusColor = (status: string) => {
+ switch (status.toLowerCase()) {
+ case 'open':
+ return 'bg-blue-100 text-blue-800 border-blue-200';
+ case 'closed':
+ return 'bg-gray-100 text-gray-800 border-gray-200';
+ case 'resolved':
+ return 'bg-green-100 text-green-800 border-green-200';
+ default:
+ return 'bg-gray-100 text-gray-800 border-gray-200';
+ }
+};
+
+const getPriorityColor = (priority: string) => {
+ switch (priority.toLowerCase()) {
+ case 'high':
+ return 'bg-red-100 text-red-800 border-red-200';
+ case 'medium':
+ return 'bg-yellow-100 text-yellow-800 border-yellow-200';
+ case 'low':
+ return 'bg-green-100 text-green-800 border-green-200';
+ default:
+ return 'bg-gray-100 text-gray-800 border-gray-200';
+ }
+};
+
+export default function SupportCenterMain() {
+ const [activeTab, setActiveTab] = useState("faq");
+ const [tickets, setTickets] = useState([]);
+ const [selectedTicket, setSelectedTicket] = useState(null);
+ const [ticketMessages, setTicketMessages] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [creating, setCreating] = useState(false);
+ const [sendingMessage, setSendingMessage] = useState(false);
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [expandedFAQ, setExpandedFAQ] = useState(null);
+ const [faqSearch, setFaqSearch] = useState("");
+ const [messageContent, setMessageContent] = useState("");
+
+ // New ticket form state
+ const [newTicket, setNewTicket] = useState({
+ subject: "",
+ category: "",
+ priority: "medium",
+ message: ""
+ });
+
+ // FAQ search functionality
+ const filteredFAQ = faqData.filter(
+ (faq) =>
+ faq.question.toLowerCase().includes(faqSearch.toLowerCase()) ||
+ faq.answer.toLowerCase().includes(faqSearch.toLowerCase())
+ );
+
+ // Fetch support tickets
+ const fetchTickets = async () => {
+ try {
+ setLoading(true);
+ const response = await fetch("/api/support/tickets", {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setTickets(data);
+ } else {
+ toast.error("Failed to load support tickets");
+ }
+ } catch (error) {
+ console.error("Error fetching tickets:", error);
+ toast.error("Failed to load support tickets");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Fetch messages for a specific ticket
+ const fetchTicketMessages = async (ticketId: string) => {
+ try {
+ const response = await fetch(`/api/support/tickets/${ticketId}/messages`, {
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const messages = await response.json();
+ setTicketMessages(messages);
+ } else {
+ toast.error("Failed to load ticket messages");
+ }
+ } catch (error) {
+ console.error("Error fetching messages:", error);
+ toast.error("Failed to load ticket messages");
+ }
+ };
+
+ // Create new support ticket
+ const createTicket = async () => {
+ if (!newTicket.subject.trim() || !newTicket.message.trim() || !newTicket.category) {
+ toast.error("Please fill in all required fields");
+ return;
+ }
+
+ try {
+ setCreating(true);
+ const response = await fetch("/api/support/tickets", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(newTicket),
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const ticket = await response.json();
+ setTickets([ticket, ...tickets]);
+ setNewTicket({
+ subject: "",
+ category: "",
+ priority: "medium",
+ message: ""
+ });
+ setShowCreateForm(false);
+ toast.success("Support ticket created successfully!");
+ } else {
+ const error = await response.json();
+ toast.error(error.message || "Failed to create support ticket");
+ }
+ } catch (error) {
+ console.error("Error creating ticket:", error);
+ toast.error("Failed to create support ticket");
+ } finally {
+ setCreating(false);
+ }
+ };
+
+ // Send message to ticket
+ const sendMessage = async () => {
+ if (!messageContent.trim() || !selectedTicket) return;
+
+ try {
+ setSendingMessage(true);
+ const response = await fetch(`/api/support/tickets/${selectedTicket.id}/messages`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ content: messageContent,
+ isAgentReply: false
+ }),
+ credentials: "include",
+ });
+
+ if (response.ok) {
+ const message = await response.json();
+ setTicketMessages([...ticketMessages, message]);
+ setMessageContent("");
+ toast.success("Message sent successfully!");
+ } else {
+ const error = await response.json();
+ toast.error(error.message || "Failed to send message");
+ }
+ } catch (error) {
+ console.error("Error sending message:", error);
+ toast.error("Failed to send message");
+ } finally {
+ setSendingMessage(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTickets();
+ }, []);
+
+ useEffect(() => {
+ if (selectedTicket) {
+ fetchTicketMessages(selectedTicket.id);
+ }
+ }, [selectedTicket]);
+
+ return (
+
+
+
+
š§ Support Center
+
Get help, find answers, and manage support tickets
+
+
+
+
+
+ {helpTabs.map((tab) => {
+ const Icon = tab.icon;
+ return (
+
+
+ {tab.label}
+
+ );
+ })}
+
+
+ {/* FAQ Tab */}
+
+
+
+
+
+ Frequently Asked Questions
+
+
+
+ setFaqSearch(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+ {filteredFAQ.length > 0 ? (
+ filteredFAQ.map((faq, index) => (
+
+
setExpandedFAQ(expandedFAQ === index ? null : index)}
+ className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 transition-colors"
+ >
+ {faq.question}
+ {expandedFAQ === index ? (
+
+ ) : (
+
+ )}
+
+ {expandedFAQ === index && (
+
+ )}
+
+ ))
+ ) : (
+
+
+
No FAQ items found matching your search.
+
+ )}
+
+
+ {faqSearch === "" && (
+
+
Still need help?
+
+ If you couldn't find the answer you're looking for, you can create a support ticket and our team will help you.
+
+
setActiveTab("tickets")}
+ variant="outline"
+ className="border-blue-200 text-blue-700 hover:bg-blue-100"
+ >
+
+ Create Support Ticket
+
+
+ )}
+
+
+
+
+ {/* Support Tickets Tab */}
+
+
+ {/* Tickets List */}
+
+
+
+
+
+ Support Tickets
+
+
setShowCreateForm(!showCreateForm)}
+ size="sm"
+ className="bg-green-600 hover:bg-green-700"
+ >
+
+ New Ticket
+
+
+
+
+ {/* Create Ticket Form */}
+ {showCreateForm && (
+
+
Create New Support Ticket
+
+
+
+ Subject *
+
+ setNewTicket({ ...newTicket, subject: e.target.value })}
+ />
+
+
+
+
+ Category *
+
+ setNewTicket({ ...newTicket, category: value })}
+ >
+
+
+
+
+ Technical Issue
+ Billing Question
+ Account Help
+ General Question
+ Feature Request
+
+
+
+
+
+ Priority
+
+ setNewTicket({ ...newTicket, priority: value })}
+ >
+
+
+
+
+ Low
+ Medium
+ High
+
+
+
+
+
+
+ Description *
+
+ setNewTicket({ ...newTicket, message: e.target.value })}
+ rows={4}
+ />
+
+
+
+ {creating ? (
+
+ ) : (
+
+ )}
+ Create Ticket
+
+
setShowCreateForm(false)}
+ variant="outline"
+ >
+ Cancel
+
+
+
+
+ )}
+
+ {/* Tickets List */}
+
+ {loading ? (
+
+
+
+ ) : tickets.length > 0 ? (
+
+ {tickets.map((ticket) => (
+
setSelectedTicket(ticket)}
+ className={cn(
+ "p-3 border rounded-lg cursor-pointer transition-colors",
+ selectedTicket?.id === ticket.id
+ ? "border-green-500 bg-green-50"
+ : "hover:bg-gray-50"
+ )}
+ >
+
+
+
+ {ticket.subject}
+
+
+ {ticket.category} ⢠{formatDistanceToNow(new Date(ticket.createdAt), { addSuffix: true })}
+
+
+
+
+ {ticket.priority}
+
+
+ {ticket.status}
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
No support tickets yet.
+
Create your first ticket to get started.
+
+ )}
+
+
+
+
+ {/* Ticket Details */}
+
+
+
+
+ {selectedTicket ? "Conversation" : "Select a Ticket"}
+
+
+
+ {selectedTicket ? (
+
+ {/* Ticket Header */}
+
+
{selectedTicket.subject}
+
+
+ {selectedTicket.priority} priority
+
+
+ {selectedTicket.status}
+
+
+ ⢠{selectedTicket.category}
+
+
+
+
+ {/* Messages */}
+
+
+ {ticketMessages.map((message) => (
+
+
+
+ {message.isAgentReply ? "Support Agent" : message.sender.name || "You"}
+
+
+ {formatDistanceToNow(new Date(message.createdAt), { addSuffix: true })}
+
+
+
{message.content}
+
+ ))}
+
+
+
+ {/* Reply Form */}
+ {selectedTicket.status !== 'closed' && (
+
+
setMessageContent(e.target.value)}
+ rows={3}
+ />
+
+
+ {sendingMessage ? (
+
+ ) : (
+
+ )}
+ Send Message
+
+
+
+ )}
+
+ ) : (
+
+
+
Select a support ticket to view the conversation.
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/SustainabilitySection.tsx b/components/SustainabilitySection.tsx
new file mode 100644
index 0000000..3de2ff3
--- /dev/null
+++ b/components/SustainabilitySection.tsx
@@ -0,0 +1,181 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import { LeafIcon, DropletIcon, SunIcon, RecycleIcon } from "./Icons";
+
+export default function SustainabilitySection() {
+ const [isVisible, setIsVisible] = useState(false);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ const element = document.getElementById('sustainability');
+ if (element) {
+ const position = element.getBoundingClientRect();
+ if (position.top < window.innerHeight - 100) {
+ setIsVisible(true);
+ }
+ }
+ };
+
+ window.addEventListener('scroll', handleScroll);
+ handleScroll(); // Check on initial load
+
+ return () => {
+ window.removeEventListener('scroll', handleScroll);
+ };
+ }, []);
+
+ return (
+
+
+
+ {/* Left Column - Text Content */}
+
+ Committed to Sustainability
+
+ At AgroMarket, we believe in sustainable farming practices that protect our environment while supporting local communities. Our platform promotes eco-friendly agriculture and responsible consumption.
+
+
+
+
+
+
+
+
+
Organic Farming
+
+ We support farmers who use organic methods, avoiding harmful pesticides and fertilizers.
+
+
+
+
+
+
+
+
+
+
Water Conservation
+
+ Our farmers implement water-saving techniques to minimize waste and protect this precious resource.
+
+
+
+
+
+
+
+
+
+
Renewable Energy
+
+ We encourage the use of solar and other renewable energy sources in farming operations.
+
+
+
+
+
+
+
+
+
+
Waste Reduction
+
+ We work to minimize food waste through efficient distribution and eco-friendly packaging.
+
+
+
+
+
+
+
+ Learn More About Our Initiatives
+
+
+
+
+ {/* Right Column - Image */}
+
+
+
+
+ {/* Overlay with stats */}
+
+
+
+
30%
+
Less Water Usage
+
+
+
40%
+
Less Food Waste
+
+
+
50%
+
Carbon Footprint Reduction
+
+
+
1000+
+
Sustainable Farmers
+
+
+
+
+
+ {/* Certification Badges */}
+
+
+
+
+
+ );
+}
diff --git a/components/ToastProvider.tsx b/components/ToastProvider.tsx
new file mode 100644
index 0000000..30cce53
--- /dev/null
+++ b/components/ToastProvider.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { Toaster } from 'react-hot-toast';
+
+export default function ToastProvider() {
+ return (
+
+ );
+}
diff --git a/components/TwoFactorAuth.tsx b/components/TwoFactorAuth.tsx
new file mode 100644
index 0000000..dee0ad9
--- /dev/null
+++ b/components/TwoFactorAuth.tsx
@@ -0,0 +1,474 @@
+"use client";
+
+import React, { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Shield,
+ Loader2,
+ Check,
+ X,
+ Copy,
+ Eye,
+ EyeOff,
+ AlertCircle,
+ Smartphone,
+ Download
+} from 'lucide-react';
+import toast from 'react-hot-toast';
+import { cn } from '@/lib/utils';
+
+interface TwoFactorAuthProps {
+ isEnabled: boolean;
+ onStatusChange: (enabled: boolean) => void;
+}
+
+interface SetupData {
+ secret: string;
+ qrCode: string;
+ manualEntryKey: string;
+}
+
+export default function TwoFactorAuth({ isEnabled, onStatusChange }: TwoFactorAuthProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [showSetup, setShowSetup] = useState(false);
+ const [setupData, setSetupData] = useState(null);
+ const [verificationCode, setVerificationCode] = useState('');
+ const [showDisableConfirm, setShowDisableConfirm] = useState(false);
+ const [disablePassword, setDisablePassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [backupCodes, setBackupCodes] = useState([]);
+ const [showBackupCodes, setShowBackupCodes] = useState(false);
+
+ const handleEnable2FA = async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/user/profile/2fa/setup', {
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to setup 2FA');
+ }
+
+ const data = await response.json();
+ setSetupData(data);
+ setShowSetup(true);
+ } catch (error) {
+ console.error('Error setting up 2FA:', error);
+ toast.error('Failed to setup 2FA');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleVerifyAndEnable = async () => {
+ if (!setupData || !verificationCode) {
+ toast.error('Please enter the verification code');
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/user/profile/2fa/setup', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ secret: setupData.secret,
+ token: verificationCode
+ }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Invalid verification code');
+ }
+
+ const data = await response.json();
+ setBackupCodes(data.backupCodes);
+ setShowBackupCodes(true);
+ setShowSetup(false);
+ setVerificationCode('');
+ onStatusChange(true);
+ toast.success('2FA enabled successfully! Please save your backup codes.');
+ } catch (error) {
+ console.error('Error enabling 2FA:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to enable 2FA');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleDisable2FA = async () => {
+ if (!disablePassword) {
+ toast.error('Please enter your password');
+ return;
+ }
+
+ try {
+ setIsLoading(true);
+ const response = await fetch('/api/user/profile/2fa/disable', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ password: disablePassword
+ }),
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Failed to disable 2FA');
+ }
+
+ setShowDisableConfirm(false);
+ setDisablePassword('');
+ onStatusChange(false);
+ toast.success('2FA disabled successfully');
+ } catch (error) {
+ console.error('Error disabling 2FA:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to disable 2FA');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const copyToClipboard = (text: string, label: string) => {
+ navigator.clipboard.writeText(text);
+ toast.success(`${label} copied to clipboard`);
+ };
+
+ const downloadBackupCodes = () => {
+ const content = `AgroMarket NG - Two-Factor Authentication Backup Codes\n\nGenerated: ${new Date().toLocaleString()}\n\nBackup Codes (use each only once):\n${backupCodes.map(code => `⢠${code}`).join('\n')}\n\nKeep these codes in a safe place. Each code can only be used once.\nIf you lose access to your authenticator app, you can use these codes to sign in.`;
+
+ const blob = new Blob([content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'agromarket-2fa-backup-codes.txt';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ toast.success('Backup codes downloaded');
+ };
+
+ if (!isEnabled && !showSetup && !showBackupCodes) {
+ return (
+
+
+
+
+
+
Two-Factor Authentication
+
Add an extra layer of security to your account
+
+
+
+ Disabled
+
+
+
+
+
+ Two-factor authentication (2FA) adds an extra layer of security to your account by requiring a verification code from your mobile device in addition to your password.
+
+
+
+
How it works:
+
+ ⢠Install an authenticator app (Google Authenticator, Authy, etc.)
+ ⢠Scan the QR code or enter the setup key
+ ⢠Enter the verification code to complete setup
+ ⢠You'll need this code every time you sign in
+
+
+
+
+ {isLoading ? (
+ <>
+
+ Setting up 2FA...
+ >
+ ) : (
+ <>
+
+ Enable Two-Factor Authentication
+ >
+ )}
+
+
+
+ );
+ }
+
+ if (isEnabled && !showDisableConfirm && !showBackupCodes) {
+ return (
+
+
+
+
+
+
Two-Factor Authentication
+
Your account is protected with 2FA
+
+
+
+ Enabled
+
+
+
+
+
+
+ 2FA is active and protecting your account
+
+
+
setShowDisableConfirm(true)}
+ variant="destructive"
+ className="w-full"
+ >
+
+ Disable Two-Factor Authentication
+
+
+
+ );
+ }
+
+ if (showSetup && setupData) {
+ return (
+
+
+
Setup Two-Factor Authentication
+
Scan the QR code with your authenticator app
+
+
+
+ {/* QR Code */}
+
+
+
+
+ {/* Manual Entry */}
+
+
+ Can't scan? Enter this key manually:
+
+
+
+ copyToClipboard(setupData.manualEntryKey, 'Setup key')}
+ >
+
+
+
+
+
+ {/* Verification */}
+
+
+ Enter verification code from your app:
+
+ setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
+ placeholder="000000"
+ className="w-full p-3 text-center text-lg border border-gray-300 rounded font-mono"
+ maxLength={6}
+ />
+
+
+
+ {
+ setShowSetup(false);
+ setSetupData(null);
+ setVerificationCode('');
+ }}
+ className="flex-1"
+ disabled={isLoading}
+ >
+ Cancel
+
+
+ {isLoading ? (
+ <>
+
+ Verifying...
+ >
+ ) : (
+ <>
+
+ Enable 2FA
+ >
+ )}
+
+
+
+
+ );
+ }
+
+ if (showDisableConfirm) {
+ return (
+
+
+
+
+
+
Disable Two-Factor Authentication
+
+ This will remove the extra security layer from your account. Enter your password to confirm.
+
+
+
+
+
+
+
+
+ Current Password
+
+
+ setDisablePassword(e.target.value)}
+ className="w-full p-3 pr-10 border border-gray-300 rounded"
+ placeholder="Enter your password"
+ />
+ setShowPassword(!showPassword)}
+ className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
+ >
+ {showPassword ? : }
+
+
+
+
+
+ {
+ setShowDisableConfirm(false);
+ setDisablePassword('');
+ }}
+ className="flex-1"
+ disabled={isLoading}
+ >
+ Cancel
+
+
+ {isLoading ? (
+ <>
+
+ Disabling...
+ >
+ ) : (
+ <>
+
+ Disable 2FA
+ >
+ )}
+
+
+
+
+ );
+ }
+
+ if (showBackupCodes && backupCodes.length > 0) {
+ return (
+
+
+
+
2FA Enabled Successfully!
+
Save these backup codes in a safe place
+
+
+
+
+
+
+
Important: Save Your Backup Codes
+
+ These codes can be used to access your account if you lose your authenticator device. Each code can only be used once.
+
+
+
+
+
+
+
Your Backup Codes:
+
+ {backupCodes.map((code, index) => (
+
+ {code}
+ copyToClipboard(code, 'Backup code')}
+ >
+
+
+
+ ))}
+
+
+
+
+
+
+ Download Codes
+
+ setShowBackupCodes(false)}
+ className="flex-1"
+ >
+
+ I've Saved My Codes
+
+
+
+ );
+ }
+
+ return null;
+}
\ No newline at end of file
diff --git a/components/UserSupportTickets.tsx b/components/UserSupportTickets.tsx
new file mode 100644
index 0000000..fc48801
--- /dev/null
+++ b/components/UserSupportTickets.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import React from 'react';
+
+export default function UserSupportTickets() {
+ return (
+
+
My Support Tickets
+
This is a placeholder for user support tickets.
+ {/* TODO: Implement fetching and displaying user support tickets */}
+
+ );
+}
\ No newline at end of file
diff --git a/components/WhyChooseUs.tsx b/components/WhyChooseUs.tsx
new file mode 100644
index 0000000..db720ec
--- /dev/null
+++ b/components/WhyChooseUs.tsx
@@ -0,0 +1,56 @@
+import React from "react";
+import { ChartPieIcon, CurrencyDollarIcon, GlobeAltIcon, HeartIcon } from "@heroicons/react/24/outline";
+
+const WhyChooseUs = () => {
+ return (
+
+
+
Why Choose Us
+
+ Discover what sets us apart and why our platform is the best choice for fresh, local, and sustainable produce.
+
+
+ {/* Benefits */}
+
+ {/* Fresh Produce */}
+
+
+
Fresh Produce
+
+ Get the freshest produce, harvested directly from local farms to ensure quality and flavor.
+
+
+
+ {/* Local Sourcing */}
+
+
+
Locally Sourced
+
+ Support local farmers and contribute to the community by sourcing locally grown products.
+
+
+
+ {/* Affordable Pricing */}
+
+
+
Transparent Pricing
+
+ Enjoy affordable, transparent pricing without middlemen, directly benefiting farmers.
+
+
+
+ {/* Sustainable Practices */}
+
+
+
Sustainable Practices
+
+ Choose sustainably grown produce and support eco-friendly farming practices.
+
+
+
+
+
+ );
+};
+
+export default WhyChooseUs;
diff --git a/components/auth/WithAuth.tsx b/components/auth/WithAuth.tsx
new file mode 100644
index 0000000..dab9df8
--- /dev/null
+++ b/components/auth/WithAuth.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import { useRouter, usePathname } from 'next/navigation';
+import { useSession } from '@/components/SessionWrapper';
+import Spinner from '@/components/Spinner';
+
+export function WithAuth(WrappedComponent: React.ComponentType
) {
+ return function WithAuthWrapper(props: P) {
+ const { session } = useSession();
+ const router = useRouter();
+ const pathname = usePathname();
+ const [isChecking, setIsChecking] = useState(true);
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ if (!session) {
+ router.replace('/signin');
+ return;
+ }
+
+ const role = session.role;
+ const isAgentPath = pathname.startsWith('/agent/dashboard') || pathname.startsWith('/dashboard/agent');
+ const isAdminPath = pathname.startsWith('/admin');
+
+ if (isAgentPath && role !== 'agent') {
+ router.replace(role === 'admin' ? '/admin/dashboard' : '/dashboard');
+ } else if (isAdminPath && role !== 'admin') {
+ router.replace(role === 'agent' ? '/agent/dashboard' : '/dashboard');
+ } else if (!isAgentPath && !isAdminPath && role !== 'user') {
+ router.replace(role === 'admin' ? '/admin/dashboard' : '/agent/dashboard');
+ } else {
+ setIsChecking(false);
+ }
+ };
+
+ checkAuth();
+ }, [session?.role, pathname]); // Only depend on role changes
+
+ if (isChecking) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+ };
+}
\ No newline at end of file
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..39601ab
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,33 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary: "bg-purple-100 text-purple-800 hover:bg-purple-200/80",
+ destructive: "bg-red-100 text-red-800 hover:bg-red-200/80",
+ success: "bg-green-100 text-green-800 hover:bg-green-200/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
\ No newline at end of file
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..8806d04
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,26 @@
+import { cn } from "@/lib/utils";
+import { ButtonHTMLAttributes, ReactNode } from "react";
+import { ButtonProps } from "@/types";
+
+export function Button({ children, className, variant = "default", size = "default", ...props }: ButtonProps) {
+ const baseStyles = "rounded-md font-medium transition-all";
+ const variants = {
+ default: "bg-green-500 text-white hover:bg-green-600",
+ outline: "border border-green-500 text-green-500 hover:bg-green-100",
+ ghost: "text-gray-700 hover:bg-gray-200",
+ destructive: "bg-red-500 text-white hover:bg-red-600",
+ link: "text-green-500 hover:text-green-600 underline-offset-4 hover:underline",
+ };
+ const sizes = {
+ default: "px-4 py-2 text-sm",
+ sm: "px-3 py-1.5 text-xs",
+ lg: "px-6 py-3 text-base",
+ icon: "h-8 w-8 p-0",
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..1c6111c
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,107 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter
+};
\ No newline at end of file
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000..9ca99d5
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,130 @@
+import React, { useState, useEffect, useRef } from "react";
+import { ReactElement } from 'react';
+import { DialogProps, DialogContentProps, DialogHeaderProps, DialogTitleProps, DialogTriggerProps, ChildProps } from '@/types';
+import { cn } from "@/lib/utils";
+
+// Create DialogContext
+const DialogContext = React.createContext<{
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+} | null>(null);
+
+const Dialog: React.FC = ({ children, open, onOpenChange }) => {
+ const [isOpen, setIsOpen] = useState(open);
+ const dialogRef = useRef(null);
+
+ useEffect(() => {
+ setIsOpen(open);
+ }, [open]);
+
+ useEffect(() => {
+ const handleEscape = (event: KeyboardEvent) => {
+ if (event.key === 'Escape' && isOpen) {
+ onOpenChange?.(false);
+ }
+ };
+
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dialogRef.current && !dialogRef.current.contains(event.target as Node) && isOpen) {
+ onOpenChange?.(false);
+ }
+ };
+
+ document.addEventListener('keydown', handleEscape);
+ document.addEventListener('mousedown', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape);
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen, onOpenChange]);
+
+ if (!isOpen) return null;
+
+ return (
+ {}) }}>
+
+
+
+ {React.Children.map(children, (child) => {
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child as ReactElement, {
+ isOpen,
+ onClose: () => onOpenChange?.(false)
+ });
+ }
+ return child;
+ })}
+
+
+
+ );
+};
+
+const DialogContent: React.FC = ({ children, className }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DialogHeader: React.FC = ({ children, className }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DialogTitle: React.FC = ({ children, className }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DialogTrigger: React.FC = ({ children, asChild = false }) => {
+ const context = React.useContext(DialogContext);
+
+ if (!context) {
+ throw new Error('DialogTrigger must be used within a Dialog');
+ }
+
+ const { onOpenChange } = context;
+
+ const handleClick = () => {
+ onOpenChange(true);
+ };
+
+ if (asChild && React.isValidElement(children)) {
+ return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, {
+ onClick: handleClick,
+ });
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+const DialogDescription: React.FC = ({ children, className }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DialogFooter: React.FC = ({ children, className }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter };
\ No newline at end of file
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..1f2fa86
--- /dev/null
+++ b/components/ui/dropdown-menu.tsx
@@ -0,0 +1,113 @@
+import React, { useState, useEffect, useRef } from "react";
+import { ReactElement, ReactNode } from 'react';
+import { DropdownMenuProps, DropdownMenuTriggerProps, DropdownMenuContentProps, DropdownMenuItemProps, DisableableDropdownMenuItemProps } from "@/types";
+
+type ChildProps = {
+ onClick?: () => void;
+ isOpen?: boolean;
+};
+
+
+const DropdownMenu: React.FC = ({ children }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const menuRef = useRef(null);
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ return (
+
+ {React.Children.map(children, (child) => {
+ if (React.isValidElement(child) && child.type === DropdownMenuTrigger) {
+ return React.cloneElement(child as ReactElement, { onClick: () => setIsOpen((prev) => !prev) });
+ }
+ if (React.isValidElement(child) && child.type === DropdownMenuContent) {
+ return React.cloneElement(child as ReactElement, { isOpen });
+ }
+ return child;
+ })}
+
+ );
+};
+
+const DropdownMenuTrigger: React.FC = ({ className, children, onClick }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DropdownMenuContent: React.FC = ({ className, children, isOpen, align = "right", sideOffset = 5, avoidCollisions = true, collisionPadding = 10 }) => {
+ const alignmentClass = align === "right" ? "right-0" : align === "left" ? "left-0" : "left-1/2 -translate-x-1/2";
+
+ return (
+ isOpen && (
+
+ {children}
+
+ )
+ );
+};
+
+const DropdownMenuItem: React.FC = ({ className, children, onClick }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DisableableDropdownMenuItem: React.FC = ({
+ disabled = false,
+ onClick,
+ children,
+ className
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DropdownMenuLabel: React.FC<{ className?: string; children: React.ReactNode }> = ({
+ className,
+ children
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const DropdownMenuSeparator: React.FC<{ className?: string }> = ({ className }) => {
+ return (
+
+ );
+};
+
+export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DisableableDropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator };
diff --git a/components/ui/file-upload.tsx b/components/ui/file-upload.tsx
new file mode 100644
index 0000000..fdb36d7
--- /dev/null
+++ b/components/ui/file-upload.tsx
@@ -0,0 +1,233 @@
+import { useState, useEffect } from "react";
+import { UploadCloud, Trash2, AlertCircle, Info } from "lucide-react";
+import Image from "next/image";
+import { FileUploadProps } from "@/types";
+import { motion, AnimatePresence } from "framer-motion";
+
+const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
+const ALLOWED_FILE_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
+
+const FileUpload: React.FC = ({ onFilesSelected, maxFiles = 5, error }) => {
+ const [images, setImages] = useState([]);
+ const [fileErrors, setFileErrors] = useState([]);
+ const [dragActive, setDragActive] = useState(false);
+
+ // Reset errors when component unmounts
+ useEffect(() => {
+ return () => {
+ setFileErrors([]);
+ };
+ }, []);
+
+ const validateFile = (file: File): string | null => {
+ // Check file type
+ if (!ALLOWED_FILE_TYPES.includes(file.type)) {
+ return `${file.name} is not a supported image format. Please use JPG, PNG, WebP, or GIF.`;
+ }
+
+ // Check file size
+ if (file.size > MAX_FILE_SIZE) {
+ return `${file.name} exceeds the 2MB size limit.`;
+ }
+
+ // Check for duplicate files by name and size
+ const isDuplicate = images.some(
+ existingFile =>
+ existingFile.name === file.name &&
+ existingFile.size === file.size
+ );
+
+ if (isDuplicate) {
+ return `${file.name} has already been added.`;
+ }
+
+ return null;
+ };
+
+ const handleImageUpload = (e: React.ChangeEvent) => {
+ const files = e.target.files;
+ if (!files || files.length === 0) return;
+
+ processFiles(Array.from(files));
+
+ // Reset the input value to allow uploading the same file again if it was removed
+ e.target.value = '';
+ };
+
+ 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.length > 0) {
+ processFiles(Array.from(e.dataTransfer.files));
+ }
+ };
+
+ const processFiles = (newFiles: File[]) => {
+ // Reset errors
+ setFileErrors([]);
+
+ // Validate each file
+ const errors: string[] = [];
+ const validFiles: File[] = [];
+
+ newFiles.forEach(file => {
+ const error = validateFile(file);
+ if (error) {
+ errors.push(error);
+ } else {
+ validFiles.push(file);
+ }
+ });
+
+ // Check if adding these files would exceed the maximum
+ if (images.length + validFiles.length > maxFiles) {
+ errors.push(`You can only upload a maximum of ${maxFiles} images.`);
+ // Only add files up to the limit
+ validFiles.splice(maxFiles - images.length);
+ }
+
+ // Update errors state
+ if (errors.length > 0) {
+ setFileErrors(errors);
+ }
+
+ // Update images state if there are valid files
+ if (validFiles.length > 0) {
+ const updatedImages = [...images, ...validFiles];
+ setImages(updatedImages);
+ onFilesSelected(updatedImages);
+ }
+ };
+
+ const removeImage = (index: number) => {
+ const updatedImages = images.filter((_, i) => i !== index);
+ setImages(updatedImages);
+ onFilesSelected(updatedImages);
+ };
+
+ return (
+
+
+
+ Upload Images (Max {maxFiles})
+
+
+
+
+
+
+
+
+ Drag & drop your images here
+ or click to browse
+ JPG, PNG, WebP, GIF ⢠Max 2MB per image
+
+
+
+ {/* Error messages */}
+
+ {(fileErrors.length > 0 || error) && (
+
+
+
+
+ {error &&
{error}
}
+ {fileErrors.map((err, i) => (
+
{err}
+ ))}
+
+
+
+ )}
+
+
+ {/* Image preview */}
+
+ {images.length > 0 && (
+
+ {images.map((image, index) => (
+
+
+
+
+
+ removeImage(index)}
+ className="bg-red-500 text-white p-1.5 rounded-full opacity-0 group-hover:opacity-100 transform translate-y-2 group-hover:translate-y-0 transition-all duration-200"
+ aria-label="Remove image"
+ >
+
+
+
+
+ {image.name}
+
+
+ ))}
+
+ )}
+
+
+ {/* Help text */}
+ {images.length === 0 && (
+
+
+ Add high-quality images to make your ad stand out. First image will be used as the main image.
+
+ )}
+
+ );
+};
+
+export default FileUpload;
diff --git a/components/ui/input-field.tsx b/components/ui/input-field.tsx
new file mode 100644
index 0000000..b0d61b4
--- /dev/null
+++ b/components/ui/input-field.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { InputFieldProps } from "@/types";
+
+const InputField: React.FC = ({
+ label,
+ type = "text",
+ name,
+ value,
+ placeholder,
+ onChange,
+ required = false,
+}) => {
+ return (
+
+ {label}{required && * }
+
+
+ );
+};
+
+export default InputField;
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..3e2e156
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,41 @@
+"use client"
+
+import * as React from "react"
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {
+ error?: string;
+}
+
+const Input = React.forwardRef(
+ ({ className, type, error, ...props }, ref) => {
+ return (
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
\ No newline at end of file
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 0000000..d337035
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,22 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cn } from "@/lib/utils"
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
\ No newline at end of file
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx
new file mode 100644
index 0000000..16bcbf9
--- /dev/null
+++ b/components/ui/progress.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import * as React from "react";
+import * as ProgressPrimitive from "@radix-ui/react-progress";
+import { cn } from "@/lib/utils";
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+));
+Progress.displayName = ProgressPrimitive.Root.displayName;
+
+export { Progress };
diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..bef78ac
--- /dev/null
+++ b/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
\ No newline at end of file
diff --git a/components/ui/select-field.tsx b/components/ui/select-field.tsx
new file mode 100644
index 0000000..418079f
--- /dev/null
+++ b/components/ui/select-field.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+import { SelectFieldProps } from "@/types";
+
+const SelectField: React.FC = ({
+ label,
+ name,
+ value,
+ options,
+ onChange,
+ required = false,
+}) => {
+ return (
+
+ {label}{required && * }
+
+ Select {label}
+ {options.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ );
+};
+
+export default SelectField;
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
new file mode 100644
index 0000000..c6ee10e
--- /dev/null
+++ b/components/ui/select.tsx
@@ -0,0 +1,100 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown } from "lucide-react"
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+ {children}
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectValue = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+SelectValue.displayName = SelectPrimitive.Value.displayName
+
+
+export {
+ Select,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+ SelectValue,
+}
\ No newline at end of file
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
new file mode 100644
index 0000000..ab03051
--- /dev/null
+++ b/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
\ No newline at end of file
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
new file mode 100644
index 0000000..c766693
--- /dev/null
+++ b/components/ui/slider.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import * as React from "react"
+import * as SliderPrimitive from "@radix-ui/react-slider"
+import { cn } from "@/lib/utils"
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = SliderPrimitive.Root.displayName
+
+export { Slider }
\ No newline at end of file
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 0000000..4d67561
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+import { cn } from "@/lib/utils";
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };
diff --git a/components/ui/table.tsx b/components/ui/table.tsx
new file mode 100644
index 0000000..0ffe0d4
--- /dev/null
+++ b/components/ui/table.tsx
@@ -0,0 +1,42 @@
+import { ReactNode } from "react";
+import { cn } from "@/lib/utils";
+import { TableProps, TableRowProps, TableCellProps, TableHeadProps } from "@/types";
+
+
+export function Table({ className, children }: TableProps) {
+ return (
+
+ );
+}
+
+export function TableHeader({ className, children }: TableProps) {
+ return {children} ;
+}
+
+export function TableBody({ className, children }: TableProps) {
+ return {children} ;
+}
+
+export function TableRow({ className, children }: TableRowProps) {
+ return {children} ;
+}
+
+export function TableCell({ className, children, isHeader = false }: TableCellProps) {
+ const baseStyles = "px-4 py-3 text-sm text-gray-900";
+ const headerStyles = "font-semibold bg-gray-100";
+ return {children} ;
+}
+
+export const TableHead: React.FC = ({ className, children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+
+
+
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..c483810
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,54 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
\ No newline at end of file
diff --git a/components/ui/text-field.tsx b/components/ui/text-field.tsx
new file mode 100644
index 0000000..4156f16
--- /dev/null
+++ b/components/ui/text-field.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { TextAreaFieldProps } from "@/types";
+
+const TextAreaField: React.FC = ({
+ label,
+ name,
+ value,
+ placeholder,
+ onChange,
+ required = false,
+}) => {
+ return (
+
+ {label}{required && * }
+
+
+ );
+};
+
+export default TextAreaField;
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..0f0afdb
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,26 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/constants/index.ts b/constants/index.ts
new file mode 100644
index 0000000..aa08231
--- /dev/null
+++ b/constants/index.ts
@@ -0,0 +1,396 @@
+import React from 'react';
+import {
+ Home, PlusCircle, BarChart, DollarSign, User, LogOut, Settings, CreditCard, Megaphone, FileText, Receipt, Wallet, AlertCircle, Image, Bell, Shield, Inbox, ShieldAlert, HelpCircle, Ticket, MessageCircle, BookmarkCheckIcon, UserPlus, Headset, LayoutDashboard,
+ Users,
+ MessageSquare,
+ Menu,
+ X,
+ Wheat,
+ Tractor,
+ Beef,
+ Sprout,
+ Wrench,
+ Activity,
+} from "lucide-react";
+
+
+export const categories = [
+ {
+ id: 'farm-animals',
+ name: 'Farm Animals',
+ items: [
+ { name: 'Cattle', href: '#' },
+ { name: 'Goats', href: '#' },
+ { name: 'Sheep', href: '#' },
+ { name: 'Pigs', href: '#' },
+ { name: 'Horses', href: '#' },
+ { name: 'Rabbits', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+ {
+ id: 'poultry',
+ name: 'Poultry',
+ items: [
+ { name: 'Chickens', href: '#' },
+ { name: 'Ducks', href: '#' },
+ { name: 'Turkeys', href: '#' },
+ { name: 'Geese', href: '#' },
+ { name: 'Quails', href: '#' },
+ { name: 'Pigeons', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+ {
+ id: 'plants',
+ name: 'Plants',
+ items: [
+ { name: 'Vegetables', href: '#' },
+ { name: 'Fruits', href: '#' },
+ { name: 'Herbs', href: '#' },
+ { name: 'Legumes', href: '#' },
+ { name: 'Tubers', href: '#' },
+ { name: 'Spices', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+ {
+ id: 'cereals',
+ name: 'Cereals & Grains',
+ items: [
+ { name: 'Maize', href: '#' },
+ { name: 'Rice', href: '#' },
+ { name: 'Wheat', href: '#' },
+ { name: 'Millet', href: '#' },
+ { name: 'Sorghum', href: '#' },
+ { name: 'Oats', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+ {
+ id: 'machinery',
+ name: 'Farm Machinery',
+ items: [
+ { name: 'Tractors', href: '#' },
+ { name: 'Ploughs', href: '#' },
+ { name: 'Harrows', href: '#' },
+ { name: 'Seeders', href: '#' },
+ { name: 'Harvesters', href: '#' },
+ { name: 'Cultivators', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+ {
+ id: 'tools',
+ name: 'Tools',
+ items: [
+ { name: 'Hoes', href: '#' },
+ { name: 'Spades', href: '#' },
+ { name: 'Rakes', href: '#' },
+ { name: 'Watering Cans', href: '#' },
+ { name: 'Sickles', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+ {
+ id: 'accessories',
+ name: 'Farm Accessories',
+ items: [
+ { name: 'Fencing', href: '#' },
+ { name: 'Feeders & Drinkers', href: '#' },
+ { name: 'Storage Bins', href: '#' },
+ { name: 'Greenhouses', href: '#' },
+ { name: 'Irrigation Systems', href: '#' },
+ { name: 'Fertilizer Dispensers', href: '#' },
+ { name: 'Browse All', href: '#' },
+ ],
+ },
+];
+
+export const navigation = {
+ categories,
+ pages: [
+ // { name: 'About Us', href: '/about' },
+ { name: 'Services', href: '/services' },
+ { name: 'Products', href: '/products' },
+ // { name: 'Success Stories', href: '/testimonials' },
+ { name: 'News', href: '/news' },
+ // { name: 'Contact Us', href: '/contact' },
+ ],
+};
+
+
+export const activities = [
+ { id: 1, description: "You posted a new ad: 'Fresh Organic Apples'", time: "2 hours ago" },
+ { id: 2, description: "Started a promotion for 'Premium Cattle Feed'", time: "1 day ago" },
+ { id: 3, description: "Boosted ad: 'Quality Farming Tools'", time: "3 days ago" },
+];
+
+export const NAV_ITEMS = [
+ { name: "Dashboard", icon: Home, route: "/dashboard" },
+ { name: "My Ads", icon: Megaphone, route: "/dashboard/my-ads" },
+ { name: "Post New Ad", icon: PlusCircle, route: "/dashboard/new-ad" },
+ { name: "Ad Promotions", icon: DollarSign, route: "/dashboard/promotions" },
+ { name: "Analytics", icon: BarChart, route: "/dashboard/analytics" },
+ { name: "Saved Searches", icon: BookmarkCheckIcon, route: "/dashboard/saved-searches" },
+ { name: "Notifications", icon: Bell, route: "/dashboard/notifications" },
+ { name: "Agent Management", icon: UserPlus, route: "/admin/agents", adminOnly: true, },
+];
+
+export const SETTINGS = [
+ { name: "Profile Settings", icon: User, route: "/dashboard/profile" },
+ { name: "Payments & Billing", icon: CreditCard, route: "/dashboard/billing" },
+ { name: "Support Center", icon: HelpCircle, route: "/dashboard/support" },
+ { name: "Sign Out", icon: LogOut, action: "logout" },
+];
+
+export const adsData = [
+ {
+ id: 1,
+ title: "Fresh Organic Tomatoes",
+ price: "$15 per kg",
+ status: "Active",
+ views: 120,
+ clicks: 30,
+ shares: 5,
+ engagement: 10,
+ },
+ {
+ id: 2,
+ title: "Premium Olive Oil",
+ price: "$40 per bottle",
+ status: "Pending",
+ views: 85,
+ clicks: 20,
+ shares: 3,
+ engagement: 8,
+ },
+ {
+ id: 3,
+ title: "Natural Honey 500ml",
+ price: "$25 per jar",
+ status: "Sold",
+ views: 200,
+ clicks: 50,
+ shares: 12,
+ engagement: 15,
+ },
+];
+
+export const boostOptions = [
+ {
+ id: 1,
+ name: "Homepage Feature",
+ duration: [7, 14],
+ price: {
+ 7: 1000,
+ 14: 2000
+ },
+ features: ["Featured on homepage", "Priority in search results"]
+ },
+ {
+ id: 2,
+ name: "Top of Category",
+ duration: [7, 14],
+ price: {
+ 7: 1000,
+ 14: 2000
+ },
+ features: ["Top position in category", "Category highlight"]
+ },
+ {
+ id: 3,
+ name: "Highlighted Listing",
+ duration: [7, 14],
+ price: {
+ 7: 1000,
+ 14: 2000
+ },
+ features: ["Visual highlight", "Search result priority"]
+ }
+];
+
+export const FREE_USER_LIMITS = {
+ maxFreeAds: 5,
+ features: {
+ listingPriority: 0,
+ featuredOnHome: false,
+ adBoostDiscount: 0,
+ analyticsAccess: false,
+ maxActiveBoosts: 0
+ }
+};
+
+export const subscriptionPlans = [
+ {
+ id: 'silver',
+ name: "Silver",
+ price: 3000,
+ duration: 30,
+ benefits: [
+ "Unlimited ad posts",
+ "Featured on homepage",
+ "Priority listing",
+ "Analytics reports"
+ ],
+ features: {
+ listingPriority: 1,
+ featuredOnHome: true,
+ adBoostDiscount: 0,
+ analyticsAccess: true,
+ maxActiveBoosts: -1
+ }
+ },
+ {
+ id: 'gold',
+ name: "Gold",
+ price: 4000,
+ duration: 30,
+ benefits: [
+ "All Silver benefits",
+ "Top of category",
+ "Ad banners",
+ "10% boost discount"
+ ],
+ features: {
+ listingPriority: 2,
+ featuredOnHome: true,
+ topOfCategory: true,
+ adBoostDiscount: 10,
+ analyticsAccess: true,
+ maxActiveBoosts: -1
+ }
+ },
+ {
+ id: 'platinum',
+ name: "Platinum",
+ price: 5000,
+ duration: 30,
+ benefits: [
+ "All Gold benefits",
+ "Exclusive placement",
+ "Priority customer support",
+ "20% boost discount"
+ ],
+ features: {
+ listingPriority: 3,
+ featuredOnHome: true,
+ topOfCategory: true,
+ exclusivePlacement: true,
+ adBoostDiscount: 20,
+ analyticsAccess: true,
+ maxActiveBoosts: -1
+ }
+ }
+];
+
+// Dummy data
+export const adPerformance = {
+ impressions: 4500,
+ clicks: 850,
+ engagementRate: "18.9%",
+};
+
+export const demographics = {
+ topLocations: [
+ { country: "Nigeria", percentage: 40 },
+ { country: "Kenya", percentage: 25 },
+ { country: "Ghana", percentage: 20 },
+ { country: "South Africa", percentage: 15 },
+ ],
+ ageGroups: [
+ { group: "18-24", percentage: 35 },
+ { group: "25-34", percentage: 40 },
+ { group: "35-44", percentage: 15 },
+ { group: "45+", percentage: 10 },
+ ],
+};
+
+export const financialData = {
+ totalSpent: 250,
+ earnings: 750,
+ profit: 500,
+};
+
+export const transactions = [
+ { id: "#12345", date: "2024-01-20", amount: "$49.99", status: "Completed" },
+ { id: "#12346", date: "2024-01-25", amount: "$19.99", status: "Pending" },
+];
+
+export const invoices = [
+ { id: "#INV-001", date: "2024-01-15", amount: "$99.99", link: "#" },
+ { id: "#INV-002", date: "2024-01-22", amount: "$49.99", link: "#" },
+];
+
+export const paymentMethods = [
+ { type: "Visa", last4: "4242", expiry: "12/26" },
+ { type: "PayPal", email: "user@example.com" },
+];
+
+export const billingTabs = [
+ { key: "transactions", label: "Transactions", icon: Wallet },
+ { key: "invoices", label: "Invoices", icon: FileText },
+ { key: "methods", label: "Payment Methods", icon: CreditCard },
+ { key: "disputes", label: "Disputes", icon: AlertCircle },
+];
+
+export const userprofile = [
+ { key: "personal", label: "Personal Info", icon: User },
+ { key: "avatar", label: "Profile Picture", icon: Image },
+ { key: "notifications", label: "Notifications", icon: Bell },
+ { key: "security", label: "Security", icon: Shield },
+ { key: "activity", label: "Activity History", icon: Activity },
+ { key: "logout", label: "Logout", icon: LogOut },
+];
+
+
+export const MessagesTabs = [
+ { key: "inbox", label: "Inbox", icon: Inbox },
+ { key: "spam", label: "Spam/Blocked", icon: ShieldAlert },
+]
+
+export const faqData = [
+ {
+ question: "How do I post an ad?",
+ answer: "Go to the 'Post Ad' section, fill in the details, and submit.",
+ },
+ {
+ question: "How do I make payments for promotions?",
+ answer: "Navigate to 'Payments & Billing' and add a payment method.",
+ },
+ {
+ question: "How can I delete my account?",
+ answer: "Contact support through the 'Support Tickets' section.",
+ },
+];
+
+export const helpTabs = [
+ { key: "faq", label: "FAQ", icon: HelpCircle },
+ { key: "tickets", label: "Support Tickets", icon: Ticket },
+];
+
+
+export const initialNotifications = [
+ { id: 1, type: "ad", message: "š Your ad 'Organic Tomatoes' has been approved!", time: "2h ago" },
+ { id: 2, type: "promotion", message: "ā³ Your featured ad boost expires in 3 days!", time: "1d ago" },
+ { id: 3, type: "payment", message: "ā
Payment for ad promotion was successful.", time: "2d ago" },
+ { id: 4, type: "payment-failed", message: "ā ļø Your payment for 'Premium Listing' failed.", time: "3d ago" },
+];
+
+export const ADMIN_NAV_ITEMS = [
+ {
+ title: "Dashboard",
+ href: "/admin/dashboard",
+ icon: LayoutDashboard,
+ },
+ {
+ title: "Manage Agents",
+ href: "/admin/agents",
+ icon: Users,
+ },
+ {
+ title: "Settings",
+ href: "/admin/settings",
+ icon: Settings,
+ },
+];
\ No newline at end of file
diff --git a/env.prod b/env.prod
new file mode 100644
index 0000000..3bac393
--- /dev/null
+++ b/env.prod
@@ -0,0 +1,27 @@
+# Environment variables declared in this file are automatically made available to Prisma.
+# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
+
+# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
+# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
+
+JWT_SECRET=57dd8ca03367128f9138d72445153e07a000263ac3c0eb29e9a022dba23cec24
+DATABASE_URL=postgresql://agromarketng:agromarketng@localhost:5432/agromarketngdb?schema=public
+EMAIL_USER=ketuojoken@gmail.com
+EMAIL_PASS=oyuisxthlkunofrk
+NEXT_PUBLIC_BASE_URL=http://localhost:3000/
+
+# Google
+GOOGLE_CLIENT_ID=1044890102124-8kbmu1kcl5jj6hgcm1rabo06oobf7eml.apps.googleusercontent.com
+GOOGLE_CLIENT_SECRET=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
+
+# Facebook
+FACEBOOK_CLIENT_ID=501787352891200
+FACEBOOK_CLIENT_SECRET=f4b91366d71cba9b1226c6062e9f7428
+
+# Twitter
+TWITTER_CLIENT_ID=U1pvNlFqaGs1T3d1SW83WlQ3OGE6MTpjaQ
+TWITTER_CLIENT_SECRET=mG0TqQ7THVav3RePL0ozkWSjpUL_05-WdBBnX3oUd9oCawfB6Q
+
+# NextAuth
+NEXTAUTH_SECRET=325c751652fdd582490ed27967b48bf87a27267ee6d2359c62976014d7967928
+NEXTAUTH_URL=http://localhost:3000/
diff --git a/fix_dynamic_routes.py b/fix_dynamic_routes.py
new file mode 100644
index 0000000..5b51a38
--- /dev/null
+++ b/fix_dynamic_routes.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""
+Script to fix Next.js dynamic route parameter types from sync to async
+"""
+
+import os
+import re
+import glob
+
+def fix_route_params(file_path):
+ """Fix route parameters in a single file"""
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Pattern to match params type definitions
+ # Looking for: params: { paramName: string } or similar
+ param_pattern = r'params:\s*\{\s*([^}]+)\s*\}'
+
+ # Replace with Promise wrapper
+ def replace_params(match):
+ inner_params = match.group(1)
+ return f'params: Promise<{{{ inner_params } }}>'
+
+ # Apply the replacement
+ new_content = re.sub(param_pattern, replace_params, content)
+
+ # Also need to fix the usage of params inside functions
+ # Look for direct access like params.paramName and replace with await params
+
+ # Find all parameter names used
+ param_names = []
+ for match in re.finditer(r'params:\s*Promise<\{\s*([^}]+)\s*\}>', new_content):
+ inner = match.group(1)
+ # Extract parameter names
+ for param_match in re.finditer(r'(\w+):\s*string', inner):
+ param_names.append(param_match.group(1))
+
+ # Replace direct params access with destructured await
+ for param_name in param_names:
+ # Look for patterns like params.paramName or context.params.paramName
+ patterns = [
+ (f'params\.{param_name}', f'(await params).{param_name}'),
+ (f'context\.params\.{param_name}', f'(await context.params).{param_name}'),
+ ]
+
+ for old_pattern, replacement in patterns:
+ new_content = re.sub(old_pattern, replacement, new_content)
+
+ # Write back if changed
+ if new_content != content:
+ with open(file_path, 'w', encoding='utf-8') as f:
+ f.write(new_content)
+ print(f"Fixed: {file_path}")
+ return True
+ else:
+ print(f"No changes needed: {file_path}")
+ return False
+
+ except Exception as e:
+ print(f"Error processing {file_path}: {str(e)}")
+ return False
+
+def main():
+ # Find all route.ts files in the app directory
+ route_files = glob.glob('app/**/route.ts', recursive=True)
+
+ fixed_count = 0
+ for file_path in route_files:
+ if fix_route_params(file_path):
+ fixed_count += 1
+
+ print(f"\nFixed {fixed_count} files out of {len(route_files)} route files")
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..60b1ece
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,9 @@
+/** @type {import('ts-jest').JestConfigWithTsJest} */
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ moduleNameMapper: {
+ '^@/lib/prisma$': '/lib/prisma.ts',
+ '^@/(.*)$': '/$1',
+ },
+};
\ No newline at end of file
diff --git a/lib/__tests__/apiUtils.test.ts b/lib/__tests__/apiUtils.test.ts
new file mode 100644
index 0000000..4e08720
--- /dev/null
+++ b/lib/__tests__/apiUtils.test.ts
@@ -0,0 +1,220 @@
+import {
+ fetchWithErrorHandling,
+ apiGet,
+ apiPost,
+ apiPut,
+ apiDelete,
+ apiPatch,
+} from '../apiUtils';
+import toast from 'react-hot-toast';
+
+// Mock react-hot-toast
+jest.mock('react-hot-toast', () => ({
+ error: jest.fn(),
+ success: jest.fn(),
+}));
+
+// Mock global fetch
+const mockFetch = jest.fn();
+global.fetch = mockFetch as any; // Cast to any to bypass TS errors for partial mock
+
+describe('apiUtils', () => {
+ beforeEach(() => {
+ mockFetch.mockClear();
+ (toast.error as jest.Mock).mockClear();
+ (toast.success as jest.Mock).mockClear();
+ // Restore original fetch after each test to avoid interference (though reassignment might be enough)
+ // global.fetch = originalFetch; // Assuming originalFetch was stored if needed
+ });
+
+ // No afterEach needed if beforeEach reassigns fetch
+
+ describe('fetchWithErrorHandling', () => {
+ it('should return data on successful fetch', async () => {
+ const mockData = { success: true, data: { id: 1, name: 'Test' } };
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockData,
+ headers: { // Added headers mock
+ get: jest.fn((headerName: string) => {
+ if (headerName.toLowerCase() === 'content-type') {
+ return 'application/json';
+ }
+ return null;
+ }),
+ } as any, // Cast headers to any
+ });
+
+ const result = await fetchWithErrorHandling('/test-url');
+ // Expect the full mockData object, not just the data field
+ expect(result).toEqual(mockData);
+ // Updated expected options to include default headers and credentials
+ expect(mockFetch).toHaveBeenCalledWith('/test-url', {
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ });
+ expect(toast.error).not.toHaveBeenCalled();
+ });
+
+ it('should throw an error and show toast on non-ok response', async () => {
+ const mockError = { success: false, message: 'Something went wrong' };
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ json: async () => mockError,
+ headers: { get: jest.fn() } as any,
+ statusText: 'Bad Request', // Add statusText for non-JSON error
+ });
+
+ await expect(fetchWithErrorHandling('/test-url')).rejects.toThrow(
+ 'Something went wrong'
+ );
+ // Updated expected options to include default headers and credentials
+ expect(mockFetch).toHaveBeenCalledWith('/test-url', {
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ });
+ expect(toast.error).toHaveBeenCalledWith('Something went wrong');
+ });
+
+ it('should throw a generic error and show toast if response is not JSON', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ json: async () => { throw new Error('Not JSON'); },
+ text: async () => 'Not JSON',
+ headers: { get: jest.fn(() => null) } as any,
+ statusText: 'Internal Server Error',
+ status: 500,
+ });
+
+ await expect(fetchWithErrorHandling('/test-url')).rejects.toThrow(
+ 'Internal Server Error'
+ );
+ expect(mockFetch).toHaveBeenCalledWith('/test-url', {
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ });
+ expect(toast.error).toHaveBeenCalledWith('Internal Server Error');
+ });
+
+ it('should handle network errors', async () => {
+ const networkError = new Error('Network request failed');
+ mockFetch.mockRejectedValueOnce(networkError);
+
+ await expect(fetchWithErrorHandling('/test-url')).rejects.toThrow(
+ 'Network request failed'
+ );
+ // Updated expected options to include default headers and credentials
+ expect(mockFetch).toHaveBeenCalledWith('/test-url', {
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ });
+ expect(toast.error).toHaveBeenCalledWith('Network request failed');
+ });
+
+ it('should include options in fetch call', async () => {
+ const mockData = { success: true, data: {} };
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockData,
+ headers: { get: jest.fn() } as any,
+ });
+ const options = { method: 'POST', body: JSON.stringify({ key: 'value' }) };
+
+ await fetchWithErrorHandling('/test-url', options);
+ // Expect merged options with default headers and credentials
+ expect(mockFetch).toHaveBeenCalledWith('/test-url', {
+ ...options,
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ });
+ });
+ });
+
+ describe('apiGet', () => {
+ it('should call fetchWithErrorHandling with GET method', async () => {
+ const mockData = { success: true, data: { id: 1 } };
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockData,
+ headers: {
+ get: jest.fn((header) => {
+ if (header === 'content-type') return 'application/json';
+ return null;
+ })
+ } as any,
+ });
+
+ const result = await apiGet('/test-url');
+ expect(result).toEqual(mockData);
+ });
+ });
+
+ describe('apiPost', () => {
+ it('should call fetchWithErrorHandling with POST method and body', async () => {
+ const mockData = { success: true, data: { id: 1 } }; // Simulate full response structure
+ jest
+ .spyOn(require('../apiUtils'), 'fetchWithErrorHandling')
+ .mockResolvedValueOnce(mockData);
+ const body = { name: 'New Item' };
+
+ const result = await apiPost('/test-url', body);
+ expect(result).toEqual(mockData); // Expect the full mockData object
+ expect(fetchWithErrorHandling).toHaveBeenCalledWith('/test-url', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ });
+ });
+
+ describe('apiPut', () => {
+ it('should call fetchWithErrorHandling with PUT method and body', async () => {
+ const mockData = { success: true, data: { id: 1 } }; // Simulate full response structure
+ jest
+ .spyOn(require('../apiUtils'), 'fetchWithErrorHandling')
+ .mockResolvedValueOnce(mockData);
+ const body = { name: 'Updated Item' };
+
+ const result = await apiPut('/test-url', body);
+ expect(result).toEqual(mockData); // Expect the full mockData object
+ expect(fetchWithErrorHandling).toHaveBeenCalledWith('/test-url', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ });
+ });
+
+ describe('apiDelete', () => {
+ it('should call fetchWithErrorHandling with DELETE method', async () => {
+ const mockData = { success: true }; // Simulate full response structure
+ jest
+ .spyOn(require('../apiUtils'), 'fetchWithErrorHandling')
+ .mockResolvedValueOnce(mockData);
+
+ const result = await apiDelete('/test-url');
+ expect(result).toEqual(mockData); // Expect the full mockData object
+ expect(fetchWithErrorHandling).toHaveBeenCalledWith('/test-url', {
+ method: 'DELETE',
+ });
+ });
+ });
+
+ describe('apiPatch', () => {
+ it('should call fetchWithErrorHandling with PATCH method and body', async () => {
+ const mockData = { success: true, data: { id: 1 } }; // Simulate full response structure
+ jest
+ .spyOn(require('../apiUtils'), 'fetchWithErrorHandling')
+ .mockResolvedValueOnce(mockData);
+ const body = { name: 'Patched Item' };
+
+ const result = await apiPatch('/test-url', body);
+ expect(result).toEqual(mockData); // Expect the full mockData object
+ expect(fetchWithErrorHandling).toHaveBeenCalledWith('/test-url', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ });
+ });
+});
diff --git a/lib/__tests__/cache.test.ts b/lib/__tests__/cache.test.ts
new file mode 100644
index 0000000..901b23b
--- /dev/null
+++ b/lib/__tests__/cache.test.ts
@@ -0,0 +1,149 @@
+import { Cache, cache } from '../cache'; // Import both the class and the instance
+
+// Mock the Date.now function to control time for TTL tests
+const mockDateNow = jest.spyOn(Date, 'now');
+
+describe('Cache', () => {
+ // Type the variable with the Cache interface
+ let cacheInstance: Cache;
+
+ beforeEach(() => {
+ // Reset the singleton instance before each test by manipulating the static property
+ (Cache as any).instance = undefined; // Cast Cache to any to access private static instance
+ // Get a new instance using the static method from the imported Cache class
+ cacheInstance = Cache.getInstance();
+ mockDateNow.mockReturnValue(0); // Start time at 0
+ });
+
+ afterEach(() => {
+ mockDateNow.mockRestore();
+ // Clean up the cache instance after each test
+ cacheInstance.clear();
+ });
+
+ it('should return the same instance (singleton)', () => {
+ // Verify that calling getInstance on the Cache class returns the same instance
+ const cache2 = Cache.getInstance();
+ expect(cacheInstance).toBe(cache2);
+ });
+
+ describe('getOrSet', () => {
+ it('should fetch and cache data if key does not exist', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('initial data');
+ const key = 'testKey';
+
+ // Use the instance variable
+ const data = await cacheInstance.getOrSet(key, fetchFn);
+
+ expect(data).toBe('initial data');
+ expect(fetchFn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return cached data if key exists and is not expired', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('initial data');
+ const key = 'testKey';
+
+ // First call to cache the data using the instance variable
+ await cacheInstance.getOrSet(key, fetchFn, 1000); // TTL of 1000ms
+
+ // Advance time, but within TTL
+ mockDateNow.mockReturnValue(500);
+
+ // Second call using the instance variable
+ const data = await cacheInstance.getOrSet(key, fetchFn, 1000); // Provide TTL again
+
+ expect(data).toBe('initial data');
+ expect(fetchFn).toHaveBeenCalledTimes(1); // fetchFn should not be called again
+ });
+
+ it('should refetch and cache data if key exists but is expired', async () => {
+ const fetchFn = jest.fn()
+ .mockResolvedValueOnce('initial data')
+ .mockResolvedValueOnce('new data');
+ const key = 'testKey';
+
+ // First call to cache the data using the instance variable
+ await cacheInstance.getOrSet(key, fetchFn, 1000); // TTL of 1000ms
+
+ // Advance time beyond TTL
+ mockDateNow.mockReturnValue(1001);
+
+ // Second call using the instance variable, provide TTL again
+ const data = await cacheInstance.getOrSet(key, fetchFn, 1000);
+
+ expect(data).toBe('new data');
+ expect(fetchFn).toHaveBeenCalledTimes(2); // fetchFn should be called again
+ });
+
+ it('should handle fetch function throwing an error', async () => {
+ const fetchFn = jest.fn().mockRejectedValue(new Error('Fetch failed'));
+ const key = 'testKey';
+
+ // Use the instance variable
+ await expect(cacheInstance.getOrSet(key, fetchFn)).rejects.toThrow('Fetch failed');
+ expect(fetchFn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should use default TTL if not provided', async () => {
+ // This test is more conceptual as default TTL is internal.
+ // We test by ensuring it behaves like a cache with some expiry.
+ const fetchFn = jest.fn()
+ .mockResolvedValueOnce('initial data')
+ .mockResolvedValueOnce('new data');
+ const key = 'testKey';
+ const defaultTTL = 5 * 60 * 1000; // Assuming default TTL is 5 minutes
+
+ // First call using the instance variable
+ await cacheInstance.getOrSet(key, fetchFn);
+
+ // Advance time beyond the assumed default TTL
+ mockDateNow.mockReturnValue(defaultTTL + 1);
+
+ // Second call using the instance variable, should use default TTL
+ const data = await cacheInstance.getOrSet(key, fetchFn);
+
+ expect(data).toBe('new data');
+ expect(fetchFn).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('clear', () => {
+ it('should clear the cache', async () => {
+ const fetchFn = jest.fn().mockResolvedValue('initial data');
+ const key = 'testKey';
+
+ await cacheInstance.getOrSet(key, fetchFn);
+ expect(fetchFn).toHaveBeenCalledTimes(1);
+
+ cacheInstance.clear();
+
+ // Fetch again, should call fetchFn as cache is cleared
+ await cacheInstance.getOrSet(key, fetchFn);
+ expect(fetchFn).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('delete', () => {
+ it('should delete a specific key from the cache', async () => {
+ const fetchFn1 = jest.fn().mockResolvedValue('data1');
+ const fetchFn2 = jest.fn().mockResolvedValue('data2');
+ const key1 = 'key1';
+ const key2 = 'key2';
+
+ await cacheInstance.getOrSet(key1, fetchFn1);
+ await cacheInstance.getOrSet(key2, fetchFn2);
+ expect(fetchFn1).toHaveBeenCalledTimes(1);
+ expect(fetchFn2).toHaveBeenCalledTimes(1);
+
+ cacheInstance.delete(key1);
+
+ // Fetch key1 again, should call fetchFn1 as it was deleted
+ await cacheInstance.getOrSet(key1, fetchFn1);
+ expect(fetchFn1).toHaveBeenCalledTimes(2);
+
+ // Fetch key2 again, should return cached data as it was not deleted
+ await cacheInstance.getOrSet(key2, fetchFn2);
+ expect(fetchFn2).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/lib/__tests__/categoryUtils.test.ts b/lib/__tests__/categoryUtils.test.ts
new file mode 100644
index 0000000..f2e6fa5
--- /dev/null
+++ b/lib/__tests__/categoryUtils.test.ts
@@ -0,0 +1,158 @@
+// Mock the navigation object structure used by categoryUtils
+const mockNavigation = {
+ categories: [
+ {
+ name: 'Electronics',
+ featured: [
+ { name: 'Laptops', href: '#' },
+ { name: 'Smartphones', href: '#' },
+ ],
+ items: [ // Flattened structure
+ { name: 'Laptops', href: '#' },
+ { name: 'Smartphones', href: '#' },
+ { name: 'Tablets', href: '#' },
+ ],
+ },
+ {
+ name: 'Fashion',
+ featured: [
+ { name: 'Men', href: '#' },
+ { name: 'Women', href: '#' },
+ ],
+ items: [ // Flattened structure
+ { name: 'Men', href: '#' },
+ { name: 'Women', href: '#' },
+ { name: 'Kids', href: '#' },
+ ],
+ },
+ {
+ name: 'Home',
+ featured: [],
+ items: [ // Flattened structure
+ { name: 'Furniture', href: '#' },
+ { name: 'Decor', href: '#' },
+ ],
+ },
+ {
+ name: 'Services',
+ featured: [],
+ items: [], // Flattened structure
+ },
+ ],
+};
+
+import {
+ getSubcategoriesForCategory,
+ mapUiCategoryToDatabase,
+ mapDatabaseCategoryToUi,
+ getAllCategoriesWithSubcategories,
+ findSubcategoryInCategories,
+} from '../categoryUtils';
+
+// Mock the navigation import
+jest.mock('@/constants', () => ({
+ navigation: mockNavigation,
+}));
+
+describe('categoryUtils', () => {
+ describe('getSubcategoriesForCategory', () => {
+ it('should return subcategories for a given category name', () => {
+ const subcategories = getSubcategoriesForCategory('Electronics');
+ expect(subcategories).toEqual([
+ { name: 'Laptops', href: '#' },
+ { name: 'Smartphones', href: '#' },
+ { name: 'Tablets', href: '#' },
+ ]);
+ });
+
+ it('should return an empty array for a category with no sections/items', () => {
+ const subcategories = getSubcategoriesForCategory('Services');
+ expect(subcategories).toEqual([]);
+ });
+
+ it('should return an empty array for a non-existent category', () => {
+ const subcategories = getSubcategoriesForCategory('NonExistent');
+ expect(subcategories).toEqual([]);
+ });
+ });
+
+ describe('mapUiCategoryToDatabase', () => {
+ it('should map UI category name to database category name', () => {
+ expect(mapUiCategoryToDatabase('Electronics')).toBe('Electronics');
+ expect(mapUiCategoryToDatabase('Fashion')).toBe('Fashion');
+ expect(mapUiCategoryToDatabase('Home')).toBe('Home');
+ expect(mapUiCategoryToDatabase('Services')).toBe('Services');
+ });
+
+ it('should return the same name if mapping is not found (fallback)', () => {
+ // Assuming no specific mapping is needed if names are the same
+ // This test confirms it doesn't break for unknown UI names
+ expect(mapUiCategoryToDatabase('UnknownCategory')).toBe('UnknownCategory');
+ });
+ });
+
+ describe('mapDatabaseCategoryToUi', () => {
+ it('should map database category name to UI category name', () => {
+ expect(mapDatabaseCategoryToUi('Electronics')).toBe('Electronics');
+ expect(mapDatabaseCategoryToUi('Fashion')).toBe('Fashion');
+ expect(mapDatabaseCategoryToUi('Home')).toBe('Home');
+ expect(mapDatabaseCategoryToUi('Services')).toBe('Services');
+ });
+
+ it('should return the same name if mapping is not found (fallback)', () => {
+ // Assuming no specific mapping is needed if names are the same
+ // This test confirms it doesn't break for unknown DB names
+ expect(mapDatabaseCategoryToUi('UnknownCategory')).toBe('UnknownCategory');
+ });
+ });
+
+ describe('getAllCategoriesWithSubcategories', () => {
+ it('should return all categories with their subcategories', () => {
+ const categoriesWithSubs = getAllCategoriesWithSubcategories();
+ expect(categoriesWithSubs).toEqual([
+ {
+ category: 'Electronics', // Updated property name
+ subcategories: [
+ { name: 'Laptops', href: '#' },
+ { name: 'Smartphones', href: '#' },
+ { name: 'Tablets', href: '#' },
+ ],
+ },
+ {
+ category: 'Fashion', // Updated property name
+ subcategories: [
+ { name: 'Men', href: '#' },
+ { name: 'Women', href: '#' },
+ { name: 'Kids', href: '#' },
+ ],
+ },
+ {
+ category: 'Home', // Updated property name
+ subcategories: [
+ { name: 'Furniture', href: '#' },
+ { name: 'Decor', href: '#' },
+ ],
+ },
+ {
+ category: 'Services', // Updated property name
+ subcategories: [],
+ },
+ ]);
+ });
+ });
+
+ describe('findSubcategoryInCategories', () => {
+ it('should find a subcategory and return its category and subcategory names', () => {
+ const result = findSubcategoryInCategories('Smartphones');
+ expect(result).toEqual({
+ category: 'Electronics', // Updated property name
+ subcategory: 'Smartphones', // Updated property name
+ });
+ });
+
+ it('should return undefined if subcategory is not found', () => {
+ const result = findSubcategoryInCategories('NonExistentSubcategory');
+ expect(result).toBeNull(); // Updated assertion to expect null
+ });
+ });
+});
diff --git a/lib/__tests__/imageUtils.test.ts b/lib/__tests__/imageUtils.test.ts
new file mode 100644
index 0000000..4581de7
--- /dev/null
+++ b/lib/__tests__/imageUtils.test.ts
@@ -0,0 +1,221 @@
+/* @jest-environment jsdom */
+
+import {
+ getResponsiveSizes,
+ getImagePlaceholder,
+ getSupportedImageFormat,
+ getOptimizedImageUrl,
+ getProductImageUrl,
+} from '../imageUtils';
+import { StaticImageData } from 'next/image';
+
+describe('imageUtils', () => {
+ describe('getResponsiveSizes', () => {
+ it('should return default sizes string if no custom sizes are provided', () => {
+ const defaultSizes = '(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 1280px';
+ expect(getResponsiveSizes()).toBe(defaultSizes);
+ });
+
+ it('should return a sizes string with custom values', () => {
+ const customSizes = {
+ small: 700,
+ medium: 1100,
+ large: 1400,
+ default: 1800,
+ };
+ const expectedSizes = '(max-width: 700px) 100vw, (max-width: 1100px) 50vw, (max-width: 1400px) 33vw, 1800px';
+ expect(getResponsiveSizes(customSizes)).toBe(expectedSizes);
+ });
+
+ it('should use default values for missing custom sizes', () => {
+ const customSizes = {
+ small: 900,
+ };
+ const expectedSizes = '(max-width: 900px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 1280px';
+ expect(getResponsiveSizes(customSizes)).toBe(expectedSizes);
+ });
+ });
+
+ describe('getImagePlaceholder', () => {
+ it('should return the placeholder SVG for a local string src', () => {
+ const src = '/assets/img/products/image.jpg';
+ const expectedPlaceholder = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjZjFmMWYxIi8+Cjwvc3ZnPg==';
+ expect(getImagePlaceholder(src)).toBe(expectedPlaceholder);
+ });
+
+ it('should return the blurDataURL for a StaticImageData src with blurDataURL', () => {
+ const src: StaticImageData = {
+ src: '/_next/static/media/image.hash.jpg',
+ height: 100,
+ width: 100,
+ blurDataURL: 'data:image/png;base64,...',
+ };
+ expect(getImagePlaceholder(src)).toBe(src.blurDataURL);
+ });
+
+ it('should return undefined for an empty string src', () => {
+ expect(getImagePlaceholder('')).toBeUndefined();
+ });
+
+ it('should return undefined for null or undefined src', () => {
+ expect(getImagePlaceholder(null as any)).toBeUndefined();
+ expect(getImagePlaceholder(undefined as any)).toBeUndefined();
+ });
+ });
+
+ describe('getSupportedImageFormat', () => {
+ // Store original global properties to restore them after each test
+ const originalSelf = global.self;
+ const originalCanvas = global.HTMLCanvasElement;
+ const originalFetch = global.fetch;
+ const originalWindow = global.window;
+
+ let mockCreateImageBitmap: jest.Mock;
+ let mockCanvasToDataURL: jest.Mock; // Use a dedicated mock for toDataURL
+
+ beforeEach(() => {
+ mockCreateImageBitmap = jest.fn();
+ mockCanvasToDataURL = jest.fn();
+
+ // Mock global objects and methods
+ global.self = { // Cast to any to allow partial mocking
+ ...originalSelf,
+ createImageBitmap: mockCreateImageBitmap,
+ } as any;
+
+ // Mock HTMLCanvasElement.prototype.toDataURL directly
+ if (global.HTMLCanvasElement) { // Check if it exists in the environment
+ jest.spyOn(global.HTMLCanvasElement.prototype, 'toDataURL').mockImplementation(mockCanvasToDataURL);
+ } else {
+ // Fallback mock if HTMLCanvasElement is not available (should not happen with jsdom)
+ global.HTMLCanvasElement = class MockCanvasElement { // Cast to any
+ getContext() { return {}; }
+ toDataURL = mockCanvasToDataURL; // Assign mock directly
+ } as any;
+ }
+
+ global.fetch = jest.fn(() =>
+ Promise.resolve({
+ blob: () => Promise.resolve(new Blob()),
+ } as Response)
+ ) as any;
+
+ // Ensure window is defined for client-side tests
+ global.window = {} as any; // Provide a minimal mock object
+ });
+
+ afterEach(() => {
+ // Restore HTMLCanvasElement.prototype.toDataURL spy
+ if (global.HTMLCanvasElement && (global.HTMLCanvasElement.prototype.toDataURL as any).mockRestore) {
+ (global.HTMLCanvasElement.prototype.toDataURL as any).mockRestore();
+ }
+
+ // Restore original global properties
+ global.self = originalSelf;
+ global.HTMLCanvasElement = originalCanvas;
+ global.fetch = originalFetch;
+ global.window = originalWindow;
+
+ });
+
+ it('should return the first supported format from the list (AVIF supported)', async () => {
+ mockCreateImageBitmap.mockResolvedValue({});
+ expect(await getSupportedImageFormat(['avif', 'webp', 'jpg'])).toBe('avif');
+ });
+
+ it('should return the next supported format if the first is not supported (WebP supported)', async () => {
+ mockCreateImageBitmap.mockRejectedValue(new Error('AVIF not supported'));
+ mockCanvasToDataURL.mockReturnValue('data:image/webp'); // Mock toDataURL for webp
+ expect(await getSupportedImageFormat(['avif', 'webp', 'jpg'])).toBe('webp');
+ });
+
+ it('should return the last format if only it is supported (JPG fallback)', async () => {
+ mockCreateImageBitmap.mockRejectedValue(new Error('AVIF not supported'));
+ mockCanvasToDataURL.mockReturnValue('data:'); // Mock toDataURL to indicate no webp support
+ expect(await getSupportedImageFormat(['avif', 'webp', 'jpg'])).toBe('jpg');
+ });
+
+ it.skip('should return the last format if running on the server', async () => {
+ // Temporarily set window to undefined to simulate server environment
+ const originalWindow = global.window; // Store original window
+ global.window = undefined as any;
+
+ try {
+ // Ensure the function returns the last format in server environment
+ expect(await getSupportedImageFormat(['avif', 'webp', 'jpg'])).toBe('jpg');
+ } finally {
+ // Restore original window
+ global.window = originalWindow;
+ }
+ });
+
+ it('should return the fallback format if no formats are supported', async () => {
+ mockCreateImageBitmap.mockRejectedValue(new Error('AVIF not supported'));
+ mockCanvasToDataURL.mockReturnValue('data:'); // Mock toDataURL to indicate no webp support
+ expect(await getSupportedImageFormat(['webp', 'png'])).toBe('png');
+ });
+
+ it('should return the last format if no formats are provided', async () => {
+ expect(await getSupportedImageFormat([])).toBe('jpg');
+ });
+ });
+
+ describe('getOptimizedImageUrl', () => {
+ it('should return the optimized image URL with specified width and format', () => {
+ const src = '/assets/img/products/image.jpg';
+ const width = 500;
+ const format = 'webp';
+ const expectedUrl = '/assets/img/products/image.jpg?w=500&q=75&fm=webp';
+ expect(getOptimizedImageUrl(src, width, format)).toBe(expectedUrl);
+ });
+
+ it('should use default format (webp) if not provided', () => {
+ const src = '/assets/img/products/image.png';
+ const width = 300;
+ const expectedUrl = '/assets/img/products/image.png?w=300&q=75&fm=webp';
+ expect(getOptimizedImageUrl(src, width)).toBe(expectedUrl);
+ });
+
+ it('should handle different image sources', () => {
+ const src = 'https://example.com/images/photo.jpeg';
+ const width = 800;
+ const expectedUrl = 'https://example.com/images/photo.jpeg';
+ expect(getOptimizedImageUrl(src, width)).toBe(expectedUrl);
+ });
+ });
+
+ describe('getProductImageUrl', () => {
+ it('should return the URL of the image at the specified index', () => {
+ const images = ['img1.jpg', 'img2.png', 'img3.webp'];
+ expect(getProductImageUrl(images, 1)).toBe('img2.png');
+ });
+
+ it('should return the placeholder URL if index is out of bounds (negative)', () => {
+ const images = ['img1.jpg', 'img2.png'];
+ expect(getProductImageUrl(images, -1)).toBe('/assets/img/products/placeholder.jpg');
+ });
+
+ it('should return the placeholder URL if index is out of bounds (positive)', () => {
+ const images = ['img1.jpg', 'img2.png'];
+ expect(getProductImageUrl(images, 5)).toBe('/assets/img/products/placeholder.jpg');
+ });
+
+ it('should return the URL of the first image if index is not provided', () => {
+ const images = ['img1.jpg', 'img2.png'];
+ expect(getProductImageUrl(images)).toBe('img1.jpg');
+ });
+
+ it('should return a placeholder URL if images array is empty', () => {
+ const images: string[] = [];
+ expect(getProductImageUrl(images)).toBe('/assets/img/products/placeholder.jpg');
+ });
+
+ it('should return a placeholder URL if images array is undefined', () => {
+ expect(getProductImageUrl(undefined)).toBe('/assets/img/products/placeholder.jpg');
+ });
+
+ it('should return a placeholder URL if images array is null', () => {
+ expect(getProductImageUrl(null as any)).toBe('/assets/img/products/placeholder.jpg');
+ });
+ });
+});
\ No newline at end of file
diff --git a/lib/__tests__/utils.test.ts b/lib/__tests__/utils.test.ts
new file mode 100644
index 0000000..95864ac
--- /dev/null
+++ b/lib/__tests__/utils.test.ts
@@ -0,0 +1,28 @@
+import { formatCurrency } from '../utils';
+
+describe('formatCurrency', () => {
+ it('should format a number as currency with default currency symbol', () => {
+ const amount = 1000;
+ const formattedAmount = formatCurrency(amount);
+ expect(formattedAmount).toBe('ā¦1,000');
+ });
+
+ it('should format a number as currency with a custom currency symbol', () => {
+ const amount = 2500.50;
+ const currency = '$';
+ const formattedAmount = formatCurrency(amount, currency);
+ expect(formattedAmount).toBe('$2,500.5');
+ });
+
+ it('should handle zero amount', () => {
+ const amount = 0;
+ const formattedAmount = formatCurrency(amount);
+ expect(formattedAmount).toBe('ā¦0');
+ });
+
+ it('should handle large numbers', () => {
+ const amount = 1000000;
+ const formattedAmount = formatCurrency(amount);
+ expect(formattedAmount).toBe('ā¦1,000,000');
+ });
+});
\ No newline at end of file
diff --git a/lib/activityLogger.ts b/lib/activityLogger.ts
new file mode 100644
index 0000000..2edaa0b
--- /dev/null
+++ b/lib/activityLogger.ts
@@ -0,0 +1,135 @@
+import prisma from '@/lib/prisma';
+import { NextRequest } from 'next/server';
+
+interface ActivityData {
+ userId: string;
+ activity: string;
+ description?: string;
+ success?: boolean;
+ metadata?: Record;
+}
+
+interface DeviceInfo {
+ browser?: string;
+ os?: string;
+ device?: string;
+}
+
+function parseUserAgent(userAgent: string): DeviceInfo {
+ const info: DeviceInfo = {};
+
+ // Parse browser
+ if (userAgent.includes('Chrome')) {
+ info.browser = 'Chrome';
+ } else if (userAgent.includes('Firefox')) {
+ info.browser = 'Firefox';
+ } else if (userAgent.includes('Safari')) {
+ info.browser = 'Safari';
+ } else if (userAgent.includes('Edge')) {
+ info.browser = 'Edge';
+ }
+
+ // Parse OS
+ if (userAgent.includes('Windows')) {
+ info.os = 'Windows';
+ } else if (userAgent.includes('Mac')) {
+ info.os = 'macOS';
+ } else if (userAgent.includes('Linux')) {
+ info.os = 'Linux';
+ } else if (userAgent.includes('Android')) {
+ info.os = 'Android';
+ } else if (userAgent.includes('iOS')) {
+ info.os = 'iOS';
+ }
+
+ // Parse device type
+ if (userAgent.includes('Mobile')) {
+ info.device = 'Mobile';
+ } else if (userAgent.includes('Tablet')) {
+ info.device = 'Tablet';
+ } else {
+ info.device = 'Desktop';
+ }
+
+ return info;
+}
+
+function getClientIP(request: NextRequest): string | null {
+ // Try multiple headers to get the real IP
+ const forwarded = request.headers.get('x-forwarded-for');
+ const realIP = request.headers.get('x-real-ip');
+ const clientIP = request.headers.get('x-client-ip');
+
+ if (forwarded) {
+ return forwarded.split(',')[0].trim();
+ }
+
+ if (realIP) {
+ return realIP;
+ }
+
+ if (clientIP) {
+ return clientIP;
+ }
+
+ // Fallback to connection remote address (may not work in all environments)
+ return null;
+}
+
+export async function logActivity(
+ data: ActivityData,
+ request?: NextRequest
+): Promise {
+ try {
+ const userAgent = request?.headers.get('user-agent') || '';
+ const ipAddress = request ? getClientIP(request) : null;
+ const deviceInfo = userAgent ? parseUserAgent(userAgent) : {};
+
+ await prisma.activityLog.create({
+ data: {
+ userId: data.userId,
+ activity: data.activity,
+ description: data.description || null,
+ ipAddress: ipAddress,
+ userAgent: userAgent || null,
+ deviceInfo: Object.keys(deviceInfo).length > 0 ? JSON.stringify(deviceInfo) : null,
+ location: null, // Could be enhanced with geolocation service
+ success: data.success !== undefined ? data.success : true,
+ metadata: data.metadata || null
+ }
+ });
+ } catch (error) {
+ console.error('Failed to log activity:', error);
+ // Don't throw error to avoid breaking the main functionality
+ }
+}
+
+// Predefined activity types
+export const ActivityTypes = {
+ // Authentication
+ LOGIN: 'login',
+ LOGOUT: 'logout',
+ LOGIN_FAILED: 'login_failed',
+
+ // Profile changes
+ PROFILE_UPDATE: 'profile_update',
+ PASSWORD_CHANGE: 'password_change',
+ EMAIL_CHANGE_REQUEST: 'email_change_request',
+ EMAIL_CHANGE_VERIFIED: 'email_change_verified',
+ AVATAR_UPDATE: 'avatar_update',
+
+ // Security
+ TWO_FA_ENABLED: '2fa_enabled',
+ TWO_FA_DISABLED: '2fa_disabled',
+ TWO_FA_BACKUP_CODES_GENERATED: '2fa_backup_codes_generated',
+
+ // Account
+ ACCOUNT_DELETED: 'account_deleted',
+
+ // Settings
+ NOTIFICATION_PREFERENCES_UPDATE: 'notification_preferences_update',
+
+ // Other activities can be added here
+} as const;
+
+export type ActivityType = typeof ActivityTypes[keyof typeof ActivityTypes];
\ No newline at end of file
diff --git a/lib/apiUtils.ts b/lib/apiUtils.ts
new file mode 100644
index 0000000..20dbcd8
--- /dev/null
+++ b/lib/apiUtils.ts
@@ -0,0 +1,164 @@
+import toast from "react-hot-toast";
+import { handleApiError, retryWithBackoff } from "./errorHandling";
+
+interface FetchOptions extends RequestInit {
+ showSuccessToast?: boolean;
+ successMessage?: string;
+ showErrorToast?: boolean;
+ errorMessage?: string;
+ retry?: boolean;
+ maxRetries?: number;
+}
+
+/**
+ * Enhanced fetch function with error handling and retries
+ * @param url The URL to fetch
+ * @param options Fetch options with additional parameters
+ * @returns Promise with the response data
+ */
+export async function fetchWithErrorHandling(
+ url: string,
+ options: FetchOptions = {}
+): Promise {
+ const {
+ showSuccessToast = false,
+ successMessage = "Operation successful",
+ showErrorToast = true,
+ errorMessage = "An error occurred. Please try again.",
+ retry = false,
+ maxRetries = 3,
+ ...fetchOptions
+ } = options;
+
+ // Set default headers if not provided
+ const headers = {
+ "Content-Type": "application/json",
+ ...fetchOptions.headers,
+ };
+
+ // Create the fetch function
+ const fetchFn = async (): Promise => {
+ try {
+ const response = await fetch(url, {
+ ...fetchOptions,
+ headers,
+ credentials: "include", // Always include credentials
+ });
+
+ // Handle non-2xx responses
+ if (!response.ok) {
+ let errorData;
+ try {
+ errorData = await response.json();
+ } catch (e) {
+ // If response is not JSON, use status text
+ errorData = { message: response.statusText };
+ }
+
+ const error = new Error(
+ errorData.message || errorData.error || errorMessage
+ );
+ (error as any).status = response.status;
+ (error as any).data = errorData;
+ throw error;
+ }
+
+ // Parse response
+ let data;
+ const contentType = response.headers.get("content-type");
+ if (contentType && contentType.includes("application/json")) {
+ data = await response.json();
+ } else {
+ data = await response.text();
+ }
+
+ // Show success toast if requested
+ if (showSuccessToast) {
+ toast.success(successMessage);
+ }
+
+ return data as T;
+ } catch (error) {
+ // Handle errors
+ if (showErrorToast) {
+ handleApiError(error, errorMessage);
+ }
+ throw error;
+ }
+ };
+
+ // Use retry with backoff if requested
+ if (retry) {
+ return retryWithBackoff(fetchFn, maxRetries);
+ }
+
+ return fetchFn();
+}
+
+/**
+ * GET request with error handling
+ */
+export function apiGet(url: string, options: FetchOptions = {}): Promise {
+ return fetchWithErrorHandling(url, {
+ method: "GET",
+ ...options,
+ });
+}
+
+/**
+ * POST request with error handling
+ */
+export function apiPost(
+ url: string,
+ data: any,
+ options: FetchOptions = {}
+): Promise {
+ return fetchWithErrorHandling(url, {
+ method: "POST",
+ body: JSON.stringify(data),
+ ...options,
+ });
+}
+
+/**
+ * PUT request with error handling
+ */
+export function apiPut(
+ url: string,
+ data: any,
+ options: FetchOptions = {}
+): Promise {
+ return fetchWithErrorHandling(url, {
+ method: "PUT",
+ body: JSON.stringify(data),
+ ...options,
+ });
+}
+
+/**
+ * DELETE request with error handling
+ */
+export function apiDelete(
+ url: string,
+ options: FetchOptions = {}
+): Promise {
+ return fetchWithErrorHandling(url, {
+ method: "DELETE",
+ ...options,
+ });
+}
+
+/**
+ * PATCH request with error handling
+ */
+export function apiPatch(
+ url: string,
+ data: any,
+ options: FetchOptions = {}
+): Promise {
+ return fetchWithErrorHandling(url, {
+ method: "PATCH",
+ body: JSON.stringify(data),
+ ...options,
+ });
+}
diff --git a/lib/auth.ts b/lib/auth.ts
new file mode 100644
index 0000000..f48076a
--- /dev/null
+++ b/lib/auth.ts
@@ -0,0 +1,27 @@
+import NextAuth, { NextAuthOptions } from 'next-auth';
+import GoogleProvider from 'next-auth/providers/google';
+import FacebookProvider from 'next-auth/providers/facebook';
+import TwitterProvider from 'next-auth/providers/twitter';
+
+export const { handlers, auth, signIn, signOut } = NextAuth({
+ providers: [
+ // Google Provider
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID as string,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
+ }),
+
+ // Facebook Provider
+ FacebookProvider({
+ clientId: process.env.FACEBOOK_CLIENT_ID as string,
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET as string,
+ }),
+
+ // Twitter Provider
+ TwitterProvider({
+ clientId: process.env.TWITTER_CLIENT_ID as string,
+ clientSecret: process.env.TWITTER_CLIENT_SECRET as string,
+ version: '2.0',
+ }),
+ ],
+});
diff --git a/lib/authLogger.ts b/lib/authLogger.ts
new file mode 100644
index 0000000..f2b37f6
--- /dev/null
+++ b/lib/authLogger.ts
@@ -0,0 +1,130 @@
+interface AuthEvent {
+ event: string;
+ userId?: string;
+ email?: string;
+ ip?: string;
+ userAgent?: string;
+ success: boolean;
+ error?: string;
+ timestamp: string;
+ metadata?: Record;
+}
+
+class AuthLogger {
+ private static instance: AuthLogger;
+
+ private constructor() {}
+
+ static getInstance(): AuthLogger {
+ if (!this.instance) {
+ this.instance = new AuthLogger();
+ }
+ return this.instance;
+ }
+
+ log(event: Omit) {
+ const authEvent: AuthEvent = {
+ ...event,
+ timestamp: new Date().toISOString(),
+ };
+
+ // In production, send to logging service (e.g., Winston, Datadog, etc.)
+ console.log('[AUTH EVENT]', JSON.stringify(authEvent));
+
+ // For critical security events, also log to stderr
+ if (this.isCriticalEvent(event.event) && !event.success) {
+ console.error('[CRITICAL AUTH EVENT]', JSON.stringify(authEvent));
+ }
+
+ // Store in database for audit trail (implement based on requirements)
+ this.storeInDatabase(authEvent);
+ }
+
+ private isCriticalEvent(eventType: string): boolean {
+ const criticalEvents = [
+ 'LOGIN_ATTEMPT',
+ 'PASSWORD_RESET_REQUEST',
+ 'ACCOUNT_LOCKED',
+ 'SUSPICIOUS_ACTIVITY'
+ ];
+ return criticalEvents.includes(eventType);
+ }
+
+ private async storeInDatabase(event: AuthEvent) {
+ // In production, implement database storage for audit trail
+ // For now, we'll just add it to the console log
+ if (process.env.NODE_ENV === 'production') {
+ // TODO: Implement database storage
+ // await prisma.authLog.create({ data: event });
+ }
+ }
+}
+
+export const authLogger = AuthLogger.getInstance();
+
+// Helper functions for common auth events
+export const logAuthEvent = {
+ loginAttempt: (email: string, success: boolean, ip?: string, error?: string, metadata?: Record) => {
+ authLogger.log({
+ event: 'LOGIN_ATTEMPT',
+ email,
+ success,
+ ip,
+ error,
+ metadata: {
+ ...metadata,
+ userType: success ? 'authenticated' : 'anonymous'
+ }
+ });
+ },
+
+ signupAttempt: (email: string, success: boolean, ip?: string, error?: string) => {
+ authLogger.log({
+ event: 'SIGNUP_ATTEMPT',
+ email,
+ success,
+ ip,
+ error
+ });
+ },
+
+ passwordResetRequest: (email: string, success: boolean, ip?: string, error?: string) => {
+ authLogger.log({
+ event: 'PASSWORD_RESET_REQUEST',
+ email,
+ success,
+ ip,
+ error
+ });
+ },
+
+ passwordResetComplete: (userId: string, success: boolean, ip?: string, error?: string) => {
+ authLogger.log({
+ event: 'PASSWORD_RESET_COMPLETE',
+ userId,
+ success,
+ ip,
+ error
+ });
+ },
+
+ emailVerification: (userId: string, email: string, success: boolean, error?: string) => {
+ authLogger.log({
+ event: 'EMAIL_VERIFICATION',
+ userId,
+ email,
+ success,
+ error
+ });
+ },
+
+ rateLimitExceeded: (ip: string, endpoint: string) => {
+ authLogger.log({
+ event: 'RATE_LIMIT_EXCEEDED',
+ success: false,
+ ip,
+ error: `Rate limit exceeded for ${endpoint}`,
+ metadata: { endpoint }
+ });
+ }
+};
\ No newline at end of file
diff --git a/lib/authOptions.ts b/lib/authOptions.ts
new file mode 100644
index 0000000..e2adee4
--- /dev/null
+++ b/lib/authOptions.ts
@@ -0,0 +1,95 @@
+import { NextAuthOptions } from 'next-auth';
+import bcrypt from 'bcryptjs';
+import GoogleProvider from 'next-auth/providers/google';
+import FacebookProvider from 'next-auth/providers/facebook';
+import TwitterProvider from 'next-auth/providers/twitter';
+import CredentialsProvider from 'next-auth/providers/credentials';
+import { PrismaAdapter } from '@next-auth/prisma-adapter';
+import prisma from '@/lib/prisma';
+
+export const authOptions: NextAuthOptions = {
+ secret: process.env.NEXTAUTH_SECRET,
+ adapter: PrismaAdapter(prisma),
+ providers: [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID!,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
+ }),
+ FacebookProvider({
+ clientId: process.env.FACEBOOK_CLIENT_ID!,
+ clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
+ }),
+ TwitterProvider({
+ clientId: process.env.TWITTER_CLIENT_ID!,
+ clientSecret: process.env.TWITTER_CLIENT_SECRET!,
+ version: '2.0',
+ }),
+ CredentialsProvider({
+ name: 'Credentials',
+ credentials: {
+ email: { label: 'Email', type: 'email' },
+ password: { label: 'Password', type: 'password' },
+ },
+ async authorize(credentials) {
+ const { email, password } = credentials!;
+ if (!email || !password) throw new Error('Email and password are required');
+
+ const user = await prisma.user.findUnique({ where: { email } });
+ if (!user || !user.verified) throw new Error('Invalid email or account not verified');
+
+ const isValidPassword = await bcrypt.compare(password, user.password || '');
+ if (!isValidPassword) throw new Error('Invalid credentials');
+
+ return { id: user.id, email: user.email, name: user.name };
+ },
+ }),
+ ],
+ callbacks: {
+ async jwt({ token, user }) {
+ if (user) {
+ token.id = user.id;
+ // Fetch the user from the database to include the role
+ const dbUser = await prisma.user.findUnique({
+ where: { id: user.id as string },
+ select: { email: true, name: true, role: true },
+ });
+ if (dbUser) {
+ token.email = dbUser.email;
+ token.name = dbUser.name;
+ token.role = dbUser.role; // Add the role to the token
+ }
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ if (token) {
+ session.user = {
+ id: token.id as string,
+ email: token.email as string,
+ name: token.name as string,
+ role: token.role as string, // Add the role to the session user object
+ };
+ }
+ return session;
+ },
+ },
+ session: {
+ strategy: 'jwt',
+ maxAge: 7 * 24 * 60 * 60, // 7 days - better UX for frequent users
+ },
+ pages: {
+ signIn: '/signin',
+ },
+ cookies: {
+ sessionToken: {
+ name: process.env.NODE_ENV === 'production' ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
+ options: {
+ httpOnly: true,
+ sameSite: 'lax',
+ path: '/',
+ secure: process.env.NODE_ENV === 'production',
+ },
+ },
+ },
+};
+
diff --git a/lib/cache.ts b/lib/cache.ts
new file mode 100644
index 0000000..8d54dd9
--- /dev/null
+++ b/lib/cache.ts
@@ -0,0 +1,45 @@
+type CacheEntry = {
+ data: T;
+ timestamp: number;
+};
+
+export class Cache {
+ private static instance: Cache;
+ private cache: Map>;
+ private readonly DEFAULT_TTL: number = 5 * 60 * 1000; // 5 minutes
+
+ private constructor() {
+ this.cache = new Map();
+ }
+
+ static getInstance(): Cache {
+ if (!Cache.instance) {
+ Cache.instance = new Cache();
+ }
+ return Cache.instance;
+ }
+
+ async getOrSet(key: string, fetchFn: () => Promise, ttl?: number): Promise {
+ const entry = this.cache.get(key);
+ const now = Date.now();
+ const cacheTTL = ttl || this.DEFAULT_TTL;
+
+ if (entry && now - entry.timestamp < cacheTTL) {
+ return entry.data;
+ }
+
+ const data = await fetchFn();
+ this.cache.set(key, { data, timestamp: now });
+ return data;
+ }
+
+ clear(): void {
+ this.cache.clear();
+ }
+
+ delete(key: string): void {
+ this.cache.delete(key);
+ }
+}
+
+export const cache = Cache.getInstance();
\ No newline at end of file
diff --git a/lib/cache/notificationsCache.ts b/lib/cache/notificationsCache.ts
new file mode 100644
index 0000000..e682981
--- /dev/null
+++ b/lib/cache/notificationsCache.ts
@@ -0,0 +1,148 @@
+import { Notification } from '@/types';
+
+// Cache keys
+export const NOTIFICATIONS_CACHE_KEY = 'user-notifications';
+export const UNREAD_COUNT_CACHE_KEY = 'unread-notifications-count';
+
+// Cache expiration time (in milliseconds)
+export const CACHE_EXPIRY = 5 * 60 * 1000; // 5 minutes
+
+interface CachedData {
+ data: T;
+ timestamp: number;
+}
+
+/**
+ * Save notifications to localStorage with timestamp
+ */
+export function cacheNotifications(notifications: Notification[]): void {
+ try {
+ const cachedData: CachedData = {
+ data: notifications,
+ timestamp: Date.now()
+ };
+ localStorage.setItem(NOTIFICATIONS_CACHE_KEY, JSON.stringify(cachedData));
+ } catch (error) {
+ console.error('Error caching notifications:', error);
+ }
+}
+
+/**
+ * Get cached notifications from localStorage
+ */
+export function getCachedNotifications(): Notification[] | null {
+ try {
+ const cachedDataStr = localStorage.getItem(NOTIFICATIONS_CACHE_KEY);
+ if (!cachedDataStr) return null;
+
+ const cachedData: CachedData = JSON.parse(cachedDataStr);
+
+ // Check if cache is expired
+ if (Date.now() - cachedData.timestamp > CACHE_EXPIRY) {
+ localStorage.removeItem(NOTIFICATIONS_CACHE_KEY);
+ return null;
+ }
+
+ return cachedData.data;
+ } catch (error) {
+ console.error('Error retrieving cached notifications:', error);
+ return null;
+ }
+}
+
+/**
+ * Save unread count to localStorage with timestamp
+ */
+export function cacheUnreadCount(count: number): void {
+ try {
+ const cachedData: CachedData = {
+ data: count,
+ timestamp: Date.now()
+ };
+ localStorage.setItem(UNREAD_COUNT_CACHE_KEY, JSON.stringify(cachedData));
+ } catch (error) {
+ console.error('Error caching unread count:', error);
+ }
+}
+
+/**
+ * Get cached unread count from localStorage
+ */
+export function getCachedUnreadCount(): number | null {
+ try {
+ const cachedDataStr = localStorage.getItem(UNREAD_COUNT_CACHE_KEY);
+ if (!cachedDataStr) return null;
+
+ const cachedData: CachedData = JSON.parse(cachedDataStr);
+
+ // Check if cache is expired
+ if (Date.now() - cachedData.timestamp > CACHE_EXPIRY) {
+ localStorage.removeItem(UNREAD_COUNT_CACHE_KEY);
+ return null;
+ }
+
+ return cachedData.data;
+ } catch (error) {
+ console.error('Error retrieving cached unread count:', error);
+ return null;
+ }
+}
+
+/**
+ * Update cached notifications when marking as read
+ */
+export function updateCachedNotificationsAsRead(ids: string[]): void {
+ try {
+ const notifications = getCachedNotifications();
+ if (!notifications) return;
+
+ const updatedNotifications = notifications.map(notification => {
+ if (ids.includes(notification.id)) {
+ return { ...notification, read: true };
+ }
+ return notification;
+ });
+
+ cacheNotifications(updatedNotifications);
+
+ // Update unread count
+ const unreadCount = updatedNotifications.filter(n => !n.read).length;
+ cacheUnreadCount(unreadCount);
+ } catch (error) {
+ console.error('Error updating cached notifications:', error);
+ }
+}
+
+/**
+ * Remove notifications from cache
+ */
+export function removeCachedNotifications(ids: string[]): void {
+ try {
+ const notifications = getCachedNotifications();
+ if (!notifications) return;
+
+ const updatedNotifications = notifications.filter(
+ notification => !ids.includes(notification.id)
+ );
+
+ cacheNotifications(updatedNotifications);
+
+ // Update unread count
+ const unreadCount = updatedNotifications.filter(n => !n.read).length;
+ cacheUnreadCount(unreadCount);
+ } catch (error) {
+ console.error('Error removing cached notifications:', error);
+ }
+}
+
+/**
+ * Clear all notification cache
+ */
+export function clearNotificationsCache(): void {
+ try {
+ localStorage.removeItem(NOTIFICATIONS_CACHE_KEY);
+ localStorage.removeItem(UNREAD_COUNT_CACHE_KEY);
+ } catch (error) {
+ console.error('Error clearing notifications cache:', error);
+ }
+}
diff --git a/lib/categoryUtils.ts b/lib/categoryUtils.ts
new file mode 100644
index 0000000..6ab083e
--- /dev/null
+++ b/lib/categoryUtils.ts
@@ -0,0 +1,114 @@
+import { navigation } from '@/constants';
+
+// Type definitions
+export interface CategoryIcon {
+ name: string;
+ icon: React.ComponentType;
+}
+
+export interface Subcategory {
+ name: string;
+ href: string;
+}
+
+interface NavigationItem {
+ name: string;
+ href: string;
+}
+
+// Updated interface based on observed structure
+interface NavigationCategory {
+ id: string;
+ name: string;
+ items: NavigationItem[];
+}
+
+
+// Get all main categories from the navigation structure
+export const getMainCategories = () => {
+ return navigation.categories.map((cat: NavigationCategory) => cat.name);
+};
+
+// Get subcategories for a specific category
+export const getSubcategoriesForCategory = (categoryName: string): Subcategory[] => {
+ const category = navigation.categories.find(
+ (cat: NavigationCategory) => cat.name.toLowerCase() === categoryName.toLowerCase()
+ );
+
+ if (!category || !category.items) return [];
+
+ return category.items
+ .filter((item: NavigationItem) => item.name !== 'Browse All') // Filter out "Browse All" items
+ .map((item: NavigationItem) => ({
+ name: item.name,
+ href: item.href
+ }));
+};
+
+// Map UI category names to database category names
+export const mapUiCategoryToDatabase = (uiCategory: string): string => {
+ // Since we're using a simplified and consistent category structure,
+ // the UI category names are the same as the database category names
+ const categoryMappings: Record = {
+ "Farm Accessories": "Farm Accessories",
+ "Farm Machinery": "Farm Machinery", // Now simplified to just "Farm Machinery"
+ "Tools": "Tools",
+ "Farm Animals": "Farm Animals",
+ "Plants": "Plants",
+ "Poultry": "Poultry",
+ "Cereals & Grains": "Cereals & Grains"
+ };
+
+ return categoryMappings[uiCategory] || uiCategory;
+};
+
+// Map database category names to UI category names
+export const mapDatabaseCategoryToUi = (dbCategory: string): string => {
+ // Since we're using a simplified and consistent category structure,
+ // the database category names are the same as the UI category names
+ const reverseCategoryMappings: Record = {
+ "Farm Accessories": "Farm Accessories",
+ "Farm Machinery": "Farm Machinery", // Now simplified to just "Farm Machinery"
+ "Tools": "Tools",
+ "Farm Animals": "Farm Animals",
+ "Plants": "Plants",
+ "Poultry": "Poultry",
+ "Cereals & Grains": "Cereals & Grains"
+ };
+
+ return reverseCategoryMappings[dbCategory] || dbCategory;
+};
+
+// Get all categories with their subcategories
+export const getAllCategoriesWithSubcategories = () => {
+ return navigation.categories.map((cat: NavigationCategory) => ({
+ category: cat.name,
+ subcategories: cat.items // Access items directly
+ .filter((item: NavigationItem) => item.name !== 'Browse All')
+ .map((item: NavigationItem) => ({
+ name: item.name,
+ href: item.href
+ }))
+ }));
+};
+
+// Find a subcategory in any category
+export const findSubcategoryInCategories = (subcategoryName: string) => {
+ for (const category of navigation.categories as NavigationCategory[]) {
+ // Access items directly
+ if (category.items) {
+ const subcategory = category.items.find(
+ (item: NavigationItem) => item.name.toLowerCase() === subcategoryName.toLowerCase()
+ );
+
+ if (subcategory) {
+ return {
+ category: category.name,
+ subcategory: subcategory.name
+ };
+ }
+ }
+ }
+
+ return null;
+};
diff --git a/lib/csrf.ts b/lib/csrf.ts
new file mode 100644
index 0000000..1861777
--- /dev/null
+++ b/lib/csrf.ts
@@ -0,0 +1,54 @@
+import { NextRequest } from 'next/server';
+import { randomBytes, createHash } from 'crypto';
+
+// CSRF token storage (use Redis in production)
+const csrfTokens = new Map