Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
52 changes: 38 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx">
</script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="./vite.svg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<link
rel="preconnect"
href="https://fonts.googleapis.com"
>
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet"
>
<title>Todo</title>
</head>

<body>
<div id="root"></div>
<script
type="module"
src="./src/main.jsx"
>
</script>
</body>

</html>
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"date-fns": "^4.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.7",
"zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
Binary file added public/assets/check-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/close.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/filter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/logo1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/note-background-smaller.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/note-background.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/set.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/signature.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/user.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AppLayout } from "./components/AppLayout"

export const App = () => {
return (
<h1>React Boilerplate</h1>
<>
<AppLayout />
</>
)
}
12 changes: 12 additions & 0 deletions src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { HeroMessage } from "./HeroMessage"
import { TaskArea } from "./TaskArea"

export const AppLayout = () => {
return (

<div className="flex flex-col lg:flex-row max-w-screen h-full bg-white m-[5px] p-[5px] rounded-[20px]">
<HeroMessage />
<TaskArea />
</div>
)
}
8 changes: 8 additions & 0 deletions src/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const EmptyState = () => {
return (
<div className="bg-white opacity-80 p-12 text-center rounded-2xl mt-8 text-black mx-auto">
<p className="text-xl">No tasks yet</p>
<p className="text-md">Click “New Task +” to get started</p>
</div>
)
}
21 changes: 21 additions & 0 deletions src/components/HeroMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@


export const HeroMessage = () => {
return (
<div>
<header className="flex justify-between items-center px-4 pt-2">
<img
src="../assets/logo1.png" alt="Logo"
className="w-full max-w-[100px] h-auto" />
<img
src="../assets/user.png" alt="User"
className="w-6 h-auto" />
</header>
<div className="flex flex-col-reverse py-10 px-12 sm:py-24 sm:px-20 max-w-[400px] sm:max-w-[500px] md:max-w-[600px] m-auto mx-auto lg:min-w-[500px] lg:px-12 lg:py-40">
<h1 className="font-medium text-3xl sm:text-5xl md:text-6xl">From Clutter to <span className="text-[#FFC116] bg-black px-2 rounded-full">Done</span> with Done-ly</h1>
<p className="font-light text-base sm:text-xl">Make it simple</p>

</div>
</div>
);
}
67 changes: 67 additions & 0 deletions src/components/TaskArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TaskButtons } from "./TaskButtons"
import { useState } from "react"
import { TaskForm } from "./TaskForm"
import { TaskList } from "./TaskList"
import { EmptyState } from "./EmptyState"
import { useTaskStore } from "../stores/useTaskStore"
import { TaskCount } from "./TaskCount"


export const TaskArea = () => {
const [showForm, setShowForm] = useState(false)

const [filter, setFilter] = useState<"all" | "completed" | "uncompleted">("all")
const tasks = useTaskStore((state) => state.tasks)

const filteredTasks = tasks.filter((task) => {
if (filter === "completed") return task.completed
if (filter === "uncompleted") return !task.completed
return true // For "all" filter
})

return (
<div className="flex flex-col gap-10 bg-[url(/assets/note-background-smaller.webp)] bg-cover bg-no-repeat bg-center h-screen w-full rounded-[20px] px-3 py-4 sm:bg-[url(/assets/note-background.webp)]">

<div className="flex justify-between items-start">
<TaskButtons
text="New Task +"
onClick={() => setShowForm(true)}
/>

{tasks.length > 0 && (
<div className="flex flex-col gap-1 ">
{["all", "completed", "uncompleted"].map((type) => (
<button
key={type}
onClick={() => setFilter(type as "all" | "completed" | "uncompleted")}
className={`px-2 py-1 rounded-2xl ${filter === type ? "bg-violet-400 text-white" : "bg-white text-black"}`}
>
{type}
</button>
))}
</div>
)}
</div>


{showForm && (
<div className="flex justify-center">
<TaskForm onClose={() => setShowForm(false)} />
</div>
)}

{tasks.length === 0 && !showForm && <EmptyState />}

{
filteredTasks.length > 0 && (
<div className="flex flex-col items-center">
<div className="scrollbar-always-show max-h-[60vh] w-full px-1">
<TaskList tasks={filteredTasks} />
</div>
<TaskCount />
</div>
)
}
</div >
)
}
20 changes: 20 additions & 0 deletions src/components/TaskButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
type TaskButtonsProps = {
text: string
onClick: () => void
}


export const TaskButtons = ({ text, onClick }: TaskButtonsProps) => {
console.log("TaskButtons rendered")

return (
<>
<button
type="button"
className="bg-white px-3 py-1 rounded-[20px] font-medium max-w-40 hover:bg-[#FFDEA6] sm:text-lg"
onClick={onClick}>
{text}
</button>
</>
)
}
17 changes: 17 additions & 0 deletions src/components/TaskCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useTaskStore } from "../stores/useTaskStore"

export const TaskCount = () => {
const totalTasks = useTaskStore((state) => state.tasks.length)
const completedTasks = useTaskStore((state) =>
state.tasks.filter((task) => task.completed).length
)
const uncompletedTasks = useTaskStore((state) =>
state.tasks.filter((task) => !task.completed).length
)

return (
<div className="bg-white px-3 py-1 rounded-lg mt-8 text-black mx-auto text-center font-semibold">
<p>Completed: {completedTasks}/{totalTasks}</p>
</div>
)
}
84 changes: 84 additions & 0 deletions src/components/TaskForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect, useState, useRef } from "react"
import { useTaskStore } from "../stores/useTaskStore"

type TaskFormProps = {
onClose: () => void
}


export const TaskForm = ({ onClose }: TaskFormProps) => {
const [text, setText] = useState("")
const [checked, setChecked] = useState(false)
const [error, setError] = useState("")
const textareaRef = useRef<HTMLTextAreaElement>(null)
const addTask = useTaskStore((state) => state.addTask)

useEffect(() => {
textareaRef.current?.focus()
}, [])

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (text.trim() === "") {
setError("Please enter a task")
textareaRef.current?.focus()
return
}
addTask(text)
setText("")
setChecked(false)
onClose()
}

return (

<form
onSubmit={handleSubmit}
className="flex flex-col gap-2">

<div className="mt-2 flex justify-between bg-white rounded-[15px] mx-auto sm:w-[450px]">
<label htmlFor="task-input" className="sr-only">Task text</label>
<textarea
id="task-input"
aria-describedby={error ? "task-error" : undefined}
ref={textareaRef}
rows={1}
value={text}
onChange={(e) => {
setText(e.target.value)
if (error) setError("")
}}
className="focus:outline-none p-2 resize-none mx-auto sm:w-[450px]"
placeholder="Write a task..."
onInput={(e) => {
e.currentTarget.style.height = "auto"
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
}}
/>

<div className="flex flex-col justify-between gap-4 items-end p-2">

<button
type="button"
className="rounded p-2"
onClick={onClose}>
<img
src="/assets/close.png"
alt="Close"
className="w-4 h-4 object-contain" />
</button>

<button
type="submit"
className="font-medium rounded-2xl py-1 px-2 bg-[#FFDEA6] sm:py-2 sm:px-4 hover:bg-[#FFC116] sm:text-lg">
Done
</button>
</div>
</div>
{error && <p
id="task-error"
className="text-red-500 bg-white px-3 font-bold text-sm mt-1 mx-2 rounded-lg sm:text-base">{error}</p>}
</form>

)
}
45 changes: 45 additions & 0 deletions src/components/TaskList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useTaskStore } from "../stores/useTaskStore"
import { format } from "date-fns"
import { Task } from "../stores/useTaskStore"

interface TaskListProps {
tasks: Task[]
}

export const TaskList: React.FC<TaskListProps> = ({ tasks }) => {
const removeTask = useTaskStore((state) => state.removeTask)
const toggleTask = useTaskStore((state) => state.toggleTask)

return (

<ul className="flex flex-col gap-3 px-2 max-w-[600px] ">
{tasks.map((task) => (
<li key={task.id} className="flex items-center gap-3">
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task.id)}
className="w-5 h-5 accent-black"
/>
<div className="bg-white text-lg rounded-xl p-2 flex items-start justify-between w-full min-w-[200px] max-w-full">
<div className="flex flex-col gap-1 min-w-0 w-full">
<p className="text-sm text-gray-500">
{format(new Date(task.createdAt), "yyyy-MM-dd")}
</p>
<p className={`flex-1 break-words whitespace-pre-wrap ${task.completed ? "line-through text-gray-400" : ""}`} style={{ wordBreak: "break-word" }}>
{task.text}
</p>
</div>
<button
onClick={() => removeTask(task.id)} className="ml-2 flex-shrink-0">
<img
src="/assets/set.png"
alt="Delete task"
className="w-5 h-5 sm:w-6 sm:h-6 object-contain" />
</button>
</div>
</li>
))}
</ul>
)
}
18 changes: 17 additions & 1 deletion src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
@import "tailwindcss";

:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-family: "Montserrat", sans-serif;
background-color: #EDEBEB;

}

/* Force scrollbars to always show */
.scrollbar-always-show {
overflow-y: scroll;
scrollbar-width: auto; /* Firefox */
scrollbar-color: #888 #EDEBEB; /* Firefox */
}

.scrollbar-always-show::-webkit-scrollbar {
display: block; /* Chrome/Safari */

}
Loading