Initial commit: installable PWA hello world
React + TypeScript + Tailwind v4 + shadcn/ui frontend (client), Express + better-sqlite3 backend (server) in a pnpm workspace. Includes vite-plugin-pwa manifest, service worker, and iOS install meta tags. GET /api/hello increments a visit counter in SQLite; backend serves the built client in production.
This commit is contained in:
66
client/src/App.tsx
Normal file
66
client/src/App.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { RefreshCw } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { fetchHello, type HelloResponse } from "@/lib/api"
|
||||
|
||||
export default function App() {
|
||||
const [data, setData] = useState<HelloResponse | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
setData(await fetchHello())
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Something went wrong")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [load])
|
||||
|
||||
return (
|
||||
<main className="dark:bg-background dark:text-foreground flex min-h-svh flex-col items-center justify-center bg-background gap-6 p-6 text-center">
|
||||
<div className="flex max-w-sm flex-col items-center gap-6">
|
||||
<span className="bg-primary/10 text-primary rounded-full px-3 py-1 text-xs font-medium">
|
||||
Pocket Pascal
|
||||
</span>
|
||||
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
{data?.message ?? "Hello, World!"}
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
A minimal installable PWA. React + shadcn/ui on the front, Express +
|
||||
SQLite on the back.
|
||||
</p>
|
||||
|
||||
<div className="bg-muted text-muted-foreground flex w-full flex-col gap-1 rounded-lg border p-4 text-sm">
|
||||
{loading ? (
|
||||
<span>Asking the server…</span>
|
||||
) : error ? (
|
||||
<span className="text-destructive">{error}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-foreground text-2xl font-semibold">
|
||||
{data?.visits.toLocaleString()}
|
||||
</span>
|
||||
<span>times this hello has been said</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button onClick={() => void load()} disabled={loading}>
|
||||
<RefreshCw className={loading ? "animate-spin" : undefined} />
|
||||
Say it again
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user