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
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.5",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@mui/styled-engine-sc": "^7.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"styled-components": "^6.1.18",
"zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
Expand Down
16 changes: 13 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { Header } from "./components/Header";
import { TodoList } from "./components/TodoList";
import { TodoForm } from "./components/TodoForm";

import { Box } from "@mui/material";

export const App = () => {
return (
<h1>React Boilerplate</h1>
)
}
<Box sx={{ minHeight: "100vh", bgcolor: "#f5f5f5", py: 4, px: 2 }}>
<Header></Header>
<TodoList />
<TodoForm></TodoForm>
</Box>
);
};
18 changes: 18 additions & 0 deletions src/components/EmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Box, Typography } from "@mui/material";
import InboxIcon from "@mui/icons-material/Inbox";

export const EmptyState = () => (
<Box
sx={{
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<InboxIcon sx={{ fontSize: 60, color: "primary.main" }} />
<Typography variant='h6'>There's nothing here...</Typography>
<Typography variant='body2'>Add your first task to get started.</Typography>
</Box>
);
34 changes: 34 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useTodoStore } from "../stores/useTodoStore";
import { Box, Typography, Paper } from "@mui/material";

export const Header = () => {
const total = useTodoStore((state) => state.tasks.length);
const completed = useTodoStore(
(state) => state.tasks.filter((task) => task.completed).length
);

return (
<Paper
component='header'
elevation={3}
sx={{
maxWidth: 600,
mx: "auto",
my: 4,
p: 3,
borderRadius: 2,
textAlign: "center",
backgroundColor: "background.paper",
borderBottom: "2px solid",
borderColor: "divider",
}}
>
<Typography variant='h2' component='h1' gutterBottom>
To do app
</Typography>
<Typography variant='subtitle1' color='text.secondary'>
{completed} of {total} tasks completed
</Typography>
</Paper>
);
};
48 changes: 48 additions & 0 deletions src/components/TaskItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Checkbox,
IconButton,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";

export const TaskItem = ({ todo, onToggle, onDelete }) => {
const labelId = `checkbox-list-label-${todo.id}`;

return (
<ListItem
disablePadding
secondaryAction={
<IconButton
edge='end'
aria-label='delete task'
onClick={() => onDelete(todo.id)}
>
<DeleteIcon />
</IconButton>
}
>
<ListItemButton dense sx={{ width: "100%" }}>
<ListItemIcon>
<Checkbox
checked={todo.completed}
onChange={() => onToggle(todo.id)}
inputProps={{ "aria-labelledby": labelId }}
/>
</ListItemIcon>
<ListItemText
id={labelId}
primary={todo.text}
sx={{
textDecoration: todo.completed ? "line-through" : "none",
color: todo.completed ? "text.disabled" : "text.primary",
wordBreak: "break-word",
whiteSpace: "pre-wrap",
}}
/>
</ListItemButton>
</ListItem>
);
};
50 changes: 50 additions & 0 deletions src/components/TodoForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useState } from "react";
import { useTodoStore } from "../stores/useTodoStore";

import { TextField, Button, Stack, Paper } from "@mui/material";

export const TodoForm = () => {
const [text, setText] = useState("");
const createTask = useTodoStore((state) => state.createTask);

const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
createTask(text.trim());
setText("");
};

return (
<Paper
component='form'
onSubmit={handleSubmit}
elevation={3}
sx={{
maxWidth: 600,
mx: "auto",
my: 2,
p: 2,
borderRadius: 2,
backgroundColor: "background.paper",
}}
>
<Stack direction='row' spacing={2}>
<TextField
label='Add a task'
variant='outlined'
fullWidth
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Button
type='submit'
variant='contained'
color='primary'
sx={{ px: 3 }}
>
+
</Button>
</Stack>
</Paper>
);
};
96 changes: 96 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useTodoStore } from "../stores/useTodoStore";
import { List, Paper, Box, Typography, Button } from "@mui/material";
import InboxIcon from "@mui/icons-material/Inbox";
import { TaskItem } from "./TaskItem";

export const TodoList = () => {
const todos = useTodoStore((state) => state.tasks);
const toggleTaskCompletion = useTodoStore(
(state) => state.toggleTaskCompletion
);
const deleteTask = useTodoStore((state) => state.deleteTask);
const clearCompletedTasks = useTodoStore(
(state) => state.clearCompletedTasks
);

const uncompleted = todos.filter((t) => !t.completed);
const completed = todos.filter((t) => t.completed);

const renderTasks = (taskList) =>
taskList.map((todo) => (
<TaskItem
key={todo.id}
todo={todo}
onToggle={toggleTaskCompletion}
onDelete={deleteTask}
/>
));

return (
<Paper
elevation={3}
sx={{
maxWidth: 600,
mx: "auto",
my: 2,
p: 2,
borderRadius: 2,
backgroundColor: "background.paper",
minHeight: 150,
}}
>
{todos.length === 0 ? (
<Box
sx={{
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
}}
>
<InboxIcon sx={{ fontSize: 60, color: "primary.main" }} />
<Typography variant='h6'>There's nothing here...</Typography>
<Typography variant='body2'>
Add your first task to get started.
</Typography>
</Box>
) : (
<Box display='flex' flexDirection='column' gap={4}>
{uncompleted.length > 0 && (
<Box>
<Typography variant='h6' sx={{ mb: 1 }}>
Tasks
</Typography>
<List>{renderTasks(uncompleted)}</List>
</Box>
)}

{completed.length > 0 && (
<Box>
<Box
sx={{
mb: 1,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant='h6'>Completed</Typography>
<Button
size='small'
color='error'
onClick={clearCompletedTasks}
sx={{ textTransform: "none" }}
>
Clear all
</Button>
</Box>
<List>{renderTasks(completed)}</List>
</Box>
)}
</Box>
)}
</Paper>
);
};
7 changes: 7 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
}

body,
html {
margin: 0;
padding: 0;
box-sizing: border-box;
}
40 changes: 40 additions & 0 deletions src/stores/useTodoStore.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";

export const useTodoStore = create(
persist(
(set) => ({
tasks: [],

createTask: (text) =>
set((state) => {
const newTask = {
id: Date.now(),
text,
completed: false,
};
return { tasks: [newTask, ...state.tasks] };
}),

toggleTaskCompletion: (id) =>
set((state) => ({
tasks: state.tasks.map((task) =>
task.id === id ? { ...task, completed: !task.completed } : task
),
})),

deleteTask: (id) =>
set((state) => ({
tasks: state.tasks.filter((task) => task.id !== id),
})),

clearCompletedTasks: () =>
set((state) => ({
tasks: state.tasks.filter((task) => !task.completed),
})),
}),
{
name: "todo-storage",
}
)
);
15 changes: 10 additions & 5 deletions vite.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
// <https://vitejs.dev/config/>
export default defineConfig({
plugins: [react()]
})
resolve: {
alias: {
"@mui/styled-engine": "@mui/styled-engine-sc",
},
},
plugins: [react()],
});