4export function App() {
5 const [projectUrl, setProjectUrl] = useState("");
6 const [apiToken, setApiToken] = useState("");
7 const [loading, setLoading] = useState(false);
8 const [message, setMessage] = useState("");
21 }
22
23 if (!apiToken.trim()) {
24 throw new Error("API Token is required");
25 }
26
28 method: "POST",
29 headers: {
30 "Authorization": `Bearer ${apiToken}`,
31 "Content-Type": "application/json",
32 },
73
74 <div>
75 <label htmlFor="apiToken" className="block text-sm font-medium text-gray-700 mb-1">
76 <a
77 href="https://www.val.town/settings/api"
78 target="_blank"
79 rel="noopener noreferrer"
80 className="text-purple-600 hover:text-purple-700"
81 >
82 Val Town API Token
83 </a>{" "}
84 (with project:write permissions)
85 </label>
86 <input
87 id="apiToken"
88 type="password"
89 value={apiToken}
90 onChange={(e: any) => setApiToken(e.target.value)}
91 placeholder="vtwn_*********"
92 className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
5## Overview
6
7Forky is a utility that enables you to create copies (forks) of any public Val Town project. It provides a straightforward interface where you can enter a Val Town project URL and your API token to create your own copy of the project.
8
9## Features
17
18- A [Val Town](https://www.val.town) account
19- A Val Town API token with project read and write permissions
20
21## Usage
22
231. Enter the URL of the Val Town project you want to fork (format: `https://www.val.town/x/username/projectname`)
242. Enter your Val Town API token with the proper `project:write` permissions
253. Click "Fork" and wait for the process to complete
264. Once successful, you'll have a new copy of the project in your Val Town account under a new generated name
30```
31├── backend
32│ └── index.ts # Backend server and API endpoints
33├── frontend
34│ ├── components
484. Preserve the project structure and metadata
49
50## API
51
52The application exposes the following API endpoint:
53
54- `POST /fork?url={encoded-url}` - Forks the specified project
193app.get("/frontend/**/*", c => serveFile(c.req.path, import.meta.url));
194
195// Add your API routes here
196// app.get("/api/data", c => c.json({ hello: "world" }));
197
198// Unwrap and rethrow Hono errors as the original error
155 }
156
157 // Handle specific routes for API
158 const url = new URL(request.url);
159 if (url.pathname === "/api/ideas" && request.method === "GET") {
160 if (!email) {
161 return new Response(JSON.stringify({
1// API handlers for IdeaScale
2import { CONFIG, THEME } from "./config";
3import { getIdeasForUser, getTagsForUser, createIdea } from "./db";
39 '${CONFIG.basePath}/manifest.json',
40 'https://val.town/favicon.png',
41 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'
42 ]);
43 })
61}
62
63// Handle GET /api/ideas request
64export async function handleGetIdeasRequest(req: Request, sqlite, email: string): Promise<Response> {
65 if (!email) {
76}
77
78// Handle GET /api/tags request
79export async function handleGetTagsRequest(req: Request, sqlite, email: string): Promise<Response> {
80 if (!email) {
91}
92
93// Handle POST /api/ideas request
94export async function handleCreateIdeaRequest(req: Request, sqlite, email: string, baseUrl = ""): Promise<Response> {
95 if (!email) {
282 <meta name="theme-color" content={THEME.primary} />
283 <link rel="manifest" href={`${baseUrl}/manifest.json`} />
284 <link rel="preconnect" href="https://fonts.googleapis.com" />
285 <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
286 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
287 <style dangerouslySetInnerHTML={{ __html: styles }} />
288 </head>
400 <h1>Add New Idea</h1>
401 <div className="card">
402 <form action={`${baseUrl}/api/ideas`} method="post">
403 <div className="form-row">
404 <label htmlFor="title">Title</label>
6 <title>char.build</title>
7 <link rel="stylesheet" href="/frontend/style.css">
8 <link rel="preconnect" href="https://fonts.googleapis.com">
9 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10 <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet">
11 <script src="https://cdn.tailwindcss.com"></script>
12 </head>
28 }
29
30 // API endpoints for user info
31 if (url.pathname === "/api/user") {
32 if (!email) {
33 return Response.json({ authenticated: false });
50 }
51
52 // Notes app API endpoints
53 if (url.pathname.startsWith("/api/notes")) {
54 return handleNotesApi(request, email);
55 }
56
89};
90
91// Handle Notes API requests
92async function handleNotesApi(request: Request, email: string | null): Promise<Response> {
93 if (!email) {
94 return Response.json({ error: 'Unauthorized' }, 401);
111
112 // Get all notes
113 if (url.pathname === "/api/notes" && request.method === "GET") {
114 const result = await db.execute(
115 'SELECT id, title, content, created_at, updated_at FROM {{table}} WHERE email = ? ORDER BY updated_at DESC',
120
121 // Create note
122 if (url.pathname === "/api/notes" && request.method === "POST") {
123 try {
124 const { title, content } = await request.json();
140
141 // Update note
142 const updateMatch = url.pathname.match(/^\/api\/notes\/(\d+)$/);
143 if (updateMatch && request.method === "PUT") {
144 try {
162
163 // Delete note
164 const deleteMatch = url.pathname.match(/^\/api\/notes\/(\d+)$/);
165 if (deleteMatch && request.method === "DELETE") {
166 try {
26 <title>char.build</title>
27 <script src="https://cdn.tailwindcss.com"></script>
28 <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&display=swap">
29 </head>
30 <body class="bg-gray-50 min-h-screen">
64});
65
66// Add your API routes here
67// app.get('/api/resource', (c) => { ... })
68// app.post('/api/resource', (c) => { ... })
69// etc.
70