# Project export: StudyWithKat

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: StudyWithKat is a Tamagotchi-style education app that gamifies flashcards, quizzes, and Pomodoro focus, rewarding learning progress with coins to care for an interactive virtual cat.
- Devpost: https://devpost.com/software/temp-2eh69k
- GitHub: https://github.com/xushengou/2x2
- Video: https://www.youtube.com/embed/KdDG7rDYqTU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 0 GitHub contributor(s) — 

## Devpost submission (written by the team)

### Inspiration

We wanted to create a tool that not only makes studying productive but also rewarding. Inspired by the Tamagotchi style, StudyWithKat enhances the learning experience to be interactive and motivating, allowing students to incentivize their learning.

### What it does

StudyWithKat is a Tamagotchi-style education app that transforms studying into a game. Users can earn coins by completing learning inspired tasks such as creating flashcards, taking quizzes, and using the focus mode. The coins can be used to take care of your interactive virtual cat, which reflects the user’s effort towards studying with Kat.

### How we built it

We built StudyWithKat using HTML, CSS, and JavaScript to create the frontend. The backend was built with Flask to send and receive information from Firebase, bridging our frontend to our database. Firebase was used for data storage and authentication, while the ChatGPT API is used for creating flashcard generation from file imports, quizzes, and daily tasks.

### Challenges we ran into

We encountered challenges connecting our HTML frontend to our Firebase database. This obstacle slowed our progress and ultimately prevented us from implementing some of the additional features we wanted to include. This experience provided us with insight into database integration and helped us understand the importance of preparing for potential connectivity issues.

### Accomplishments we're proud of

While developing StudyWithKat, we took pride in planning early and monitoring tasks with a well-organized task list. Having a clear structure allowed us to make progress promptly. Completing the frontend felt like a major accomplishment, as it was one crucial component to establishing the app.

### What we learned

As our codebase became more complex, we realized the importance of outlining our plans for the frontend and backend components. Keeping active communication between both sides was vital to ensuring that features were implemented effectively. This taught us that efficient collaboration and planning are just as crucial as writing code when it comes to developing a functional app.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 20 recognized source files, 123 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Firebase (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (27 of 27)

```
.DS_Store
.gitignore
.vscode/settings.json
app.py
flashcardtest.txt
requirements.txt
template/.DS_Store
template/dashboard.html
template/login.html
template/scripts/daily_quests.js
template/scripts/firebase_sync.js
template/scripts/flashcards_study_modal.js
template/scripts/flashcards_sync.js
template/scripts/focus_timer.js
template/scripts/logout_fix.js
template/scripts/pet_chat_ai.js
template/scripts/pet_sync.js
template/scripts/pet.js
template/scripts/profile_edit.js
template/scripts/script_auth.js
template/scripts/script.js
template/scripts/sidebar_hover_fix.js
template/scripts/timer.js
template/scripts/todo_sync.js
template/signup.html
template/styles/.DS_Store
template/styles/style.css
```

### Dependencies

- requirements.txt: firebase-admin@==6.5.0, Flask@==3.0.3, flask-cors@==4.0.1, gunicorn@==22.0.0, openai, python-dotenv@==1.0.1, requests@==2.32.3

### Recent commits (newest first)

- Initial clean commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
Flask==3.0.3
flask-cors==4.0.1
firebase-admin==6.5.0
python-dotenv==1.0.1
requests==2.32.3
gunicorn==22.0.0
openai
```

### app.py

```python
import os
import random
import datetime
from functools import wraps

import requests
from dotenv import load_dotenv

from flask import Flask, request, jsonify, session, send_from_directory, redirect
from flask_cors import CORS

import firebase_admin
from firebase_admin import credentials, auth, firestore

# OpenAI is optional (only needed for /api/petchat)
try:
    from openai import OpenAI
except Exception:
    OpenAI = None

# ==============================
# Config / init
# ==============================
load_dotenv()

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TEMPLATE_DIR = os.path.join(BASE_DIR, "template")

# Serve HTML from /template and also serve static assets from /template
# so links like /styles/style.css and /scripts/script.js work.
app = Flask(
    __name__,
    template_folder="template",
    static_folder="template",
    static_url_path="",
)

# Cookies (helps with session)
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
)

CORS(app, supports_credentials=True)

# REQUIRED ENV VARS:
# - FLASK_SECRET_KEY
# - FIREBASE_API_KEY
# - GOOGLE_APPLICATION_CREDENTIALS (path to serviceAccountKey.json)
app.secret_key = os.getenv("FLASK_SECRET_KEY", "dev_change_me")

FIREBASE_API_KEY = os.getenv("FIREBASE_API_KEY")
SERVICE_ACCOUNT_JSON = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")

# Friendly fallback: if GOOGLE_APPLICATION_CREDENTIALS is not set,
# try serviceAccountKey.json in the project folder.
if not SERVICE_ACCOUNT_JSON:
    local_key = os.path.join(BASE_DIR, "serviceAccountKey.json")
    if os.path.exists(local_key):
        SERVICE_ACCOUNT_JSON = local_key

if not FIREBASE_API_KEY:
    raise RuntimeError("Missing FIREBASE_API_KEY in .env")
if not SERVICE_ACCOUNT_JSON:
    raise RuntimeError(
        "Missing GOOGLE_APPLICATION_CREDENTIALS in .env (or serviceAccountKey.json not found next to app.py)"
    )

if not firebase_admin._apps:
    firebase_admin.initialize_app(credentials.Certificate(SERVICE_ACCOUNT_JSON))

db = firestore.client()

# OpenAI client (optional)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
openai_client = OpenAI(api_key=OPENAI_API_KEY) if (OPENAI_API_KEY and OpenAI) else None


# ==============================
# Helpers
# ==============================

def login_required(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        if not session.get("uid"):
            return jsonify({"error": "Not logged in"}), 401
        return fn(*args, **kwargs)

    return wrapper


def user_ref(uid: str):
    return db.collection("users").document(uid)


def ensure_user_doc(uid: str, email: str, username: str = ""):
    ref = user_ref(uid)
    snap = ref.get()
    if snap.exists:
        return ref

    ref.set(
        {
            "email": email,
            "username": username,
            "coins": 0,
            "pet": {"hunger": 50, "thirst": 50, "health": 50, "happiness": 50},
            "flashcard_counter": 0,
            "todo_counter": 0,
            "chat": {
                # No logs stored. Just a persona/settings blob.
                "persona": "You are a friendly virtual pet. Keep replies short, playful, and encouraging.",
            },
        }
    )
    return ref


def parse_int(v, default=None):
    try:
        return int(v)
    except Exception:
        return default


def next_counter(ref, field_name: str) -> int:
    tx = db.transaction()

    @firestore.transactional
    def _inc(transaction):
        snap = ref.get(transaction=transaction)
        current = int(snap.get(field_name) or 0)
        new_val = current + 1
        transaction.update(ref, {field_name: new_val})
        return new_val

    return _inc(tx)


def firebase_password_login(email: str, password: str):
    """Firebase Admin SDK cannot verify email/password.
    Use Firebase Auth REST API signInWithPassword.
    """
    url = (
        "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword"
        f"?key={FIREBASE_API_KEY}"
    )
    payload = {"email": email, "password": password, "returnSecureToken": True}
    r = requests.post(url, json=payload, timeout=15)
    if r.status_code != 200:
        try:
            code = r.json().get("error", {}).get("message", "LOGIN_FAILED")
        except Exception:
            code = "LOGIN_FAILED"
        return None, code
    return r.json(), None


# ==============================
# Pages
# ==============================

@app.get("/")
def root():
    return redirect("/login.html")

@app.get("/login.html")
def login_page():
    return send_from_directory("template", "login.html")

@app.get("/signup.html")
def signup_page():
    return send_from_directory("template", "signup.html")

@app.get("/dashboard.html")
def dashboard_page():
    return send_from_directory("template", "dashboard.html")


# ==============================
# Auth APIs
# ==============================

@app.post("/api/signup")
def api_signup():
    body = request.get_json(silent=True) or {}
    username = str(body.get("username", "")).strip()
    email = str(body.get("email", "")).strip()
    password = str(body.get("password", "")).strip()

    if not username or not email or not password:
        return jsonify({"error": "username, email, password required"}), 400

    try:
        u = auth.create_user(email=email, password=password, display_name=username)
        ensure_user_doc(u.uid, email=email, username=username)

        # auto login
        data, err = firebase_password_login(email, password)
        if err:
            return jsonify({"error": "Signup succeeded, but login failed", "code": err}), 401

        session["uid"] = data["localId"]
        session["email"] = email
        return jsonify({"ok": True})

    except auth.EmailAlreadyExistsError:
        return jsonify({"error": "Email already in use"}), 400
    except Exception as e:
        return jsonify({"error": "Signup failed", "detail": str(e)}), 400


@app.post("/api/login")
def api_login():
    body = request.get_js
[truncated — 11725 more characters]
```

### template/login.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Study-Gotchi | Login</title>
    <link rel="stylesheet" href="/styles/style.css" />
    <script src="/scripts/script_auth.js"></script>
  </head>
  <body>
    <div class="screen-container">
      <div class="auth-card">
        <h1 style="color: var(--primary-dark)">😸 StudyWithKat</h1>
        <p
          style="
            color: var(--text);
            font-family: &quot;Fredoka&quot;, sans-serif;
            font-size: 20px;
            font-weight: bold;
            line-height: 1.4;
          "
        >
          Welcome back!
        </p>

        <!-- Require email for the firebase -->
        <input type="email" id="email-input" placeholder="Email Address" />
        <input type="password" id="pass-input" placeholder="Password" />

        <div
          id="auth-error"
          style="
            color: #e74c3c;
            font-size: 12px;
            margin-top: 6px;
            display: none;
          "
        ></div>

        <button
          class="cute-btn"
          style="width: 100%; margin-top: 15px"
          onclick="login()"
        >
          Login
        </button>

        <div class="toggle-link" onclick="goTo('signup.html')">
          Create Account
        </div>
      </div>
    </div>
  </body>
</html>

```

### template/signup.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Study-Gotchi | Sign Up</title>
    <link rel="stylesheet" href="/styles/style.css" />
    <script src="/scripts/script_auth.js"></script>
  </head>
  <body>
    <div class="screen-container">
      <div class="auth-card">
        <h1 style="color: var(--primary-dark)">StudyWithKat</h1>
        <p
          style="
            color: var(--text);
            font-family: &quot;Fredoka&quot;, sans-serif;
            font-size: 20px;
            font-weight: bold;
            line-height: 1.4;
          "
        >
          Join The Club!
        </p>

        <input type="text" id="user-input" placeholder="Username" />
        <input type="email" id="email-input" placeholder="Email Address" />
        <input type="password" id="pass-input" placeholder="Password" />
        <div
          id="auth-error"
          style="
            color: #e74c3c;
            font-size: 12px;
            margin-top: 6px;
            display: none;
          "
        ></div>

        <button
          class="cute-btn"
          style="width: 100%; margin-top: 15px"
          onclick="signup()"
        >
          Sign-In
        </button>

        <div class="toggle-link" onclick="goTo('login.html')">
          Already a member? Login here!
        </div>
      </div>
    </div>
  </body>
</html>

```

### template/scripts/logout_fix.js

```javascript
async function logout() {
  try {
    await fetch("/api/logout", { method: "POST", credentials: "include" });
  } catch (e) {
    // incase of a fail request, it redirects the user away from the dashboard
    console.warn("Logout request failed:", e);
  }
  window.location.href = "login.html";
}

```

### template/dashboard.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Study-Gotchi | Dashboard</title>
    <link rel="stylesheet" href="/styles/style.css">
  </head>
  <body>
    
    <div id="app-screen">
      <div
        class="sidebar-arrow"
        onmouseenter="openMenu()"
        onmouseleave="scheduleClose()"
      >
        ◀
      </div>

      <div
        id="sidebar"
        class="sidebar"
        onmouseenter="keepOpen()"
        onmouseleave="scheduleClose()"
      >
        <div class="sidebar-header">
  <span>💰 Coins: <span id="coin-display">50</span></span>

  <div class="profile-container">
    <div 
      class="profile-icon" 
      onclick="document.getElementById('pop-menu').classList.toggle('show')"
      style="cursor: pointer;"
    >
      👤
    </div>

    <div id="pop-menu" class="profile-popup">
      <div
        style="
          text-align: center;
          font-weight: bold;
          color: var(--primary-dark);
          margin-bottom: 10px;
        "
      >
        My Profile
      </div>

      <span class="profile-label">Username:</span>
      <input
        id="profile-user"
        class="profile-value"
        value="Student"
        disabled
      />

      <span class="profile-label">Email:</span>
      <input
        id="profile-email"
        class="profile-value"
        value="student@study.com"
        disabled
      />

      <span class="profile-label">Password:</span>
      <input
        id="profile-password"
        class="profile-value"
        type="password"
        value="********"
        disabled
      />

      <div id="new-password-wrap" style="display:none;">
        <span class="profile-label">New Password:</span>
        <input
          id="profile-pass-new"
          class="profile-value"
          type="password"
          placeholder="New password (optional)"
        />

        <span class="profile-label">Confirm Password:</span>
        <input
          id="profile-pass-new2"
          class="profile-value"
          type="password"
          placeholder="Confirm new password"
        />
      </div>

      <div
        id="profile-error"
        style="color: #e74c3c; font-size: 12px; margin-top: 6px; display: none;"
      ></div>

      <button
        id="edit-save-btn"
        class="cute-btn"
        style="margin-top: 10px; background: var(--primary); font-size: 12px;"
        onclick="toggleEditSaveProfile()"
      >
        ✏️ Edit
      </button>

      <button
        class="logout-btn"
        style="margin-top: 5px; padding: 8px; font-size: 12px"
        onclick="logout()"
      >
        🚪 Logout
      </button>
    </div>
  </div>
</div>

        <button class="accordion-btn" onclick="openSection('chat-sec')">
          💬 Chat with Pet <span>▼</span>
        </button>
        <div id="chat-sec" class="accordion-content">
          <div class="content-pad">
            <div id="chat-history" class="chat-history">
              <div class="chat-bubble chat-pet">
                Hi bestie! How can I help you study today?
              </div>
            </div>
            <div class="chat-input-row">
              <input
                id="chat-input"
                class="mini-input"
                style="flex: 1; margin: 0"
                placeholder="Say something..."
              />
              <button
                class="cute-btn"
                style="width: auto; margin: 0; padding: 8px 15px"
                onclick="sendMessage()"
              >
                ➤
              </button>
            </div>
          </div>
        </div>

        <button class="accordion-btn" onclick="openSection('ai-quest')">
          🤖 AI Daily Quest <span>▼</span>
        </button>
        <div id="ai-quest" class="accordion-content">
          <div class="content-pad">
            <div id="ai-start-view">
              <div id="ai-quest-list">
              </div>
              <p style="font-size: 12px; color: var(--text-secondary)">
                Earn coins from qu
              </p>
              <button
                class="cute-btn"
                style="background: #2d3436"
                onclick="generateAIQuest()"
              >
                Generate Quest
              </button>
            </div>
            <div
              id="ai-loading-view"
              class="hidden"
              style="text-align: center; color: var(--primary)"
            >
              [AI] Generating...
            </div>
            <div id="ai-challenge-view" class="hidden">
              <div
                style="
                  background: #2d3436;
                  color: #00ff00;
                  padding: 10px;
                  border-radius: 10px;
                  font-family: monospace;
                  margin-bottom: 10px;
                "
              >
                <strong>[MISSION]</strong><br /><span
                  id="ai-prompt-text"
                ></span>
              </div>
              <input
                id="ai-answer-input"
                placeholder="Answer..."
                class="mini-input"
              />
              <button
                class="cute-btn"
                style="margin-top: 5px; background: #00b894"
                onclick="submitAIQuest()"
              >
                Submit
              </button>
            </div>
          </div>
        </div>

<button class="accordion-btn" onclick="openSection('todo')">
  📝 To-Do List <span>▼</span>
</button>

<div id="todo" class="accordion-content">
  <div class="content-pad">
    <div style="display: flex; gap: 10px; margin-bottom: 15px; align-items: center;">
      <input 
        type="text" 
        id="new-task" 
        class="mini-input" 
        style="flex: 1;" 
        placeholder="Add task..."
      >

      <button 
        class="todo-circle-btn todo-add-btn" 
        onclick=
[truncated — 14721 more characters]
```

### template/scripts/sidebar_hover_fix.js

```javascript
// Sidebar hover fix: defines keepOpen() and scheduleClose() used in dashboard.html
// Prevents ReferenceErrors and keeps sidebar from closing while hovered.

(() => {
  let closeTimer = null;

  function clearCloseTimer() {
    if (closeTimer) {
      clearTimeout(closeTimer);
      closeTimer = null;
    }
  }

  // Called when mouse enters sidebar
  window.keepOpen = function keepOpen() {
    clearCloseTimer();
  };

  // Called when mouse leaves sidebar or arrow area
  window.scheduleClose = function scheduleClose(delay = 250) {
    clearCloseTimer();
    closeTimer = setTimeout(() => {
      const sidebar = document.getElementById("sidebar");
      if (!sidebar) return;
      // If you use an 'open' class, remove it. Otherwise hide inline.
      sidebar.classList.remove("open");
      sidebar.style.transform = ""; // let your CSS control it
    }, delay);
  };
})();

```

### template/scripts/pet_chat_ai.js

```javascript
// Pet Chat AI (v3)
// - Overrides placeholder sendMessage() from script.js
// - Shows backend error detail on screen to help debugging
// - Matches your dashboard.html IDs: #chat-input and #chat-history

(() => {
  const input = document.getElementById("chat-input");
  const history = document.getElementById("chat-history");
  if (!input || !history) return;

  function addBubble(text, who) {
    const div = document.createElement("div");
    div.className = `chat-bubble ${who === "user" ? "chat-user" : "chat-pet"}`;
    div.textContent = text;
    history.appendChild(div);
    history.scrollTop = history.scrollHeight;
  }

  async function aiSendMessage() {
    const msg = (input.value || "").trim();
    if (!msg) return;

    addBubble(msg, "user");
    input.value = "";

    const typing = document.createElement("div");
    typing.className = "chat-bubble chat-pet";
    typing.textContent = "Kat is thinking...";
    history.appendChild(typing);
    history.scrollTop = history.scrollHeight;

    try {
      const res = await fetch("/api/petchat", {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: msg }),
      });

      const data = await res.json().catch(() => ({}));
      typing.remove();

      if (!res.ok) {
        const detail = data.detail ? ` (${data.detail})` : "";
        addBubble((data.error || `Chat failed (${res.status})`) + detail, "pet");
        return;
      }

      addBubble(data.reply || "(no reply)", "pet");
    } catch (e) {
      typing.remove();
      addBubble("Network error. Make sure Flask is running and you opened the site via http://127.0.0.1:5001 (not Live Server).", "pet");
    }
  }

  window.sendMessage = aiSendMessage;

  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter") {
      e.preventDefault();
      aiSendMessage();
    }
  });
})();

```

### template/scripts/pet_sync.js

```javascript
// Sync the pet stat to the database 

let __petLoadedOnce = false;
let __petSaveTimer = null;

function __clamp(v, fallback = 50) {
  const n = Number(v);
  if (!Number.isFinite(n)) return fallback;
  return Math.max(0, Math.min(100, Math.round(n)));
}

function __getStats() {
  try {
    if (typeof stats !== "undefined" && stats) return stats;
  } catch (_) {}
  return null;
}

async function loadPetFromServer() {
  try {
    const r = await fetch("/api/pet", { credentials: "include" });
    if (!r.ok) return;

    const pet = await r.json();
    const s = __getStats();
    if (!s) {
      console.warn("[pet_sync] stats not found; is script.js loaded before pet_sync_v2.js?");
      return;
    }

    s.hunger    = __clamp(pet.hunger, 50);
    s.hydration = __clamp(pet.thirst, 50);
    s.health    = __clamp(pet.health, 50);
    s.happiness = __clamp(pet.happiness, 50);

    __petLoadedOnce = true;

    if (typeof updateUI === "function") updateUI();
  } catch (e) {
    console.warn("Failed to load pet:", e);
  }
}

function savePetToServerDebounced() {
  if (!__petLoadedOnce) return;

  if (__petSaveTimer) clearTimeout(__petSaveTimer);
  __petSaveTimer = setTimeout(async () => {
    try {
      const s = __getStats();
      if (!s) return;

      const payload = {
        hunger: s.hunger,
        thirst: s.hydration,
        health: s.health,
        happiness: s.happiness
      };

      const res = await fetch("/api/pet", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify(payload)
      });

      if (!res.ok) {
        const txt = await res.text().catch(() => "");
        console.warn("[pet_sync] PUT /api/pet failed:", res.status, txt);
      }
    } catch (e) {
      console.warn("Failed to save pet:", e);
    }
  }, 500);
}

// Wrap updateUI
function patchUpdateUIForPetSave() {
  if (typeof updateUI !== "function") return;

  const original = updateUI;
  if (original.__petWrappedV2) return;

  function wrappedUpdateUI() {
    const ret = original.apply(this, arguments);
    savePetToServerDebounced();
    return ret;
  }
  wrappedUpdateUI.__petWrappedV2 = true;

  updateUI = wrappedUpdateUI;
}

window.addEventListener("DOMContentLoaded", () => {
  patchUpdateUIForPetSave();
  loadPetFromServer();
});

```

### template/scripts/script_auth.js

```javascript
function goTo(page) {
  window.location.href = page;
}

function setAuthError(msg) {
  const box = document.getElementById("auth-error");
  if (!box) {
    if (msg) alert(msg);
    return;
  }
  if (!msg) {
    box.style.display = "none";
    box.innerText = "";
    return;
  }
  box.innerText = msg;
  box.style.display = "block";
}

function friendlyAuthMessage(server) {
  const code = (server?.code || "").toString();
  const err  = (server?.error || "").toString();
  const det  = (server?.detail || "").toString();
  const raw  = (code + " " + err + " " + det).toUpperCase();

  if (raw.includes("WEAK_PASSWORD") || raw.includes("AT LEAST 6") || raw.includes("6 CHAR")) {
    return "Password must be at least 6 characters.";
  }
  if (raw.includes("INVALID_PASSWORD")) return "Incorrect password.";
  if (raw.includes("EMAIL_NOT_FOUND")) return "No account found with that email.";
  if (raw.includes("INVALID_EMAIL")) return "Please enter a valid email address.";
  if (raw.includes("EMAIL_EXISTS") || raw.includes("ALREADY IN USE")) return "That email is already in use.";

  return server?.error || "Request failed.";
}

async function login() {
  setAuthError("");

  const email = document.getElementById("email-input")?.value.trim();
  const password = document.getElementById("pass-input")?.value.trim();

  if (!email || !password) {
    setAuthError("Email and password required.");
    return;
  }

  const r = await fetch("/api/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ email, password })
  });

  let data = {};
  try { data = await r.json(); } catch (_) {}

  if (!r.ok) {
    setAuthError(friendlyAuthMessage(data));
    return;
  }

  window.location.href = "dashboard.html";
}

async function signup() {
  setAuthError("");

  const username = document.getElementById("user-input")?.value.trim();
  const email = document.getElementById("email-input")?.value.trim();
  const password = document.getElementById("pass-input")?.value.trim();

  if (!username || !email || !password) {
    setAuthError("All fields required.");
    return;
  }

  const r = await fetch("/api/signup", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ username, email, password })
  });

  let data = {};
  try { data = await r.json(); } catch (_) {}

  if (!r.ok) {
    setAuthError(friendlyAuthMessage(data));
    return;
  }

  window.location.href = "dashboard.html";
}

async function logout() {
  await fetch("/api/logout", { method: "POST", credentials: "include" });
  window.location.href = "login.html";
}

// Optional: page protection helper
async function protectDashboard() {
  const r = await fetch("/api/me", { credentials: "include" });
  if (!r.ok) window.location.href = "login.html";
}

```

[11 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]