Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions client/src/App.css
Original file line number Diff line number Diff line change
@@ -1,51 +1,60 @@
/* Main App container */
.App {
max-width: 600px;
margin: auto;
margin-top: 2em;
padding: 1em;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 8px 0px rgba(0, 0, 0, 0.1);
max-width: 600px; /* Limit width of app */
margin: auto; /* Center horizontally */
margin-top: 2em; /* Space from top */
padding: 1em; /* Inner spacing */
background: white; /* White background */
border: 1px solid #ddd; /* Light gray border */
border-radius: 8px; /* Rounded corners */
box-shadow: 0 4px 8px 0px rgba(0, 0, 0, 0.1); /* Subtle shadow */
}

/* Title styles */
h1 {
font-weight: normal;
margin: 0;
font-weight: normal; /* Not bold */
margin: 0; /* Remove default spacing */
}

/* Task list styles */
.tasks {
list-style: none;
padding: 0;
list-style: none; /* Remove bullet points */
padding: 0; /* Remove default list padding */
}

/* Completed task style */
.done {
text-decoration: line-through;
opacity: 0.5;
text-decoration: line-through; /* Strike-through text */
opacity: 0.5; /* Make it look faded */
}

/* Input fields (text + submit button) */
input[type="text"],
input[type="submit"] {
font-size: 0.9em;
padding: 4px 8px;
font-size: 0.9em; /* Slightly smaller font */
padding: 4px 8px; /* Inner spacing */
}

/* Task label (checkbox + title + delete button) */
label {
vertical-align: top;
vertical-align: top; /* Align with checkbox nicely */
}

/* Delete button (SVG icon) */
.delete-button {
padding-top: 4px;
margin-left: 8px;
cursor: pointer;
opacity: 0.3;
visibility: hidden;
margin-left: 8px; /* Space after task text */
cursor: pointer; /* Show pointer on hover */
opacity: 0.3; /* Light/faded by default */
visibility: hidden; /* Hidden until hover */
}

/* Show delete button only when task row is hovered */
li:hover .delete-button {
visibility: visible;
}

/* Darken delete button on hover */
.delete-button:hover {
opacity: 0.6;
}
24 changes: 17 additions & 7 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,56 @@ import './App.css';

import TasksList from './components/TaskList';

// Main App component
const App = () => {
// State: all tasks fetched from backend
const [tasks, setTasks] = useState([]);

// State: new task input value
const [newTaskTitle, setNewTaskTitle] = useState('');

// Function to fetch all tasks from backend
// useCallback used so reference of function doesn't change unnecessarily
const getTasks = useCallback(() => {
fetch('/api/tasks')
.then(res => res.json())
.then(setTasks);
.then(setTasks); // update tasks state
});

// Fetch tasks when component first mounts
useEffect(() => {
getTasks();
}, []);
}, []); // [] means only run once on mount

// Add a new task
const clickAddTask = event => {
event.preventDefault();
event.preventDefault(); // prevent form reload

fetch('/api/tasks/add', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTaskTitle }),
body: JSON.stringify({ title: newTaskTitle }), // send new title
}).then(() => {
setNewTaskTitle('');
getTasks();
setNewTaskTitle(''); // clear input
getTasks(); // refresh task list
});
};

return (
<div className="App">
<h1>My Tasks</h1>

{/* Task list component */}
<TasksList tasks={tasks} updateTasks={getTasks} />

{/* Form to add new task */}
<form onSubmit={clickAddTask}>
<input
type="text"
size="30"
placeholder="New Task"
value={newTaskTitle}
onChange={event => setNewTaskTitle(event.target.value)}
onChange={event => setNewTaskTitle(event.target.value)} // update state
/>
<input className="btn-primary" type="submit" value="Add" />
</form>
Expand Down
23 changes: 18 additions & 5 deletions client/src/components/TaskList.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
import React from 'react';

// TaskList component
// Props:
// - tasks: Array of task objects (fetched from backend)
// - updateTasks: Function to refresh tasks after add/delete/update
const TaskList = ({ tasks, updateTasks }) => {

// Function to delete a task
const clickDeleteTask = (event, task) => {
event.preventDefault();
event.preventDefault(); // Prevent default link/submit behavior

// Call backend API to delete task by ID
fetch(`/api/tasks/delete/${task._id}`, {
method: 'delete',
})
.then(res => res.json())
.then(() => updateTasks());
.then(res => res.json()) // Parse response
.then(() => updateTasks()); // Refresh task list after deletion
};

// Function to toggle task "done" status
const toggleDone = task => {
// Call backend API to update "done" field (true/false)
fetch(`/api/tasks/update/${task._id}`, {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: !task.done }),
}).then(() => updateTasks());
body: JSON.stringify({ done: !task.done }), // Toggle current state
}).then(() => updateTasks()); // Refresh tasks after update
};

return (
<ul className="tasks">
{/* Loop through tasks and render each one */}
{tasks.map(task => (
<li key={task._id}>
<label className={task.done ? 'done' : ''}>
{/* Checkbox to mark task as done/undone */}
<input
type="checkbox"
checked={task.done}
onChange={() => toggleDone(task)}
/>{' '}
{task.title}

{/* Delete button (SVG icon) */}
<svg
onClick={event => clickDeleteTask(event, task)}
className="delete-button"
Expand Down
12 changes: 10 additions & 2 deletions client/src/index.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/* Remove default margin and padding from body */
body {
margin: 0;
padding: 0;

/* Use system font stack for better performance and native look */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

/* Smooth font rendering on Mac and Windows */
-webkit-font-smoothing: antialiased; /* for WebKit browsers (Chrome, Safari) */
-moz-osx-font-smoothing: grayscale; /* for Firefox on Mac */

/* Base font size for whole app */
font-size: 16px;
}

/* Style for inline <code> blocks */
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
Expand Down
14 changes: 13 additions & 1 deletion client/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
// Import React library to use JSX and component features
import React from 'react';

// Import ReactDOM for rendering React components into the actual DOM
import ReactDOM from 'react-dom';

// Import global styles (index.css file applies to the whole app)
import './index.css';

// Import the main App component (our root component)
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));
// Render the App component into the HTML element with id="root"
// This connects React with our HTML page (public/index.html)
ReactDOM.render(
<App />, // Main React component (like container for whole UI)
document.getElementById('root') // Target DOM node (from public/index.html)
);
24 changes: 21 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
// Import required modules
const express = require('express'); // Express framework for building web server
const path = require('path'); // Node.js module to handle file/directory paths
const bodyParser = require('body-parser'); // Middleware to parse incoming JSON request bodies

// Initialize Express application
const app = express();

// Import routes for tasks (CRUD operations are inside this file)
const routeTasks = require('./src/routes/tasks');

// Serve static files from React build folder
// e.g. after running `npm run build` in client, it serves /client/build
app.use(express.static(path.join(__dirname, 'client/build')));

// Middleware to parse JSON bodies in incoming requests
// Example: { "title": "My Task" } will be available in req.body
app.use(bodyParser.json());

// Mount task routes at /api/tasks
// Example: GET /api/tasks → will go to tasks route
// ⚠️ NOTE: The `(req, res) => res.sendStatus(401)` looks wrong
// because it will always return "Unauthorized". Normally, you only need: app.use('/api/tasks', routeTasks);
app.use('/api/tasks', routeTasks, (req, res) => res.sendStatus(401));

// Fallback route: If no API/route matches, serve React index.html
// This is needed for React Router in production (client-side routing)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname + '/client/build/index.html'));
});

// Define port (use environment variable or default to 5000)
const port = process.env.PORT || 5000;

// Start the server and listen on defined port
app.listen(port);

// Log a message to confirm server is running
console.log(`listening on ${port}`);
16 changes: 15 additions & 1 deletion src/db.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
// Import mongoose library
// Mongoose is used to connect Node.js to MongoDB
// It provides schemas & models for structured data handling
const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/simple-mern');
// Connect to MongoDB database
// - 'mongodb://localhost/simple-mern'
// → mongodb:// → protocol
// → localhost → database server running on local machine
// → simple-mern → database name (created automatically if not exists)
mongoose.connect('mongodb://localhost/simple-mern', {
useNewUrlParser: true, // Recommended option to avoid deprecation warnings
useUnifiedTopology: true, // Better server discovery & monitoring
})
.then(() => console.log('✅ Connected to MongoDB'))
.catch(err => console.error('❌ MongoDB connection error:', err));

// Export mongoose instance so that the same connection can be used in other files
module.exports = mongoose;
8 changes: 8 additions & 0 deletions src/models/task.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
// Import mongoose connection instance
// We are importing from "../db" where mongoose.connect() is defined
const mongoose = require('../db');

// Define Task schema
// - title: String, required (every task must have a title)
// - done: Boolean, default value = false (by default, task is not completed)
const task = new mongoose.Schema({
title: { type: String, required: true },
done: { type: Boolean, default: false }
});

// Create Task model based on the schema
// This will create a "tasks" collection in MongoDB (pluralized automatically)
const Task = mongoose.model('Task', task);

// Export Task model so it can be used in routes (CRUD operations)
module.exports = Task;
Loading