Spaces:
Running
Running
<!-- src/App.svelte content here --> | |
<script lang="ts"> | |
import { onMount } from 'svelte'; | |
interface Todo { | |
id: number; | |
text: string; | |
completed: boolean; | |
} | |
let todos: Todo[] = []; | |
let newTodo = ''; | |
let nextId = 1; | |
onMount(() => { | |
const savedTodos = localStorage.getItem('todos'); | |
if (savedTodos) { | |
todos = JSON.parse(savedTodos); | |
nextId = todos.length > 0 ? Math.max(...todos.map(t => t.id)) + 1 : 1; | |
} | |
}); | |
$: localStorage.setItem('todos', JSON.stringify(todos)); | |
function addTodo() { | |
if (newTodo.trim() === '') return; | |
todos = [...todos, { | |
id: nextId++, | |
text: newTodo.trim(), | |
completed: false | |
}]; | |
newTodo = ''; | |
} | |
function toggleTodo(id: number) { | |
todos = todos.map(todo => | |
todo.id === id ? { ...todo, completed: !todo.completed } : todo | |
); | |
} | |
function deleteTodo(id: number) { | |
todos = todos.filter(todo => todo.id !== id); | |
} | |
function handleKeyPress(event: KeyboardEvent) { | |
if (event.key === 'Enter') { | |
addTodo(); | |
} | |
} | |
</script> | |
<div class="app"> | |
<header> | |
<h1>Todo App</h1> | |
</header> | |
<main> | |
<div class="input-container"> | |
<input | |
type="text" | |
bind:value={newTodo} | |
on:keydown={handleKeyPress} | |
placeholder="Add a new todo..." | |
aria-label="Add a new todo" | |
/> | |
<button on:click={addTodo} class="add-btn" disabled={!newTodo.trim()}> | |
Add | |
</button> | |
</div> | |
<div class="todos-container"> | |
{#if todos.length === 0} | |
<p class="empty-state">No todos yet. Add one above!</p> | |
{:else} | |
<ul class="todos-list" role="list"> | |
{#each todos as todo (todo.id)} | |
<li class="todo-item" class:completed={todo.completed}> | |
<input | |
type="checkbox" | |
checked={todo.completed} | |
on:change={() => toggleTodo(todo.id)} | |
aria-label={todo.completed ? `Mark ${todo.text} as incomplete` : `Mark ${todo.text} as complete`} | |
/> | |
<span class="todo-text">{todo.text}</span> | |
<button | |
class="delete-btn" | |
on:click={() => deleteTodo(todo.id)} | |
aria-label={`Delete ${todo.text}`} | |
> | |
× | |
</button> | |
</li> | |
{/each} | |
</ul> | |
{/if} | |
</div> | |
{#if todos.length > 0} | |
<div class="stats"> | |
<span>{todos.filter(t => !t.completed).length} remaining</span> | |
<span>{todos.filter(t => t.completed).length} completed</span> | |
</div> | |
{/if} | |
</main> | |
</div> | |
<style> | |
.app { | |
max-width: 600px; | |
margin: 0 auto; | |
padding: 2rem 1rem; | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |
} | |
header { | |
text-align: center; | |
margin-bottom: 2rem; | |
} | |
h1 { | |
color: #333; | |
font-weight: 600; | |
} | |
.input-container { | |
display: flex; | |
gap: 0.5rem; | |
margin-bottom: 2rem; | |
} | |
input { | |
flex: 1; | |
padding: 0.75rem; | |
border: 2px solid #ddd; | |
border-radius: 4px; | |
font-size: 1rem; | |
} | |
input:focus { | |
outline: none; | |
border-color: #4a90e2; | |
} | |
.add-btn { | |
padding: 0.75rem 1.5rem; | |
background-color: #4a90e2; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
} | |
.add-btn:hover:not(:disabled) { | |
background-color: #357abd; | |
} | |
.add-btn:disabled { | |
background-color: #ccc; | |
cursor: not-allowed; | |
} | |
.todos-container { | |
min-height: 200px; | |
} | |
.empty-state { | |
text-align: center; | |
color: #777; | |
font-style: italic; | |
padding: 2rem; | |
} | |
.todos-list { | |
list-style: none; | |
padding: 0; | |
margin: 0; | |
} | |
.todo-item { | |
display: flex; | |
align-items: center; | |
padding: 1rem; | |
border-bottom: 1px solid #eee; | |
gap: 1rem; | |
transition: background-color 0.2s; | |
} | |
.todo-item:hover { | |
background-color: #f9f9f9; | |
} | |
.todo-item.completed { | |
opacity: 0.7; | |
} | |
.todo-item.completed .todo-text { | |
text-decoration: line-through; | |
color: #888; | |
} | |
.todo-text { | |
flex: 1; | |
font-size: 1.1rem; | |
} | |
.delete-btn { | |
background: none; | |
border: none; | |
color: #e74c3c; | |
font-size: 1.5rem; | |
cursor: pointer; | |
width: 36px; | |
height: 36px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 50%; | |
transition: background-color 0.2s; | |
} | |
.delete-btn:hover { | |
background-color: rgba(231, 76, 60, 0.1); | |
} | |
.stats { | |
display: flex; | |
justify-content: space-between; | |
margin-top: 1.5rem; | |
padding: 1rem; | |
background-color: #f5f5f5; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
color: #555; | |
} | |
@media (max-width: 600px) { | |
.app { | |
padding: 1rem; | |
} | |
.input-container { | |
flex-direction: column; | |
} | |
.add-btn { | |
padding: 0.75rem; | |
} | |
} | |
</style> |