diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 842f50ca..f3922b14 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -9,12 +9,12 @@ passport.use( try { const user = await User.findOne( {email} ); if (!user) { - return done(null, false, { message: 'Email is invalid '}); + return done(null, false, { message: 'Invalid email or password' }); } const isMatch = await user.comparePassword(password); if (!isMatch) { - return done(null, false, { message: 'Invalid password' }); + return done(null, false, { message: 'Invalid email or password' }); } return done(null, { diff --git a/backend/data/discussions.json b/backend/data/discussions.json new file mode 100644 index 00000000..e474e8ee --- /dev/null +++ b/backend/data/discussions.json @@ -0,0 +1,30 @@ +{ + "discussions": [ + { + "id": "b3808b42-91e4-4d81-991a-07a96d7a549f", + "title": "Test discussion from shell", + "body": "This is a sufficiently long discussion body to pass validation and save.", + "category": "Help", + "tags": [ + "test", + "api" + ], + "authorId": "8d706914-51d9-40ca-981c-ac9dcb6a1881", + "authorName": "Guest", + "likes": [ + "00000000-0000-0000-0000-000000000000" + ], + "comments": [ + { + "id": "d1b1bcad-6989-49ff-a587-667f8426198e", + "text": "Yes", + "authorId": "00000000-0000-0000-0000-000000000000", + "authorName": "demo-user", + "createdAt": "2026-05-27T12:56:14.816Z" + } + ], + "createdAt": "2026-05-27T11:55:11.307Z", + "updatedAt": "2026-05-27T13:00:24.816Z" + } + ] +} \ No newline at end of file diff --git a/backend/data/users.json b/backend/data/users.json new file mode 100644 index 00000000..b4d801da --- /dev/null +++ b/backend/data/users.json @@ -0,0 +1,10 @@ +{ + "users": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "username": "demo-user", + "email": "user@example.com", + "password": "$2a$10$00000000000000000000000000000000000000000000000000000" + } + ] +} \ No newline at end of file diff --git a/backend/models/Discussion.js b/backend/models/Discussion.js new file mode 100644 index 00000000..94705f09 --- /dev/null +++ b/backend/models/Discussion.js @@ -0,0 +1,81 @@ +const mongoose = require('mongoose'); + +const CommentSchema = new mongoose.Schema( + { + text: { + type: String, + required: true, + trim: true, + maxlength: 1000, + }, + authorId: { + type: String, + required: true, + }, + authorName: { + type: String, + required: true, + trim: true, + }, + }, + { timestamps: true } +); + +const DiscussionSchema = new mongoose.Schema( + { + title: { + type: String, + required: true, + trim: true, + minlength: 4, + maxlength: 140, + }, + body: { + type: String, + required: true, + trim: true, + minlength: 20, + maxlength: 4000, + }, + category: { + type: String, + required: true, + trim: true, + maxlength: 60, + }, + tags: { + type: [ + { + type: String, + trim: true, + maxlength: 30, + }, + ], + default: [], + }, + authorId: { + type: String, + required: true, + }, + authorName: { + type: String, + required: true, + trim: true, + }, + likes: { + type: [ + { + type: String, + }, + ], + default: [], + }, + comments: { + type: [CommentSchema], + default: [], + }, + }, + { timestamps: true } +); + +module.exports = mongoose.model('Discussion', DiscussionSchema); \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..05736de8 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,15 +1,137 @@ const express = require("express"); const passport = require("passport"); +const fs = require("fs/promises"); +const path = require("path"); +const bcrypt = require("bcryptjs"); +const crypto = require("crypto"); const User = require("../models/User"); const { signupSchema, loginSchema } = require("../validators/authValidator"); const { validateRequest } = require("../validators/validationRequest"); const router = express.Router(); +const getUseMongoAuth = () => typeof global.mongooseConnected !== "undefined" ? global.mongooseConnected : Boolean(process.env.MONGO_URI); +const dataDir = path.join(__dirname, "..", "data"); +const usersFile = path.join(dataDir, "users.json"); +const usersLockFile = `${usersFile}.lock`; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const acquireUsersLock = async () => { + const retries = 80; + const delayMs = 25; + + for (let attempt = 0; attempt < retries; attempt += 1) { + try { + const handle = await fs.open(usersLockFile, "wx"); + return handle; + } catch (error) { + if (error.code !== "EEXIST") { + throw error; + } + + await sleep(delayMs); + } + } + + throw new Error("Could not acquire users file lock"); +}; + +const withUsersLock = async (callback) => { + const lockHandle = await acquireUsersLock(); + + try { + return await callback(); + } finally { + await lockHandle.close(); + await fs.unlink(usersLockFile).catch(() => {}); + } +}; + +const ensureUsersFileUnlocked = async () => { + await fs.mkdir(dataDir, { recursive: true }); + + try { + await fs.access(usersFile); + } catch { + await fs.writeFile(usersFile, JSON.stringify({ users: [] }, null, 2), "utf8"); + } +}; + +const readUsersUnlocked = async () => { + await ensureUsersFileUnlocked(); + const raw = await fs.readFile(usersFile, "utf8"); + + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed.users) ? parsed.users : []; + } catch (error) { + throw new Error(`Failed to parse users file at ${usersFile}: ${error.message}`); + } +}; + +const writeUsersUnlocked = async (users) => { + await ensureUsersFileUnlocked(); + const tempFile = `${usersFile}.${process.pid}.${Date.now()}.tmp`; + + // Write to a temporary file first, then atomically replace the target file. + await fs.writeFile(tempFile, JSON.stringify({ users }, null, 2), "utf8"); + await fs.rename(tempFile, usersFile); +}; + +const readUsersWithLock = async () => withUsersLock(readUsersUnlocked); + +const createUserIfNotExistsWithLock = async (newUser) => withUsersLock(async () => { + const users = await readUsersUnlocked(); + const existingUser = users.find((user) => user.email === newUser.email || user.username === newUser.username); + + if (existingUser) { + return false; + } + + users.push(newUser); + await writeUsersUnlocked(users); + return true; +}); + +const createSessionUser = (req, user) => { + req.session.authUser = { + id: user.id, + username: user.username, + email: user.email, + }; + + req.user = req.session.authUser; +}; + // Signup route router.post("/signup", validateRequest(signupSchema), async (req, res) => { const { username, email, password } = req.body; + if (!getUseMongoAuth()) { + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const newUser = { + id: crypto.randomUUID(), + username, + email, + password: hashedPassword, + }; + + const created = await createUserIfNotExistsWithLock(newUser); + + if (!created) { + return res.status(400).json({ message: 'User already exists' }); + } + + return res.status(201).json({ message: 'User created successfully' }); + } catch (err) { + return res.status(500).json({ message: 'Error creating user', error: err.message }); + } + } + try { const existingUser = await User.findOne({ $or: [{ email }, { username }], @@ -31,13 +153,73 @@ router.post("/signup", validateRequest(signupSchema), async (req, res) => { }); // Login route -router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => { - res.status(200).json( { message: 'Login successful', user: req.user } ); +router.post("/login", validateRequest(loginSchema), async (req, res, next) => { + if (!getUseMongoAuth()) { + try { + const { email, password } = req.body; + const users = await readUsersWithLock(); + const user = users.find((item) => item.email === email); + + if (!user) { + return res.status(401).json({ message: 'Invalid email or password' }); + } + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(401).json({ message: 'Invalid email or password' }); + } + + req.session.regenerate((err) => { + if (err) { + return res.status(500).json({ message: 'Login failed', error: err.message }); + } + + createSessionUser(req, user); + + return res.status(200).json({ + message: 'Login successful', + user: req.user, + }); + }); + } catch (err) { + return res.status(500).json({ message: 'Login failed', error: err.message }); + } + } + + return passport.authenticate('local', (err, user, info) => { + if (err) { + return next(err); + } + + if (!user) { + return res.status(401).json({ message: info?.message || 'Invalid credentials' }); + } + + req.logIn(user, (loginErr) => { + if (loginErr) { + return next(loginErr); + } + + return res.status(200).json({ message: 'Login successful', user: req.user }); + }); + })(req, res, next); }); // Logout route router.get("/logout", (req, res) => { + if (!getUseMongoAuth()) { + req.session.destroy((err) => { + if (err) { + return res.status(500).json({ message: 'Logout failed', error: err.message }); + } + + return res.status(200).json({ message: 'Logged out successfully' }); + }); + + return; + } + req.logout((err) => { if (err) diff --git a/backend/routes/discussions.js b/backend/routes/discussions.js new file mode 100644 index 00000000..8c981697 --- /dev/null +++ b/backend/routes/discussions.js @@ -0,0 +1,351 @@ +const express = require('express'); +const fs = require('fs/promises'); +const path = require('path'); +const crypto = require('crypto'); +const { discussionSchema, commentSchema } = require('../validators/discussionValidator'); +const { validateRequest } = require('../validators/validationRequest'); + +const router = express.Router(); + +const dataDir = path.join(__dirname, '..', 'data'); +const dataFile = path.join(dataDir, 'discussions.json'); +let storeMutex = Promise.resolve(); + +const ensureDataFile = async () => { + await fs.mkdir(dataDir, { recursive: true }); + + try { + await fs.access(dataFile); + } catch { + await fs.writeFile(dataFile, JSON.stringify({ discussions: [] }, null, 2), 'utf8'); + } +}; + +const readStore = async () => { + await ensureDataFile(); + const raw = await fs.readFile(dataFile, 'utf8'); + + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed.discussions) ? parsed.discussions : []; + } catch (error) { + throw new Error(`Invalid discussions store JSON in ${dataFile}: ${error.message}`); + } +}; + +const writeStore = async (discussions) => { + await ensureDataFile(); + const tempFile = `${dataFile}.${process.pid}.${Date.now()}.tmp`; + + // Write to a temporary file first, then atomically replace the target file. + await fs.writeFile(tempFile, JSON.stringify({ discussions }, null, 2), 'utf8'); + await fs.rename(tempFile, dataFile); +}; + +const updateStore = async (updater) => { + let releaseLock; + const waitForTurn = storeMutex; + storeMutex = new Promise((resolve) => { + releaseLock = resolve; + }); + + await waitForTurn; + + try { + const discussions = await readStore(); + const result = await updater(discussions); + const nextDiscussions = Array.isArray(result?.discussions) ? result.discussions : discussions; + + if (result?.persist !== false) { + await writeStore(nextDiscussions); + } + + return { ...result, discussions: nextDiscussions }; + } finally { + releaseLock(); + } +}; + +const normalizeTags = (tags = []) => + tags + .map((tag) => String(tag).trim()) + .filter(Boolean) + .map((tag) => tag.replace(/^#?/, '').toLowerCase()); + +const requireAuth = (req, res, next) => { + if (!req.user) { + return res.status(401).json({ message: 'Authentication required' }); + } + + return next(); +}; + +const getAuthenticatedIdentity = (req) => ({ + id: req.user._id?.toString?.() || req.user.id || req.user.email || 'user', + name: req.user.username || req.user.email || 'Member', +}); + +const getCommunityIdentity = (req) => { + if (req.user) { + return { + id: req.user._id?.toString?.() || req.user.id || req.user.email || 'user', + name: req.user.username || req.user.email || 'Member', + }; + } + + if (!req.session.communityUserId) { + req.session.communityUserId = crypto.randomUUID(); + } + + if (!req.session.communityUserName) { + req.session.communityUserName = 'Guest'; + } + + return { + id: req.session.communityUserId, + name: req.session.communityUserName, + }; +}; + +const toPublicDiscussion = (discussion, currentUserId) => ({ + id: discussion.id, + title: discussion.title, + body: discussion.body, + category: discussion.category, + tags: discussion.tags, + author: { + id: discussion.authorId, + name: discussion.authorName, + }, + likesCount: discussion.likes.length, + commentsCount: discussion.comments.length, + likedByCurrentUser: currentUserId ? discussion.likes.includes(currentUserId) : false, + canEdit: currentUserId ? String(discussion.authorId) === String(currentUserId) : false, + comments: discussion.comments.map((comment) => ({ + id: comment.id, + text: comment.text, + author: { + id: comment.authorId, + name: comment.authorName, + }, + createdAt: comment.createdAt, + })), + createdAt: discussion.createdAt, + updatedAt: discussion.updatedAt, +}); + +router.get('/', async (req, res) => { + try { + const discussions = await readStore(); + const { search = '', category = '', tag = '', sort = 'recent' } = req.query; + const currentUserId = req.user ? getCommunityIdentity(req).id : (req.session?.communityUserId || null); + + let filtered = discussions; + + if (category) { + filtered = filtered.filter((discussion) => discussion.category === category); + } + + if (tag) { + const searchTag = String(tag).toLowerCase(); + filtered = filtered.filter((discussion) => discussion.tags.includes(searchTag)); + } + + if (search) { + const term = String(search).toLowerCase(); + filtered = filtered.filter((discussion) => { + const haystack = [discussion.title, discussion.body, discussion.category, discussion.tags.join(' ')].join(' ').toLowerCase(); + return haystack.includes(term); + }); + } + + filtered = filtered.sort((left, right) => { + if (sort === 'trending') { + const leftScore = left.likes.length * 2 + left.comments.length; + const rightScore = right.likes.length * 2 + right.comments.length; + + if (rightScore !== leftScore) { + return rightScore - leftScore; + } + } + + return new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime(); + }); + + return res.status(200).json({ + items: filtered.map((discussion) => toPublicDiscussion(discussion, currentUserId)), + categories: Array.from(new Set(filtered.map((discussion) => discussion.category))).sort(), + isAuthenticated: !!req.user, + }); + } catch (error) { + return res.status(500).json({ message: 'Unable to load discussions', error: error.message }); + } +}); + +router.post('/', requireAuth, validateRequest(discussionSchema), async (req, res) => { + try { + const identity = getAuthenticatedIdentity(req); + const now = new Date().toISOString(); + + const discussion = { + id: crypto.randomUUID(), + title: String(req.body.title).trim(), + body: String(req.body.body).trim(), + category: String(req.body.category).trim(), + tags: normalizeTags(req.body.tags || []), + authorId: identity.id, + authorName: identity.name, + likes: [], + comments: [], + createdAt: now, + updatedAt: now, + }; + + await updateStore((discussions) => { + discussions.unshift(discussion); + return { discussions }; + }); + + return res.status(201).json({ + message: 'Discussion created successfully', + discussion: toPublicDiscussion(discussion, identity.id), + }); + } catch (error) { + return res.status(500).json({ message: 'Unable to create discussion', error: error.message }); + } +}); + +router.put('/:id', requireAuth, validateRequest(discussionSchema), async (req, res) => { + try { + const identity = getAuthenticatedIdentity(req); + const result = await updateStore((discussions) => { + const discussion = discussions.find((item) => item.id === req.params.id); + + if (!discussion) { + return { status: 404, message: 'Discussion not found', persist: false }; + } + + if (String(discussion.authorId) !== String(identity.id)) { + return { status: 403, message: 'You can only edit your own discussion', persist: false }; + } + + discussion.title = String(req.body.title).trim(); + discussion.body = String(req.body.body).trim(); + discussion.category = String(req.body.category).trim(); + discussion.tags = normalizeTags(req.body.tags || []); + discussion.updatedAt = new Date().toISOString(); + + return { discussions, discussion }; + }); + + if (result.status) { + return res.status(result.status).json({ message: result.message }); + } + + return res.status(200).json({ + message: 'Discussion updated successfully', + discussion: toPublicDiscussion(result.discussion, identity.id), + }); + } catch (error) { + return res.status(500).json({ message: 'Unable to update discussion', error: error.message }); + } +}); + +router.delete('/:id', requireAuth, async (req, res) => { + try { + const identity = getAuthenticatedIdentity(req); + const result = await updateStore((discussions) => { + const discussionIndex = discussions.findIndex((item) => item.id === req.params.id); + + if (discussionIndex === -1) { + return { status: 404, message: 'Discussion not found', persist: false }; + } + + if (String(discussions[discussionIndex].authorId) !== String(identity.id)) { + return { status: 403, message: 'You can only delete your own discussion', persist: false }; + } + + discussions.splice(discussionIndex, 1); + return { discussions }; + }); + + if (result.status) { + return res.status(result.status).json({ message: result.message }); + } + + return res.status(200).json({ message: 'Discussion deleted successfully' }); + } catch (error) { + return res.status(500).json({ message: 'Unable to delete discussion', error: error.message }); + } +}); + +router.post('/:id/likes', requireAuth, async (req, res) => { + try { + const identity = getAuthenticatedIdentity(req); + const result = await updateStore((discussions) => { + const discussion = discussions.find((item) => item.id === req.params.id); + + if (!discussion) { + return { status: 404, message: 'Discussion not found', persist: false }; + } + + const alreadyLiked = discussion.likes.includes(identity.id); + discussion.likes = alreadyLiked + ? discussion.likes.filter((likeId) => String(likeId) !== String(identity.id)) + : [...discussion.likes, identity.id]; + discussion.updatedAt = new Date().toISOString(); + + return { discussions, discussion, alreadyLiked }; + }); + + if (result.status) { + return res.status(result.status).json({ message: result.message }); + } + + return res.status(200).json({ + message: result.alreadyLiked ? 'Discussion unliked' : 'Discussion liked', + discussion: toPublicDiscussion(result.discussion, identity.id), + }); + } catch (error) { + return res.status(500).json({ message: 'Unable to update like state', error: error.message }); + } +}); + +router.post('/:id/comments', requireAuth, validateRequest(commentSchema), async (req, res) => { + try { + const commentText = req.body.text; + + const identity = getAuthenticatedIdentity(req); + const result = await updateStore((discussions) => { + const discussion = discussions.find((item) => item.id === req.params.id); + + if (!discussion) { + return { status: 404, message: 'Discussion not found', persist: false }; + } + + discussion.comments.push({ + id: crypto.randomUUID(), + text: commentText, + authorId: identity.id, + authorName: identity.name, + createdAt: new Date().toISOString(), + }); + discussion.updatedAt = new Date().toISOString(); + + return { discussions, discussion }; + }); + + if (result.status) { + return res.status(result.status).json({ message: result.message }); + } + + return res.status(201).json({ + message: 'Comment added successfully', + discussion: toPublicDiscussion(result.discussion, identity.id), + }); + } catch (error) { + return res.status(500).json({ message: 'Unable to add comment', error: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 48d6ccfb..3b9883fa 100644 --- a/backend/server.js +++ b/backend/server.js @@ -36,18 +36,48 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); +global.mongooseConnected = false; + +app.use((req, res, next) => { + if (!global.mongooseConnected) { + if (req.session?.authUser) { + req.user = req.session.authUser; + } + } + + next(); +}); + // Routes const authRoutes = require('./routes/auth'); +const discussionRoutes = require('./routes/discussions'); app.use('/api/auth', authRoutes); +app.use('/api/discussions', discussionRoutes); -// Connect to MongoDB -mongoose.connect(process.env.MONGO_URI, {}).then(() => { - logger.info('Connected to MongoDB'); +const startServer = () => { + const port = process.env.PORT || 5000; - const PORT = process.env.PORT || 5000; - app.listen(PORT, () => { - logger.info(`Server running on port ${PORT}`); + app.listen(port, () => { + logger.info(`Server running on port ${port}`); }); -}).catch((err) => { - logger.error('MongoDB connection error', err); -}); +}; + +// Connect to MongoDB when available, but do not block community discussions if it is not. +if (process.env.MONGO_URI) { + mongoose.connect(process.env.MONGO_URI, {}) + .then(() => { + logger.info('Connected to MongoDB'); + global.mongooseConnected = true; + startServer(); + }) + .catch((err) => { + logger.error('MongoDB connection error', err); + logger.warn('Starting without MongoDB; falling back to JSON file-backed authentication'); + global.mongooseConnected = false; + startServer(); + }); +} else { + logger.warn('MONGO_URI is not set; starting without MongoDB'); + global.mongooseConnected = false; + startServer(); +} diff --git a/backend/validators/discussionValidator.js b/backend/validators/discussionValidator.js new file mode 100644 index 00000000..2beb637a --- /dev/null +++ b/backend/validators/discussionValidator.js @@ -0,0 +1,30 @@ +const { z } = require('zod'); + +const discussionSchema = z.object({ + title: z + .string() + .trim() + .min(4, 'Title must be at least 4 characters long') + .max(140, 'Title must be at most 140 characters long'), + body: z + .string() + .trim() + .min(20, 'Post body must be at least 20 characters long') + .max(4000, 'Post body must be at most 4000 characters long'), + category: z + .string() + .trim() + .min(2, 'Category is required') + .max(60, 'Category must be at most 60 characters long'), + tags: z.array(z.string().trim().min(1).max(30)).max(6).default([]), +}); + +const commentSchema = z.object({ + text: z + .string() + .trim() + .min(2, 'Comment must be at least 2 characters long') + .max(1000, 'Comment must be at most 1000 characters long'), +}); + +module.exports = { discussionSchema, commentSchema }; \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8eafb448..f7924f31 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,7 @@ function App() { {!isFullscreen && } -
+
diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 874ef7e7..7fc2f4e2 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -8,7 +8,9 @@ import Login from "../pages/Login/Login.tsx"; import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; import Home from "../pages/Home/Home.tsx"; import Activity from "../pages/Activity.tsx"; +import Community from "../pages/Community/Community.tsx"; import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; // ✅ Updated import path to match your new folder structure +import TermsOfService from "../pages/Terms/TermsOfService.tsx"; const Router = () => { return ( @@ -22,9 +24,12 @@ const Router = () => { } /> } /> } /> + } /> + } /> {/* Privacy Policy page route */} } /> + } /> ); }; diff --git a/src/components/AuthShell.tsx b/src/components/AuthShell.tsx new file mode 100644 index 00000000..afe9cd3f --- /dev/null +++ b/src/components/AuthShell.tsx @@ -0,0 +1,101 @@ +import React, { ReactNode } from "react"; + +type Highlight = { + title: string; + description: string; +}; + +interface AuthShellProps { + mode: "dark" | "light"; + badge: string; + title: string; + subtitle: string; + highlights: Highlight[]; + children: ReactNode; + footer: ReactNode; +} + +const AuthShell: React.FC = ({ mode, badge, title, subtitle, highlights, children, footer }) => { + const surfaceClass = + mode === "dark" + ? "bg-slate-950 text-white" + : "bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.92),_rgba(226,232,240,0.92))] text-slate-900"; + + const panelClass = + mode === "dark" + ? "border-white/10 bg-white/10 text-white shadow-[0_24px_80px_rgba(15,23,42,0.45)]" + : "border-slate-200/80 bg-white/85 text-slate-900 shadow-[0_24px_80px_rgba(15,23,42,0.12)]"; + + return ( +
+
+
+
+
+
+
+ +
+ + +
+
+ {children} +
{footer}
+
+
+
+
+ ); +}; + +export default AuthShell; \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 3ad55184..5850e406 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,189 +1,259 @@ -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { Link } from 'react-router-dom'; +import toast from 'react-hot-toast'; import { + FaArrowRight, + FaArrowUp, + FaBookOpen, + FaChartLine, + FaDiscord, + FaExternalLinkAlt, FaGithub, FaTwitter, - FaDiscord, - FaArrowRight, - FaEnvelope, - FaInfoCircle, - FaShieldAlt, // ✅ Added Privacy Icon + FaUsers, } from 'react-icons/fa'; +import { ThemeContext } from '../context/ThemeContext'; function Footer() { + const themeContext = useContext(ThemeContext); const [email, setEmail] = useState(''); + const mode = themeContext?.mode ?? 'light'; + + const isDark = mode === 'dark'; + const footerClass = isDark + ? 'relative w-full overflow-hidden border-t border-white/10 bg-[#1e2130] text-slate-100 shadow-[0_-24px_80px_rgba(2,6,23,0.45)]' + : 'relative w-full overflow-hidden border-t border-slate-200 bg-gradient-to-b from-white via-slate-50 to-slate-100 text-slate-700 shadow-[0_-16px_50px_rgba(15,23,42,0.08)]'; + const outerGlowClass = isDark + ? 'absolute inset-0 pointer-events-none bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.16),transparent_30%),radial-gradient(circle_at_top_right,rgba(168,85,247,0.12),transparent_24%),linear-gradient(180deg,rgba(15,23,42,0.98),rgba(2,6,23,1))]' + : 'absolute inset-0 pointer-events-none bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.12),transparent_28%),radial-gradient(circle_at_top_right,rgba(14,165,233,0.08),transparent_22%),linear-gradient(180deg,rgba(248,250,252,0.96),rgba(226,232,240,0.88))]'; + const panelClass = isDark + ? 'rounded-3xl border border-white/10 bg-white/5 p-6 shadow-2xl shadow-black/30 backdrop-blur-xl sm:p-8 lg:p-10' + : 'rounded-3xl border border-slate-200/80 bg-white/80 p-6 shadow-[0_24px_60px_rgba(15,23,42,0.08)] backdrop-blur-xl sm:p-8 lg:p-10'; + const titleClass = isDark ? 'text-slate-400' : 'text-slate-500'; + const bodyClass = isDark ? 'text-slate-300' : 'text-slate-600'; + const cardChipClass = isDark + ? 'inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold text-slate-200 transition-all duration-300 hover:-translate-y-0.5 hover:border-blue-400/50 hover:bg-white/10 hover:text-white' + : 'inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-4 py-2 text-xs font-semibold text-slate-700 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:border-blue-200 hover:bg-blue-50 hover:text-slate-900'; + const secondaryChipClass = isDark + ? 'inline-flex items-center gap-2 rounded-full border border-white/10 bg-slate-900/60 px-4 py-2 text-xs font-semibold text-slate-200 transition-all duration-300 hover:-translate-y-0.5 hover:border-white/20 hover:bg-slate-800/80 hover:text-white' + : 'inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-4 py-2 text-xs font-semibold text-slate-700 transition-all duration-300 hover:-translate-y-0.5 hover:border-slate-300 hover:bg-slate-100 hover:text-slate-900'; + const iconShellClass = isDark + ? 'flex h-8 w-8 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-slate-300 transition-all duration-300 group-hover:border-blue-400/40 group-hover:bg-blue-500/10 group-hover:text-blue-300' + : 'flex h-8 w-8 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 shadow-sm transition-all duration-300 group-hover:border-blue-200 group-hover:bg-blue-50 group-hover:text-blue-600'; + const dividerClass = isDark + ? 'my-8 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent' + : 'my-8 h-px bg-gradient-to-r from-transparent via-slate-200 to-transparent'; const handleSubscribe = (e: React.FormEvent) => { e.preventDefault(); - // Replace with API call - alert('Thank you for subscribing!'); + const normalizedEmail = email.trim(); + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!normalizedEmail) { + toast.error('Enter an email address to subscribe.'); + return; + } + if (!emailPattern.test(normalizedEmail)) { + toast.error('Enter a valid email address.'); + return; + } + + toast.success('Subscription saved. Welcome aboard.'); setEmail(''); }; - return ( -