diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a59b9875b --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# 🚀 Node Modules +node_modules/ +frontend/node_modules/ +backend/node_modules/ + +# 🚀 Environment Variables +.env +frontend/.env +backend/.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# 🚀 Logs & Debugging +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 🚀 Build & Output Files +/build +/dist +/out +frontend/dist/ +frontend/build/ +frontend/out/ +backend/dist/ +backend/build/ +backend/out/ + +# 🚀 Database Files +backend/database.sqlite +backend/database.sqlite3 +backend/*.db +backend/*.sqlite + +# 🚀 Dependency Lock Files (Optional: Keep only one) +package-lock.json + +# 🚀 IDE & OS-Specific Files +.vscode/ +.idea/ +.DS_Store +Thumbs.db + +# 🚀 Testing Coverage +/coverage diff --git a/README.md b/README.md index cb8f88a16..8131a3816 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,75 @@ -# Full-Stack Coding Challenge - -**Deadline**: Sunday, Feb 23th 11:59 pm PST - ---- - -## Overview - -Create a “Task Management” application with **React + TypeScript** (frontend), **Node.js** (or **Nest.js**) (backend), and **PostgreSQL** (database). The application should: - -1. **Register** (sign up) and **Log in** (sign in) users. -2. After logging in, allow users to: - - **View a list of tasks**. - - **Create a new task**. - - **Update an existing task** (e.g., mark complete, edit). - - **Delete a task**. - -Focus on **correctness**, **functionality**, and **code clarity** rather than visual design. -This challenge is intended to be completed within ~3 hours, so keep solutions minimal yet functional. - ---- - -## Requirements - -### 1. Authentication - -- **User Model**: - - `id`: Primary key - - `username`: Unique string - - `password`: Hashed string -- **Endpoints**: - - `POST /auth/register` – Create a new user - - `POST /auth/login` – Login user, return a token (e.g., JWT) -- **Secure the Tasks Routes**: Only authenticated users can perform task operations. - - **Password Hashing**: Use `bcrypt` or another hashing library to store passwords securely. - - **Token Verification**: Verify the token (JWT) on each request to protected routes. - -### 2. Backend (Node.js or Nest.js) - -- **Tasks CRUD**: - - `GET /tasks` – Retrieve a list of tasks (optionally filtered by user). - - `POST /tasks` – Create a new task. - - `PUT /tasks/:id` – Update a task (e.g., mark as complete, edit text). - - `DELETE /tasks/:id` – Delete a task. -- **Task Model**: - - `id`: Primary key - - `title`: string - - `description`: string (optional) - - `isComplete`: boolean (default `false`) - - _(Optional)_ `userId` to link tasks to the user who created them -- **Database**: PostgreSQL - - Provide instructions/migrations to set up: - - `users` table (with hashed passwords) - - `tasks` table -- **Setup**: - - `npm install` to install dependencies - - `npm run start` (or `npm run dev`) to run the server - - Document any environment variables (e.g., database connection string, JWT secret) - -### 3. Frontend (React + TypeScript) - -- **Login / Register**: - - Simple forms for **Register** and **Login**. - - Store JWT (e.g., in `localStorage`) upon successful login. - - If not authenticated, the user should not see the tasks page. -- **Tasks Page**: - - Fetch tasks from `GET /tasks` (including auth token in headers). - - Display the list of tasks. - - Form to create a new task (`POST /tasks`). - - Buttons/fields to update a task (`PUT /tasks/:id`). - - Button to delete a task (`DELETE /tasks/:id`). -- **Navigation**: - - Show `Login`/`Register` if not authenticated. - - Show `Logout` if authenticated. -- **Setup**: - - `npm install` then `npm start` (or `npm run dev`) to run. - - Document how to point the frontend at the backend (e.g., `.env` file, base URL). - ---- - -## Deliverables - -1. **Fork the Public Repository**: **Fork** this repo into your own GitHub account. -2. **Implement Your Solution** in the forked repository. Make sure you're README file has: - - Steps to set up the database (migrations, environment variables). - - How to run the backend. - - How to run the frontend. - - Any relevant notes on testing. - - Salary Expectations per month (Mandatory) -3. **Short Video Demo**: Provide a link (in a `.md` file in your forked repo) to a brief screen recording showing: - - Registering a user - - Logging in - - Creating, updating, and deleting tasks -4. **Deadline**: Submissions are due **Sunday, Feb 23th 11:59 pm PST**. - -> **Note**: Please keep your solution minimal. The entire project is intended to be completed in around 3 hours. Focus on core features (registration, login, tasks CRUD) rather than polished UI or extra features. - ---- - -## Evaluation Criteria - -1. **Functionality** - - Does registration and login work correctly (with password hashing)? - - Are tasks protected by authentication? - - Does the tasks CRUD flow work end-to-end? - -2. **Code Quality** - - Is the code structured logically and typed in TypeScript? - - Are variable/function names descriptive? - -3. **Clarity** - - Is the `README.md` (in your fork) clear and detailed about setup steps? - - Easy to run and test? - -4. **Maintainability** - - Organized logic (controllers/services, etc.) - - Minimal hard-coded values - -Good luck, and we look forward to your submission! +Step 1 - Create the database using terminal + +createdb -U task_db + +Step 2 - Connect to the database + +psql -U -d task_db + +Step 3 - It will take you inside the database (task_db=#) so create the tables by entering : + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + password TEXT NOT NULL +); + +CREATE TABLE tasks ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + isComplete BOOLEAN DEFAULT FALSE, + userId INTEGER REFERENCES users(id) ON DELETE CASCADE +); + +Step 4 - Verify the tables you created + +\dt + +You should get : + List of relations + Schema | Name | Type | Owner +--------+--------+-------+---------------- + public | tasks | table | + public | users | table | +(2 rows) + + +Step 5 - Exit psql + +\q + +You can connect DBeaver to the database by starting a new connection and connecting it to task_db. It will allow for easy management and checking of the database entries. + +Now clone the repository and cd to the repository + +Now coming to the setup of the .env files +.env file for backend - It should be in the backend directory + +PORT= (I used 5001) +DATABASE_URL=postgres://:@localhost:/task_db +JWT_SECRET= + +.env file for the frontend - It should be in the frontend directory + +REACT_APP_API_URL=http://localhost: (I used 5001 for the backend so entered that here> + +Once the .env files are setup, considering you are in the main repo directory, follow these steps: + +1. cd backend +2. npm install +3. To start the backend, enter node src/server.js + +4. cd .. +5. cd frontend +6. npm install +7. npm start + +Now, the frontend and backend both are running and the database is also connected (Make sure you follow the database connection steps and ensure the postgresql service is active through your terminal). + +Pay Expectations (Required to be stated) +As far as the pay is concerned, I am comfortable with the pay stated on the job posting, which is 20-30 per hour. I am doing this to gain further experience and exposure to software engineering and full stack development + +Video (Sorry for delay - I realised late that I had not uploaded the video link) +https://drive.google.com/file/d/10f-BETNStuWpGHRZQ_oqNLnp-gI6s_U1/view?usp=sharing +the working flow can be seen here diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 000000000..6072b2e36 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "bcryptjs": "^3.0.2", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "helmet": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "pg": "^8.13.3" + } +} diff --git a/backend/src/config/db.js b/backend/src/config/db.js new file mode 100644 index 000000000..e1b165c4b --- /dev/null +++ b/backend/src/config/db.js @@ -0,0 +1,8 @@ +const { Pool } = require("pg"); +require("dotenv").config(); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +module.exports = pool; diff --git a/backend/src/constants/constants.js b/backend/src/constants/constants.js new file mode 100644 index 000000000..1011746cd --- /dev/null +++ b/backend/src/constants/constants.js @@ -0,0 +1,33 @@ +module.exports = { + // Auth related constants + JWT: { + SECRET_KEY: process.env.JWT_SECRET, + EXPIRY: "1h" + }, + + // HTTP Status codes + HTTP_STATUS: { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + SERVER_ERROR: 500 + }, + + // Error messages + ERROR_MESSAGES: { + ACCESS_DENIED: "Access Denied", + INVALID_TOKEN: "Invalid Token", + INVALID_CREDENTIALS: "Invalid credentials", + USER_EXISTS: "User already exists", + SERVER_ERROR: "Server error" + }, + + // Success messages + SUCCESS_MESSAGES: { + TASK_DELETED: "Task deleted" + }, + + // Crypto related + BCRYPT_SALT_ROUNDS: 10 +}; \ No newline at end of file diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js new file mode 100644 index 000000000..36565be4e --- /dev/null +++ b/backend/src/controllers/authController.js @@ -0,0 +1,24 @@ +const authService = require("../services/authService"); +const { HTTP_STATUS, ERROR_MESSAGES } = require("../constants/constants"); + +const register = async (req, res) => { + const { username, password } = req.body; + try { + const result = await authService.register(username, password); + res.status(HTTP_STATUS.CREATED).json(result); + } catch (error) { + res.status(HTTP_STATUS.SERVER_ERROR).json({ error: ERROR_MESSAGES.USER_EXISTS }); + } +}; + +const login = async (req, res) => { + const { username, password } = req.body; + try { + const result = await authService.login(username, password); + res.json(result); + } catch (error) { + res.status(HTTP_STATUS.BAD_REQUEST).json({ message: error.message }); + } +}; + +module.exports = { register, login }; diff --git a/backend/src/controllers/taskController.js b/backend/src/controllers/taskController.js new file mode 100644 index 000000000..db50d0880 --- /dev/null +++ b/backend/src/controllers/taskController.js @@ -0,0 +1,44 @@ +const taskService = require("../services/taskService"); +const { HTTP_STATUS, ERROR_MESSAGES } = require("../constants/constants"); + +const getTasks = async (req, res) => { + try { + const tasks = await taskService.getTasks(req.user.id); + res.json(tasks); + } catch (error) { + res.status(HTTP_STATUS.SERVER_ERROR).json({ error: ERROR_MESSAGES.SERVER_ERROR }); + } +}; + +const createTask = async (req, res) => { + const { title, description } = req.body; + try { + const task = await taskService.createTask(title, description, req.user.id); + res.json(task); + } catch (error) { + res.status(HTTP_STATUS.SERVER_ERROR).json({ error: ERROR_MESSAGES.SERVER_ERROR }); + } +}; + +const updateTask = async (req, res) => { + const { id } = req.params; + const { title, description, isComplete } = req.body; + try { + const task = await taskService.updateTask(id, title, description, isComplete); + res.json(task); + } catch (error) { + res.status(HTTP_STATUS.SERVER_ERROR).json({ error: ERROR_MESSAGES.SERVER_ERROR }); + } +}; + +const deleteTask = async (req, res) => { + const { id } = req.params; + try { + const result = await taskService.deleteTask(id); + res.json(result); + } catch (error) { + res.status(HTTP_STATUS.SERVER_ERROR).json({ error: ERROR_MESSAGES.SERVER_ERROR }); + } +}; + +module.exports = { getTasks, createTask, updateTask, deleteTask }; diff --git a/backend/src/database/queries.js b/backend/src/database/queries.js new file mode 100644 index 000000000..14fae819f --- /dev/null +++ b/backend/src/database/queries.js @@ -0,0 +1,17 @@ +const queries = { + // User queries + users: { + insert: "INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id, username", + findByUsername: "SELECT * FROM users WHERE username = $1", + }, + + // Task queries + tasks: { + getAll: "SELECT * FROM tasks WHERE userId = $1", + insert: "INSERT INTO tasks (title, description, userId) VALUES ($1, $2, $3) RETURNING *", + update: "UPDATE tasks SET title=$1, description=$2, isComplete=$3 WHERE id=$4 RETURNING *", + delete: "DELETE FROM tasks WHERE id=$1", + } +}; + +module.exports = queries; \ No newline at end of file diff --git a/backend/src/middlewares/authMiddleware.js b/backend/src/middlewares/authMiddleware.js new file mode 100644 index 000000000..c420bddc3 --- /dev/null +++ b/backend/src/middlewares/authMiddleware.js @@ -0,0 +1,17 @@ +const jwt = require("jsonwebtoken"); +const { JWT, HTTP_STATUS, ERROR_MESSAGES } = require("../constants/constants"); + +const authMiddleware = (req, res, next) => { + const token = req.header("Authorization")?.split(" ")[1]; + if (!token) return res.status(HTTP_STATUS.UNAUTHORIZED).json({ message: ERROR_MESSAGES.ACCESS_DENIED }); + + try { + const verified = jwt.verify(token, JWT.SECRET_KEY); + req.user = verified; + next(); + } catch (err) { + res.status(HTTP_STATUS.BAD_REQUEST).json({ message: ERROR_MESSAGES.INVALID_TOKEN }); + } +}; + +module.exports = authMiddleware; diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js new file mode 100644 index 000000000..79f6d491c --- /dev/null +++ b/backend/src/routes/authRoutes.js @@ -0,0 +1,9 @@ +const express = require("express"); +const { register, login } = require("../controllers/authController"); + +const router = express.Router(); + +router.post("/register", register); +router.post("/login", login); + +module.exports = router; diff --git a/backend/src/routes/taskRoutes.js b/backend/src/routes/taskRoutes.js new file mode 100644 index 000000000..47533c6b1 --- /dev/null +++ b/backend/src/routes/taskRoutes.js @@ -0,0 +1,12 @@ +const express = require("express"); +const { getTasks, createTask, updateTask, deleteTask } = require("../controllers/taskController"); +const authMiddleware = require("../middlewares/authMiddleware"); + +const router = express.Router(); + +router.get("/", authMiddleware, getTasks); +router.post("/", authMiddleware, createTask); +router.put("/:id", authMiddleware, updateTask); +router.delete("/:id", authMiddleware, deleteTask); + +module.exports = router; diff --git a/backend/src/server.js b/backend/src/server.js new file mode 100644 index 000000000..de1f27411 --- /dev/null +++ b/backend/src/server.js @@ -0,0 +1,20 @@ +const express = require("express"); +const cors = require("cors"); +const helmet = require("helmet"); +const dotenv = require("dotenv"); + +dotenv.config(); + +const authRoutes = require("./routes/authRoutes"); +const taskRoutes = require("./routes/taskRoutes"); + +const app = express(); +app.use(cors()); +app.use(helmet()); +app.use(express.json()); + +app.use("/auth", authRoutes); +app.use("/tasks", taskRoutes); + +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/backend/src/services/authService.js b/backend/src/services/authService.js new file mode 100644 index 000000000..9ebc49c7e --- /dev/null +++ b/backend/src/services/authService.js @@ -0,0 +1,30 @@ +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const pool = require("../config/db"); +const queries = require("../database/queries"); +const { JWT, BCRYPT_SALT_ROUNDS, ERROR_MESSAGES } = require("../constants/constants"); + +class AuthService { + async register(username, password) { + const hashedPassword = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS); + const result = await pool.query(queries.users.insert, [username, hashedPassword]); + return result.rows[0]; + } + + async login(username, password) { + const user = await pool.query(queries.users.findByUsername, [username]); + if (user.rows.length === 0) { + throw new Error(ERROR_MESSAGES.INVALID_CREDENTIALS); + } + + const isMatch = await bcrypt.compare(password, user.rows[0].password); + if (!isMatch) { + throw new Error(ERROR_MESSAGES.INVALID_CREDENTIALS); + } + + const token = jwt.sign({ id: user.rows[0].id }, JWT.SECRET_KEY, { expiresIn: JWT.EXPIRY }); + return { token }; + } +} + +module.exports = new AuthService(); \ No newline at end of file diff --git a/backend/src/services/taskService.js b/backend/src/services/taskService.js new file mode 100644 index 000000000..853199ad4 --- /dev/null +++ b/backend/src/services/taskService.js @@ -0,0 +1,27 @@ +const pool = require("../config/db"); +const queries = require("../database/queries"); +const { SUCCESS_MESSAGES } = require("../constants/constants"); + +class TaskService { + async getTasks(userId) { + const tasks = await pool.query(queries.tasks.getAll, [userId]); + return tasks.rows; + } + + async createTask(title, description, userId) { + const task = await pool.query(queries.tasks.insert, [title, description, userId]); + return task.rows[0]; + } + + async updateTask(id, title, description, isComplete) { + const task = await pool.query(queries.tasks.update, [title, description, isComplete, id]); + return task.rows[0]; + } + + async deleteTask(id) { + await pool.query(queries.tasks.delete, [id]); + return { message: SUCCESS_MESSAGES.TASK_DELETED }; + } +} + +module.exports = new TaskService(); diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..b87cb0044 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,46 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..e640eb683 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,45 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.126", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.2.0", + "react-scripts": "5.0.1", + "typescript": "^4.9.5", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 000000000..5046dda96 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + Task Manager + + + +
+ + diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/frontend/public/logo192.png differ diff --git a/frontend/public/logo512.png b/frontend/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/frontend/public/logo512.png differ diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 000000000..040f6fbf4 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Task Manager", + "name": "Task Manager", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 000000000..34ad6fc84 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import NavBar from './components/NavBar'; +import Home from './components/Home'; +import Login from './components/Login'; +import Register from './components/Register'; +import Tasks from './components/Tasks'; +import ProtectedRoute from './routes/ProtectedRoute'; + +const App: React.FC = () => { + return ( + <> + + + {/* Public Routes */} + } /> + } /> + } /> + + {/* Protected Routes */} + + + + } + /> + + {/* Fallback Route */} + 404 - Not Found} /> + + + ); +}; + +export default App; diff --git a/frontend/src/components/Home.tsx b/frontend/src/components/Home.tsx new file mode 100644 index 000000000..b817df410 --- /dev/null +++ b/frontend/src/components/Home.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import '../styles/Home.css'; + +const Home: React.FC = () => { + const token = localStorage.getItem('token'); + + return ( +
+
+

Welcome to Task Manager

+

+ A simple and efficient way to organize your daily tasks. Keep track of your + to-dos, mark them complete, and stay productive. +

+
+
+

âś“ Create Tasks

+

Easily add new tasks with titles and descriptions

+
+
+

⚡ Track Progress

+

Mark tasks as complete as you finish them

+
+
+

đź“ť Edit Anytime

+

Update task details whenever needed

+
+
+ {!token ? ( +
+ Get Started + Login +
+ ) : ( +
+ Go to Tasks +
+ )} +
+
+ ); +}; + +export default Home; \ No newline at end of file diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx new file mode 100644 index 000000000..f45aae26f --- /dev/null +++ b/frontend/src/components/Login.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; +import { useApi } from '../hooks/useApi'; +import '../styles/Auth.css'; + +const Login: React.FC = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const { login } = useAuth(); + const api = useApi(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const data = await api.post('/auth/login', { username, password }); + login(data.token); + } catch (error) { + console.error('Error:', error); + alert('Error logging in'); + } + }; + + return ( +
+

Login

+
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+ Don't have an account? Register here +
+
+ ); +}; + +export default Login; diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx new file mode 100644 index 000000000..2dfe6682e --- /dev/null +++ b/frontend/src/components/NavBar.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; +import '../styles/NavBar.css'; + +const NavBar: React.FC = () => { + const { isAuthenticated, logout } = useAuth(); + + return ( + + ); +}; + +export default NavBar; diff --git a/frontend/src/components/Register.tsx b/frontend/src/components/Register.tsx new file mode 100644 index 000000000..6bae87d2c --- /dev/null +++ b/frontend/src/components/Register.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import '../styles/Auth.css'; + +const Register: React.FC = () => { + const navigate = useNavigate(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + const data = await response.json(); + + if (response.ok) { + navigate('/login'); + } else { + alert(data.error || 'Registration failed'); + } + } catch (error) { + console.error('Error:', error); + alert('Error registering'); + } + }; + + return ( +
+

Register

+
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+ Already have an account? Login here +
+
+ ); +}; + +export default Register; diff --git a/frontend/src/components/Tasks.tsx b/frontend/src/components/Tasks.tsx new file mode 100644 index 000000000..c4614dd4c --- /dev/null +++ b/frontend/src/components/Tasks.tsx @@ -0,0 +1,281 @@ +import React, { useEffect, useState } from 'react'; +import '../styles/Tasks.css'; + +interface Task { + id: number; + title: string; + description: string; + iscomplete: boolean; // or "isComplete" depending on your API + userid?: number; // or "userId" +} + +const Tasks: React.FC = () => { + const [tasks, setTasks] = useState([]); + const [showModal, setShowModal] = useState(false); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [editingTask, setEditingTask] = useState(null); + const token = localStorage.getItem('token'); + + // Fetch tasks on mount + useEffect(() => { + const fetchTasks = async () => { + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/tasks`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const data = await response.json(); + if (response.ok) { + setTasks(data); + } else { + alert(data.error || 'Failed to fetch tasks'); + } + } catch (error) { + console.error('Error fetching tasks:', error); + } + }; + fetchTasks(); + }, [token]); + + // Create a new task + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch(`${process.env.REACT_APP_API_URL}/tasks`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ title, description }), + }); + const data = await response.json(); + if (response.ok) { + // Add the new task to state + setTasks((prev) => [...prev, data]); + // Reset form + setTitle(''); + setDescription(''); + setShowModal(false); + } else { + alert(data.error || 'Failed to create task'); + } + } catch (error) { + console.error('Error creating task:', error); + } + }; + + // Toggle complete status + const handleToggleComplete = async (taskId: number, currentStatus: boolean) => { + try { + const existingTask = tasks.find(t => t.id === taskId); + if (!existingTask) return; + + const response = await fetch( + `${process.env.REACT_APP_API_URL}/tasks/${taskId}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + title: existingTask.title, + description: existingTask.description, + isComplete: !currentStatus // Changed from iscomplete to isComplete + }), + } + ); + const data = await response.json(); + if (response.ok) { + setTasks((prev) => + prev.map((task) => (task.id === taskId ? data : task)) + ); + } else { + alert(data.error || 'Failed to update task status'); + } + } catch (error) { + console.error('Error updating task status:', error); + } + }; + + // Save edited task + const handleSaveEdit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!editingTask) return; + + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/tasks/${editingTask.id}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + title: editingTask.title, + description: editingTask.description, + }), + } + ); + const data = await response.json(); + if (response.ok) { + setTasks((prev) => + prev.map((task) => (task.id === editingTask.id ? data : task)) + ); + setEditingTask(null); + } else { + alert(data.error || 'Failed to update task'); + } + } catch (error) { + console.error('Error updating task:', error); + } + }; + + // Delete a task + const handleDelete = async (taskId: number) => { + try { + const response = await fetch( + `${process.env.REACT_APP_API_URL}/tasks/${taskId}`, + { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + const data = await response.json(); + if (response.ok) { + setTasks((prev) => prev.filter((task) => task.id !== taskId)); + alert(data.message || 'Task deleted'); + } else { + alert(data.error || 'Failed to delete task'); + } + } catch (error) { + console.error('Error deleting task:', error); + } + }; + + return ( +
+
+

Tasks

+ +
+ + {/* Modal */} + {showModal && ( +
+
+ +

Create New Task

+
+
+ + setTitle(e.target.value)} + required + /> +
+
+ +