summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md109
-rw-r--r--astro.config.mjs11
-rw-r--r--package-lock.json481
-rw-r--r--package.json11
-rw-r--r--schema.sql15
-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
-rw-r--r--wrangler.jsonc16
13 files changed, 994 insertions, 91 deletions
diff --git a/.gitignore b/.gitignore
index e593397..a3c6cb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,4 @@ pnpm-debug.log*
.idea/
.claude/
+.wrangler/
diff --git a/README.md b/README.md
index 649dd07..4eeb434 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,103 @@
-# Astro Base
+# Full-Stack Astro + Cloudflare
-A minimal template for new Astro projects. Includes
+A complete full-stack application built with Astro, running on Cloudflare Workers with D1 database.
-- TypeScript
-- Tailwind CSS
-- React
-- Tailwind Typography
+## Features
-## Start a New Project
+- **Frontend**: Astro with React components and Tailwind CSS
+- **Backend**: Cloudflare Workers with REST API endpoints
+- **Database**: Cloudflare D1 SQLite database
+- **Local Development**: Integrated Cloudflare Workers runtime via platformProxy
-```sh
-$ npm create astro@latest new-project -- --template yuvadm/astro-base
-``` \ No newline at end of file
+## Quick Start
+
+### Prerequisites
+
+- Node.js 18+
+- npm or yarn
+- Cloudflare account (for deployment)
+
+### Local Development
+
+1. **Install dependencies:**
+ ```bash
+ npm install
+ ```
+
+2. **Set up local database:**
+ ```bash
+ npm run db:migrate
+ ```
+
+3. **Start development server:**
+ ```bash
+ npm run dev
+ ```
+
+ The app will be available at `http://localhost:4321` (or next available port)
+
+## How It Works
+
+This project uses Astro's Cloudflare adapter with `platformProxy.enabled: true`, which provides:
+
+- **Automatic Cloudflare Workers Runtime**: The `workerd` binary runs alongside Astro's dev server
+- **Local D1 Database**: A local SQLite database is automatically created and managed
+- **Seamless API Integration**: API endpoints receive a real D1 database instance without additional setup
+- **Production Parity**: The local environment closely matches the production Cloudflare Workers environment
+
+The platform proxy eliminates the need for separate `wrangler dev` commands while providing full D1 database functionality during development.
+
+### Deployment Setup
+
+1. **Create D1 database:**
+ ```bash
+ npm run db:create
+ ```
+
+2. **Update wrangler.toml:**
+ - Copy the database ID from the previous command output
+ - Replace `your-database-id-here` in `wrangler.toml`
+
+3. **Initialize production database:**
+ ```bash
+ npm run db:init
+ ```
+
+4. **Build and deploy:**
+ ```bash
+ npm run build
+ npm run deploy
+ ```
+
+## API Endpoints
+
+- `GET /api/tasks` - Get all tasks
+- `POST /api/tasks` - Create a new task
+- `PUT /api/tasks/[id]` - Update a task
+- `DELETE /api/tasks/[id]` - Delete a task
+
+## Project Structure
+
+```
+src/
+├── components/
+│ └── TaskManager.tsx # React component for task management
+├── pages/
+│ ├── api/
+│ │ ├── tasks.ts # Tasks API endpoints
+│ │ └── tasks/[id].ts # Individual task operations
+│ └── index.astro # Main page
+├── layouts/
+│ └── StandardLayout.astro
+└── env.d.ts # TypeScript definitions
+
+schema.sql # Database schema
+wrangler.toml # Cloudflare Workers configuration
+```
+
+## Local vs Production
+
+- **Local**: Uses Wrangler's local D1 database simulator
+- **Production**: Uses actual Cloudflare D1 database in the cloud
+
+The same codebase works seamlessly in both environments thanks to Astro's Cloudflare adapter and Wrangler's local development tools. \ No newline at end of file
diff --git a/astro.config.mjs b/astro.config.mjs
index 0f737ef..cc75542 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,18 +1,21 @@
// @ts-check
import { defineConfig } from 'astro/config';
-
import tailwindcss from '@tailwindcss/vite';
-
import react from '@astrojs/react';
+import cloudflare from '@astrojs/cloudflare';
// https://astro.build/config
export default defineConfig({
+ output: 'server',
+ adapter: cloudflare({
+ platformProxy: {
+ enabled: true
+ }
+ }),
vite: {
plugins: [tailwindcss()]
},
-
integrations: [react()],
-
devToolbar: {
enabled: false,
},
diff --git a/package-lock.json b/package-lock.json
index b9ec0bc..0d7d3ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "astro-base",
"version": "0.0.1",
"dependencies": {
+ "@astrojs/cloudflare": "^12.6.0",
"@astrojs/react": "^4.3.0",
"@tailwindcss/vite": "^4.1.7",
"@types/react": "^19.1.5",
@@ -15,7 +16,8 @@
"astro": "^5.10.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "tailwindcss": "^4.1.7"
+ "tailwindcss": "^4.1.7",
+ "wrangler": "^4.22.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16"
@@ -34,6 +36,23 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@astrojs/cloudflare": {
+ "version": "12.6.0",
+ "resolved": "https://registry.npmjs.org/@astrojs/cloudflare/-/cloudflare-12.6.0.tgz",
+ "integrity": "sha512-pQ8bokC59GEiXvyXpC4swBNoL7C/EknP+82KFzQwgR/Aeo5N1oPiAoPHgJbpPya/YF4E26WODdCQfBQDvLRfuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@astrojs/internal-helpers": "0.6.1",
+ "@astrojs/underscore-redirects": "1.0.0",
+ "@cloudflare/workers-types": "^4.20250507.0",
+ "tinyglobby": "^0.2.13",
+ "vite": "^6.3.5",
+ "wrangler": "^4.14.1"
+ },
+ "peerDependencies": {
+ "astro": "^5.0.0"
+ }
+ },
"node_modules/@astrojs/compiler": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.2.tgz",
@@ -125,6 +144,12 @@
"node": "18.20.8 || ^20.3.0 || >=22.0.0"
}
},
+ "node_modules/@astrojs/underscore-redirects": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@astrojs/underscore-redirects/-/underscore-redirects-1.0.0.tgz",
+ "integrity": "sha512-qZxHwVnmb5FXuvRsaIGaqWgnftjCuMY+GSbaVZdBmE4j8AfgPqKPxYp8SUERyJcjpKCEmO4wD6ybuGH8A2kVRQ==",
+ "license": "MIT"
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -423,6 +448,141 @@
"fontkit": "^2.0.2"
}
},
+ "node_modules/@cloudflare/kv-asset-handler": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz",
+ "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "mime": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@cloudflare/unenv-preset": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.3.tgz",
+ "integrity": "sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==",
+ "license": "MIT OR Apache-2.0",
+ "peerDependencies": {
+ "unenv": "2.0.0-rc.17",
+ "workerd": "^1.20250508.0"
+ },
+ "peerDependenciesMeta": {
+ "workerd": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-64": {
+ "version": "1.20250617.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250617.0.tgz",
+ "integrity": "sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-arm64": {
+ "version": "1.20250617.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250617.0.tgz",
+ "integrity": "sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-64": {
+ "version": "1.20250617.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250617.0.tgz",
+ "integrity": "sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-arm64": {
+ "version": "1.20250617.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250617.0.tgz",
+ "integrity": "sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-windows-64": {
+ "version": "1.20250617.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250617.0.tgz",
+ "integrity": "sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workers-types": {
+ "version": "4.20250627.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20250627.0.tgz",
+ "integrity": "sha512-1il2InPF6NwucUwDl9RL6KLUfJuqaLazGTa9IZRO/XqMFggtGOXuc9X9wZ+IH1PTOWkdJ+jN+woyjj0W2CcTKg==",
+ "license": "MIT OR Apache-2.0"
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
@@ -833,6 +993,15 @@
"node": ">=18"
}
},
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
@@ -2077,6 +2246,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-walk": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
+ "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@@ -2201,6 +2379,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/as-table": {
+ "version": "1.0.55",
+ "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz",
+ "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "printable-characters": "^1.0.42"
+ }
+ },
"node_modules/astro": {
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.10.1.tgz",
@@ -2330,6 +2517,12 @@
],
"license": "MIT"
},
+ "node_modules/blake3-wasm": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
+ "license": "MIT"
+ },
"node_modules/blob-to-buffer": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz",
@@ -2571,7 +2764,6 @@
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
- "optional": true,
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
@@ -2585,7 +2777,6 @@
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
- "optional": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -2597,15 +2788,13 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "license": "MIT",
- "optional": true
+ "license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
- "optional": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
@@ -2697,6 +2886,12 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
+ "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2937,6 +3132,24 @@
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
+ "node_modules/exit-hook": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
+ "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
+ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
+ "license": "MIT"
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -3034,12 +3247,28 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-source": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
+ "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==",
+ "license": "Unlicense",
+ "dependencies": {
+ "data-uri-to-buffer": "^2.0.0",
+ "source-map": "^0.6.1"
+ }
+ },
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -3304,8 +3533,7 @@
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
- "license": "MIT",
- "optional": true
+ "license": "MIT"
},
"node_modules/is-docker": {
"version": "3.0.0",
@@ -4525,6 +4753,65 @@
],
"license": "MIT"
},
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/miniflare": {
+ "version": "4.20250617.4",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250617.4.tgz",
+ "integrity": "sha512-IAoApFKxOJlaaFkym5ETstVX3qWzVt3xyqCDj6vSSTgEH3zxZJ5417jZGg8iQfMHosKCcQH1doPPqqnOZm/yrw==",
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.8.1",
+ "acorn": "8.14.0",
+ "acorn-walk": "8.3.2",
+ "exit-hook": "2.2.1",
+ "glob-to-regexp": "0.4.1",
+ "sharp": "^0.33.5",
+ "stoppable": "1.1.0",
+ "undici": "^5.28.5",
+ "workerd": "1.20250617.0",
+ "ws": "8.18.0",
+ "youch": "3.3.4",
+ "zod": "3.22.3"
+ },
+ "bin": {
+ "miniflare": "bootstrap.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/miniflare/node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/miniflare/node_modules/zod": {
+ "version": "3.22.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
+ "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -4576,6 +4863,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "license": "MIT",
+ "bin": {
+ "mustache": "bin/mustache"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4782,6 +5078,18 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -4842,6 +5150,12 @@
"node": ">=4"
}
},
+ "node_modules/printable-characters": {
+ "version": "1.0.42",
+ "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
+ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
+ "license": "Unlicense"
+ },
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -5228,7 +5542,6 @@
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "optional": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
@@ -5283,7 +5596,6 @@
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
- "optional": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
@@ -5306,6 +5618,15 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5325,6 +5646,26 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/stacktracey": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz",
+ "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==",
+ "license": "Unlicense",
+ "dependencies": {
+ "as-table": "^1.0.36",
+ "get-source": "^2.0.12"
+ }
+ },
+ "node_modules/stoppable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
+ "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "npm": ">=6"
+ }
+ },
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@@ -5527,12 +5868,37 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
},
+ "node_modules/undici": {
+ "version": "5.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
+ "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
+ "node_modules/unenv": {
+ "version": "2.0.0-rc.17",
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.17.tgz",
+ "integrity": "sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==",
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "exsolve": "^1.0.4",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "ufo": "^1.6.1"
+ }
+ },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
@@ -6018,6 +6384,60 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/workerd": {
+ "version": "1.20250617.0",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250617.0.tgz",
+ "integrity": "sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "workerd": "bin/workerd"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@cloudflare/workerd-darwin-64": "1.20250617.0",
+ "@cloudflare/workerd-darwin-arm64": "1.20250617.0",
+ "@cloudflare/workerd-linux-64": "1.20250617.0",
+ "@cloudflare/workerd-linux-arm64": "1.20250617.0",
+ "@cloudflare/workerd-windows-64": "1.20250617.0"
+ }
+ },
+ "node_modules/wrangler": {
+ "version": "4.22.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.22.0.tgz",
+ "integrity": "sha512-m8qVO3YxhUTII+4U889G/f5UuLSvMkUkCNatupV2f/SJ+iqaWtP1QbuQII8bs2J/O4rqxsz46Wu2S50u7tKB5Q==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@cloudflare/kv-asset-handler": "0.4.0",
+ "@cloudflare/unenv-preset": "2.3.3",
+ "blake3-wasm": "2.1.5",
+ "esbuild": "0.25.4",
+ "miniflare": "4.20250617.4",
+ "path-to-regexp": "6.3.0",
+ "unenv": "2.0.0-rc.17",
+ "workerd": "1.20250617.0"
+ },
+ "bin": {
+ "wrangler": "bin/wrangler.js",
+ "wrangler2": "bin/wrangler.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^4.20250617.0"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ }
+ },
"node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
@@ -6035,6 +6455,27 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xxhash-wasm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
@@ -6098,6 +6539,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/youch": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz",
+ "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^0.7.1",
+ "mustache": "^4.2.0",
+ "stacktracey": "^2.1.8"
+ }
+ },
+ "node_modules/youch/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/zod": {
"version": "3.25.23",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.23.tgz",
diff --git a/package.json b/package.json
index af86bf6..af4c89c 100644
--- a/package.json
+++ b/package.json
@@ -4,11 +4,17 @@
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
+ "dev:wrangler": "wrangler pages dev --compatibility-date=2024-06-27 --d1=DB=astro-base-db -- astro dev",
"build": "astro build",
- "preview": "astro preview",
+ "preview": "wrangler pages dev dist",
+ "deploy": "wrangler pages deploy dist",
+ "db:create": "wrangler d1 create astro-base-db",
+ "db:init": "wrangler d1 execute astro-base-db --file=./schema.sql",
+ "db:migrate": "wrangler d1 execute astro-base-db --file=./schema.sql --local",
"astro": "astro"
},
"dependencies": {
+ "@astrojs/cloudflare": "^12.6.0",
"@astrojs/react": "^4.3.0",
"@tailwindcss/vite": "^4.1.7",
"@types/react": "^19.1.5",
@@ -16,7 +22,8 @@
"astro": "^5.10.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "tailwindcss": "^4.1.7"
+ "tailwindcss": "^4.1.7",
+ "wrangler": "^4.22.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16"
diff --git a/schema.sql b/schema.sql
new file mode 100644
index 0000000..15bdacb
--- /dev/null
+++ b/schema.sql
@@ -0,0 +1,15 @@
+-- Create a simple tasks table for our POC
+CREATE TABLE IF NOT EXISTS tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ description TEXT,
+ completed BOOLEAN DEFAULT FALSE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Insert some sample data
+INSERT INTO tasks (title, description) VALUES
+ ('Set up Cloudflare Workers', 'Configure the worker environment'),
+ ('Create D1 database', 'Set up the database schema'),
+ ('Build API endpoints', 'Create REST API for tasks'); \ No newline at end of file
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>
diff --git a/wrangler.jsonc b/wrangler.jsonc
new file mode 100644
index 0000000..2c58a61
--- /dev/null
+++ b/wrangler.jsonc
@@ -0,0 +1,16 @@
+{
+ // Cloudflare Workers configuration
+ "name": "astro-base-cf",
+ "compatibility_date": "2024-06-27",
+ "compatibility_flags": ["nodejs_compat"],
+
+ // D1 Database configuration
+ "d1_databases": [
+ {
+ "binding": "DB",
+ "database_name": "astro-base-db",
+ "database_id": "your-database-id-here",
+ "preview_database_id": "your-preview-database-id-here"
+ }
+ ]
+} \ No newline at end of file