Desktop-App (Tauri)

Landingpage (Astro)

<aside>


Wiki

Git Workflow & Standards

Domain Endings

Notizen Brigitte

Assessment: SQ Assurance

Web User Research

Task Board

Untitled

Drive


Proton Drive

<aside>

Core App Features

Feature List

</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>

image.png