diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/SimpleTest.tsx | 32 | ||||
| -rw-r--r-- | src/components/TaskManager.tsx | 202 | ||||
| -rw-r--r-- | src/env.d.ts | 13 | ||||
| -rw-r--r-- | src/pages/api/tasks.ts | 66 | ||||
| -rw-r--r-- | src/pages/api/tasks/[id].ts | 57 | ||||
| -rw-r--r-- | src/pages/index.astro | 71 |
6 files changed, 376 insertions, 65 deletions
diff --git a/src/components/SimpleTest.tsx b/src/components/SimpleTest.tsx new file mode 100644 index 0000000..cfabeab --- /dev/null +++ b/src/components/SimpleTest.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +export default function SimpleTest() { + const [count, setCount] = useState(0); + + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Simple Test Component</h1> + <div className="space-y-4"> + <p>This is a simple React component to test if the infinite refresh is caused by the API calls.</p> + <div className="flex items-center space-x-4"> + <button + onClick={() => setCount(count - 1)} + className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" + > + - + </button> + <span className="text-xl font-bold">{count}</span> + <button + onClick={() => setCount(count + 1)} + className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" + > + + + </button> + </div> + <p className="text-gray-600"> + If this component works without refreshing, the issue is in the TaskManager API calls. + </p> + </div> + </div> + ); +}
\ No newline at end of file diff --git a/src/components/TaskManager.tsx b/src/components/TaskManager.tsx new file mode 100644 index 0000000..a10a5f9 --- /dev/null +++ b/src/components/TaskManager.tsx @@ -0,0 +1,202 @@ +import { useState, useEffect, useCallback } from 'react'; + +interface Task { + id: number; + title: string; + description: string; + completed: boolean; + created_at?: string; + updated_at?: string; +} + +export default function TaskManager() { + const [tasks, setTasks] = useState<Task[]>([]); + const [newTask, setNewTask] = useState({ title: '', description: '' }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + const fetchTasks = useCallback(async () => { + try { + setError(null); + const response = await fetch('/api/tasks'); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log('Fetched tasks:', data); + setTasks(Array.isArray(data) ? data : []); + } catch (error) { + console.error('Failed to fetch tasks:', error); + setError(error instanceof Error ? error.message : 'Failed to fetch tasks'); + setTasks([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchTasks(); + }, [fetchTasks]); + + const createTask = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newTask.title.trim()) return; + + try { + const response = await fetch('/api/tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newTask) + }); + + if (response.ok) { + setNewTask({ title: '', description: '' }); + fetchTasks(); + } + } catch (error) { + console.error('Failed to create task:', error); + } + }; + + const toggleTask = async (task: Task) => { + try { + await fetch(`/api/tasks/${task.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...task, completed: !task.completed }) + }); + fetchTasks(); + } catch (error) { + console.error('Failed to update task:', error); + } + }; + + const deleteTask = async (id: number) => { + try { + await fetch(`/api/tasks/${id}`, { method: 'DELETE' }); + fetchTasks(); + } catch (error) { + console.error('Failed to delete task:', error); + } + }; + + if (loading) { + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Task Manager</h1> + <div className="p-4 text-center">Loading tasks...</div> + </div> + ); + } + + if (error) { + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Task Manager</h1> + <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> + <p className="text-red-700">Error: {error}</p> + <button + onClick={fetchTasks} + className="mt-2 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" + > + Retry + </button> + </div> + </div> + ); + } + + return ( + <div className="max-w-2xl mx-auto p-6"> + <h1 className="text-3xl font-bold mb-6">Task Manager</h1> + + <form onSubmit={createTask} className="mb-8 space-y-4"> + <div> + <input + type="text" + placeholder="Task title" + value={newTask.title} + onChange={(e) => setNewTask({ ...newTask, title: e.target.value })} + className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + </div> + <div> + <textarea + placeholder="Task description (optional)" + value={newTask.description} + onChange={(e) => setNewTask({ ...newTask, description: e.target.value })} + className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + rows={3} + /> + </div> + <button + type="submit" + className="w-full bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors" + > + Add Task + </button> + </form> + + <div className="space-y-4"> + {tasks.length === 0 ? ( + <p className="text-gray-500 text-center">No tasks yet. Create your first task above!</p> + ) : ( + tasks.map((task) => ( + <div + key={task.id} + className={`p-4 border rounded-lg ${ + task.completed ? 'bg-green-50 border-green-200' : 'bg-white border-gray-200' + }`} + > + <div className="flex items-start justify-between"> + <div className="flex-1"> + <h3 + className={`font-semibold ${ + task.completed ? 'line-through text-gray-500' : 'text-gray-900' + }`} + > + {task.title} + </h3> + {task.description && ( + <p + className={`mt-1 ${ + task.completed ? 'line-through text-gray-400' : 'text-gray-600' + }`} + > + {task.description} + </p> + )} + {task.created_at && ( + <p className="text-xs text-gray-400 mt-2"> + Created: {new Date(task.created_at).toLocaleString()} + </p> + )} + </div> + <div className="flex space-x-2 ml-4"> + <button + onClick={() => toggleTask(task)} + className={`px-3 py-1 rounded text-sm ${ + task.completed + ? 'bg-yellow-500 text-white hover:bg-yellow-600' + : 'bg-green-500 text-white hover:bg-green-600' + }`} + > + {task.completed ? 'Undo' : 'Complete'} + </button> + <button + onClick={() => deleteTask(task.id)} + className="px-3 py-1 bg-red-500 text-white rounded text-sm hover:bg-red-600" + > + Delete + </button> + </div> + </div> + </div> + )) + )} + </div> + </div> + ); +}
\ No newline at end of file diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..359b598 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,13 @@ +/// <reference types="astro/client" /> + +type D1Database = import('@cloudflare/workers-types').D1Database; + +declare namespace App { + interface Locals { + runtime: { + env: { + DB: D1Database; + }; + }; + } +}
\ No newline at end of file diff --git a/src/pages/api/tasks.ts b/src/pages/api/tasks.ts new file mode 100644 index 0000000..00328e8 --- /dev/null +++ b/src/pages/api/tasks.ts @@ -0,0 +1,66 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = async ({ locals }) => { + try { + const db = locals.runtime?.env?.DB; + + if (!db) { + console.error('Database not available'); + return new Response(JSON.stringify([]), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + + const { results } = await db.prepare('SELECT * FROM tasks ORDER BY created_at DESC').all(); + + return new Response(JSON.stringify(results || []), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + console.error('Database error:', error); + return new Response(JSON.stringify([]), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } +}; + +export const POST: APIRoute = async ({ request, locals }) => { + try { + const db = locals.runtime?.env?.DB; + + if (!db) { + return new Response(JSON.stringify({ error: 'Database not available' }), { + status: 503, + headers: { 'Content-Type': 'application/json' } + }); + } + + const body = await request.json(); + const { title, description } = body; + + if (!title) { + return new Response(JSON.stringify({ error: 'Title is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + const { results } = await db.prepare( + 'INSERT INTO tasks (title, description) VALUES (?, ?) RETURNING *' + ).bind(title, description || '').all(); + + return new Response(JSON.stringify(results?.[0] || { id: Date.now(), title, description, completed: false }), { + status: 201, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + console.error('Database error:', error); + return new Response(JSON.stringify({ error: 'Failed to create task' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +};
\ No newline at end of file diff --git a/src/pages/api/tasks/[id].ts b/src/pages/api/tasks/[id].ts new file mode 100644 index 0000000..e9341b6 --- /dev/null +++ b/src/pages/api/tasks/[id].ts @@ -0,0 +1,57 @@ +import type { APIRoute } from 'astro'; + +export const PUT: APIRoute = async ({ params, request, locals }) => { + try { + const db = locals.runtime.env.DB; + const id = params.id; + const body = await request.json(); + const { title, description, completed } = body; + + const { results } = await db.prepare( + 'UPDATE tasks SET title = ?, description = ?, completed = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? RETURNING *' + ).bind(title, description || '', completed || false, id).all(); + + if (results.length === 0) { + return new Response(JSON.stringify({ error: 'Task not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response(JSON.stringify(results[0]), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ error: 'Failed to update task' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +}; + +export const DELETE: APIRoute = async ({ params, locals }) => { + try { + const db = locals.runtime.env.DB; + const id = params.id; + + const { success } = await db.prepare('DELETE FROM tasks WHERE id = ?').bind(id).run(); + + if (!success) { + return new Response(JSON.stringify({ error: 'Task not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' } + }); + } + + return new Response(JSON.stringify({ message: 'Task deleted successfully' }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } catch (error) { + return new Response(JSON.stringify({ error: 'Failed to delete task' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } +};
\ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index a88712c..09a80cc 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,78 +1,19 @@ --- -import Counter from "../components/Counter"; +import TaskManager from "../components/TaskManager"; import StandardLayout from "../layouts/StandardLayout.astro"; -import { getCollection } from "astro:content"; - -// Get the latest 3 blog posts -const latestPosts = await getCollection("blog"); -latestPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); -const featuredPosts = latestPosts.slice(0, 3); --- -<StandardLayout title="Astro - Home"> +<StandardLayout title="Full-Stack Astro + Cloudflare"> <section class="py-12"> <div class="text-center mb-12"> <h1 class="text-5xl font-bold text-gray-900 mb-4"> - Welcome to Astro + Full-Stack Astro </h1> <p class="text-xl text-gray-600 max-w-3xl mx-auto"> - A modern framework for building fast, content-focused websites. + Powered by Cloudflare Workers and D1 Database </p> - <div class="mt-8"> - <Counter client:load /> - </div> - </div> - </section> - - <section - class="py-12 bg-gray-50 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8" - > - <div class="max-w-7xl mx-auto"> - <div class="flex justify-between items-center mb-8"> - <h2 class="text-3xl font-bold text-gray-900"> - Latest Blog Posts - </h2> - <a - href="/blog" - class="text-blue-600 hover:text-blue-800 font-medium" - >View all posts →</a - > - </div> - - <div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3"> - { - featuredPosts.map((post) => ( - <article class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"> - <a href={`/blog/${post.slug}`} class="block"> - {post.data.image && ( - <img - src={post.data.image} - alt={post.data.title} - class="w-full h-48 object-cover" - /> - )} - <div class="p-6"> - <h3 class="text-xl font-semibold text-gray-900 mb-2"> - {post.data.title} - </h3> - <p class="text-sm text-gray-500 mb-3"> - {new Date( - post.data.pubDate, - ).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - })} - </p> - <p class="text-gray-700"> - {post.data.description} - </p> - </div> - </a> - </article> - )) - } - </div> </div> + + <TaskManager client:load /> </section> </StandardLayout> |
