diff --git a/Procfile b/Procfile deleted file mode 100644 index dc14c0b63..000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: npm start --prefix backend \ No newline at end of file diff --git a/README.md b/README.md index 31466b54c..9cb961fa6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,30 @@ -# Final Project +# Frontend part of Final Project -Replace this readme with your own information about your project. +The ADHD Community Project is a web application designed to support and empower individuals with ADHD by providing a safe space and understanding community. Users can sign up as either a "Listener" or a "Seeker" to offer or receive support, respectively. The application includes features such as event listings, community guidelines, and user profiles. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +## About the project -## The problem +Developed using React for the frontend and Node.js with Express for the backend, the application integrates MongoDB for data storage and utilizes Passport.js with JWT for secure authentication. Key features include user authentication with roles (Listener and Seeker), event listings, and user profile management -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +Future enhancements include a live chat feature to facilitate real-time communication within the community + +## Challenges + +Handling Modal Popups: + +Challenge: Managing the state and functionality of modal popups for login and signup forms. +Solution: Created a custom modal context using React's Context API to manage the display and state of modals. This centralized approach streamlined modal management, making it easier to maintain and extend the functionality. + +API Integration: + +Challenge: Integrating the frontend with the backend API and ensuring seamless data flow. +Solution: Developed a consistent API service layer to handle requests and responses between the frontend and backend. Implemented error handling and loading states to enhance user experience during data fetching operations. + +Profile Management: + +Challenge: Allowing users to view and update their profiles securely. +Solution: Implemented profile fetching and updating functionalities with secure token-based authentication. Ensured data validation and used React forms to allow users to edit their profiles, providing real-time feedback and updates. ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. \ No newline at end of file +https://adhd-connect.netlify.app \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index d1438c910..2ef8dede2 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,8 +1,19 @@ # Backend part of Final Project -This project includes the packages and babel setup for an express server, and is just meant to make things a little simpler to get up and running with. +The backend serves as the core logic and data management system for the application. It provides the necessary endpoints to handle user authentication, registration and profile management. -## Getting Started +## Process -1. Install the required dependencies using `npm install`. -2. Start the development server using `npm run dev`. \ No newline at end of file +Technologies Used: + +Node.js +Express.js +MongoDB +Mongoose +JWT +Passport.js +Multer + +## LINK + +https://project-final-rmn2.onrender.com diff --git a/backend/config/passport.js b/backend/config/passport.js new file mode 100644 index 000000000..aa3d25004 --- /dev/null +++ b/backend/config/passport.js @@ -0,0 +1,41 @@ +import passport from "passport"; +import { Strategy as LocalStrategy } from "passport-local"; +import User from "../models/User"; + +passport.use( + new LocalStrategy( + { usernameField: "username" }, + async (username, password, done) => { + try { + const user = await User.findOne({ username }); + if (!user) { + return done(null, false, { message: "Incorrect username." }); + } + + const isMatch = await user.comparePassword(password); + if (!isMatch) { + return done(null, false, { message: "Incorrect password." }); + } + + return done(null, user); + } catch (error) { + return done(error); + } + } + ) +); + +passport.serializeUser((user, done) => { + done(null, user.id); +}); + +passport.deserializeUser(async (id, done) => { + try { + const user = await User.findById(id); + done(null, user); + } catch (error) { + done(error); + } +}); + +export default passport; diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 000000000..1c0844e21 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,29 @@ +import mongoose from "mongoose"; +import bcrypt from "bcryptjs"; + +const userSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true, minlength: 3 }, + password: { type: String, required: true, minlength: 6 }, + role: { type: String, enum: ["Listener", "Seeker"], required: true }, + name: { type: String }, + bio: { type: String }, + hobby: { type: String }, +}); + +// Middleware to hash the password before saving +userSchema.pre("save", async function (next) { + if (!this.isModified("password")) { + return next(); + } + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + next(); +}); + +// Method to compare the entered password with the hashed password +userSchema.methods.comparePassword = function (enteredPassword) { + return bcrypt.compare(enteredPassword, this.password); +}; + +const User = mongoose.model("User", userSchema); +export default User; diff --git a/backend/package.json b/backend/package.json index 08f29f244..654e07e77 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,15 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcryptjs": "^2.4.3", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.17.3", + "express-list-endpoints": "^7.1.0", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.4.0", - "nodemon": "^3.0.1" + "nodemon": "^3.0.1", + "passport": "^0.7.0", + "passport-local": "^1.0.0" } -} \ No newline at end of file +} diff --git a/backend/routes/authProfile.js b/backend/routes/authProfile.js new file mode 100644 index 000000000..d1493c1c9 --- /dev/null +++ b/backend/routes/authProfile.js @@ -0,0 +1,138 @@ +import express from "express"; +import jwt from "jsonwebtoken"; +import passport from "../config/passport"; +import User from "../models/User"; + +const router = express.Router(); + +// Function to generate JWT access token +const generateAccessToken = (userId) => { + try { + return jwt.sign({ userId }, process.env.JWT_SECRET, { + expiresIn: "24h", + }); + } catch (error) { + console.error("Error generating token:", error); + throw new Error("Token generation failed"); + } +}; + +// Middleware to athenticate the token +const authenticateToken = async (req, res, next) => { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (token == null) { + return res.status(401).json({ error: "Token is missing" }); + } + + jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ error: "Token is invalid" }); + } + req.user = user; + next(); + }); +}; + +// Route to register a new user +router.post("/users", async (req, res) => { + const { username, password, role } = req.body; + if (password.length < 6) { + return res + .status(400) + .json({ error: "Password must be at least 6 characters long" }); + } + + try { + const newUser = new User({ username, password, role }); + await newUser.save(); + const accessToken = generateAccessToken(newUser._id); + res.status(201).json({ id: newUser._id, accessToken }); + } catch (error) { + console.error("Error registering user:", error); + if (error.code === 11000) { + res.status(400).json({ error: "Username already exists" }); + } else { + res.status(500).json({ error: "Something went wrong" }); + } + } +}); + +// Route to log in a user +router.post("/sessions", async (req, res, next) => { + passport.authenticate("local", { session: false }, (err, user, info) => { + if (err || !user) { + console.log("Error during authentication:", err); + return res + .status(400) + .json({ error: info ? info.message : "Login failed" }); + } + req.login(user, { session: false }, async (err) => { + if (err) { + return res.send(err); + } + const token = generateAccessToken(user._id); + const userWithoutAccessToken = user.toObject(); + delete userWithoutAccessToken.accessToken; + return res.json({ user: userWithoutAccessToken, token }); + }); + })(req, res, next); +}); + + +// Route for the current session (logged-in user) +router.get("/session", authenticateToken, async (req, res) => { + try { + const user = req.user; + const loggedInUser = await User.findById(user.userId).select("-password"); + if (!loggedInUser) { + return res.status(404).json({ error: "User not found" }); + } + res.json(loggedInUser); + } catch (error) { + console.error("Error fetching logged-in user:", error); + res.status(500).json({ error: "Failed to fetch logged-in user" }); + } +}); + +// Route to fetch profile +router.get("/profile", authenticateToken, async (req, res) => { + console.log("GET /profile called"); + try { + const user = await User.findById(req.user.userId).select("-password"); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + res.json(user); + } catch (error) { + console.error("Error fetching profile:", error); + res.status(500).json({ error: "Failed to fetch profile" }); + } +}); + +//Route to update user profile +router.put("/profile", authenticateToken, async (req, res) => { + const { name, bio, hobby } = req.body; + const updatedData = { name, bio, hobby }; + + try { + const user = await User.findByIdAndUpdate(req.user.userId, updatedData, { + new: true, + }); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + res.json(user); + } catch (error) { + console.error("Error updating profile:", error); + res.status(500).json({ error: "Failed to update profile" }); + } +}); + +// Authenticated endpoint +router.get("/secrets", authenticateToken, (req, res) => { + res.json({ secret: "This is secret content" }); +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index 2c00e4802..e4e2d1846 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,31 +1,39 @@ -import express from "express"; import cors from "cors"; -import mongoose from 'mongoose' +import dotenv from "dotenv"; +import express from "express"; +import expressListEndpoints from "express-list-endpoints"; +import mongoose from "mongoose"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/flowershop" -mongoose.connect(mongoUrl) -mongoose.Promise = Promise +import passport from "./config/passport"; +import authProfileRoutes from "./routes/authProfile"; +dotenv.config(); +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-final"; +mongoose.connect(mongoUrl); +mongoose.Promise = global.Promise; -// Defines the port the app will run on. Defaults to 8080, but can be overridden -// when starting the server. Example command to overwrite PORT env variable value: -// PORT=9000 npm start -const port = process.env.PORT || 8080; +const port = process.env.PORT || 9000; const app = express(); -// Add middlewares to enable cors and json body parsing -app.use(cors()); +// middlewares to enable cors and json body parsing +const corsOptions = { + origin: "https://adhd-connect.netlify.app", + credentials: true, +}; + +app.use(cors(corsOptions)); app.use(express.json()); +app.use(passport.initialize()); + +app.use("/api", authProfileRoutes); -// Start defining your routes here -// http://localhost:8080/ app.get("/", (req, res) => { - res.send("Hello Technigo!"); + const endpoints = expressListEndpoints(app); + res.json(endpoints); }); - // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); -}); \ No newline at end of file +}); diff --git a/frontend/README.md b/frontend/README.md index 5cdb1d9cf..5108a6476 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,8 +1,37 @@ # Frontend part of Final Project -This boilerplate is designed to give you a head start in your React projects, with a focus on understanding the structure and components. As a student of Technigo, you'll find this guide helpful in navigating and utilizing the repository. +This document provides an overview of the frontend development process, tools used, and key features implemented for the ADHD Community project. -## Getting Started +# Tools and Technologies -1. Install the required dependencies using `npm install`. -2. Start the development server using `npm run dev`. \ No newline at end of file +React: Building the user interface. +Tailwind CSS: Styling the application. +Axios: HTTP requests to the backend. +React Router: Navigation between pages. +Context API: Managing global state. + +# Key Components + +1. # AuthForm.jsx + Handles both login and signup functionalities within a modal. + Switches between login and signup modes. + Validates user input and displays error messages. + Integrates with the backend for authentication. + Shows a loading animation during network requests. +2. # ModalContext.jsx + Provides context for showing and hiding modals. +3. # EventCard.jsx + Displays event details with a flip animation for more information. + Responsive design for different screen sizes. +4. #ProfilePage.jsx + Displays and updates user profile information. + Allows users to log out. +5. # Menu.jsx and Footer.jsx + Navigation menu and footer for the application. +6. # AboutUsPage.jsx and FindOutMorePage.jsx + About Us page with information on the community's mission, values, and team. + Find Out More page with facts about ADHD and roles in the community. + +# Conclusion + +This project integrates various frontend technologies to create a responsive, user-friendly web application. Using React, Tailwind CSS, and context API, it offers seamless user experiences and robust authentication mechanisms. diff --git a/frontend/index.html b/frontend/index.html index 664410b5b..01602a047 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Technigo React Vite Boiler Plate + ADHD Connect
diff --git a/frontend/netlify.toml b/frontend/netlify.toml new file mode 100644 index 000000000..f3634fd06 --- /dev/null +++ b/frontend/netlify.toml @@ -0,0 +1,9 @@ +[build] + base = "frontend" + publish = "dist" + command = "npm run build" + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 41a2a3164..5c30886ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,17 +10,22 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.2", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", + "autoprefixer": "^10.4.19", "eslint": "^8.45.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", "vite": "^4.4.5" } -} \ No newline at end of file +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/icons/burger-menu.svg b/frontend/public/icons/burger-menu.svg new file mode 100644 index 000000000..9ddd45b6a --- /dev/null +++ b/frontend/public/icons/burger-menu.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/public/icons/gear.svg b/frontend/public/icons/gear.svg new file mode 100644 index 000000000..19c27265a --- /dev/null +++ b/frontend/public/icons/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons/hamburger.png b/frontend/public/icons/hamburger.png new file mode 100644 index 000000000..f4ee40792 Binary files /dev/null and b/frontend/public/icons/hamburger.png differ diff --git a/frontend/public/icons/profile.png b/frontend/public/icons/profile.png new file mode 100644 index 000000000..389355efe Binary files /dev/null and b/frontend/public/icons/profile.png differ diff --git a/frontend/public/images/Villads.jpg b/frontend/public/images/Villads.jpg new file mode 100644 index 000000000..ad12991ca Binary files /dev/null and b/frontend/public/images/Villads.jpg differ diff --git a/frontend/public/images/alice.jpg b/frontend/public/images/alice.jpg new file mode 100644 index 000000000..1a8bdc314 Binary files /dev/null and b/frontend/public/images/alice.jpg differ diff --git a/frontend/public/images/event-hero.jpg b/frontend/public/images/event-hero.jpg new file mode 100644 index 000000000..c2f75c63f Binary files /dev/null and b/frontend/public/images/event-hero.jpg differ diff --git a/frontend/public/images/guidelines.jpg b/frontend/public/images/guidelines.jpg new file mode 100644 index 000000000..8483a9ddd Binary files /dev/null and b/frontend/public/images/guidelines.jpg differ diff --git a/frontend/public/images/hero-image.jpg b/frontend/public/images/hero-image.jpg new file mode 100644 index 000000000..2cc409f18 Binary files /dev/null and b/frontend/public/images/hero-image.jpg differ diff --git a/frontend/public/images/jake.jpg b/frontend/public/images/jake.jpg new file mode 100644 index 000000000..423e15e74 Binary files /dev/null and b/frontend/public/images/jake.jpg differ diff --git a/frontend/public/images/john.jpg b/frontend/public/images/john.jpg new file mode 100644 index 000000000..6546c4ebb Binary files /dev/null and b/frontend/public/images/john.jpg differ diff --git a/frontend/public/images/logo.png b/frontend/public/images/logo.png new file mode 100644 index 000000000..e5b69c177 Binary files /dev/null and b/frontend/public/images/logo.png differ diff --git a/frontend/public/images/mindful-event.jpg b/frontend/public/images/mindful-event.jpg new file mode 100644 index 000000000..edb626f29 Binary files /dev/null and b/frontend/public/images/mindful-event.jpg differ diff --git a/frontend/public/images/plan-event.jpg b/frontend/public/images/plan-event.jpg new file mode 100644 index 000000000..d7f789198 Binary files /dev/null and b/frontend/public/images/plan-event.jpg differ diff --git a/frontend/public/images/profile-bg.jpg b/frontend/public/images/profile-bg.jpg new file mode 100644 index 000000000..70b8e7e03 Binary files /dev/null and b/frontend/public/images/profile-bg.jpg differ diff --git a/frontend/public/images/signUp.jpg b/frontend/public/images/signUp.jpg new file mode 100644 index 000000000..c4d9cf7ae Binary files /dev/null and b/frontend/public/images/signUp.jpg differ diff --git a/frontend/public/images/social-event.jpg b/frontend/public/images/social-event.jpg new file mode 100644 index 000000000..953afe3f2 Binary files /dev/null and b/frontend/public/images/social-event.jpg differ diff --git a/frontend/public/images/support-event.jpg b/frontend/public/images/support-event.jpg new file mode 100644 index 000000000..01ba1723c Binary files /dev/null and b/frontend/public/images/support-event.jpg differ diff --git a/frontend/public/images/team.jpg b/frontend/public/images/team.jpg new file mode 100644 index 000000000..6a20b090f Binary files /dev/null and b/frontend/public/images/team.jpg differ diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg deleted file mode 100644 index e7b8dfb1b..000000000 --- a/frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0a24275e6..7dc85eef0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,28 @@ -export const App = () => { +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { EventsPage } from "./components/eventPage/EventsPage"; +import { AboutUsPage } from "./components/AboutUsPage"; +import { CommunityGuidelines } from "./components/CommunityGuidelines"; +import { Profile } from "./components/Auth/Profile"; +import { ModalProvider } from "./components/Auth/ModalContext"; +import { FindOutMorePage } from "./mainPage/FindOutMorePage"; +import { HomePage } from "./mainPage/HomePage"; +export const App = () => { return ( - <> -

Welcome to Final Project!

- + + + + } /> + } /> + } /> + } /> + } /> + } + /> + + + ); }; diff --git a/frontend/src/assets/boiler-plate.svg b/frontend/src/assets/boiler-plate.svg deleted file mode 100644 index c9252833b..000000000 --- a/frontend/src/assets/boiler-plate.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/technigo-logo.svg b/frontend/src/assets/technigo-logo.svg deleted file mode 100644 index 3f0da3e57..000000000 --- a/frontend/src/assets/technigo-logo.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/axiosConfig.js b/frontend/src/axiosConfig.js new file mode 100644 index 000000000..505d9ed52 --- /dev/null +++ b/frontend/src/axiosConfig.js @@ -0,0 +1,10 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: "https://project-final-rmn2.onrender.com/api", + headers: { + "Content-Type": "application/json", + }, +}); + +export default api; diff --git a/frontend/src/common/Button.jsx b/frontend/src/common/Button.jsx new file mode 100644 index 000000000..ee9b401d8 --- /dev/null +++ b/frontend/src/common/Button.jsx @@ -0,0 +1,25 @@ +import { Link } from "react-router-dom"; + +export const Button = ({ + text, + link, + className = "", + onClick, + variant = "primary", +}) => { + const baseClasses = + "px-4 py-2 md:px-6 md:py-3 rounded-full text-md md:text-lg"; + const variantClasses = { + primary: "bg-dark text-white hover:bg-secondary", + light: "bg-light text-dark hover:bg-lighter", + }; + return ( + + {text} + + ); +}; diff --git a/frontend/src/common/Footer.jsx b/frontend/src/common/Footer.jsx new file mode 100644 index 000000000..17d84e93e --- /dev/null +++ b/frontend/src/common/Footer.jsx @@ -0,0 +1,81 @@ +import { Link } from "react-router-dom"; + +const Footer = () => { + const currentYear = new Date().getFullYear(); + return ( + + ); +}; + +export default Footer; diff --git a/frontend/src/common/HeroSection.jsx b/frontend/src/common/HeroSection.jsx new file mode 100644 index 000000000..050ed0d2e --- /dev/null +++ b/frontend/src/common/HeroSection.jsx @@ -0,0 +1,17 @@ +const HeroSection = ({ imageUrl, title, subtitle, className, style }) => { + return ( +
+
+
+

{title}

+

{subtitle}

+
+
+
+ ); +}; + +export default HeroSection; diff --git a/frontend/src/common/Menu.jsx b/frontend/src/common/Menu.jsx new file mode 100644 index 000000000..60efb52cc --- /dev/null +++ b/frontend/src/common/Menu.jsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import logo from "/images/logo.png"; +import { useModal } from "../components/Auth/ModalContext"; +import { AuthForm } from "../components/Auth/AuthForm"; + +const Menu = () => { + const [isOpen, setIsOpen] = useState(false); + const { isAuthenticated, showModal } = useModal(); + const navigate = useNavigate(); + + const handleLinkClick = () => { + setIsOpen(false); + }; + + const handleLoginClick = () => { + showModal( navigate("/profile")} />); + }; + + const handleProfileClick = () => { + navigate("/profile"); + }; + + return ( + + ); +}; + +export default Menu; diff --git a/frontend/src/components/AboutUsPage.jsx b/frontend/src/components/AboutUsPage.jsx new file mode 100644 index 000000000..d6d4ea95d --- /dev/null +++ b/frontend/src/components/AboutUsPage.jsx @@ -0,0 +1,139 @@ +import { Button } from "../common/Button"; +import Footer from "../common/Footer"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import HeroSection from "../common/HeroSection"; +import Menu from "../common/Menu"; +import { AuthForm } from "./Auth/AuthForm"; +import { useModal } from "./Auth/ModalContext"; + +export const AboutUsPage = () => { + const { showModal } = useModal(); + const navigate = useNavigate(); + + const handleJoinUsClick = () => { + showModal( + navigate("/profile")} /> + ); + }; + + const FAQItem = ({ question, answer }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+

setIsOpen(!isOpen)} + > + {question} + {isOpen ? "▲" : "▼"} +

+ {isOpen &&

{answer}

} +
+ ); + }; + + return ( +
+ + + +
+
+
+

Mission

+

+ We aim to support and empower individuals with ADHD by providing a + safe space and understanding community +

+
+
+

Values

+

+ We believe in inclusivity, empathy, and the power of community. + Our value guide us in creating a welcoming environment for + everyone +

+
+
+

Team

+

+ Our team is dedicated to providing resources, support, and events + that cater to the unique needs of those with ADHD. +

+
+
+
+ +
+
+

Join Our Community

+

+ Become part of our community and connect with others who understand + your journey. Toghether, we can achieve more. +

+
+
+
+
+ + {/* FAQ Section */} +
+
+

FAQs

+
+ + + + + +
+
+
+ +
+
+

Contact Us

+

+ If you have any questions or need support, feel free to reach out to + us at{" "} + + support@adhdcommunity.com + + . +

+
+
+
+
+ ); +}; diff --git a/frontend/src/components/Auth/AuthForm.jsx b/frontend/src/components/Auth/AuthForm.jsx new file mode 100644 index 000000000..308295743 --- /dev/null +++ b/frontend/src/components/Auth/AuthForm.jsx @@ -0,0 +1,162 @@ +import { useState } from "react"; +import { useModal } from "./ModalContext"; +import { signup, login as authLogin } from "./AuthService"; +import signUpImage from "/images/signUp.jpg"; + +export const AuthForm = ({ type, onSuccess }) => { + const { hideModal, login } = useModal(); + const [formMode, setFormMode] = useState(type); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [usernameError, setUsernameError] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [error, setError] = useState(null); + const [role, setRole] = useState("Listener"); + const [loading, setLoading] = useState(false); + + const handleRoleChange = (e) => { + setRole(e.target.value); + }; + + const handleUsernameChange = (e) => { + const newUsername = e.target.value; + setUsername(newUsername); + if (newUsername.length < 3) { + setUsernameError("Username must be at least 3 characters long."); + } else { + setUsernameError(""); + } + }; + + const handlePasswordChange = (e) => { + const newPassword = e.target.value; + setPassword(newPassword); + if (newPassword.length < 6) { + setPasswordError("Password must be at least 6 characters long."); + } else { + setPasswordError(""); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (usernameError || passwordError) { + setError("Please fix the errors before submitting."); + return; + } + + setLoading(true); + setError(null); + + try { + let userData; + if (formMode === "signup") { + userData = await signup(username, password, role); + } else { + userData = await authLogin(username, password); + } + login(userData); + hideModal(); + onSuccess(); + } catch (error) { + setError( + error.response?.data?.error || + "An error occurred during the authentication process.Try Again!" + ); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ +
+ Sign up +
+
+ {error &&
{error}
} +
+ + {usernameError && ( +
{usernameError}
+ )} + + + {passwordError && ( +
{passwordError}
+ )} + + {formMode === "signup" && ( +
+ + +
+ )} + +
+

+ {formMode === "signup" + ? "Already have an account?" + : "Don't have an account?"}{" "} + +

+
+
+
+ ); +}; diff --git a/frontend/src/components/Auth/AuthService.jsx b/frontend/src/components/Auth/AuthService.jsx new file mode 100644 index 000000000..b6aae3708 --- /dev/null +++ b/frontend/src/components/Auth/AuthService.jsx @@ -0,0 +1,81 @@ +import api from "../../axiosConfig"; + +const setAuthToken = (token) => { + localStorage.setItem("authToken", token); +}; + +export const signup = async (username, password, role) => { + try { + const response = await api.post("/users", { username, password, role }); + const { accessToken } = response.data; + + setAuthToken(accessToken); + return response.data; + } catch (error) { + console.error( + "Signup failed:", + error.response?.data?.message || error.message + ); + throw error; + } +}; + +export const login = async (username, password) => { + try { + const response = await api.post("/sessions", { username, password }); + const { token } = response.data; + setAuthToken(token); + return response.data; + } catch (error) { + console.error( + "Login failed:", + error.response?.data?.message || error.message + ); + throw error; + } +}; + +export const getProfile = async () => { + try { + const token = localStorage.getItem("authToken"); + if (!token) { + throw new Error("No auth token found"); + } + const response = await api.get("/profile", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return response.data; + } catch (error) { + console.error( + "Failed to fetch profile:", + error.response?.data?.message || error.message + ); + throw error; + } +}; + +export const updateProfile = async (userData) => { + try { + const token = localStorage.getItem("authToken"); + + const response = await api.put("/profile", userData, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error( + "Failed to update profile:", + error.response?.data?.message || error.message + ); + throw error; + } +}; + +export const logout = () => { + localStorage.removeItem("authToken"); +}; diff --git a/frontend/src/components/Auth/Community.jsx b/frontend/src/components/Auth/Community.jsx new file mode 100644 index 000000000..1bbe6fd76 --- /dev/null +++ b/frontend/src/components/Auth/Community.jsx @@ -0,0 +1,34 @@ +import communityData from "../../data/communityData.json"; +import defaultProfilePicture from "/icons/profile.png"; + +export const Community = () => { + return ( +
+

+ Meet The Community +

+
+ {communityData.map((member) => ( +
+
+ {member.name} +
+

{member.name}

+

{member.bio}

+ + {member.role} + +
+ ))} +
+
+ ); +}; diff --git a/frontend/src/components/Auth/ModalContext.jsx b/frontend/src/components/Auth/ModalContext.jsx new file mode 100644 index 000000000..85cc3fb3b --- /dev/null +++ b/frontend/src/components/Auth/ModalContext.jsx @@ -0,0 +1,58 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; +import { getProfile } from "../Auth/AuthService"; + +const ModalContext = createContext(); + +export const useModal = () => useContext(ModalContext); + +export const ModalProvider = ({ children }) => { + const [isVisible, setIsVisible] = useState(false); + const [content, setContent] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [user, setUser] = useState(null); + + const showModal = (component) => { + setContent(component); + setIsVisible(true); + }; + + const hideModal = () => { + setIsVisible(false); + setContent(null); + }; + + useEffect(() => { + const token = localStorage.getItem("authToken"); + if (token) { + getProfile() + .then((userData) => { + setUser(userData); + setIsAuthenticated(true); + }) + .catch((error) => { + console.error("Error fetching profile:", error); + localStorage.removeItem("authToken"); + }); + } + }, []); + + const login = (userData) => { + setUser(userData); + setIsAuthenticated(true); + }; + + const logout = () => { + localStorage.removeItem("authToken"); + setUser(null); + setIsAuthenticated(false); + }; + + return ( + + {children} + {isVisible && content} + + ); +}; diff --git a/frontend/src/components/Auth/Profile.jsx b/frontend/src/components/Auth/Profile.jsx new file mode 100644 index 000000000..e1387610f --- /dev/null +++ b/frontend/src/components/Auth/Profile.jsx @@ -0,0 +1,164 @@ +import { Button } from "../../common/Button"; +import Footer from "../../common/Footer"; +import gearIcon from "/icons/gear.svg"; +import defaultProfilePicture from "/icons/profile.png"; +import bgImage from "/images/profile-bg.jpg"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import Menu from "../../common/Menu"; +import { getProfile, updateProfile } from "./AuthService"; +import { useModal } from "./ModalContext"; +import { AuthForm } from "./AuthForm"; +import { Community } from "./Community"; +import { ProfileForm } from "./ProfileForm"; + +export const Profile = () => { + const [userData, setUserData] = useState({ + username: "", + name: "", + bio: "", + hobby: "", + }); + const [role, setRole] = useState(""); + const [editMode, setEditMode] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [notification, setNotification] = useState(""); + const navigate = useNavigate(); + const { showModal, logout } = useModal(); + + const fetchProfile = async () => { + try { + const profileData = await getProfile(); + setUserData({ + username: profileData.username || "", + name: profileData.name || "", + bio: profileData.bio || "", + hobby: profileData.hobby || "", + }); + setRole(profileData.role || ""); + } catch (error) { + console.error("Error fetching profile:", error); + if (error.response?.status === 401) { + localStorage.removeItem("authToken"); + setError("Unauthorized. Please log in again."); + showModal(); + } else { + setError( + error.response?.data?.error || + "An error occurred while fetching profile." + ); + } + } finally { + setLoading(false); + } + }; + + useEffect(() => { + const token = localStorage.getItem("authToken"); + if (token) { + fetchProfile(); + } else { + showModal(); + setLoading(false); + } + }, []); + + const handleUpdateProfile = async (e) => { + e.preventDefault(); + setError(""); + setNotification(""); + try { + const updatedData = await updateProfile(userData); + setUserData(updatedData); + setEditMode(false); + setNotification("Profile updated successfully!"); + setTimeout(() => setNotification(""), 3000); + } catch (err) { + console.error("Failed to update profile:", err); + setError("Failed to update profile."); + setNotification(); + } + }; + + const handleLogout = () => { + logout(); + navigate("/", { replace: true }); + window.location.reload(); + }; + + if (loading) { + return
Loading...
; + } + if (error) { + return

{error}

; + } + if (!userData) { + return
No user found
; + } + + return ( +
+ +
+
+
+ Profile +
+

+ Hello, {userData.name || userData.username} +

+

Role: {role || "User"}

+

Bio: {userData.bio || ""}

+

I like: {userData.hobby || ""}

+
+
+ {!editMode && ( + Edit Profile setEditMode(true)} + /> + )} + + {editMode && ( + + )} + {notification && ( +

{notification}

+ )} +
+
+
+ +
+
+
+ ); +}; diff --git a/frontend/src/components/Auth/ProfileForm.jsx b/frontend/src/components/Auth/ProfileForm.jsx new file mode 100644 index 000000000..5e2c8c091 --- /dev/null +++ b/frontend/src/components/Auth/ProfileForm.jsx @@ -0,0 +1,52 @@ +export const ProfileForm = ({ userData, setUserData, handleUpdateProfile }) => { + const handleChange = (e) => { + const { name, value } = e.target; + setUserData((prevData) => ({ + ...prevData, + [name]: value, + })); + }; + + return ( +
+
+ + +
+
+ +