From 11e71fc08dce3bd67341be275f119efcf624ac23 Mon Sep 17 00:00:00 2001 From: Akhil-R Date: Sun, 10 May 2026 15:23:11 +0530 Subject: [PATCH 1/2] feat: complete auth system and basic editor --- .gitignore | 29 ++ backend/.env.example | 8 + backend/.gitignore | 3 + backend/package.json | 32 ++ backend/src/config/database.ts | 16 + backend/src/controllers/authController.ts | 74 ++++ backend/src/models/User.ts | 24 ++ backend/src/routes/authRoutes.ts | 14 + backend/src/server.ts | 35 ++ backend/src/validation/authValidation.ts | 18 + backend/tsconfig.json | 26 ++ frontend/.env.example | 2 + frontend/.gitignore | 27 ++ frontend/README.md | 73 ++++ frontend/eslint.config.js | 22 ++ frontend/index.html | 13 + frontend/package.json | 35 ++ frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 ++ frontend/src/App.tsx | 33 ++ frontend/src/api/authApi.ts | 18 + frontend/src/auth/AuthContext.ts | 17 + frontend/src/auth/AuthProvider.tsx | 68 ++++ frontend/src/auth/useAuth.ts | 11 + frontend/src/components/Navbar.tsx | 24 ++ frontend/src/components/ProtectedRoute.tsx | 22 ++ frontend/src/editor/TextEditor.tsx | 30 ++ frontend/src/editor/editorTypes.ts | 8 + frontend/src/main.tsx | 19 + frontend/src/pages/EditorPage.tsx | 68 ++++ frontend/src/pages/LoginPage.tsx | 120 +++++++ frontend/src/pages/RegisterPage.tsx | 140 ++++++++ frontend/src/styles/main.css | 391 +++++++++++++++++++++ frontend/src/validation/authValidation.ts | 18 + frontend/tsconfig.app.json | 25 ++ frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 24 ++ frontend/vite.config.ts | 7 + package.json | 13 + 39 files changed, 1539 insertions(+) create mode 100644 .gitignore create mode 100644 backend/.env.example create mode 100644 backend/.gitignore create mode 100644 backend/package.json create mode 100644 backend/src/config/database.ts create mode 100644 backend/src/controllers/authController.ts create mode 100644 backend/src/models/User.ts create mode 100644 backend/src/routes/authRoutes.ts create mode 100644 backend/src/server.ts create mode 100644 backend/src/validation/authValidation.ts create mode 100644 backend/tsconfig.json create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/authApi.ts create mode 100644 frontend/src/auth/AuthContext.ts create mode 100644 frontend/src/auth/AuthProvider.tsx create mode 100644 frontend/src/auth/useAuth.ts create mode 100644 frontend/src/components/Navbar.tsx create mode 100644 frontend/src/components/ProtectedRoute.tsx create mode 100644 frontend/src/editor/TextEditor.tsx create mode 100644 frontend/src/editor/editorTypes.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/EditorPage.tsx create mode 100644 frontend/src/pages/LoginPage.tsx create mode 100644 frontend/src/pages/RegisterPage.tsx create mode 100644 frontend/src/styles/main.css create mode 100644 frontend/src/validation/authValidation.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..6967e2a83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +build/ + +# Environment variables +.env +.env.local +.env.production + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Package manager files +package-lock.json +yarn.lock +pnpm-lock.yaml +EOF \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 000000000..82cddd46c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +# Backend port +PORT=5000 + +# MongoDB connection string +MONGO_URI=mongodb://127.0.0.1:27017/vi-notes + +# Secret key used to create login tokens +JWT_SECRET=change-this-secret-key diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 000000000..b3eb616a3 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +dist/ \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 000000000..6109f00d2 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,32 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "tsx watch src/server.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "dependencies": { + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mongoose": "^9.6.2", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^25.6.2", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } +} diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts new file mode 100644 index 000000000..d2d5cb8bb --- /dev/null +++ b/backend/src/config/database.ts @@ -0,0 +1,16 @@ +import mongoose from "mongoose"; + +// This function connects our backend to MongoDB. +const connectDB = async () => { + try { + // MONGO_URI comes from the .env file. + await mongoose.connect(process.env.MONGO_URI!); + console.log("MongoDB connected"); + } catch (error) { + // If database connection fails, stop the backend. + console.error("MongoDB connection failed:", error); + process.exit(1); + } +}; + +export default connectDB; diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts new file mode 100644 index 000000000..60d1d215f --- /dev/null +++ b/backend/src/controllers/authController.ts @@ -0,0 +1,74 @@ +import { Request, Response } from "express"; +import bcrypt from "bcryptjs"; +import jwt from "jsonwebtoken"; +import User from "../models/User.js"; +import { registerSchema, loginSchema } from "../validation/authValidation.js"; +import { z } from "zod"; + +// This function handles new user registration. +export const register = async (req: Request, res: Response) => { + // First we check if name, email, and password are valid. + const result = registerSchema.safeParse(req.body); + if (!result.success) { + res.status(400).json({ errors: z.treeifyError(result.error) }); + return; + } + + // After validation, we can safely use these values. + const { name, email, password } = result.data; + + // Do not allow two accounts with the same email. + const exists = await User.findOne({ email }); + if (exists) { + res.status(409).json({ error: "Email already registered" }); + return; + } + + // Hash the password so the real password is not stored in MongoDB. + const hashedPassword = await bcrypt.hash(password, 10); + + // Save the new user in the database. + const user = await User.create({ name, email, password: hashedPassword }); + + // Create a token so the frontend knows the user is logged in. + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET!, { + expiresIn: "7d", + }); + + // Send back user details without sending the password. + res.status(201).json({ token, user: { id: user._id, name, email } }); +}; + +// This function handles login for an existing user. +export const login = async (req: Request, res: Response) => { + // First we check if email and password are valid. + const result = loginSchema.safeParse(req.body); + if (!result.success) { + res.status(400).json({ errors: z.treeifyError(result.error) }); + return; + } + + const { email, password } = result.data; + + // Find the account that uses this email. + const user = await User.findOne({ email }); + if (!user) { + res.status(401).json({ message: "Invalid credentials" }); + return; + } + + // Compare the typed password with the hashed password in the database. + const match = await bcrypt.compare(password, user.password); + if (!match) { + res.status(401).json({ message: "Invalid credentials" }); + return; + } + + // If password is correct, create a new login token. + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET!, { + expiresIn: "7d", + }); + + // Send back the token and basic user details. + res.json({ token, user: { id: user._id, name: user.name, email } }); +}; diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts new file mode 100644 index 000000000..fcdcb84f9 --- /dev/null +++ b/backend/src/models/User.ts @@ -0,0 +1,24 @@ +import mongoose, { Document, Schema } from "mongoose"; + +// This tells TypeScript what a user should contain. +export interface IUser extends Document { + name: string; + email: string; + password: string; +} + +// This tells MongoDB what fields to store for every user. +const UserSchema: Schema = new Schema( + { + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + }, + { + // This adds createdAt and updatedAt automatically. + timestamps: true, + }, +); + +// This User model is used in controllers to create and find users. +export default mongoose.model("User", UserSchema); diff --git a/backend/src/routes/authRoutes.ts b/backend/src/routes/authRoutes.ts new file mode 100644 index 000000000..206d82ddd --- /dev/null +++ b/backend/src/routes/authRoutes.ts @@ -0,0 +1,14 @@ +import { Router } from "express"; +import { register, login } from "../controllers/authController.js"; + +// This file connects auth URLs to their controller functions. +const router = Router(); + +// When frontend calls POST /api/auth/register, run the register function. +router.post("/register", register); + +// When frontend calls POST /api/auth/login, run the login function. +router.post("/login", login); + +// server.ts uses this router under /api/auth. +export default router; diff --git a/backend/src/server.ts b/backend/src/server.ts new file mode 100644 index 000000000..f9d75f295 --- /dev/null +++ b/backend/src/server.ts @@ -0,0 +1,35 @@ +import express from "express"; +import cors from "cors"; +import dotenv from "dotenv"; +import connectDB from "./config/database.js"; +import authRoutes from "./routes/authRoutes.js"; + +// This loads values like MONGO_URI and JWT_SECRET from the .env file. +dotenv.config(); + +// This creates the Express backend app. +const app = express(); + +// cors allows the frontend to call this backend. +app.use(cors()); + +// This lets the backend read JSON data sent from forms. +app.use(express.json()); + +const PORT = process.env.PORT || 5000; + +// This route is just used to check if the backend is running. +app.get("/", (req, res) => { + res.json({ message: "Vi-Notes API running" }); +}); + +// All login and register routes start with /api/auth. +app.use("/api/auth", authRoutes); + +// Connect to MongoDB before the server starts accepting requests. +connectDB(); + +// This starts the backend server. +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/backend/src/validation/authValidation.ts b/backend/src/validation/authValidation.ts new file mode 100644 index 000000000..e85f071f1 --- /dev/null +++ b/backend/src/validation/authValidation.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +// These are the backend rules for creating a new account. +export const registerSchema = z.object({ + name: z.string().min(2, "Name must be atleast 2 characters long"), + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters long"), +}); + +// These are the backend rules for logging in. +export const loginSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(1, "Password is required"), +}); + +// These types are useful if we need the same shape in other backend files. +export type RegisterInput = z.infer; +export type LoginInput = z.infer; diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 000000000..3bd9541fc --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + /* Base Options */ + "target": "ES2023", // Node 24 supports almost all ES2023 features natively + "module": "NodeNext", // Required for modern ESM in Node + "moduleResolution": "NodeNext", // Ensures imports work correctly in Node + "lib": ["ES2023"], // No "DOM" here because backend doesn't have a browser window + + /* Build Settings */ + "outDir": "./dist", // Where the compiled JS goes + "rootDir": "./src", // Where your TS source code lives + "noEmit": false, // MUST be false for backend so we can actually build it + + /* Strictness & Safety */ + "strict": true, // Enables all strict type-checking options + "noImplicitAny": true, // Prevents accidental "any" types + "esModuleInterop": true, // Allows you to import CommonJS packages (like bcryptjs) easily + "skipLibCheck": true, // Speeds up compilation by skipping type checks of node_modules + + /* Cleanliness */ + "removeComments": true, // Keeps the production JS files small + "sourceMap": true // Helps you debug the TS code even after it's compiled to JS + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 000000000..4e7b888e8 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Backend API URL used by the React app +VITE_API_URL=http://localhost:5000 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..bff37cddc --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +.env +.env.* +!.env.example +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..7dbf7ebf3 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 000000000..ef614d25c --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..0fca6f043 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..a03e86625 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "axios": "^1.16.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-hook-form": "^7.75.0", + "react-router-dom": "^7.15.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.10" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 000000000..6893eb132 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 000000000..e9522193d --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 000000000..9bd182cc2 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,33 @@ +import { Suspense, lazy } from "react"; +import { Navigate, Route, Routes } from "react-router-dom"; +import ProtectedRoute from "./components/ProtectedRoute"; + +// These pages are loaded only when they are needed. +const Login = lazy(() => import("./pages/LoginPage")); +const Register = lazy(() => import("./pages/RegisterPage")); +const EditorPage = lazy(() => import("./pages/EditorPage")); + +const App = () => { + return ( + // This shows a small loading message while a page is loading. + Loading...}> + + {/* When someone opens the home page, send them to the editor. */} + } /> + } /> + } /> + {/* The editor is protected, so only logged-in users can open it. */} + + + + } + /> + + + ); +}; + +export default App; diff --git a/frontend/src/api/authApi.ts b/frontend/src/api/authApi.ts new file mode 100644 index 000000000..c57c3bbbd --- /dev/null +++ b/frontend/src/api/authApi.ts @@ -0,0 +1,18 @@ +import axios from "axios"; + +// This is the backend URL. If .env is missing, it uses localhost. +const API = import.meta.env.VITE_API_URL || "http://localhost:5000"; + +// Axios is like a messenger that sends data to our server +// This sends new user details to the backend register route. +export const registerAPI = (name: string, email: string, password: string) => + axios.post(`${API}/api/auth/register`, { name, email, password }); + +// This sends login details to the backend login route. +export const loginAPI = (email: string, password: string) => + axios.post(`${API}/api/auth/login`, { email, password }); + +// This can be used later when an API route needs the user's token. +export const createAuthHeader = (token: string) => ({ + headers: { Authorization: `Bearer ${token}` } +}); diff --git a/frontend/src/auth/AuthContext.ts b/frontend/src/auth/AuthContext.ts new file mode 100644 index 000000000..2b5c0b888 --- /dev/null +++ b/frontend/src/auth/AuthContext.ts @@ -0,0 +1,17 @@ +import { createContext } from "react"; + +// This is the user information we keep after login. +export type User = { id: string; name: string; email: string }; + +// This tells TypeScript what values our auth system will share. +export type AuthContextType = { + user: User | null; + token: string | null; + loading: boolean; + register: (name: string, email: string, password: string) => Promise; + login: (email: string, password: string) => Promise; + logout: () => void; +}; + +// This is where React stores the login data for the app. +export const AuthContext = createContext(null); diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx new file mode 100644 index 000000000..2a5819935 --- /dev/null +++ b/frontend/src/auth/AuthProvider.tsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import { AuthContext } from "./AuthContext"; +import type { User } from "./AuthContext"; +import { registerAPI, loginAPI } from "../api/authApi"; + +// This component keeps all login information in one place. +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + // First we check localStorage so the user stays logged in after refresh. + const [user, setUser] = useState(() => { + const savedUser = localStorage.getItem("vi-user"); + return savedUser ? JSON.parse(savedUser) : null; + }); + + // The token proves that the user has logged in. + const [token, setToken] = useState(() => { + return localStorage.getItem("vi-token"); + }); + + // This helps us show loading text while login or register is running. + const [loading, setLoading] = useState(false); + + // This creates a new account by calling the backend. + const register = async (name: string, email: string, password: string) => { + setLoading(true); + try { + const { data } = await registerAPI(name, email, password); + setUser(data.user); + setToken(data.token); + + // Save the user and token so refresh does not log them out. + localStorage.setItem("vi-user", JSON.stringify(data.user)); + localStorage.setItem("vi-token", data.token); + } finally { + setLoading(false); + } + }; + + // This logs in an existing user by checking email and password. + const login = async (email: string, password: string) => { + setLoading(true); + try { + const { data } = await loginAPI(email, password); + setUser(data.user); + setToken(data.token); + + // Store login details in the browser for the next page refresh. + localStorage.setItem("vi-user", JSON.stringify(data.user)); + localStorage.setItem("vi-token", data.token); + } finally { + setLoading(false); + } + }; + + // This removes login data when the user clicks logout. + const logout = () => { + setToken(null); + setUser(null); + localStorage.removeItem("vi-token"); + localStorage.removeItem("vi-user"); + }; + + // All pages inside this provider can use user, token, login, register, and logout. + return ( + + {children} + + ); +}; diff --git a/frontend/src/auth/useAuth.ts b/frontend/src/auth/useAuth.ts new file mode 100644 index 000000000..c1b539386 --- /dev/null +++ b/frontend/src/auth/useAuth.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +import { AuthContext } from "./AuthContext"; + +// This small hook lets any component get the current login data. +export const useAuth = () => { + const ctx = useContext(AuthContext); + + // If this error happens, it means the component is outside AuthProvider. + if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); + return ctx; +}; diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx new file mode 100644 index 000000000..4998937d6 --- /dev/null +++ b/frontend/src/components/Navbar.tsx @@ -0,0 +1,24 @@ +import { useAuth } from "../auth/useAuth"; + +// This top bar shows the app name, current user, and logout button. +const Navbar = () => { + const { user, logout } = useAuth(); + + return ( + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 000000000..47c44cb7b --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,22 @@ +import { Navigate } from "react-router-dom"; +import { useAuth } from "../auth/useAuth"; + +// This component is used for pages that need login. +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { token, logout } = useAuth(); + + // If there is no token, the user has not logged in. + if (!token) return ; + + // A normal JWT token has three parts separated by dots. + const parts = token.split('.'); + if (parts.length !== 3) { + logout(); + return ; + } + + // If the token exists, show the protected page. + return <>{children}; +}; + +export default ProtectedRoute; diff --git a/frontend/src/editor/TextEditor.tsx b/frontend/src/editor/TextEditor.tsx new file mode 100644 index 000000000..e1a45ae64 --- /dev/null +++ b/frontend/src/editor/TextEditor.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import type { EditorProps } from "./editorTypes"; + +// This is the simple text editor where the user writes their content. +const Editor = React.memo(function Editor({ + content, + onTextChange, + onKeyDown, + onKeyUp, + onPaste, +}: EditorProps) { + return ( +
+ {/* The textarea is controlled by React, so the page always knows the latest text. */} +