patx/relay-lang

const state = {
  csrfToken: null,
  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, "&")
    .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 (state.csrfToken && !["GET", "HEAD"].includes((request.method || "GET").toUpperCase())) {
    request.headers["x-csrf-token"] = state.csrfToken;
  }

  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.csrfToken = data.csrf_token;
  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();