<aside>
<aside>
</aside>
import { useState, useEffect } from "react";
import { setupLocalDb, getDb } from "./utils/db";
import { supabase } from "./utils/supabase";
import "./App.css";
interface Todo {
id: string;
name: string;
created_at: string;
dirty: number;
}
function App() {
// Auth-States
const [session, setSession] = useState<any>(null);
const [authEmail, setAuthEmail] = useState("");
const [authPassword, setAuthPassword] = useState("");
// NEU: State für den Namen des Nutzers bei der Registrierung
const [authName, setAuthName] = useState("");
const [isSignUp, setIsSignUp] = useState(false);
const [authLoading, setAuthLoading] = useState(false);
const [authMessage, setAuthMessage] = useState("");
// App-States
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState("");
const [isDbReady, setIsDbReady] = useState(false);
const [isOnlineMode, setIsOnlineMode] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
// Zeigt den Login-Dialog in der App an, wenn der Sync-Knopf betätigt wird
const [showAuthModal, setShowAuthModal] = useState(false);
useEffect(() => {
// Session beim Laden abrufen
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
// Listener für Änderungen
const { data } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
// Lokale DB direkt nach dem Start initialisieren
initApp();
return () => data.subscription.unsubscribe();
}, []);
async function initApp() {
await setupLocalDb();
setIsDbReady(true);
await fetchTodos();
}
// Automatischer Sync, wenn Online-Modus aktiviert wird
useEffect(() => {
if (isOnlineMode && isDbReady) {
handleSync();
}
}, [isOnlineMode]);
async function fetchTodos() {
const db = await getDb();
const data = await db.select<Todo[]>(
"SELECT * FROM todos ORDER BY created_at DESC"
);
setTodos(data);
}
async function handleSync() {
if (!isOnlineMode || isSyncing) return;
// Falls der Nutzer nicht angemeldet ist, wird der Sync-Vorgang unterbrochen
// und der Login-Dialog angezeigt.
if (!session) {
setShowAuthModal(true);
setIsOnlineMode(false);
return;
}
setIsSyncing(true);
try {
const db = await getDb();
const dirtyTodos = await db.select<Todo[]>(
"SELECT * FROM todos WHERE dirty = 1"
);
for (const todo of dirtyTodos) {
const { error } = await supabase.from("todos").upsert({
id: todo.id,
name: todo.name,
created_at: todo.created_at,
});
if (!error) {
await db.execute("UPDATE todos SET dirty = 0 WHERE id = ?", [
todo.id,
]);
}
}
const { data: remoteData } = await supabase.from("todos").select("*");
if (remoteData) {
for (const r of remoteData) {
await db.execute(
"INSERT OR REPLACE INTO todos (id, name, created_at, dirty) VALUES (?, ?, ?, 0)",
[r.id, r.name, r.created_at]
);
}
}
await fetchTodos();
} catch (err) {
console.error("Sync error:", err);
} finally {
setIsSyncing(false);
}
}
async function addTodo(e: React.FormEvent) {
e.preventDefault();
if (!newTodo.trim() || !isDbReady) return;
const db = await getDb();
const id = crypto.randomUUID();
const createdAt = new Date().toISOString();
await db.execute(
"INSERT INTO todos (id, name, created_at, dirty) VALUES (?, ?, ?, ?)",
[id, newTodo, createdAt, 1]
);
setNewTodo("");
await fetchTodos();
if (isOnlineMode) {
handleSync();
}
}
// Anmelde-Handler für das Pop-up
const handleAuthSubmit = async (event: React.FormEvent) => {
event.preventDefault();
setAuthLoading(true);
setAuthMessage("");
if (isSignUp) {
// Registrierung mit E-Mail, Passwort und dem Namen in den Metadaten
const { error } = await supabase.auth.signUp({
email: authEmail,
password: authPassword,
options: {
data: {
full_name: authName, // Hier wird der Name gespeichert
},
},
});
if (error) {
setAuthMessage(error.message);
} else {
setAuthMessage("Erfolgreich registriert! Bestätige ggf. deine E-Mail.");
}
} else {
const { error } = await supabase.auth.signInWithPassword({
email: authEmail,
password: authPassword,
});
if (error) {
setAuthMessage(error.message);
} else {
setAuthMessage("Erfolgreich angemeldet!");
// Nach erfolgreicher Anmeldung wird das Modal geschlossen und der Sync erneut aufgerufen
setTimeout(() => {
setShowAuthModal(false);
setAuthMessage("");
}, 1500);
}
}
setAuthLoading(false);
};
// Anzeige während dem Laden der Datenbank
if (!isDbReady) {
return <div className="p-10 text-white bg-black">Initialisiere...</div>;
}
return (
<main className="min-h-screen bg-black text-white p-8 flex flex-col items-center">
{/* Header / Sync Switch & Login-Button / Logout-Button */}
<div className="w-full max-w-md flex justify-between items-center mb-12">
<h1 className="text-xl font-medium">Inventar</h1>
<div className="flex items-center gap-6">
{session ? (
<button
onClick={() => supabase.auth.signOut()}
className="text-xs opacity-50 hover:opacity-100 transition-opacity"
>
Ausloggen
</button>
) : (
<button
onClick={() => setShowAuthModal(true)}
className="text-xs opacity-50 hover:opacity-100 transition-opacity"
>
Anmelden
</button>
)}
<div
onClick={() => setIsOnlineMode(!isOnlineMode)}
className="flex items-center gap-3 cursor-pointer group"
>
<span className="text-xs uppercase tracking-widest opacity-50 group-hover:opacity-100 transition-opacity">
{isSyncing ? "Syncing" : isOnlineMode ? "Online" : "Offline"}
</span>
<div
className={`w-3 h-3 rounded-full ${
isOnlineMode ? "bg-green-500" : "bg-red-500"
}`}
/>
</div>
</div>
</div>
<div className="w-full max-w-md">
{/* Input */}
<form onSubmit={addTodo} className="mb-10">
<input
className="w-full bg-transparent border-b border-gray-800 focus:border-white outline-none pb-2 transition-colors"
value={newTodo}
onChange={(e) => setNewTodo(e.currentTarget.value)}
placeholder="Neues Element hinzufügen..."
/>
</form>
{/* List */}
<ul className="space-y-6">
{todos.map((todo) => (
<li
key={todo.id}
className="flex justify-between items-center group"
>
<span className="text-gray-300 group-hover:text-white transition-colors">
{todo.name}
</span>
{/* Status Indicator Circle */}
<div
className={`w-2 h-2 rounded-full ${
todo.dirty === 1 ? "bg-blue-500" : "bg-green-500 opacity-50"
}`}
title={
todo.dirty === 1 ? "Lokal gespeichert" : "Synchronisiert"
}
/>
</li>
))}
</ul>
{todos.length === 0 && (
<p className="text-center text-gray-600 text-sm mt-10">
Keine Einträge
</p>
)}
</div>
{/* Auth-Modal Overlay, das aufpoppt, wenn man ohne Anmeldung synchronisieren möchte */}
{showAuthModal && (
<div className="fixed inset-0 bg-black bg-opacity-80 flex justify-center items-center p-4 z-50">
<div className="w-full max-w-md p-6 bg-gray-950 border border-gray-800 rounded-lg">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-medium">
{isSignUp ? "Registrierung" : "Anmeldung erforderlich"}
</h1>
<button
onClick={() => {
setShowAuthModal(false);
setIsOnlineMode(false);
}}
className="text-sm text-gray-500 hover:text-white"
>
✕ Schließen
</button>
</div>
<p className="text-sm text-gray-400 mb-6">
{isSignUp
? "Erstelle ein Konto, um deine Daten in der Cloud zu sichern."
: "Melde dich an, um dein Inventar mit Supabase zu synchronisieren."}
</p>
<form onSubmit={handleAuthSubmit} className="space-y-4">
<div>
{/* NEU: Eingabefeld für den Namen, nur sichtbar wenn man auf Registrieren ist */}
{isSignUp && (
<input
type="text"
placeholder="Dein Name"
required={isSignUp}
value={authName}
onChange={(e) => setAuthName(e.target.value)}
className="w-full bg-transparent border-b border-gray-800 focus:border-white outline-none pb-2 transition-colors mb-4"
/>
)}
<input
type="email"
placeholder="Ihre E-Mail-Adresse"
required
value={authEmail}
onChange={(e) => setAuthEmail(e.target.value)}
className="w-full bg-transparent border-b border-gray-800 focus:border-white outline-none pb-2 transition-colors mb-4"
/>
<input
type="password"
placeholder="Passwort"
required
value={authPassword}
onChange={(e) => setAuthPassword(e.target.value)}
className="w-full bg-transparent border-b border-gray-800 focus:border-white outline-none pb-2 transition-colors"
/>
</div>
<button
type="submit"
disabled={authLoading}
className="w-full py-2 px-4 bg-white text-black font-medium rounded hover:bg-gray-200 transition-colors disabled:opacity-50"
>
{authLoading
? "Wird verarbeitet..."
: isSignUp
? "Registrieren"
: "Anmelden"}
</button>
</form>
{authMessage && (
<p className="mt-4 text-sm text-gray-300 bg-gray-900 p-3 rounded">
{authMessage}
</p>
)}
<div className="mt-6 text-center">
<button
onClick={() => {
setIsSignUp(!isSignUp);
setAuthMessage("");
}}
className="text-xs text-gray-400 hover:text-white underline"
>
{isSignUp
? "Hast du schon ein Konto? Hier anmelden."
: "Noch kein Konto? Hier registrieren."}
</button>
</div>
</div>
</div>
)}
</main>
);
}
export default App;
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>E-Mail verifiziert</title>
<script>
window.onload = function() {
// Das URL-Fragment (alles nach dem #) auslesen
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
// Parameter für die App vorbereiten
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const expiresIn = params.get('expires_in');
const tokenType = params.get('token_type');
const type = params.get('type');
// Den Deep Link für die Desktop-App zusammenbauen
let deepLinkUrl = `inventar://login?` +
`access_token=${encodeURIComponent(accessToken || '')}&` +
`refresh_token=${encodeURIComponent(refreshToken || '')}&` +
`expires_in=${encodeURIComponent(expiresIn || '')}&` +
`token_type=${encodeURIComponent(tokenType || '')}&` +
`type=${encodeURIComponent(type || '')}`;
// Weiterleitung durchführen
setTimeout(() => {
window.location.href = deepLinkUrl;
}, 1000);
};
</script>
</head>
<body style="font-family: sans-serif; text-align: center; margin-top: 50px;">
<h2>Registrierung erfolgreich!</h2>
<p>Du wirst gleich in die App weitergeleitet...</p>
<a href="inventar://login">Falls die Weiterleitung nicht funktioniert, hier klicken.</a>
</body>
</html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>E-Mail verifiziert</title>
<script>
window.onload = function() {
// Das URL-Fragment (alles nach dem #) auslesen
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
// Parameter für die App vorbereiten
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const expiresIn = params.get('expires_in');
const tokenType = params.get('token_type');
const type = params.get('type');
// Den Deep Link für die Desktop-App zusammenbauen
let deepLinkUrl = `inventar://login?` +
`access_token=${encodeURIComponent(accessToken || '')}&` +
`refresh_token=${encodeURIComponent(refreshToken || '')}&` +
`expires_in=${encodeURIComponent(expiresIn || '')}&` +
`token_type=${encodeURIComponent(tokenType || '')}&` +
`type=${encodeURIComponent(type || '')}`;
// Weiterleitung durchführen
setTimeout(() => {
window.location.href = deepLinkUrl;
}, 1000);
};
</script>
</head>
<body style="font-family: sans-serif; text-align: center; margin-top: 50px;">
<h2>Registrierung erfolgreich!</h2>
<p>Du wirst gleich in die App weitergeleitet...</p>
<a href="inventar://login">Falls die Weiterleitung nicht funktioniert, hier klicken.</a>
</body>
</html>
