<script setup lang="ts">
import { ref, computed, onMounted, watch } from "vue";
// ============================================
// Types
// ============================================
interface User {
id: number;
name: string;
email: string;
}
interface Todo {
id: number;
text: string;
completed: boolean;
}
// ============================================
// Composables (Reusable Logic)
// ============================================
// useFetch - Data fetching with loading/error states
function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const loading = ref(true);
const error = ref<Error | null>(null);
const fetchData = async () => {
loading.value = true;
error.value = null;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
data.value = await res.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
};
onMounted(fetchData);
return { data, loading, error, refetch: fetchData };
}
// useLocalStorage - Persistent state
function useLocalStorage<T>(key: string, defaultValue: T) {
const storedValue = localStorage.getItem(key);
const data = ref<T>(storedValue ? JSON.parse(storedValue) : defaultValue);
watch(
data,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
},
{ deep: true },
);
return data;
}
// ============================================
// Component State & Logic
// ============================================
// Reactive state
const count = ref(0);
const newTodo = ref("");
const todos = useLocalStorage<Todo[]>("todos", []);
// Computed properties
const completedCount = computed(
() => todos.value.filter((t) => t.completed).length,
);
const remainingCount = computed(
() => todos.value.length - completedCount.value,
);
// Methods
function increment() {
count.value++;
}
function addTodo() {
if (!newTodo.value.trim()) return;
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false,
});
newTodo.value = "";
}
function removeTodo(id: number) {
const index = todos.value.findIndex((t) => t.id === id);
if (index !== -1) {
todos.value.splice(index, 1);
}
}
function toggleTodo(id: number) {
const todo = todos.value.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
// Data fetching example
const {
data: users,
loading: usersLoading,
error: usersError,
} = useFetch<User[]>("https://jsonplaceholder.typicode.com/users");
</script>
<template>
<div class="app">
<!-- Counter Example -->
<section class="counter">
<h2>Counter: {{ count }}</h2>
<button @click="increment">Increment</button>
<button @click="count--">Decrement</button>
<button @click="count = 0">Reset</button>
</section>
<!-- Todo List Example -->
<section class="todos">
<h2>Todo List</h2>
<p>{{ remainingCount }} remaining, {{ completedCount }} completed</p>
<form @submit.prevent="addTodo" class="todo-form">
<input
v-model="newTodo"
placeholder="Add a todo..."
class="todo-input"
/>
<button type="submit">Add</button>
</form>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.id"
:class="{ completed: todo.completed }"
>
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">×</button>
</li>
</ul>
</section>
<!-- Data Fetching Example -->
<section class="users">
<h2>Users (Fetched)</h2>
<div v-if="usersLoading">Loading...</div>
<div v-else-if="usersError" class="error">
Error: {{ usersError.message }}
</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }} ({{ user.email }})
</li>
</ul>
</section>
</div>
</template>
<style scoped>
.app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
button {
padding: 8px 16px;
margin: 4px;
cursor: pointer;
}
.todo-form {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.todo-input {
flex: 1;
padding: 8px;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #888;
}
.error {
color: red;
}
</style>