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..d17beb7a0 --- /dev/null +++ b/backend/src/config/database.ts @@ -0,0 +1,32 @@ +import mongoose from "mongoose"; + +// Gets environment variables or crashes if missing +const getEnv = (key: string): string => { + const value = process.env[key]; + if (!value) { + console.error(`Missing required environment variable: ${key}`); + console.error( + "Please check your .env file and ensure all required variables are set.", + ); + process.exit(1); + } + return value; +}; + +// This function connects our backend to MongoDB. +const connectDB = async () => { + try { + // Get MongoDB connection string from environment + const MONGO_URI = getEnv("MONGO_URI"); + + // Actually connect to MongoDB + await mongoose.connect(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..4d908eccd --- /dev/null +++ b/backend/src/controllers/authController.ts @@ -0,0 +1,116 @@ +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"; + +// Simple helper to get required environment variables safely. +const getEnv = (key: string): string => { + const value = process.env[key]; + if (!value) { + console.error(`Missing required environment variable: ${key}`); + process.exit(1); + } + return value; +}; + +// 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 }, getEnv("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 }, getEnv("JWT_SECRET"), { + expiresIn: "7d", + }); + + // Send back the token and basic user details. + res.json({ token, user: { id: user._id, name: user.name, email } }); +}; + +// This function returns the current user from a valid JWT token. +export const me = async (req: Request, res: Response) => { + try { + // The user ID is attached to the request by the authenticateToken middleware. + const userId = req.user?.id; + + if (!userId) { + res.status(401).json({ error: "User not found" }); + return; + } + + // Find the user in the database. + const user = await User.findById(userId).select("-password"); + + if (!user) { + res.status(401).json({ error: "User not found" }); + return; + } + + // Return user details without the password. + res.json({ + user: { + id: user._id, + name: user.name, + email: user.email, + }, + }); + } catch (error) { + res.status(500).json({ error: "Server error" }); + } +}; diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts new file mode 100644 index 000000000..4e0bfa549 --- /dev/null +++ b/backend/src/middleware/auth.ts @@ -0,0 +1,45 @@ +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; + +// Simple helper to get required environment variables safely. +const getEnv = (key: string): string => { + const value = process.env[key]; + if (!value) { + console.error(`Missing required environment variable: ${key}`); + process.exit(1); + } + return value; +}; + +// This adds the user's ID to the request when a valid JWT is provided. +export const authenticateToken = (req: Request, res: Response, next: NextFunction) => { + // Get the token from the Authorization header. + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; // Bearer + + if (!token) { + res.status(401).json({ error: "Access token required" }); + return; + } + + // Verify the token with our secret key. + jwt.verify(token, getEnv("JWT_SECRET"), (err, decoded) => { + if (err) { + res.status(401).json({ error: "Invalid or expired token" }); + return; + } + + // Add the user's ID to the request for use in protected routes. + req.user = { id: (decoded as any).id }; + next(); + }); +}; + +// Extend the Request type to include user information. +declare global { + namespace Express { + interface Request { + user?: { id: string }; + } + } +} 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..0a02762f6 --- /dev/null +++ b/backend/src/routes/authRoutes.ts @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { register, login, me } from "../controllers/authController.js"; +import { authenticateToken } from "../middleware/auth.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); + +// When frontend calls GET /api/auth/me, verify JWT and return current user. +router.get("/me", authenticateToken, me); + +// 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..4c4c073f6 --- /dev/null +++ b/frontend/src/api/authApi.ts @@ -0,0 +1,22 @@ +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 gets the current user info using the stored token. +export const meAPI = (token: string) => + axios.get(`${API}/api/auth/me`, createAuthHeader(token)); + +// 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..6a34a4d4a --- /dev/null +++ b/frontend/src/auth/AuthProvider.tsx @@ -0,0 +1,108 @@ +import { useState, useEffect } from "react"; +import { AuthContext } from "./AuthContext"; +import type { User } from "./AuthContext"; +import { registerAPI, loginAPI, meAPI } 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 helps us show loading while validating the stored token on app mount. + const [validating, setValidating] = useState(true); + + // This validates the stored token when the app loads. + useEffect(() => { + const validateToken = async () => { + const storedToken = localStorage.getItem("vi-token"); + + if (!storedToken) { + setValidating(false); + return; + } + + try { + const { data } = await meAPI(storedToken); + setUser(data.user); + setToken(storedToken); + } catch { + // Token is invalid or expired, clear stored data + localStorage.removeItem("vi-token"); + localStorage.removeItem("vi-user"); + setUser(null); + setToken(null); + } finally { + setValidating(false); + } + }; + + validateToken(); + }, []); + + // 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..f2382a004 --- /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 { user, loading } = useAuth(); + + // Show loading while validating the token. + if (loading) { + return
Loading...
; + } + + // If there is no user (token validation failed), redirect to login. + if (!user) { + return ; + } + + // If the user 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. */} +