-
Notifications
You must be signed in to change notification settings - Fork 55
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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 |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
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' | ||
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> | ||
) |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
) | ||
} |
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> | ||
) | ||
} |
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> | ||
) | ||
} |
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>, | ||
) |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
with external libraries first (with react hooks first for clarity, but the others in albhabetical 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> | ||
) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
})) | ||
})) |
There was a problem hiding this comment.
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 🥳