summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/SimpleTest.tsx32
-rw-r--r--src/components/TaskManager.tsx202
-rw-r--r--src/env.d.ts13
-rw-r--r--src/pages/api/tasks.ts66
-rw-r--r--src/pages/api/tasks/[id].ts57
-rw-r--r--src/pages/index.astro71
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>