patx/relay-lang

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Relay Chirp</title>
    <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%23da5a2a'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='30' font-family='Georgia' fill='white'%3ERC%3C/text%3E%3C/svg%3E">
    <style>
:root {
  --bg: #f7f1e8;
  --bg-deep: #e6dcc9;
  --card: rgba(255, 251, 245, 0.82);
  --card-strong: #fffdf9;
  --ink: #1f2421;
  --muted: #5f685f;
  --line: rgba(31, 36, 33, 0.12);
  --accent: #da5a2a;
  --accent-deep: #b6431c;
  --accent-soft: #ffe0d3;
  --cool: #1f7a8c;
  --shadow: 0 24px 60px rgba(60, 48, 34, 0.12);
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: "Trebuchet MS", "Gill Sans", sans-serif;
  color: var(--ink);
  background:
    radial-gradient(circle at top left, rgba(218, 90, 42, 0.18), transparent 28rem),
    radial-gradient(circle at top right, rgba(31, 122, 140, 0.14), transparent 24rem),
    linear-gradient(180deg, #fbf7f1 0%, var(--bg) 42%, var(--bg-deep) 100%);
  position: relative;
}

.backdrop {
  position: fixed;
  inset: auto;
  border-radius: 999px;
  filter: blur(10px);
  opacity: 0.45;
  pointer-events: none;
}

.backdrop-one {
  width: 18rem;
  height: 18rem;
  top: 6rem;
  right: 5vw;
  background: rgba(218, 90, 42, 0.16);
}

.backdrop-two {
  width: 26rem;
  height: 26rem;
  bottom: -8rem;
  left: -6rem;
  background: rgba(31, 122, 140, 0.12);
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.4rem;
  backdrop-filter: blur(14px);
  background: rgba(247, 241, 232, 0.82);
  border-bottom: 1px solid var(--line);
}

.brand-link,
.identity,
.ghost-button,
.primary-button {
  transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease;
}

.brand-link {
  display: inline-flex;
  align-items: center;
  gap: 0.9rem;
  color: inherit;
  text-decoration: none;
}

.brand-link strong {
  display: block;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.15rem;
}

.brand-link small {
  display: block;
  color: var(--muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.brand-mark {
  display: inline-grid;
  place-items: center;
  width: 2.8rem;
  height: 2.8rem;
  border-radius: 1rem;
  background: linear-gradient(135deg, var(--accent) 0%, #f08e4f 100%);
  color: white;
  font-weight: 700;
  box-shadow: 0 18px 34px rgba(218, 90, 42, 0.22);
}

.topbar-nav {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  flex-wrap: wrap;
}

.layout {
  width: min(1180px, calc(100vw - 2rem));
  margin: 1.4rem auto 3rem;
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr);
  gap: 1.2rem;
}

.panel {
  background: var(--card);
  border: 1px solid rgba(255, 255, 255, 0.6);
  box-shadow: var(--shadow);
  backdrop-filter: blur(12px);
}

.sidebar,
.content {
  border-radius: 1.6rem;
  overflow: hidden;
}

.sidebar {
  padding: 1.2rem;
  align-self: start;
}

.content {
  padding: 1.2rem;
  min-height: 70vh;
}

.stack {
  display: grid;
  gap: 1rem;
}

.hero-card,
.auth-card,
.info-card,
.composer-card,
.feed-shell,
.profile-card,
.post-card,
.empty-state,
.error-banner {
  background: var(--card-strong);
  border: 1px solid var(--line);
  border-radius: 1.2rem;
}

.hero-card,
.auth-card,
.info-card,
.composer-card,
.feed-shell,
.profile-card,
.empty-state,
.error-banner {
  padding: 1.1rem;
}

.eyebrow {
  margin: 0 0 0.35rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--cool);
  font-size: 0.72rem;
  font-weight: 700;
}

h2,
h3,
.profile-card h2 {
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  line-height: 1.1;
}

.muted {
  color: var(--muted);
}

.toggle-row,
.button-row,
.composer-actions,
.feed-top,
.profile-top,
.post-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.toggle-button,
.ghost-button,
.primary-button {
  appearance: none;
  border: 0;
  border-radius: 999px;
  padding: 0.72rem 1rem;
  font: inherit;
  cursor: pointer;
}

.toggle-button,
.ghost-button {
  background: #f1e7d7;
  color: var(--ink);
}

.toggle-button.active,
.primary-button {
  background: linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);
  color: white;
  box-shadow: 0 18px 28px rgba(218, 90, 42, 0.18);
}

.ghost-button:hover,
.primary-button:hover,
.toggle-button:hover,
.brand-link:hover,
.identity:hover {
  transform: translateY(-1px);
}

.ghost-button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.wide {
  width: 100%;
  text-align: center;
  text-decoration: none;
}

.session-pill {
  padding: 0.55rem 0.85rem;
  border-radius: 999px;
  background: rgba(31, 122, 140, 0.12);
  color: var(--cool);
  font-size: 0.92rem;
}

.field {
  display: grid;
  gap: 0.35rem;
}

.field span {
  font-size: 0.88rem;
  color: var(--muted);
}

input,
textarea {
  width: 100%;
  border: 1px solid rgba(31, 36, 33, 0.14);
  border-radius: 1rem;
  padding: 0.82rem 0.95rem;
  font: inherit;
  background: #fffdfa;
  color: var(--ink);
}

textarea {
  resize: vertical;
}

input:focus,
textarea:focus {
  outline: 2px solid rgba(31, 122, 140, 0.24);
  border-color: rgba(31, 122, 140, 0.28);
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.8rem;
  margin-top: 0.9rem;
}

.stats-grid div {
  padding: 0.85rem;
  border-radius: 1rem;
  background: linear-gradient(180deg, rgba(255, 224, 211, 0.52), rgba(255, 255, 255, 0.9));
  border: 1px solid rgba(218, 90, 42, 0.14);
}

.stats-grid strong,
.sequence {
  display: block;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.05rem;
}

.stats-grid span,
.sequence {
  color: var(--muted);
  font-size: 0.84rem;
}

.info-list {
  margin: 0;
  padding-left: 1.1rem;
  color: var(--muted);
}

.info-list li + li {
  margin-top: 0.45rem;
}

.composer-form {
  display: grid;
  gap: 0.8rem;
}

.composer-actions {
  color: var(--muted);
  font-size: 0.88rem;
}

.feed-shell {
  display: grid;
  gap: 0.9rem;
}

.post-card {
  padding: 1rem;
}

.identity {
  display: inline-grid;
  gap: 0.12rem;
  text-decoration: none;
  color: inherit;
}

.identity span {
  color: var(--muted);
  font-size: 0.88rem;
}

.post-body {
  margin: 0.8rem 0 0;
  white-space: pre-wrap;
  line-height: 1.55;
}

.empty-state,
.error-banner {
  text-align: center;
}

.error-banner {
  background: #fff3ec;
  border-color: rgba(218, 90, 42, 0.24);
}

@media (max-width: 960px) {
  .layout {
    grid-template-columns: 1fr;
  }

  .topbar {
    align-items: flex-start;
  }
}

@media (max-width: 640px) {
  .layout {
    width: min(100vw - 1rem, 100%);
    margin-top: 0.8rem;
  }

  .topbar {
    padding: 0.9rem;
  }

  .sidebar,
  .content {
    padding: 0.95rem;
    border-radius: 1.2rem;
  }

  .stats-grid {
    grid-template-columns: 1fr;
  }
}

</style>
</head>
<body>
    <div class="backdrop backdrop-one"></div>
    <div class="backdrop backdrop-two"></div>

    <header class="topbar">
        <div class="brand">
            <a class="brand-link" href="/" data-nav-home>
                <span class="brand-mark">RC</span>
                <span>
                    <strong>Relay Chirp</strong>
                    <small>example project</small>
                </span>
            </a>
        </div>
        <nav class="topbar-nav">
            <button class="ghost-button" type="button" data-nav-home>Home</button>
            <button class="ghost-button" type="button" data-nav-discover>Discover</button>
            <div class="session-pill" id="session-pill">Checking session...</div>
        </nav>
    </header>

    <main class="layout">
        <aside class="panel sidebar" id="sidebar"></aside>
        <section class="panel content" id="content"></section>
    </main>

    <script>
const state = {
  authMode: "signup",
  currentFeed: "discover",
  error: "",
  feedItems: [],
  loading: true,
  me: null,
  profile: null,
  profilePosts: [],
  route: parseRoute(window.location.pathname),
  viewToken: 0,
};

const sidebarEl = document.getElementById("sidebar");
const contentEl = document.getElementById("content");
const sessionPillEl = document.getElementById("session-pill");

function parseRoute(pathname) {
  const match = pathname.match(/^\/u\/([^/]+)$/);
  if (match) {
    return { name: "profile", username: decodeURIComponent(match[1]) };
  }
  return { name: "home" };
}

function escapeHtml(value) {
  return String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function profilePath(username) {
  return `/u/${encodeURIComponent(username)}`;
}

async function fetchJson(url, options = {}) {
  const request = { ...options };
  request.headers = { ...(request.headers || {}) };

  if (request.body && typeof request.body !== "string") {
    request.headers["content-type"] = "application/json";
    request.body = JSON.stringify(request.body);
  }

  const response = await fetch(url, request);
  const text = await response.text();
  let data = {};

  if (text) {
    try {
      data = JSON.parse(text);
    } catch (_error) {
      data = { raw: text };
    }
  }

  if (!response.ok) {
    const error = data.error || {};
    const message = error.message || data.raw || `Request failed with ${response.status}`;
    const problem = new Error(message);
    problem.code = error.code || "request_failed";
    problem.status = response.status;
    problem.details = error.details;
    throw problem;
  }

  return data;
}

async function refreshSession() {
  const data = await fetchJson("/api/me");
  state.me = data.authenticated ? data.user : null;
  state.currentFeed = state.me ? state.currentFeed || "home" : "discover";
  if (!state.me) {
    state.currentFeed = "discover";
  }
}

async function loadFeed() {
  const feed = state.me && state.currentFeed === "home" ? "home" : "discover";
  state.currentFeed = feed;
  const data = await fetchJson(`/api/feed/${feed}`);
  state.feedItems = data.items || [];
}

async function loadProfile(username) {
  const data = await fetchJson(`/api/users/${encodeURIComponent(username)}`);
  state.profile = data.profile;
  state.profilePosts = data.posts || [];
}

async function loadCurrentView() {
  const token = ++state.viewToken;
  state.loading = true;
  render();

  try {
    await refreshSession();

    if (state.route.name === "profile") {
      await loadProfile(state.route.username);
    } else {
      state.profile = null;
      state.profilePosts = [];
      await loadFeed();
    }

    if (token !== state.viewToken) {
      return;
    }

    state.error = "";
  } catch (error) {
    if (token !== state.viewToken) {
      return;
    }

    state.error = error.message;
    if (state.route.name === "home") {
      state.feedItems = [];
    } else {
      state.profile = null;
      state.profilePosts = [];
    }
  } finally {
    if (token === state.viewToken) {
      state.loading = false;
      render();
    }
  }
}

function navigateTo(pathname) {
  if (window.location.pathname !== pathname) {
    window.history.pushState({}, "", pathname);
  }
  state.route = parseRoute(pathname);
  loadCurrentView();
}

function setError(message) {
  state.error = message;
  render();
}

function encodeFormBody(fields) {
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(fields)) {
    if (value === undefined || value === null) {
      continue;
    }
    params.set(key, String(value));
  }
  return params.toString();
}

function renderPostList(posts, emptyTitle, emptyBody) {
  if (!Array.isArray(posts)) {
    return `
      <div class="error-banner">
        <strong>Bad feed payload</strong>
        <p>The server returned an unexpected posts shape.</p>
      </div>
    `;
  }

  if (!posts.length) {
    return `
      <div class="empty-state">
        <h3>${escapeHtml(emptyTitle)}</h3>
        <p>${escapeHtml(emptyBody)}</p>
      </div>
    `;
  }

  return posts
    .map((post) => {
      const body = escapeHtml(post.body || "");
      const username = escapeHtml(post.author.username);
      const bio = escapeHtml(post.author.bio || "");
      return `
        <article class="post-card">
          <div class="post-head">
            <a href="${profilePath(post.author.username)}" data-profile-link="${username}" class="identity">
              <strong>@${username}</strong>
              <span>${bio || "Relay user"}</span>
            </a>
            <span class="sequence">post #${escapeHtml(post.seq)}</span>
          </div>
          <p class="post-body">${body}</p>
        </article>
      `;
    })
    .join("");
}

function renderSidebar() {
  if (state.me) {
    const me = state.me;
    return `
      <section class="stack">
        <div class="hero-card">
          <p class="eyebrow">Signed in</p>
          <h2>@${escapeHtml(me.username)}</h2>
          <p class="muted">${escapeHtml(me.bio || "No bio yet.")}</p>
          <div class="stats-grid">
            <div><strong>${escapeHtml(me.posts_count)}</strong><span>posts</span></div>
            <div><strong>${escapeHtml(me.followers_count)}</strong><span>followers</span></div>
            <div><strong>${escapeHtml(me.following_count)}</strong><span>following</span></div>
          </div>
          <div class="button-row">
            <a class="ghost-button wide" href="${profilePath(me.username)}" data-profile-link="${escapeHtml(me.username)}">View profile</a>
            <button class="ghost-button wide" type="button" data-logout>Log out</button>
          </div>
        </div>
        <div class="info-card">
          <p class="eyebrow">What this shows</p>
          <ul class="info-list">
            <li>Relay modules imported into one app entrypoint</li>
            <li>Session-backed signup and login</li>
            <li>Mongo-backed posts, profiles, and follow graph</li>
            <li>A plain JS SPA talking to Relay JSON routes</li>
          </ul>
        </div>
      </section>
    `;
  }

  const signupActive = state.authMode === "signup" ? "active" : "";
  const loginActive = state.authMode === "login" ? "active" : "";

  return `
    <section class="stack">
      <div class="hero-card">
        <p class="eyebrow">Simple Twitter clone</p>
        <h2>Build a tiny social graph.</h2>
        <p class="muted">Create an account, post short updates, follow people, and switch between a home feed and a global discover feed.</p>
      </div>
      <div class="auth-card">
        <div class="toggle-row">
          <button class="toggle-button ${signupActive}" type="button" data-auth-mode="signup">Sign up</button>
          <button class="toggle-button ${loginActive}" type="button" data-auth-mode="login">Log in</button>
        </div>
        <form id="auth-form" class="stack">
          ${
            state.authMode === "signup"
              ? `
                <label class="field">
                  <span>Username</span>
                  <input name="username" placeholder="ada_labs" minlength="3" maxlength="20" autocomplete="username" required>
                </label>
                <label class="field">
                  <span>Password</span>
                  <input name="password" type="password" minlength="6" maxlength="72" autocomplete="new-password" required>
                </label>
                <label class="field">
                  <span>Bio</span>
                  <textarea name="bio" rows="3" maxlength="160" placeholder="Building fast little servers in Relay."></textarea>
                </label>
                <button class="primary-button" type="submit" data-auth-submit="signup">Create account</button>
              `
              : `
                <label class="field">
                  <span>Username</span>
                  <input name="username" placeholder="ada_labs" autocomplete="username" required>
                </label>
                <label class="field">
                  <span>Password</span>
                  <input name="password" type="password" autocomplete="current-password" required>
                </label>
                <button class="primary-button" type="submit" data-auth-submit="login">Log in</button>
              `
          }
        </form>
      </div>
    </section>
  `;
}

function renderComposer() {
  if (!state.me || state.route.name !== "home") {
    return "";
  }

  return `
    <section class="composer-card">
      <div>
        <p class="eyebrow">Compose</p>
        <h3>What is happening in your Relay app?</h3>
      </div>
      <form id="composer-form" class="composer-form">
        <textarea name="body" maxlength="280" rows="4" placeholder="Ship a small feature. Follow another user. Stress the JSON routes." required></textarea>
        <div class="composer-actions">
          <span>280 character max</span>
          <button class="primary-button" type="submit">Post</button>
        </div>
      </form>
    </section>
  `;
}

function renderFeedView() {
  const homeActive = state.currentFeed === "home" ? "active" : "";
  const discoverActive = state.currentFeed === "discover" ? "active" : "";
  const title = state.currentFeed === "home" ? "Home feed" : "Discover";
  const subtitle =
    state.currentFeed === "home"
      ? "Posts from you and the people you follow."
      : "Everything posted in this example app.";

  return `
    ${renderComposer()}
    <section class="feed-shell">
      <div class="feed-top">
        <div>
          <p class="eyebrow">${escapeHtml(title)}</p>
          <h2>${escapeHtml(subtitle)}</h2>
        </div>
        <div class="toggle-row">
          <button class="toggle-button ${homeActive}" type="button" data-feed="home" ${state.me ? "" : "disabled"}>Home</button>
          <button class="toggle-button ${discoverActive}" type="button" data-feed="discover">Discover</button>
        </div>
      </div>
      ${renderPostList(
        state.feedItems,
        state.currentFeed === "home" ? "Your home feed is empty." : "No posts yet.",
        state.currentFeed === "home"
          ? "Create your first post or follow another account to fill the timeline."
          : "Sign up and post something from the composer."
      )}
    </section>
  `;
}

function renderProfileView() {
  if (!state.profile) {
    return `
      <div class="empty-state">
        <h3>Profile unavailable</h3>
        <p>That user could not be loaded.</p>
      </div>
    `;
  }

  const profile = state.profile;
  const followAction =
    state.me && !profile.is_self
      ? `
        <button
          class="${profile.viewer_is_following ? "ghost-button" : "primary-button"}"
          type="button"
          ${profile.viewer_is_following ? `data-unfollow-user="${escapeHtml(profile.username)}"` : `data-follow-user="${escapeHtml(profile.username)}"`}
        >
          ${profile.viewer_is_following ? "Unfollow" : "Follow"}
        </button>
      `
      : "";

  return `
    <section class="profile-card">
      <div class="profile-top">
        <div>
          <p class="eyebrow">Profile</p>
          <h2>@${escapeHtml(profile.username)}</h2>
          <p class="muted">${escapeHtml(profile.bio || "No bio yet.")}</p>
        </div>
        <div class="button-row">
          <button class="ghost-button" type="button" data-nav-home>Back home</button>
          ${followAction}
        </div>
      </div>
      <div class="stats-grid">
        <div><strong>${escapeHtml(profile.posts_count)}</strong><span>posts</span></div>
        <div><strong>${escapeHtml(profile.followers_count)}</strong><span>followers</span></div>
        <div><strong>${escapeHtml(profile.following_count)}</strong><span>following</span></div>
      </div>
    </section>
    <section class="feed-shell">
      <div class="feed-top">
        <div>
          <p class="eyebrow">Timeline</p>
          <h2>Recent posts from @${escapeHtml(profile.username)}</h2>
        </div>
      </div>
      ${renderPostList(
        state.profilePosts,
        "No posts yet.",
        "This user has not posted anything yet."
      )}
    </section>
  `;
}

function renderContent() {
  if (state.loading) {
    return `
      <div class="empty-state">
        <h3>Loading view...</h3>
        <p>Fetching the latest state from Relay.</p>
      </div>
    `;
  }

  if (state.error) {
    return `
      <div class="error-banner">
        <strong>Request failed</strong>
        <p>${escapeHtml(state.error)}</p>
      </div>
      ${state.route.name === "profile" ? renderProfileView() : renderFeedView()}
    `;
  }

  return state.route.name === "profile" ? renderProfileView() : renderFeedView();
}

function renderSessionPill() {
  if (state.me) {
    sessionPillEl.textContent = `Signed in as @${state.me.username}`;
    return;
  }
  sessionPillEl.textContent = "Public discover mode";
}

function render() {
  sidebarEl.innerHTML = renderSidebar();
  contentEl.innerHTML = renderContent();
  renderSessionPill();
}

async function handleAuthSubmit(form) {
  const mode = state.authMode;
  const formData = new FormData(form);
  const payload = {
    username: (formData.get("username") || "").toString(),
    password: (formData.get("password") || "").toString(),
  };

  if (mode === "signup") {
    payload.bio = (formData.get("bio") || "").toString();
  }

  try {
    await fetchJson(`/api/auth/${mode}`, {
      method: "POST",
      headers: {
        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
      },
      body: encodeFormBody(payload),
      credentials: "same-origin",
    });
    state.currentFeed = "home";
    state.route = { name: "home" };
    window.history.pushState({}, "", "/");
    await loadCurrentView();
  } catch (error) {
    setError(error.message);
  }
}

async function handleLogout() {
  try {
    await fetchJson("/api/auth/logout", {
      method: "POST",
      credentials: "same-origin",
    });
    state.route = { name: "home" };
    window.history.pushState({}, "", "/");
    state.currentFeed = "discover";
    await loadCurrentView();
  } catch (error) {
    setError(error.message);
  }
}

async function handleComposerSubmit(form) {
  const formData = new FormData(form);
  const body = (formData.get("body") || "").toString();

  try {
    await fetchJson("/api/posts", {
      method: "POST",
      headers: {
        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
      },
      body: encodeFormBody({ body }),
      credentials: "same-origin",
    });
    await loadCurrentView();
  } catch (error) {
    setError(error.message);
  }
}

async function handleFollowChange(username, action) {
  try {
    await fetchJson(`/api/users/${encodeURIComponent(username)}/${action}`, {
      method: "POST",
      credentials: "same-origin",
    });
    await loadCurrentView();
  } catch (error) {
    setError(error.message);
  }
}

document.addEventListener("click", (event) => {
  const target = event.target;
  if (!(target instanceof HTMLElement)) {
    return;
  }

  const homeButton = target.closest("[data-nav-home]");
  if (homeButton) {
    event.preventDefault();
    navigateTo("/");
    return;
  }

  const discoverButton = target.closest("[data-nav-discover]");
  if (discoverButton) {
    event.preventDefault();
    state.currentFeed = "discover";
    navigateTo("/");
    return;
  }

  const authToggle = target.closest("[data-auth-mode]");
  if (authToggle) {
    state.authMode = authToggle.getAttribute("data-auth-mode") || "signup";
    state.error = "";
    render();
    return;
  }

  const profileLink = target.closest("[data-profile-link]");
  if (profileLink) {
    event.preventDefault();
    const username = profileLink.getAttribute("data-profile-link");
    if (username) {
      navigateTo(profilePath(username));
    }
    return;
  }

  const followButton = target.closest("[data-follow-user]");
  if (followButton) {
    const username = followButton.getAttribute("data-follow-user");
    if (username) {
      handleFollowChange(username, "follow");
    }
    return;
  }

  const unfollowButton = target.closest("[data-unfollow-user]");
  if (unfollowButton) {
    const username = unfollowButton.getAttribute("data-unfollow-user");
    if (username) {
      handleFollowChange(username, "unfollow");
    }
    return;
  }

  const logoutButton = target.closest("[data-logout]");
  if (logoutButton) {
    handleLogout();
    return;
  }

  const feedToggle = target.closest("[data-feed]");
  if (feedToggle) {
    const feed = feedToggle.getAttribute("data-feed");
    if (feed === "home" && !state.me) {
      return;
    }
    state.currentFeed = feed || "discover";
    state.route = { name: "home" };
    window.history.pushState({}, "", "/");
    loadCurrentView();
  }
});

document.addEventListener("submit", (event) => {
  const form = event.target;
  if (!(form instanceof HTMLFormElement)) {
    return;
  }

  if (form.id === "auth-form") {
    event.preventDefault();
    handleAuthSubmit(form);
    return;
  }

  if (form.id === "composer-form") {
    event.preventDefault();
    handleComposerSubmit(form);
  }
});

window.addEventListener("popstate", () => {
  state.route = parseRoute(window.location.pathname);
  loadCurrentView();
});

render();
loadCurrentView();

</script>
</body>
</html>