<script lang="ts">
import { onMount } from 'svelte';
// ============================================
// Types
// ============================================
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface User {
id: number;
name: string;
email: string;
}
// ============================================
// State
// ============================================
let count = $state(0);
let todos = $state<Todo[]>([]);
let newTodoText = $state('');
let users = $state<User[]>([]);
let loading = $state(true);
let error = $state<string | null>(null);
// ============================================
// Derived State
// ============================================
let completedCount = $derived(todos.filter(t => t.completed).length);
let remainingCount = $derived(todos.length - completedCount);
let doubled = $derived(count * 2);
// ============================================
// Effects & Lifecycle
// ============================================
// Load todos from localStorage on mount
onMount(() => {
const saved = localStorage.getItem('svelte-todos');
if (saved) {
todos = JSON.parse(saved);
}
// Fetch users
fetchUsers();
});
// Save todos to localStorage when changed
$effect(() => {
localStorage.setItem('svelte-todos', JSON.stringify(todos));
});
// ============================================
// Functions
// ============================================
function increment() {
count++;
}
function decrement() {
count--;
}
function addTodo() {
if (!newTodoText.trim()) return;
todos = [...todos, {
id: Date.now(),
text: newTodoText.trim(),
completed: false
}];
newTodoText = '';
}
function toggleTodo(id: number) {
todos = todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
);
}
function removeTodo(id: number) {
todos = todos.filter(t => t.id !== id);
}
async function fetchUsers() {
loading = true;
error = null;
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
users = await res.json();
} catch (e) {
error = (e as Error).message;
} finally {
loading = false;
}
}
</script>
<div class="app">
<!-- Counter Section -->
<section class="counter">
<h2>Counter: {count}</h2>
<p>Doubled: {doubled}</p>
<div class="buttons">
<button onclick={decrement}>-</button>
<button onclick={increment}>+</button>
<button onclick={() => count = 0}>Reset</button>
</div>
</section>
<!-- Todo Section -->
<section class="todos">
<h2>Todos</h2>
<p>{remainingCount} remaining, {completedCount} completed</p>
<form onsubmit={(e) => { e.preventDefault(); addTodo(); }}>
<input
type="text"
bind:value={newTodoText}
placeholder="Add a todo..."
/>
<button type="submit">Add</button>
</form>
<ul>
{#each todos as todo (todo.id)}
<li class:completed={todo.completed}>
<input
type="checkbox"
checked={todo.completed}
onchange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onclick={() => removeTodo(todo.id)}>×</button>
</li>
{/each}
</ul>
</section>
<!-- Users Section -->
<section class="users">
<h2>Users (Fetched)</h2>
{#if loading}
<p>Loading...</p>
{:else if error}
<p class="error">Error: {error}</p>
{:else}
<ul>
{#each users as user (user.id)}
<li>{user.name} ({user.email})</li>
{/each}
</ul>
{/if}
</section>
</div>
<style>
.app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: system-ui, sans-serif;
}
section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.buttons {
display: flex;
gap: 8px;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #e5e5e5;
}
form {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
input[type="text"] {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
ul {
list-style: none;
padding: 0;
}
.todos li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-bottom: 1px solid #eee;
}
.completed span {
text-decoration: line-through;
color: #888;
}
.error {
color: red;
}
</style>