Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
410 changes: 410 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

Binary file modified bun.lockb
Binary file not shown.
29 changes: 29 additions & 0 deletions lib/admin/time-ago.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function timeAgo(date: Date, timezone: string = "UTC"): string {
const now = new Date()
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
minute: "numeric",
hour12: true,
})

const formattedTime = formatter.format(date)
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000)

if (seconds < 60) {
return `${seconds}s ago (${formattedTime})`
}

const minutes = Math.floor(seconds / 60)
if (minutes < 60) {
return `${minutes}m ago (${formattedTime})`
}

const hours = Math.floor(minutes / 60)
if (hours < 24) {
return `${hours}h ago (${formattedTime})`
}

const days = Math.floor(hours / 24)
return `${days}d ago (${formattedTime})`
}
99 changes: 99 additions & 0 deletions lib/middleware/with-ctx-react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { renderToString } from "react-dom/server"
import type { Middleware } from "winterspec"
import React from "react"
import type { ReactNode } from "react"
import { timeAgo } from "lib/admin/time-ago"

export const withCtxReact: Middleware<
{},
{ react: (component: ReactNode) => Response }
> = async (req, ctx, next) => {
ctx.react = (component: ReactNode) => {
const pathComponents = new URL(req.url).pathname.split("/").filter(Boolean)
const timezone = req.headers.get("X-Timezone") || "UTC"
return new Response(
renderToString(
<html lang="en">
<script src="https://cdn.tailwindcss.com" />
<style
type="text/tailwindcss"
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{
__html: `
.btn {
@apply text-white visited:text-white m-1 bg-blue-500 hover:bg-blue-700 font-bold py-2 px-4 rounded
}
a {
@apply underline text-blue-600 hover:text-blue-800 visited:text-purple-800 m-1
}
h2 {
@apply text-xl font-bold my-2
}
input, select {
@apply border border-gray-300 rounded p-1 ml-0.5
}
form {
@apply inline-flex flex-col gap-2 border border-gray-300 rounded p-2 m-2 text-xs
}
button {
@apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded
}
`,
}}
/>
<body>
<div>
<div className="border-b border-gray-300 py-1 flex justify-between items-center">
<div>
<span className="px-1 pr-2">admin panel</span>
{pathComponents.map((component, index) => {
return (
<span key={index}>
<span className="px-0.5 text-gray-500">/</span>
<a
href={`/${pathComponents.slice(0, index + 1).join("/")}`}
>
{component}
</a>
</span>
)
})}
</div>
<div className="mr-2 flex items-center">
<div className="text-xs text-gray-500 mr-1">
{timeAgo(new Date(), timezone)}
</div>
<div
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{
__html: `
<select
id="timezone-select"
class="text-xs"
value="${timezone}"
onchange="document.cookie = 'timezone=' + this.value + ';path=/'; location.reload();"
>
<option ${timezone === "UTC" ? "selected" : ""} value="UTC">UTC</option>
<option ${timezone === "America/Los_Angeles" ? "selected" : ""} value="America/Los_Angeles">Pacific</option>
<option ${timezone === "Asia/Kolkata" ? "selected" : ""} value="Asia/Kolkata">IST</option>
</select>
`,
}}
/>
</div>
</div>
<div className="flex flex-col text-xs p-1">{component}</div>
</div>
</body>
</html>,
),
{
headers: {
"Content-Type": "text/html",
},
},
)
}

return await next(req, ctx)
}
9 changes: 7 additions & 2 deletions lib/middleware/with-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import type { DbClient } from "lib/db/db-client"
import { createDatabase } from "lib/db/db-client"
import type { Middleware } from "winterspec/middleware"

// Create a singleton instance of the database
let dbInstance: DbClient | null = null

export const withDb: Middleware<
{},
{
db: DbClient
}
> = async (req, ctx, next) => {
if (!ctx.db) {
ctx.db = createDatabase()
if (!dbInstance) {
dbInstance = createDatabase()
}

ctx.db = dbInstance
return next(req, ctx)
}
3 changes: 2 additions & 1 deletion lib/middleware/with-winter-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createWithWinterSpec } from "winterspec"
import { withDb } from "./with-db"
import { withCtxReact } from "./with-ctx-react"

export const withRouteSpec = createWithWinterSpec({
apiName: "tscircuit Debug API",
productionServerUrl: "https://debug-api.tscircuit.com",
beforeAuthMiddleware: [],
authMiddleware: {},
afterAuthMiddleware: [withDb],
afterAuthMiddleware: [withDb, withCtxReact],
})
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
"module": "index.ts",
"type": "module",
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
"@types/react": "18.3.4",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"ky": "^1.8.1",
"next": "^14.2.5"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"winterspec": "^0.0.107",
"zod": "^3.23.8",
"zustand": "^4.5.5",
Expand Down
197 changes: 197 additions & 0 deletions routes/_fake/admin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"
import type { Thing } from "lib/db/schema"
import React from "react"

// Admin page component to display thing resources
const AdminPage: React.FC<{ things: Thing[] }> = ({ things }) => {
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Thing Resources Admin</h1>

{/* Create new thing form */}
<div className="mb-6">
<h2>Create New Thing</h2>
<form
action="/things/create"
method="POST"
className="w-full max-w-md"
onSubmit={(e) => {
// Use JavaScript for enhanced experience, but allow traditional form submission as fallback
e.preventDefault()
console.log("Form submitted")

const form = e.currentTarget
const nameInput = form.elements.namedItem(
"name",
) as HTMLInputElement
const descriptionInput = form.elements.namedItem(
"description",
) as HTMLInputElement

if (!nameInput || !descriptionInput) {
console.error("Form elements not found")
return
}

const data = {
name: nameInput.value,
description: descriptionInput.value,
}

console.log("Submitting data:", data)

fetch("/things/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => {
console.log("Create response status:", response.status)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
})
.then((responseData) => {
console.log("Create response data:", responseData)
// Reset form fields
form.reset()
// Reload the page to show the new thing
window.location.href = "/_fake/admin"
})
.catch((error) => {
console.error("Error creating thing:", error)
alert(`Error creating thing: ${error.message}`)
})
}}
>
<div className="mb-2">
<label className="block text-gray-700 text-sm font-bold mb-1">
Name:
<input type="text" name="name" className="w-full" required />
</label>
</div>
<div className="mb-2">
<label className="block text-gray-700 text-sm font-bold mb-1">
Description:
<input
type="text"
name="description"
className="w-full"
required
/>
</label>
</div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Create Thing
</button>
</form>
</div>

{/* Display thing resources */}
<div>
<h2>Thing Resources</h2>
{things.length === 0 ? (
<p className="text-gray-500 italic">No things found</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-2">
{things.map((thing) => (
<div
key={thing.thing_id}
className="border border-gray-300 rounded p-4 hover:shadow-md transition-shadow"
>
<div className="font-bold text-lg">{thing.name}</div>
<div className="text-gray-700">{thing.description}</div>
<div className="text-gray-500 text-xs mt-2">
ID: {thing.thing_id}
</div>
<div className="flex justify-between items-center mt-3 pt-2 border-t border-gray-200">
<span className="text-xs text-gray-500">Actions:</span>
<form
action="/things/delete"
method="POST"
onSubmit={(e) => {
e.preventDefault()
console.log(
`Attempting to delete thing with ID: ${thing.thing_id}`,
)

if (
confirm(
`Are you sure you want to delete "${thing.name}"?`,
)
) {
console.log("Confirmed deletion, sending request...")

const formData = new FormData()
formData.append("thing_id", thing.thing_id)

fetch("/things/delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ thing_id: thing.thing_id }),
})
.then((response) => {
console.log(
"Delete response status:",
response.status,
)
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`,
)
}
return response.json()
})
.then((data) => {
console.log("Delete response data:", data)
// Redirect to admin page to show the updated list
window.location.href = "/_fake/admin"
})
.catch((error) => {
console.error("Error deleting thing:", error)
alert(`Error deleting thing: ${error.message}`)
})
} else {
console.log("Deletion cancelled by user")
}
}}
>
<input
type="hidden"
name="thing_id"
value={thing.thing_id}
/>
<button
type="submit"
className="bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-3 rounded"
>
Delete
</button>
</form>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}

export default withRouteSpec({
methods: ["GET"],
})((req, ctx) => {
// Get things from the database
const things = ctx.db.things

// Render the admin page using the React middleware
return ctx.react(<AdminPage things={things} />)
})
13 changes: 13 additions & 0 deletions routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { withRouteSpec } from "lib/middleware/with-winter-spec"

export default withRouteSpec({
methods: ["GET"],
})((req, ctx) => {
// Redirect to the admin page
return new Response(null, {
status: 302,
headers: {
Location: "/_fake/admin",
},
})
})
Loading