Skip to content
Merged
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
237 changes: 224 additions & 13 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -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); }
60 changes: 27 additions & 33 deletions frontend/src/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="auth">
<h2>Login</h2>
{err && <p className="error">{err}</p>}
<form onSubmit={submit}>
<input
type="email"
placeholder="Email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
<button type="submit">Login</button>
</form>
<p>
Don’t have an account? <Link to="/register">Register</Link>
</p>
<div className="center-page">
<div className="auth">
<h2>Welcome back</h2>

{err && <p className="error">{err}</p>}

<form onSubmit={submit}>
<input type="email" placeholder="Email" value={form.email} onChange={handle("email")} />
<input type="password" placeholder="Password" value={form.password} onChange={handle("password")} />
<button type="submit">Log in</button>
</form>

<p>Don’t have an account? <Link to="/register">Create one →</Link></p>
</div>
</div>
);
}
48 changes: 18 additions & 30 deletions frontend/src/Register.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,32 +21,21 @@ export default function Register({ onAuth }) {
};

return (
<div className="auth">
<h2>Register</h2>
{err && <p className="error">{err}</p>}
<form onSubmit={submit}>
<input
placeholder="Username"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
/>
<input
type="email"
placeholder="Email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
<button type="submit">Register</button>
</form>
<p>
Already have an account? <Link to="/login">Login</Link>
</p>
<div className="center-page">
<div className="auth">
<h2>Create account</h2>

{err && <p className="error">{err}</p>}

<form onSubmit={submit}>
<input placeholder="Username" value={form.username} onChange={handle("username")} />
<input type="email" placeholder="Email" value={form.email} onChange={handle("email")} />
<input type="password" placeholder="Password" value={form.password} onChange={handle("password")} />
<button type="submit">Sign up</button>
</form>

<p>Already have an account? <Link to="/login">Log in</Link></p>
</div>
</div>
);
}
Loading
Loading