Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

To do app - Jenny A #42

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
2,376 changes: 1,778 additions & 598 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^11.11.17",
"lottie-react": "^2.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0"
"react-lottie": "^1.2.9",
"react-router-dom": "^6.18.0",
"styled-components": "^6.1.13",
"zustand": "^5.0.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
Expand Down
File renamed without changes
17 changes: 14 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
export const App = () => {
return <div>Find me in App.jsx!</div>;
};
// App.jsx
import { Home } from './pages/Home'
import { GlobalStyle } from './styles/GlobalStyle'

const App = () => {
return (
<>
<GlobalStyle />
<Home />
</>
)
}

export default App
1 change: 1 addition & 0 deletions src/assets/todo-animation.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/assets/tumbleweed.json

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions src/components/EmptyState.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// components/EmptyState.jsx
import styled from 'styled-components'
import Lottie from 'lottie-react'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice with some lottie animations 🥳

import tumbleweedAnimation from '../assets/tumbleweed.json'

const Container = styled.div`
text-align: center;
padding: 28px 20px;
color: #3E0B9D;
opacity: 0.7;
`

const AnimationContainer = styled.div`
max-width: 250px;
max-height: 120px;
margin: 0 auto 16px;
align-content: center;
`

const Text = styled.p`
font-size: 18px;
margin: 0;
`

export const EmptyState = () => (
<Container>
<AnimationContainer>
<Lottie
animationData={tumbleweedAnimation}
loop={true}
autoplay={true}
/>
</AnimationContainer>
<Text>No tasks yet! Add your first task above.</Text>
</Container>
)
53 changes: 53 additions & 0 deletions src/components/TodoInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// components/TodoInput.jsx
import styled from 'styled-components'
import { useState } from 'react'
import { useTodoStore } from '../stores/TodoStore'

const InputWrapper = styled.form`
width: 100%;
margin-bottom: 24px;
`

const Input = styled.input`
width: 100%;
padding: 20px 24px;
background: rgb(171,255,45);
border: none;
border-radius: 50px;
font-size: 18px;
color: #000;
transition: all 0.3s ease;

&::placeholder {
color: #333;
}

&:focus {
outline: none;
}
`

export const TodoInput = () => {
const [input, setInput] = useState('') // Local state for input field
const addTodo = useTodoStore(state => state.addTodo) // ToDo from Zustand

const handleSubmit = (e) => {
e.preventDefault() // Stops page refresh on submit
if (input.trim()) { // Check if input isn't just spaces
addTodo(input.trim())
setInput('') // Clear input after adding a task
}
}

return (
<InputWrapper onSubmit={handleSubmit}>
<Input
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget labels!

type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a task.."
aria-label="New todo input"
/>
</InputWrapper>
)
}
71 changes: 71 additions & 0 deletions src/components/TodoItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// components/TodoItem.jsx
import styled from 'styled-components'
import { useTodoStore } from '../stores/TodoStore'

const Item = styled.div`
display: flex;
align-items: center;
padding: 14px;
background: #E8E0FF;
border-radius: 20px;
transition: all 0.3s ease;
`

const Checkbox = styled.input.attrs({ type: 'checkbox' })`
width: 24px;
height: 24px;
margin-right: 16px;
cursor: pointer;
border: 2px solid #000;
border-radius: 4px;

&:checked {
accent-color: #251F30;
}
`

const Text = styled.span`
font-size: 18px;
color: #000;
text-decoration: ${props => props.completed ? 'line-through' : 'none'};
flex-grow: 1;
`

const DeleteButton = styled.button`
background: none;
border: none;
color: #666;
font-size: 20px;
cursor: pointer;
padding: 0 8px;
opacity: 0.6;
transition: opacity 0.2s;

&:hover {
opacity: 1;
}
`

export const TodoItem = ({ todo }) => {
// Functions from our Zustand store
const toggleTodo = useTodoStore(state => state.toggleTodo)
const removeTodo = useTodoStore(state => state.removeTodo)

return (
<Item>
<Checkbox
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<Text completed={todo.completed}>
{todo.text}
</Text>
<DeleteButton
onClick={() => removeTodo(todo.id)}
aria-label={`Delete ${todo.text}`}
>
×
</DeleteButton>
</Item>
)
}
34 changes: 34 additions & 0 deletions src/components/TodoList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// components/TodoList.jsx
import styled from 'styled-components'
import { TodoItem } from './TodoItem'
import { EmptyState } from './EmptyState'

const ListContainer = styled.div`
background: white;
border-radius: 32px;
padding: 24px;
margin: 24px 0 40px 0;
`

const List = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
`

export const TodoList = ({ todos }) => {
return (
<ListContainer>
{/* Show empty state */}
{todos.length === 0 ? (
<EmptyState />
) : (
<List>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</List>
)}
</ListContainer>
)
}
15 changes: 8 additions & 7 deletions src/main.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App.jsx";
import "./index.css";
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById("root")).render(
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
</React.StrictMode>,
)
114 changes: 114 additions & 0 deletions src/pages/Home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// pages/Home.jsx
import styled from 'styled-components'
import { useState } from 'react'
import { useTodoStore } from '../stores/TodoStore'
import { TodoInput } from '../components/TodoInput'
import { TodoList } from '../components/TodoList'
import Lottie from 'lottie-react'
import todoAnimation from '../assets/todo-animation.json'
Comment on lines +2 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor thing, but your code looks so good, so I don't have much to comment on 😉

You can order your imports like this:

import { useState } from 'react'
import Lottie from 'lottie-react'
import styled from 'styled-components'
import todoAnimation from '../assets/todo-animation.json'
import { useTodoStore } from '../stores/TodoStore'
import { TodoInput } from '../components/TodoInput'
import { TodoList } from '../components/TodoList'

with external libraries first (with react hooks first for clarity, but the others in albhabetical order).
Secondly assets, then stores, followed by components (in alphabetical order)


const Animation = styled.div`
width: 100%;
max-width: 400px;
margin: 0 auto;
`

const Container = styled.div`
min-height: 100vh;
background: #251F30;
padding: 32px 20px;
`

const Content = styled.div`
max-width: 600px;
margin: 0 auto;
`

const Title = styled.h1`
color: #fff;
font-size: 28px;
margin: 20px 0 40px 0;
font-weight: 500;
text-align: center;
`

const FilterContainer = styled.div`
display: flex;
justify-content: left;
gap: 12px;
margin-bottom: 32px;

@media (min-width: 650px) {
justify-content: center;
}
`

const FilterButton = styled.button`
padding: 12px 14px;
background: ${props => props.active ? '#FFA5FC' : '#fff'};
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
`

export const Home = () => {
const todos = useTodoStore(state => state.todos)
const [filter, setFilter] = useState('all') // Local state because it only affects this view

// Calculating the counts:
const completedTodos = todos.filter(todo => todo.completed).length
const activeTodos = todos.length - completedTodos

// Function that handles the text in the filter buttons
const getFilterLabel = (filterType) => {
switch(filterType) {
case 'all':
return `All (${todos.length})` // Counts total amount of items
case 'active':
return `Active (${activeTodos})`
case 'completed':
return `Completed (${completedTodos})`
default:
return filterType
}
}

// Filtering the todos
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed
if (filter === 'completed') return todo.completed
return true
})

return (
<Container>
<Content>
<Animation>
<Lottie
animationData={todoAnimation}
loop={true}
autoplay={true}
/>
</Animation>
<Title>Here are today's tasks:</Title>
<TodoInput />

{/* Filter buttons that update when you click them */}
<FilterContainer>
{['all', 'active', 'completed'].map((filterType) => (
<FilterButton
key={filterType}
active={filter === filterType}
onClick={() => setFilter(filterType)}
>
{getFilterLabel(filterType)}
</FilterButton>
))}
</FilterContainer>
<TodoList todos={filteredTodos} />
</Content>
</Container>
)
}
25 changes: 25 additions & 0 deletions src/stores/TodoStore.jsx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well structured with clear names on your actions and easy-to-follow code 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// stores/TodoStore.jsx
import { create } from 'zustand'

export const useTodoStore = create((set) => ({
todos: [], // empty array the todos
// Function to add a todo
// Uses text you type, creates a unique ID, sets completed to false (new task)
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }]
})),

// Function to check a todo to completed/incomplete
// Finds the specific todo ID and flips completed from false to true
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),

//Function to remove a todo
//Filters through the todos and keeps all except the one with the ID you want to remove
removeTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
}))
}))
Loading