diff --git a/frontend/src/App.css b/frontend/src/App.css index 805d917..e1f73df 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,24 +1,235 @@ -/* reset and full-height */ +/* ======================================================= + GLOBAL THEME & UTILS + ======================================================= */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap'); + +:root { + /* palette */ + --bg : #f8fafc; /* page background */ + --text : #0f172a; /* primary text */ + --accent : #3b82f6; /* indigo buttons / links */ + --accent-hover : #2563eb; + --danger : #ef4444; + + --card-bg : #ffffff; + --shadow-md : 0 10px 25px rgba(0,0,0,0.08); + --shadow-sm : 0 8px 18px rgba(0,0,0,0.06); +} + html, body, #root { height: 100%; margin: 0; + font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + -webkit-font-smoothing: antialiased; } -/* center everything */ -.App-container { +ul { margin: 0; padding: 0; list-style: none; } +h1,h2,h3,h4,h5,h6 { margin: 0; } + +/* ---------- button utility ---------- */ +.btn { + background: var(--accent); + color: #fff; + border: none; + border-radius: 20px; + padding: 0.6rem 1.25rem; + font-weight: 600; + cursor: pointer; + transition: transform .15s, box-shadow .15s; +} +.btn:hover { + background: var(--accent-hover); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(59,130,246,.35); +} + +/* ======================================================= + LOGIN / REGISTER (center-page + auth card) + ======================================================= */ +.center-page { + width: 100vw; height: 100vh; display: flex; justify-content: center; - align-items: flex-start; - min-height: 100%; - padding: 2rem 1rem; - box-sizing: border-box; - background: #121212; + align-items: center; } -/* constrain auth/login and tasks panels */ -.auth, -.tasks { +.auth { width: 100%; - max-width: 400px; - margin: 2rem auto; + max-width: 420px; + background: var(--card-bg); + border-radius: 24px; + padding: 2.75rem 3rem; + box-shadow: var(--shadow-md); + display: flex; + flex-direction: column; + gap: 1.3rem; +} + +.auth h2 { + text-align: center; + font-size: 1.9rem; + color: var(--accent-hover); + font-weight: 600; +} + +.auth form { display: flex; flex-direction: column; gap: 1rem; } + +.auth input, +.auth select { + padding: 0.75rem 1rem; + font-size: 1rem; + border: 2px solid #e2e8f0; + border-radius: 16px; + background: #f9fafb; + color: var(--text); + transition: border-color .2s, box-shadow .2s; +} +.auth input:focus, +.auth select:focus { + outline: none; + border-color: #60a5fa; + box-shadow: 0 0 0 3px rgba(96,165,250,.35); +} + +.auth button { margin-top: .25rem; } + +.error { + background: #fee2e2; + color: #b91c1c; + padding: .8rem 1rem; + border-radius: 12px; + text-align: center; + font-size: .9rem; +} + +.auth p { font-size: .9rem; text-align: center; } +.auth a { color: var(--accent); font-weight: 600; text-decoration: none; } +.auth a:hover { text-decoration: underline; } + +/* ======================================================= + TOP BAR + ======================================================= */ +.topbar { + background: var(--text); + color: #fff; + display: flex; + justify-content: space-between; + align-items: center; + padding: .9rem 1.4rem; +} +.topbar .brand { font-weight: 600; font-size: 1.15rem; } +.topbar .logout { + background: var(--danger); +} +.topbar .logout:hover { + background: #f87171; + box-shadow: 0 4px 12px rgba(239,68,68,.35); +} + +/* ======================================================= + TASKS WRAPPER + ======================================================= */ +.tasks-wrapper { + max-width: 960px; + margin: 2.5rem auto; + padding: 0 1rem; + display: flex; + flex-direction: column; + gap: 1.75rem; +} + +.tasks-wrapper h1 { + text-align: center; + font-size: 2.3rem; + font-weight: 600; +} + +/* ---------- new task form ---------- */ +.task-form { + display: grid; + grid-template-columns: repeat(auto-fit,minmax(140px,1fr)); + gap: .8rem; +} +.task-form input, +.task-form select { + padding: .6rem .9rem; + border: 2px solid #e2e8f0; + border-radius: 12px; + background: #f9fafb; + font-size: .95rem; +} + +/* ======================================================= + TASK CARDS GRID + ======================================================= */ +.task-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill,minmax(260px,1fr)); +} + +.card { + position: relative; + background: var(--card-bg); + border-radius: 18px; + box-shadow: var(--shadow-sm); + padding: 1.25rem 1rem 1rem; + display: flex; + flex-direction: column; + gap: .7rem; + transition: transform .12s; +} +.card:hover { transform: translateY(-3px); } +.card.overdue { background: #fff1f2; } + +.card input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; } + +.card h2 { + font-size: 1.05rem; + line-height: 1.35; + font-weight: 600; + margin: 0; + cursor: pointer; +} +.strike { text-decoration: line-through; opacity: .65; } + +.card input[type="text"] { + flex: 1; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: .45rem .7rem; + font-size: .95rem; +} + +/* meta row */ +.meta { + display: flex; + justify-content: space-between; + font-size: .85rem; + align-items: center; +} + +.badge { + padding: .15rem .6rem; + border-radius: 9999px; + font-weight: 600; + color: #fff; +} +.badge.low { background: #22c55e; } +.badge.med { background: #f97316; } +.badge.high { background: var(--danger); } + +/* delete icon */ +.del { + position: absolute; + top: 9px; + right: 12px; + border: none; + background: transparent; + font-size: 1.3rem; + color: #9ca3af; + cursor: pointer; } +.del:hover { color: var(--danger); } diff --git a/frontend/src/Login.jsx b/frontend/src/Login.jsx index 126d289..a5a0818 100644 --- a/frontend/src/Login.jsx +++ b/frontend/src/Login.jsx @@ -4,44 +4,38 @@ import api from "./api"; export default function Login({ onAuth }) { const [form, setForm] = useState({ email: "", password: "" }); - const [err, setErr] = useState(""); + const [err, setErr] = useState(""); const nav = useNavigate(); - const submit = async (e) => { - e.preventDefault(); - try { - const { data } = await api.post("/api/auth/login", form); - localStorage.setItem("token", data.token); - onAuth(data.token); - nav("/"); - } catch (e) { - setErr(e.response?.data?.message || "Login failed"); - } -}; + const handle = (key) => (e) => setForm({ ...form, [key]: e.target.value }); + const submit = async (e) => { + e.preventDefault(); + try { + const { data } = await api.post("/api/auth/login", form); + localStorage.setItem("token", data.token); + onAuth(data.token); + nav("/"); + } catch (e) { + setErr(e.response?.data?.message || "Login failed"); + } + }; return ( -
-

Login

- {err &&

{err}

} -
- setForm({ ...form, email: e.target.value })} - /> - setForm({ ...form, password: e.target.value })} - /> - -
-

- Don’t have an account? Register -

+
+
+

Welcome back

+ + {err &&

{err}

} + +
+ + + +
+ +

Don’t have an account? Create one →

+
); } diff --git a/frontend/src/Register.jsx b/frontend/src/Register.jsx index 7044f68..79678f9 100644 --- a/frontend/src/Register.jsx +++ b/frontend/src/Register.jsx @@ -3,13 +3,12 @@ import { Link, useNavigate } from "react-router-dom"; import api from "./api"; export default function Register({ onAuth }) { - // form data const [form, setForm] = useState({ username: "", email: "", password: "" }); - // error message - const [err, setErr] = useState(""); + const [err, setErr] = useState(""); const nav = useNavigate(); - // submit registration + const handle = (key) => (e) => setForm({ ...form, [key]: e.target.value }); + const submit = async (e) => { e.preventDefault(); try { @@ -22,32 +21,21 @@ export default function Register({ onAuth }) { }; return ( -
-

Register

- {err &&

{err}

} -
- setForm({ ...form, username: e.target.value })} - /> - setForm({ ...form, email: e.target.value })} - /> - setForm({ ...form, password: e.target.value })} - /> - -
-

- Already have an account? Login -

+
+
+

Create account

+ + {err &&

{err}

} + +
+ + + + +
+ +

Already have an account? Log in

+
); } diff --git a/frontend/src/Tasks.jsx b/frontend/src/Tasks.jsx index 98b4c48..1356cda 100644 --- a/frontend/src/Tasks.jsx +++ b/frontend/src/Tasks.jsx @@ -1,231 +1,187 @@ import { useState, useEffect } from "react"; import api from "./api"; +/* -------------------------------------------------- */ +/* Main component */ +/* -------------------------------------------------- */ export default function Tasks({ onLogout }) { + /* state */ const [tasks, setTasks] = useState([]); - const [title, setTitle] = useState(""); - const [deadline, setDeadline] = useState(""); - const [importance, setImportance] = useState("2"); - const [difficulty, setDifficulty] = useState("3"); - const [editTaskId, setEditTaskId] = useState(null); - const [editTaskText, setEditTaskText] = useState(""); - - - /* load tasks once */ + const [form, setForm] = useState({ + title: "", + deadline: "", + importance: "2", + difficulty: "3", + }); + const [editId, setEditId] = useState(null); + const [editTxt, setEditTxt] = useState(""); + + /* load once */ useEffect(() => { - api - .get("/api/tasks") - .then((res) => setTasks(res.data)) - .catch(console.error); + api.get("/api/tasks").then((r) => setTasks(r.data)).catch(console.error); }, []); - /* create new task */ - const add = async (e) => { + /* helpers */ + const handleForm = (k) => (e) => setForm({ ...form, [k]: e.target.value }); + + const addTask = async (e) => { e.preventDefault(); - if (!title.trim()) return; + if (!form.title.trim()) return; try { - const { data } = await api.post("/api/tasks", { title, deadline, importance, difficulty }); + const { data } = await api.post("/api/tasks", form); setTasks([data, ...tasks]); - setTitle(""); - setDeadline(""); - setImportance("2"); - setDifficulty("3"); - } catch (err) { - console.error(err); - alert("Create failed"); + setForm({ title: "", deadline: "", importance: "2", difficulty: "3" }); + } catch { + alert("Failed to create task"); } }; - /* delete a task */ - const remove = async (id) => { + const delTask = async (id) => { try { await api.delete(`/api/tasks/${id}`); setTasks(tasks.filter((t) => t._id !== id)); - } catch (err) { - console.error(err); + } catch { alert("Delete failed"); } }; - /* toggle task completion between 0 and 100 */ - const toggle = async (task) => { - const newComp = task.completion === 100 ? 0 : 100; + const toggleComplete = async (t) => { + const newComp = t.completion === 100 ? 0 : 100; try { - const { data } = await api.put(`/api/tasks/${task._id}`, { - ...task, + const { data } = await api.put(`/api/tasks/${t._id}`, { + ...t, completion: newComp, }); - setTasks(tasks.map((t) => (t._id === data._id ? data : t))); - } catch (err) { - console.error(err); + setTasks(tasks.map((task) => (task._id === data._id ? data : task))); + } catch { alert("Update failed"); } }; - const handleUpdateTask = async (id) => { + const saveEdit = async (id) => { try { - const { data } = await api.put(`/api/tasks/${id}`, { - title: editTaskText, - }); - const updated = tasks.map((t) => (t._id === data._id ? data : t)); - setTasks(updated); - setEditTaskId(null); - setEditTaskText(""); - } catch (err) { - console.error(err); - alert("Failed to update task."); + const { data } = await api.put(`/api/tasks/${id}`, { title: editTxt }); + setTasks(tasks.map((t) => (t._id === data._id ? data : t))); + setEditId(null); + setEditTxt(""); + } catch { + alert("Edit failed"); } }; + /* -------------------------------------------------- */ return ( -
- -

Your Tasks

- -
- setTitle(e.target.value)} - /> - setDeadline(e.target.value)} - /> - - setDifficulty(e.target.value)} - /> - -
- -
    - {tasks.map((t) => { - const diffMs = t.deadline ? new Date(t.deadline) - new Date() : null; - const isOverdue = diffMs !== null && diffMs < 0 && t.completion !== 100; - - let dueText = ""; - if (t.deadline) { - if (isOverdue) { - dueText = "Overdue!"; - } else { - const days = Math.floor(diffMs / 86400000); - const hours = Math.floor((diffMs % 86400000) / 3600000); - if (days === 0 && hours === 0) { - dueText = "Due now"; - } else if (days === 0) { - dueText = `Due in ${hours}h`; - } else { - dueText = `Due in ${days}d ${hours}h`; + <> + {/* top bar */} + + + {/* page content */} +
    +

    Your Tasks

    + + {/* new-task form */} +
    + + + + + +
    + + {/* task list */} +
      + {tasks.map((t) => { + /* --- helpers per task --- */ + const diffMs = t.deadline ? new Date(t.deadline) - new Date() : null; + const overdue = diffMs !== null && diffMs < 0 && t.completion !== 100; + + let dueTxt = ""; + if (t.deadline) { + if (overdue) dueTxt = "Overdue!"; + else { + const d = Math.floor(diffMs / 86400000); + const h = Math.floor((diffMs % 86400000) / 3600000); + dueTxt = d === 0 && h === 0 ? "Due now" + : `Due in ${d ? d + "d " : ""}${h}h`; } } - } - - let priorityLabel = "Low"; - let priorityColor = "green"; - if (t.score > 15) { - priorityLabel = "High"; - priorityColor = "red"; - } else if (t.score > 8) { - priorityLabel = "Medium"; - priorityColor = "orange"; - } - - return ( -
    • - {/* completion checkbox */} - toggle(t)} - /> - - {/* task title */} - {editTaskId === t._id ? ( + + /* score → priority badge */ + let pri = "Low", badge = "badge low"; + if (t.score > 15) { pri = "High"; badge = "badge high"; } + else if (t.score > 8) { pri = "Med"; badge = "badge med"; } + + return ( +
    • + {/* complete toggle */} setEditTaskText(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleUpdateTask(t._id); - }} - onBlur={() => setEditTaskId(null)} - style={{ - flexGrow: 1, - padding: "4px 8px", - fontSize: "1rem", - }} - autoFocus + type="checkbox" + checked={t.completion === 100} + onChange={() => toggleComplete(t)} /> - ) : ( - { - setEditTaskId(t._id); - setEditTaskText(t.title); - }} - style={{ - flexGrow: 1, - textDecoration: - t.completion === 100 ? "line-through" : "none", - opacity: t.completion === 100 ? 0.6 : 1, - cursor: "pointer", - userSelect: "none", - }} - > - {t.title} - - )} - - {/* NEW: Priority Label */} - - ({priorityLabel}) - - - {/* due date display */} - {t.deadline && ( - {dueText} - )} - - {/* delete button */} - -
    • - ); - })} -
    -
+ + {/* title or edit */} + {editId === t._id ? ( + setEditTxt(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && saveEdit(t._id)} + onBlur={() => setEditId(null)} + autoFocus + /> + ) : ( +

{ + setEditId(t._id); + setEditTxt(t.title); + }} + className={t.completion === 100 ? "strike" : ""} + > + {t.title} +

+ )} + + {/* meta row */} +
+ {pri} + {t.deadline && {dueTxt}} +
+ + {/* delete */} + + + ); + })} + + + ); } - - -//GitHub testing to see if terminal works diff --git a/frontend/src/index.css b/frontend/src/index.css index 08a3ac9..4460eb6 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,11 +1,15 @@ +/* ---- Global theme (light) ---- */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap'); + :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + /* light mode by default */ + color-scheme: light; + color: #0f172a; /* dark slate text */ + background-color: #f8fafc; /* soft light background */ font-synthesis: none; text-rendering: optimizeLegibility; @@ -13,28 +17,25 @@ -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - +/* ---- Elements ---- */ body { - margin: 0; - display: flex; - place-items: center; + margin: 0; /* remove default margin */ + /* remove the old flex + place-items that pushed content left */ min-width: 320px; min-height: 100vh; } -h1 { - font-size: 3.2em; - line-height: 1.1; +a { + font-weight: 500; + color: #3b82f6; + text-decoration: none; +} +a:hover { + color: #2563eb; } +h1 { font-size: 3.2em; line-height: 1.1; } + button { border-radius: 8px; border: 1px solid transparent; @@ -42,27 +43,23 @@ button { font-size: 1em; font-weight: 500; font-family: inherit; - background-color: #1a1a1a; + background-color: #1d4ed8; + color: #ffffff; cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; + transition: border-color 0.25s, background-color 0.25s; } +button:hover { background-color: #2563eb; } button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} +button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } -@media (prefers-color-scheme: light) { +/* ---- Dark-mode override ---- */ +@media (prefers-color-scheme: dark) { :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; + color: rgba(255, 255, 255, 0.87); + background-color: #1e293b; /* slate-900 */ } + a { color: #93c5fd; } + a:hover { color: #bfdbfe; } + button { background-color: #3b82f6; } + button:hover { background-color: #60a5fa; } }