feat: add sort and tick to dashboard UI

This commit is contained in:
Mpho10111
2026-08-05 01:10:09 +02:00
parent eb353598c7
commit f7efd57a85
6 changed files with 581 additions and 157 deletions

View File

@@ -1,6 +1,8 @@
"use client";
import { useState } from "react";
import Column from "@/components/Column";
import AddTaskForm from "@/components/AddTaskForm";
export default function Dashboard() {
const [tasks, setTasks] = useState({
@@ -22,19 +24,12 @@ export default function Dashboard() {
done: [],
});
const addTask = () => {
const title = prompt("Task title:");
if (!title) return;
const description = prompt("Description:") || "";
const dueDate = prompt("Due date (YYYY-MM-DD):") || "";
const topic = prompt("Topic:") || "";
const newTask = { title, description, dueDate, topic };
const [showForm, setShowForm] = useState(false);
const addTask = (task: any) => {
setTasks((prev) => ({
...prev,
todo: [...prev.todo, newTask],
todo: [...prev.todo, task],
}));
};
@@ -43,70 +38,27 @@ export default function Dashboard() {
<h1 style={styles.title}>Planner Danner</h1>
<div style={styles.board}>
<Column title="To Do" tasks={tasks.todo} onAdd={addTask} />
<Column title="To Do" tasks={tasks.todo} onAdd={() => setShowForm(true)} />
<Column title="In Progress" tasks={tasks.inProgress} />
<Column title="Done" tasks={tasks.done} />
</div>
{showForm && (
<AddTaskForm
onSubmit={addTask}
onClose={() => setShowForm(false)}
/>
)}
</main>
);
}
function Column({
title,
tasks,
onAdd,
}: {
title: string;
tasks: any[];
onAdd?: () => void;
}) {
return (
<div style={styles.column}>
<div style={styles.columnHeader}>
<h2 style={styles.columnTitle}>{title}</h2>
<div style={styles.headerActions}>
{onAdd && (
<button onClick={onAdd} style={styles.iconButton}>
+
</button>
)}
{/* ✅ FIXED: sort now uses SAME button style */}
<button style={styles.iconButton}></button>
<button style={styles.iconButton}></button>
</div>
</div>
{tasks.map((task, index) => (
<div key={index} style={styles.card}>
<div style={styles.cardHeader}>
<div style={styles.cardTitle}>{task.title}</div>
{/* same exact style */}
<button style={styles.iconButton}></button>
</div>
<div style={styles.cardDescription}>
{task.description.slice(0, 30)}
{task.description.length > 30 && "..."}
</div>
<div style={styles.cardDate}>📅 {task.dueDate}</div>
</div>
))}
</div>
);
}
const styles = {
container: {
minHeight: "100vh",
background: "linear-gradient(to bottom, #38bdf8, #0ea5e9)",
padding: "30px",
},
title: {
textAlign: "center" as const,
fontSize: "90px",
@@ -119,84 +71,10 @@ const styles = {
`,
marginBottom: "30px",
},
board: {
display: "flex",
gap: "20px",
justifyContent: "center",
},
};
column: {
background: "#f8fafc",
padding: "15px",
borderRadius: "12px",
flex: 1,
minHeight: "65vh",
maxHeight: "65vh",
overflowY: "auto",
boxShadow: "0 10px 25px rgba(0,0,0,0.1)",
},
columnHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "15px",
},
columnTitle: {
color: "#0f172a",
fontWeight: "700",
fontSize: "25px",
margin: 0,
},
/* ✅ SINGLE SOURCE OF TRUTH FOR ALL ICONS */
iconButton: {
background: "transparent",
border: "none",
color: "#0f172a",
fontSize: "22px",
fontWeight: "600",
cursor: "pointer",
padding: "0",
},
headerActions: {
display: "flex",
gap: "10px",
alignItems: "center",
},
card: {
background: "#dae4f0",
padding: "18px",
borderRadius: "12px",
marginBottom: "15px",
color: "#0f172a",
fontSize: "16px",
},
cardHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "6px",
},
cardTitle: {
fontWeight: "700",
marginBottom: "6px",
},
cardDescription: {
fontSize: "13px",
opacity: 0.7,
marginBottom: "8px",
},
cardDate: {
fontSize: "12px",
opacity: 0.6,
},
};

View File

@@ -1,26 +1,35 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
--primary-bg: linear-gradient(to bottom, #38bdf8, #0ea5e9);
--card-bg: #dae4f0;
--text-dark: #0f172a;
--text-light: #e0f2fe;
--accent: #0284c7;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
background: var(--primary-bg);
}
h1, h2, h3 {
font-weight: 900;
letter-spacing: 2px;
}
/* ✅ Consistent placeholder styling */
input::placeholder,
textarea::placeholder {
color: #6b7280; /* same grey as Title/Description */
font-family: inherit;
font-size: 14px;
}
/* ✅ Style the dropdown placeholder option */
select option[value=""] {
color: #6b7280; /* same grey */
font-family: inherit;
font-size: 14px;
}

183
components/AddTaskForm.tsx Normal file
View File

@@ -0,0 +1,183 @@
"use client";
import { useState } from "react";
export default function AddTaskForm({
onSubmit,
onClose,
}: {
onSubmit: (task: any) => void;
onClose: () => void;
}) {
const [formData, setFormData] = useState({
title: "",
description: "",
dueDate: "",
topic: "",
});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.topic) {
alert("Please select a topic before saving.");
return;
}
onSubmit(formData);
onClose();
};
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2 style={styles.title}>Add New Task</h2>
<form onSubmit={handleSubmit} style={styles.form}>
<input
name="title"
placeholder="Title"
value={formData.title}
onChange={handleChange}
required
style={styles.input}
/>
<textarea
name="description"
placeholder="Description"
value={formData.description}
onChange={handleChange}
style={styles.textarea}
/>
<input
name="dueDate"
type="date"
value={formData.dueDate}
onChange={handleChange}
required
style={styles.input}
/>
{/* ✅ Dropdown with placeholder */}
<select
name="topic"
value={formData.topic}
onChange={handleChange}
required
style={styles.input}
>
<option value="">Select a topic...</option>
<option value="Academic">Academic</option>
<option value="Work">Work</option>
<option value="Personal">Personal</option>
<option value="Creative">Creative</option>
<option value="Household">Household</option>
<option value="Other">Other</option>
</select>
<div style={styles.actions}>
<button type="submit" style={styles.saveButton}>
Save
</button>
<button type="button" onClick={onClose} style={styles.cancelButton}>
Cancel
</button>
</div>
</form>
</div>
</div>
);
}
const styles = {
overlay: {
position: "fixed" as const,
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "rgba(0,0,0,0.4)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1000,
},
modal: {
background: "#f8fafc",
padding: "25px",
borderRadius: "12px",
boxShadow: "0 10px 25px rgba(0,0,0,0.2)",
width: "350px",
},
title: {
marginBottom: "15px",
fontSize: "22px",
fontWeight: "700",
color: "var(--text-dark)",
textAlign: "center" as const,
},
form: {
display: "flex",
flexDirection: "column" as const,
gap: "12px",
},
input: {
padding: "10px",
borderRadius: "8px",
border: "1px solid #cbd5e1",
fontSize: "14px",
color: "var(--text-dark)", // dark text
fontFamily: "inherit", // same font as title/description
},
textarea: {
padding: "10px",
borderRadius: "8px",
border: "1px solid #cbd5e1",
fontSize: "14px",
color: "var(--text-dark)",
fontFamily: "inherit",
minHeight: "60px",
},
select: {
padding: "10px",
borderRadius: "8px",
border: "1px solid #cbd5e1",
fontSize: "14px",
color: "var(--text-dark)",
fontFamily: "inherit",
backgroundColor: "white", // consistent with inputs
},
actions: {
display: "flex",
justifyContent: "space-between",
marginTop: "15px",
},
saveButton: {
background: "#0ea5e9",
color: "white",
border: "none",
padding: "8px 16px",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "600",
},
cancelButton: {
background: "#e2e8f0",
color: "#0f172a",
border: "none",
padding: "8px 16px",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "600",
},
};

36
components/Board.tsx Normal file
View File

@@ -0,0 +1,36 @@
"use client";
import { useState } from "react";
import Column from "./Column";
export default function Board() {
const [tasks, setTasks] = useState([
{ id: 1, title: "Task 1", description: "Do something", status: "Todo", dueDate: "2026-08-10", topic: "Lab" },
{ id: 2, title: "Task 2", description: "Another thing", status: "In-Progress", dueDate: "2026-08-12", topic: "Project" },
]);
const handleStatusChange = (taskId: number, newStatus: "Todo" | "In-Progress" | "Complete") => {
setTasks(prev =>
prev.map(t => (t.id === taskId ? { ...t, status: newStatus } : t))
);
};
return (
<div style={{ display: "flex", gap: "20px" }}>
<Column
title="Todo"
tasks={tasks.filter(t => t.status === "Todo")}
onStatusChange={handleStatusChange}
/>
<Column
title="In-Progress"
tasks={tasks.filter(t => t.status === "In-Progress")}
onStatusChange={handleStatusChange}
/>
<Column
title="Complete"
tasks={tasks.filter(t => t.status === "Complete")}
onStatusChange={handleStatusChange}
/>
</div>
);
}

318
components/Column.tsx Normal file
View File

@@ -0,0 +1,318 @@
"use client";
import { useState } from "react";
export default function Column({
title,
tasks,
onAdd,
onStatusChange,
}: {
title: string;
tasks: any[];
onAdd?: () => void;
onStatusChange?: (index: number, newStatus: "Todo" | "In-Progress" | "Complete") => void;
}) {
const [popupTaskIndex, setPopupTaskIndex] = useState<number | null>(null);
const [sortMode, setSortMode] = useState<"none" | "topic" | "dueDate">("none");
const [showMenu, setShowMenu] = useState(false);
const handleCircleClick = (index: number) => {
const task = tasks[index];
if (task.status !== "Complete") {
setPopupTaskIndex(index);
}
};
const handleConfirm = (yes: boolean) => {
if (popupTaskIndex !== null && onStatusChange) {
const task = tasks[popupTaskIndex];
if (task.status === "Todo" && yes) {
onStatusChange(popupTaskIndex, "In-Progress");
} else if (task.status === "In-Progress" && yes) {
onStatusChange(popupTaskIndex, "Complete");
}
}
setPopupTaskIndex(null);
};
const handleSort = (mode: "none" | "topic" | "dueDate") => {
setSortMode(mode);
setShowMenu(false);
};
const sortedTasks = [...tasks].sort((a, b) => {
if (sortMode === "topic") {
return a.topic.localeCompare(b.topic);
}
if (sortMode === "dueDate") {
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
}
return 0;
});
return (
<div style={styles.column}>
<div style={styles.columnHeader}>
<h2 style={styles.columnTitle}>{title}</h2>
<div style={styles.headerActions}>
{onAdd && (
<button onClick={onAdd} style={styles.iconButton} title="Add Task">
+
</button>
)}
{/* Sort button */}
<div style={{ position: "relative" }}>
<button
onClick={() => setShowMenu(!showMenu)}
style={styles.iconButton}
title="Sort Tasks"
>
</button>
{showMenu && (
<div style={styles.dropdown}>
<button onClick={() => handleSort("topic")} style={styles.dropdownItem}>
Sort by Topic
</button>
<button onClick={() => handleSort("dueDate")} style={styles.dropdownItem}>
Sort by Due Date
</button>
<button onClick={() => handleSort("none")} style={styles.dropdownItem}>
Clear Sorting
</button>
</div>
)}
</div>
<button style={styles.iconButton} title="More Options"></button>
</div>
</div>
{sortMode !== "none" && (
<div style={styles.sortLabel}>
Currently sorted by: {sortMode === "topic" ? "Topic" : "Due Date"}
</div>
)}
{sortedTasks.map((task, index) => (
<div key={index} style={styles.card}>
<div style={styles.cardHeader}>
{/* Hollow circle */}
<div
style={{
...styles.circle,
cursor: task.status === "Complete" ? "default" : "pointer",
}}
onClick={() => handleCircleClick(index)}
title="Change Status"
>
{task.status === "In-Progress" || task.status === "Complete" ? "✔" : ""}
</div>
<div style={styles.cardTitle}>{task.title}</div>
<button style={styles.iconButton}></button>
</div>
<div style={styles.cardDescription}>
{task.description.slice(0, 30)}
{task.description.length > 30 && "..."}
</div>
<div style={styles.cardDate}>📅 {task.dueDate}</div>
{task.topic && (
<div style={styles.cardTopic}>
<em>{task.topic}</em>
</div>
)}
</div>
))}
{/* Popup */}
{popupTaskIndex !== null && (
<div style={styles.overlay}>
<div style={styles.modal}>
<h3 style={styles.title}>
{tasks[popupTaskIndex].status === "Todo"
? "Are you ready to start the task?"
: "Have you finished this task?"}
</h3>
<div style={styles.actions}>
<button onClick={() => handleConfirm(true)} style={styles.saveButton}>
Yes
</button>
<button onClick={() => handleConfirm(false)} style={styles.cancelButton}>
No
</button>
</div>
</div>
</div>
)}
</div>
);
}
const styles = {
column: {
background: "#f8fafc",
padding: "15px",
borderRadius: "12px",
flex: 1,
minHeight: "65vh",
maxHeight: "65vh",
overflowY: "auto" as const,
boxShadow: "0 10px 25px rgba(0,0,0,0.1)",
},
columnHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "15px",
},
columnTitle: {
color: "#0f172a",
fontWeight: "700",
fontSize: "25px",
margin: 0,
},
iconButton: {
background: "transparent",
border: "none",
color: "#0f172a",
fontSize: "22px",
fontWeight: "600",
cursor: "pointer",
padding: "0",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
headerActions: {
display: "flex",
gap: "10px",
alignItems: "center",
},
sortLabel: {
fontSize: "13px",
opacity: 0.7,
marginBottom: "10px",
fontStyle: "italic",
color: "#0f172a",
},
card: {
background: "#dae4f0",
padding: "18px",
borderRadius: "12px",
marginBottom: "15px",
color: "#0f172a",
fontSize: "16px",
},
cardHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "6px",
gap: "10px",
},
cardTitle: {
fontWeight: "700",
flex: 1,
},
cardDescription: {
fontSize: "13px",
opacity: 0.7,
marginBottom: "8px",
},
cardDate: {
fontSize: "12px",
opacity: 0.6,
},
cardTopic: {
fontSize: "12px",
opacity: 0.7,
textAlign: "right" as const,
marginTop: "6px",
fontStyle: "italic",
},
circle: {
width: "20px",
height: "20px",
border: "2px solid black",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "14px",
fontWeight: "bold",
backgroundColor: "white",
},
overlay: {
position: "fixed" as const,
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "rgba(0,0,0,0.4)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 1000,
},
modal: {
background: "#f8fafc",
padding: "25px",
borderRadius: "12px",
boxShadow: "0 10px 25px rgba(0,0,0,0.2)",
width: "300px",
textAlign: "center" as const,
},
title: {
marginBottom: "15px",
fontSize: "18px",
fontWeight: "600",
color: "#0f172a",
},
actions: {
display: "flex",
justifyContent: "space-between",
marginTop: "15px",
},
saveButton: {
background: "#0ea5e9",
color: "white",
border: "none",
padding: "8px 16px",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "600",
},
cancelButton: {
background: "#e2e8f0",
color: "#0f172a",
border: "none",
padding: "8px 16px",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "600",
},
dropdown: {
position: "absolute" as const,
top: "30px",
right: 0,
background: "#f8fafc",
border: "1px solid #cbd5e1",
borderRadius: "8px",
boxShadow: "0 5px 15px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column" as const,
zIndex: 100,
},
dropdownItem: {
padding: "8px 12px",
background: "transparent",
border: "none",
textAlign: "left" as const,
cursor: "pointer",
fontSize: "14px",
color: "#0f172a",
},
};

0
components/TaskCard.tsx Normal file
View File