# 09 | Login and project screens

## Goal

Replace placeholders with working account forms and a project list.

## What you are building

Login uses the shared API helper to load the anonymous session, submit register/login, and replace cached session state after rotation. Loading guards disable duplicate submissions and error messages remain visible. ProjectList requests only authorized records from the API and creates a project from a form. Internal project anchors use data-teloce-link. The server owns authorization; TypeScript types and disabled buttons do not enforce permissions.

## Build this chapter

Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.

## Step 1: EDIT - replace the entire file - modules/auth/ui/pages/Login.html

Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript.

Select all existing contents of this file and replace them with the complete code below.

```html
<template>
  <section class="narrow">
    <h1>Your account</h1>
    <p v-if="busy" role="status">Please wait…</p>
    <p v-if="message" role="alert">{{ message }}</p>
    <div v-if="user">
      <p>Signed in as {{ user.name }}.</p>
      <a href="/projects" data-teloce-link>Open projects</a>
      <button type="button" @click="logout" :disabled="busy">Sign out</button>
    </div>
    <form v-show="!user" @submit.prevent="submit">
      <label v-show="registering"
        >Name<input
          name="name"
          v-model="name"
          maxlength="80"
          :required="registering"
          autocomplete="name"
      /></label>
      <label
        >Email<input
          name="email"
          type="email"
          v-model="email"
          required
          autocomplete="username"
      /></label>
      <label
        >Password<input
          name="password"
          type="password"
          v-model="password"
          minlength="12"
          maxlength="128"
          required
          autocomplete="current-password"
      /></label>
      <button :disabled="busy">{{ registering ? "Create account" : "Sign in" }}</button>
      <button
        type="button"
        class="secondary"
        @click="registering = !registering"
        :disabled="busy"
      >
        {{ registering ? "Use an existing account" : "Register instead" }}
      </button>
    </form>
  </section>
</template>
<script lang="ts">
import { api, loadSession } from "../../../../ui/api.ts";
import type { Session } from "../../../../ui/types.ts";
export default {
  data() {
    return {
      user: null,
      registering: false,
      name: "",
      email: "",
      password: "",
      busy: false,
      message: "",
    };
  },
  async mounted() {
    try {
      this.user = (await loadSession()).user;
    } catch (error) {
      this.message = error.message;
    }
  },
  methods: {
    async submit() {
      if (this.busy) return;
      this.busy = true;
      this.message = "";
      try {
        const path = this.registering ? "/api/auth/register" : "/api/auth/login";
        const result = await api<Session>(path, "POST", {
          name: this.name,
          email: this.email,
          password: this.password,
        });
        this.user = result.user;
        this.password = "";
        window.__TELOCE_ROUTER__.navigate("/projects");
      } catch (error) {
        this.message = error.message;
      } finally {
        this.busy = false;
      }
    },
    async logout() {
      this.busy = true;
      try {
        await api("/api/auth/logout", "POST", {});
        this.user = null;
        this.message = "Signed out.";
      } catch (error) {
        this.message = error.message;
      } finally {
        this.busy = false;
      }
    },
  },
};
</script>
<style scoped>
* {
  box-sizing: border-box;
}
a {
  color: #1d4ed8;
}
h1 {
  font-size: clamp(1.8rem, 5vw, 3rem);
  line-height: 1.15;
}
h2 {
  font-size: 1.2rem;
}
p {
  line-height: 1.6;
}
[role="alert"] {
  color: #a21c2b;
  padding: 0.8rem;
  border-left: 4px solid;
}
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
label {
  display: grid;
  gap: 0.4rem;
  margin: 0.9rem 0;
  font-weight: 600;
}
input,
select,
textarea,
button {
  font: inherit;
  border-radius: 7px;
  padding: 0.7rem;
  min-height: 44px;
}
input,
select,
textarea {
  width: 100%;
  border: 1px solid #abb9cf;
  background: white;
  color: #17243a;
}
button {
  display: inline-block;
  border: 0;
  background: #2563eb;
  color: white;
  padding: 0.7rem 1rem;
  cursor: pointer;
  margin: 0.3rem 0.5rem 0.3rem 0;
}
button:disabled {
  opacity: 0.55;
  cursor: wait;
}
.secondary {
  background: #e4eaf5;
  color: #17243a;
}
.danger {
  background: #b42332;
}
.narrow {
  max-width: 480px;
  margin: auto;
}
</style>
```

**Say:** "Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript."

**Type:** Follow the action above in `modules/auth/ui/pages/Login.html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 2: EDIT - replace the entire file - modules/projects/ui/pages/ProjectList.html

Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped.

Select all existing contents of this file and replace them with the complete code below.

```html
<template>
  <section>
    <h1>Your projects</h1>
    <p>Choose a project to manage its tasks.</p>
    <p v-if="message" role="alert">
      {{ message }} <a href="/login" data-teloce-link>Account</a>
    </p>
    <p v-if="loading" role="status">Loading projects…</p>
    <form @submit.prevent="createProject" class="card">
      <h2>Create a project</h2>
      <label
        >Name<input name="project-name" v-model="name" required maxlength="120"
      /></label>
      <label
        >Description<textarea v-model="description" maxlength="1000"></textarea>
      </label>
      <button :disabled="busy">{{ busy ? "Saving…" : "Create project" }}</button>
    </form>
    <p v-if="!loading && projects.length === 0">
      No projects yet. Create your first one above.
    </p>
    <div class="cards">
      <a
        v-for="project in projects"
        :key="project.id"
        :href="'/projects/' + project.id"
        data-teloce-link
        class="card"
      >
        <h2>{{ project.name }}</h2>
        <p>{{ project.description }}</p>
      </a>
    </div>
  </section>
</template>
<script lang="ts">
import { api } from "../../../../ui/api.ts";
import type { Project } from "../../../../ui/types.ts";
export default {
  data() {
    return {
      projects: [],
      name: "",
      description: "",
      message: "",
      loading: true,
      busy: false,
    };
  },
  async mounted() {
    try {
      this.projects = await api<Project[]>("/api/projects/");
    } catch (error) {
      this.message = error.message;
    } finally {
      this.loading = false;
    }
  },
  methods: {
    async createProject() {
      if (this.busy) return;
      this.busy = true;
      this.message = "";
      try {
        const project = await api<Project>("/api/projects/", "POST", {
          name: this.name,
          description: this.description,
        });
        this.projects = [project, ...this.projects];
        this.name = "";
        this.description = "";
      } catch (error) {
        this.message = error.message;
      } finally {
        this.busy = false;
      }
    },
  },
};
</script>
<style scoped>
* {
  box-sizing: border-box;
}
a {
  color: #1d4ed8;
}
h1 {
  font-size: clamp(1.8rem, 5vw, 3rem);
  line-height: 1.15;
}
h2 {
  font-size: 1.2rem;
}
p {
  line-height: 1.6;
}
[role="alert"] {
  color: #a21c2b;
  padding: 0.8rem;
  border-left: 4px solid;
}
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
.card {
  display: block;
  background: white;
  border: 1px solid #d8e0ed;
  border-radius: 12px;
  padding: 1.2rem;
  margin: 1rem 0;
  color: inherit;
  text-decoration: none;
}
label {
  display: grid;
  gap: 0.4rem;
  margin: 0.9rem 0;
  font-weight: 600;
}
input,
select,
textarea,
button {
  font: inherit;
  border-radius: 7px;
  padding: 0.7rem;
  min-height: 44px;
}
input,
select,
textarea {
  width: 100%;
  border: 1px solid #abb9cf;
  background: white;
  color: #17243a;
}
button {
  display: inline-block;
  border: 0;
  background: #2563eb;
  color: white;
  padding: 0.7rem 1rem;
  cursor: pointer;
  margin: 0.3rem 0.5rem 0.3rem 0;
}
button:disabled {
  opacity: 0.55;
  cursor: wait;
}
.secondary {
  background: #e4eaf5;
  color: #17243a;
}
.danger {
  background: #b42332;
}
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 1rem;
}
</style>
```

**Say:** "Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped."

**Type:** Follow the action above in `modules/projects/ui/pages/ProjectList.html`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Run and check

```bash
python management.py runserver
# Open /login; register; create a project on /projects.
# Project details are completed in chapter 10.
```

## Expected result

Register in /login, open /projects and create a project. Sign out and sign back in. The new project survives the restart.

## Common errors

Always inspect response.ok. A resolved fetch promise can still represent a 400/403/500. Relative imports are resolved from each HTML component. Registration must refresh the cached CSRF token.

## Short exercise

Show the validation message for an empty project name and make the same denied request directly to the API.

## What to say and show

Say: "Replace placeholders with working account forms and a project list. The server remains responsible for persistence and authorization; the browser presents the result."

Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.

## Save your checkpoint

```bash
git add .
git commit -m "Complete chapter 09: Login and project screens"
```
