Compare commits
4 Commits
fix/issue-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cf5646bbd9 | |||
| 9800c05314 | |||
| e4c7baab73 | |||
| 8a8270dbae |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ drizzle/
|
||||
.mise.local.toml
|
||||
.env.local
|
||||
.env.*.local
|
||||
# Test by hermes
|
||||
|
||||
936
package-lock.json
generated
936
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.451 0.211 262.881);
|
||||
--primary: oklch(0.145 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
@@ -23,7 +23,7 @@
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.451 0.211 262.881);
|
||||
--ring: oklch(0.145 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
@@ -38,8 +38,8 @@
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.585 0.233 262.881);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.145 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
@@ -49,7 +49,7 @@
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.585 0.233 262.881);
|
||||
--ring: oklch(0.985 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
|
||||
@@ -5,7 +5,7 @@ const router = createRouter({
|
||||
routeTree,
|
||||
basepath: "/agw",
|
||||
defaultErrorComponent: ({ error }) => (
|
||||
<div style={{ padding: 20, color: "red" }}>
|
||||
<div className="p-5 text-destructive">
|
||||
<h2>Error</h2>
|
||||
<pre>{error instanceof Error ? error.message : String(error)}</pre>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function UpcomingVotes() {
|
||||
{item.summary}
|
||||
</p>
|
||||
)}
|
||||
<span className="inline-block mt-2 text-xs font-medium text-blue-600">
|
||||
<span className="inline-block mt-2 text-xs font-medium text-primary">
|
||||
Jetzt abstimmen →
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function LegislationDetail({ legislationId }: LegislationDetailProps) {
|
||||
{legislation.title}
|
||||
</h1>
|
||||
{legislation.beratungsstand && (
|
||||
<span className="inline-block mt-2 text-xs px-2 py-1 rounded-full bg-blue-100 text-blue-800">
|
||||
<span className="inline-block mt-2 text-xs px-2 py-1 rounded-full bg-secondary text-secondary-foreground">
|
||||
{legislation.beratungsstand}
|
||||
</span>
|
||||
)}
|
||||
@@ -78,7 +78,7 @@ export function LegislationDetail({ legislationId }: LegislationDetailProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowFullText(!showFullText)}
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
className="text-sm text-primary hover:underline"
|
||||
>
|
||||
{showFullText ? "Volltext ausblenden" : "Volltext anzeigen"}
|
||||
</button>
|
||||
|
||||
@@ -12,13 +12,13 @@ interface VoteResultProps {
|
||||
}
|
||||
|
||||
const VOTE_COLORS: Record<string, string> = {
|
||||
yes: "bg-green-100 text-green-800",
|
||||
ja: "bg-green-100 text-green-800",
|
||||
no: "bg-red-100 text-red-800",
|
||||
nein: "bg-red-100 text-red-800",
|
||||
abstain: "bg-gray-100 text-gray-600",
|
||||
enthaltung: "bg-gray-100 text-gray-600",
|
||||
no_show: "bg-gray-50 text-gray-400",
|
||||
yes: "bg-secondary text-secondary-foreground",
|
||||
ja: "bg-secondary text-secondary-foreground",
|
||||
no: "bg-muted text-muted-foreground",
|
||||
nein: "bg-muted text-muted-foreground",
|
||||
abstain: "bg-accent text-accent-foreground",
|
||||
enthaltung: "bg-accent text-accent-foreground",
|
||||
no_show: "bg-muted/50 text-muted-foreground/70",
|
||||
}
|
||||
|
||||
const VOTE_LABELS: Record<string, string> = {
|
||||
@@ -36,8 +36,8 @@ export function VoteResult({ userVote, politicianVotes }: VoteResultProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 rounded-lg bg-blue-50 border border-blue-200">
|
||||
<p className="text-xs text-blue-600 font-medium">Dein Vote</p>
|
||||
<div className="p-3 rounded-lg bg-primary/5 border border-border">
|
||||
<p className="text-xs text-primary font-medium">Dein Vote</p>
|
||||
<p className="text-sm font-semibold mt-1">{userLabel}</p>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function VoteResult({ userVote, politicianVotes }: VoteResultProps) {
|
||||
{label}
|
||||
</span>
|
||||
{matches && (
|
||||
<span className="text-xs text-green-600">= Dein Vote</span>
|
||||
<span className="text-xs text-primary">= Dein Vote</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,12 @@ interface VoteWidgetProps {
|
||||
}
|
||||
|
||||
const choices: { value: UserVoteChoice; label: string; color: string }[] = [
|
||||
{ value: "ja", label: "Ja", color: "bg-green-600 hover:bg-green-700" },
|
||||
{ value: "nein", label: "Nein", color: "bg-red-600 hover:bg-red-700" },
|
||||
{ value: "ja", label: "Ja", color: "bg-primary hover:bg-primary/90" },
|
||||
{ value: "nein", label: "Nein", color: "bg-secondary hover:bg-secondary/80" },
|
||||
{
|
||||
value: "enthaltung",
|
||||
label: "Enthaltung",
|
||||
color: "bg-gray-500 hover:bg-gray-600",
|
||||
color: "bg-muted hover:bg-muted/80",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
const VOTE_COLORS: Record<string, { bg: string; text: string; label: string }> =
|
||||
{
|
||||
yes: {
|
||||
bg: "bg-green-100 dark:bg-green-900/40",
|
||||
text: "text-green-800 dark:text-green-300",
|
||||
bg: "bg-secondary",
|
||||
text: "text-secondary-foreground",
|
||||
label: "Ja",
|
||||
},
|
||||
no: {
|
||||
bg: "bg-red-100 dark:bg-red-900/40",
|
||||
text: "text-red-800 dark:text-red-300",
|
||||
bg: "bg-muted",
|
||||
text: "text-muted-foreground",
|
||||
label: "Nein",
|
||||
},
|
||||
abstain: {
|
||||
|
||||
@@ -287,7 +287,7 @@ export function SettingsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
{devHealth && (
|
||||
<span
|
||||
className={`text-xs ${devHealth === "ok" ? "text-green-600" : "text-destructive"}`}
|
||||
className={`text-xs ${devHealth === "ok" ? "text-foreground" : "text-destructive"}`}
|
||||
>
|
||||
{devHealth}
|
||||
</span>
|
||||
@@ -314,7 +314,7 @@ export function SettingsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
{devPush && (
|
||||
<span
|
||||
className={`text-xs ${devPush === "ok" ? "text-green-600" : "text-destructive"}`}
|
||||
className={`text-xs ${devPush === "ok" ? "text-foreground" : "text-destructive"}`}
|
||||
>
|
||||
{devPush}
|
||||
</span>
|
||||
@@ -344,7 +344,9 @@ export function SettingsPage() {
|
||||
<span className="text-sm">Alle Themen folgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devTopics && (
|
||||
<span className="text-xs text-green-600">{devTopics}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{devTopics}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -368,7 +370,7 @@ export function SettingsPage() {
|
||||
<span className="text-sm">Allen Themen entfolgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devUnfollowTopics && (
|
||||
<span className="text-xs text-green-600">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{devUnfollowTopics}
|
||||
</span>
|
||||
)}
|
||||
@@ -389,7 +391,7 @@ export function SettingsPage() {
|
||||
<span className="text-sm">Alle Abgeordnete folgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devPoliticians && (
|
||||
<span className="text-xs text-green-600">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{devPoliticians}
|
||||
</span>
|
||||
)}
|
||||
@@ -421,7 +423,7 @@ export function SettingsPage() {
|
||||
<span className="text-sm">Allen Abgeordneten entfolgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devUnfollowPoliticians && (
|
||||
<span className="text-xs text-green-600">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{devUnfollowPoliticians}
|
||||
</span>
|
||||
)}
|
||||
@@ -442,7 +444,9 @@ export function SettingsPage() {
|
||||
<span className="text-sm">Abgeordnete neu laden</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devReload && (
|
||||
<span className="text-xs text-green-600">{devReload}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{devReload}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -11,18 +11,91 @@ async function initDb(db: PGlite): Promise<PGlite> {
|
||||
return db
|
||||
}
|
||||
|
||||
/** Get the singleton PGlite instance. Safe to call multiple times — only creates once. */
|
||||
/** Detect if we're on iOS Safari which has IndexedDB limitations */
|
||||
function isIOSSafari(): boolean {
|
||||
const userAgent = navigator.userAgent
|
||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent)
|
||||
const isSafari =
|
||||
/Safari/.test(userAgent) && !/Chrome|CriOS|FxiOS/.test(userAgent)
|
||||
return isIOS || (isSafari && "ontouchstart" in window)
|
||||
}
|
||||
|
||||
/** Get the singleton PGlite instance with iOS fallback. Safe to call multiple times — only creates once. */
|
||||
export function getDb(): Promise<PGlite> {
|
||||
if (instance) return Promise.resolve(instance)
|
||||
if (!initPromise) {
|
||||
initPromise = initDb(new PGlite("idb://agw")).then((db) => {
|
||||
initPromise = createDbWithFallback()
|
||||
.then((db) => {
|
||||
instance = db
|
||||
return db
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Database initialization failed:", error)
|
||||
// Reset promise to allow retry
|
||||
initPromise = null
|
||||
throw error
|
||||
})
|
||||
}
|
||||
return initPromise
|
||||
}
|
||||
|
||||
async function createDbWithFallback(): Promise<PGlite> {
|
||||
// Add timeout to prevent hanging
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(
|
||||
() => reject(new Error("Database initialization timeout")),
|
||||
10000,
|
||||
)
|
||||
})
|
||||
|
||||
try {
|
||||
// First try IndexedDB with timeout
|
||||
const dbPromise = Promise.race([
|
||||
initDb(new PGlite("idb://agw")),
|
||||
timeoutPromise,
|
||||
])
|
||||
|
||||
const db = await dbPromise
|
||||
console.log("Database initialized successfully with IndexedDB")
|
||||
return db
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"IndexedDB initialization failed, falling back to memory:",
|
||||
error,
|
||||
)
|
||||
|
||||
// iOS Safari fallback: use in-memory database
|
||||
if (isIOSSafari()) {
|
||||
console.log("iOS Safari detected, using in-memory database")
|
||||
return await initDb(new PGlite())
|
||||
}
|
||||
|
||||
// For other browsers, try one more time with shorter timeout
|
||||
try {
|
||||
const shortTimeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(
|
||||
() => reject(new Error("Database initialization timeout (retry)")),
|
||||
5000,
|
||||
)
|
||||
})
|
||||
|
||||
const retryDb = await Promise.race([
|
||||
initDb(new PGlite("idb://agw")),
|
||||
shortTimeoutPromise,
|
||||
])
|
||||
|
||||
console.log("Database initialized successfully with IndexedDB (retry)")
|
||||
return retryDb
|
||||
} catch (retryError) {
|
||||
console.warn(
|
||||
"IndexedDB retry failed, using in-memory database:",
|
||||
retryError,
|
||||
)
|
||||
return await initDb(new PGlite())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a fresh in-memory PGlite for tests. */
|
||||
export async function createTestDb(): Promise<PGlite> {
|
||||
return initDb(new PGlite())
|
||||
|
||||
@@ -3,14 +3,22 @@ import type { PGlite } from "@electric-sql/pglite"
|
||||
|
||||
/** One-time migration: read existing localStorage data, insert into PGlite, remove localStorage keys. */
|
||||
export async function migrateFromLocalStorage(db: PGlite): Promise<void> {
|
||||
console.log("Starting localStorage migration...")
|
||||
|
||||
try {
|
||||
// device ID
|
||||
const deviceId = localStorage.getItem(STORAGE_KEYS.deviceId)
|
||||
if (deviceId) {
|
||||
try {
|
||||
await db.query(
|
||||
"INSERT INTO device (id) VALUES ($1) ON CONFLICT DO NOTHING",
|
||||
[deviceId],
|
||||
)
|
||||
localStorage.removeItem(STORAGE_KEYS.deviceId)
|
||||
console.log("Migrated device ID")
|
||||
} catch (error) {
|
||||
console.warn("Failed to migrate device ID:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// follows
|
||||
@@ -23,15 +31,20 @@ export async function migrateFromLocalStorage(db: PGlite): Promise<void> {
|
||||
label: string
|
||||
}>
|
||||
for (const f of follows) {
|
||||
try {
|
||||
await db.query(
|
||||
"INSERT INTO follows (type, entity_id, label) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||
[f.type, f.entity_id, f.label],
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn("Failed to migrate follow item:", f, error)
|
||||
}
|
||||
} catch {
|
||||
// corrupt data — skip
|
||||
}
|
||||
localStorage.removeItem(STORAGE_KEYS.follows)
|
||||
console.log("Migrated follows")
|
||||
} catch (error) {
|
||||
console.warn("Failed to migrate follows:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// feed cache
|
||||
@@ -49,10 +62,11 @@ export async function migrateFromLocalStorage(db: PGlite): Promise<void> {
|
||||
[JSON.stringify({ items: parsed.items }), parsed.updatedAt],
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// corrupt data — skip
|
||||
}
|
||||
localStorage.removeItem(STORAGE_KEYS.feedCache)
|
||||
console.log("Migrated feed cache")
|
||||
} catch (error) {
|
||||
console.warn("Failed to migrate feed cache:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// geo cache
|
||||
@@ -64,26 +78,46 @@ export async function migrateFromLocalStorage(db: PGlite): Promise<void> {
|
||||
{ timestamp: number; result: unknown }
|
||||
>
|
||||
for (const [bundesland, entry] of Object.entries(cache)) {
|
||||
try {
|
||||
await db.query(
|
||||
`INSERT INTO geo_cache (bundesland, data, cached_at) VALUES ($1, $2, to_timestamp($3 / 1000.0))
|
||||
ON CONFLICT (bundesland) DO UPDATE SET data = $2, cached_at = to_timestamp($3 / 1000.0)`,
|
||||
[bundesland, JSON.stringify(entry.result), entry.timestamp],
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"Failed to migrate geo cache entry:",
|
||||
bundesland,
|
||||
error,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// corrupt data — skip
|
||||
}
|
||||
localStorage.removeItem(STORAGE_KEYS.geoCache)
|
||||
console.log("Migrated geo cache")
|
||||
} catch (error) {
|
||||
console.warn("Failed to migrate geo cache:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// push enabled
|
||||
const pushEnabled = localStorage.getItem(STORAGE_KEYS.pushEnabled)
|
||||
if (pushEnabled) {
|
||||
try {
|
||||
await db.query(
|
||||
`INSERT INTO push_state (key, value) VALUES ('enabled', $1)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $1`,
|
||||
[pushEnabled],
|
||||
)
|
||||
localStorage.removeItem(STORAGE_KEYS.pushEnabled)
|
||||
console.log("Migrated push enabled state")
|
||||
} catch (error) {
|
||||
console.warn("Failed to migrate push enabled state:", error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("localStorage migration completed")
|
||||
} catch (error) {
|
||||
console.error("Migration failed with critical error:", error)
|
||||
// Don't throw - let the app continue with empty database
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,146 @@
|
||||
import type { PGlite } from "@electric-sql/pglite"
|
||||
import { type ReactNode, createContext, use } from "react"
|
||||
import {
|
||||
Component,
|
||||
type ErrorInfo,
|
||||
type ReactNode,
|
||||
Suspense,
|
||||
createContext,
|
||||
use,
|
||||
} from "react"
|
||||
import { getDb } from "./client"
|
||||
import { migrateFromLocalStorage } from "./migrate-from-localstorage"
|
||||
|
||||
const dbPromise = getDb().then(async (db) => {
|
||||
const dbPromise = getDb()
|
||||
.then(async (db) => {
|
||||
try {
|
||||
await migrateFromLocalStorage(db)
|
||||
console.log("Database migration completed successfully")
|
||||
return db
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("Database migration failed, continuing anyway:", error)
|
||||
// Continue with database even if migration fails
|
||||
return db
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Database initialization completely failed:", error)
|
||||
// Re-throw to be caught by ErrorBoundary
|
||||
throw error
|
||||
})
|
||||
|
||||
const DbContext = createContext<PGlite | null>(null)
|
||||
|
||||
export function DbProvider({ children }: { children: ReactNode }) {
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
class DbErrorBoundary extends Component<
|
||||
{ children: ReactNode; onReset?: () => void },
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: { children: ReactNode; onReset?: () => void }) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error("DbErrorBoundary caught error:", error, errorInfo)
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: undefined })
|
||||
if (this.props.onReset) {
|
||||
this.props.onReset()
|
||||
} else {
|
||||
// Default: reload the page
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center mb-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-8 h-8 text-red-600 dark:text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
role="img"
|
||||
aria-label="Error icon"
|
||||
>
|
||||
<title>Error</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold mb-2">
|
||||
Datenbank-Initialisierung fehlgeschlagen
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mb-4 max-w-sm">
|
||||
Die App konnte nicht geladen werden. Dies kann auf iOS Safari
|
||||
vorkommen.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.handleReset}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
{this.state.error && (
|
||||
<details className="mt-4 text-xs text-gray-600 max-w-sm">
|
||||
<summary className="cursor-pointer">Technische Details</summary>
|
||||
<pre className="mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-left text-xs overflow-auto">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
function DbProviderInner({ children }: { children: ReactNode }) {
|
||||
const db = use(dbPromise)
|
||||
return <DbContext value={db}>{children}</DbContext>
|
||||
}
|
||||
|
||||
export function DbProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DbErrorBoundary>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Datenbank wird geladen...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DbProviderInner>{children}</DbProviderInner>
|
||||
</Suspense>
|
||||
</DbErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDb(): PGlite {
|
||||
const db = use(DbContext)
|
||||
if (!db) throw new Error("useDb must be used within DbProvider")
|
||||
|
||||
Reference in New Issue
Block a user