Newer
Older
bugtrail / packages / extension / src / options / Options.vue
<script setup lang="ts">
import { onMounted, ref } from "vue";
import browser from "webextension-polyfill";

interface SettingsView {
  serverUrl: string;
  token: string | null;
  user: { id: string; nickname: string; email: string } | null;
  defaultProjectId: string | null;
}

interface ProjectView {
  id: string;
  name: string;
}

const settings = ref<SettingsView>({ serverUrl: "http://localhost:8001", token: null, user: null, defaultProjectId: null });
const projects = ref<ProjectView[]>([]);
const email = ref("");
const password = ref("");
const status = ref<string | null>(null);
const error = ref<string | null>(null);
const busy = ref(false);

async function sendMessage<T>(message: Record<string, unknown>): Promise<T> {
  const response = (await browser.runtime.sendMessage(message)) as { ok: boolean; data?: T; error?: string };
  if (!response?.ok) throw new Error(response?.error ?? "Request failed");
  return response.data as T;
}

async function load() {
  try {
    settings.value = await sendMessage<SettingsView>({ type: "settings_get" });
    if (settings.value.token) await loadProjects();
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err);
  }
}

async function loadProjects() {
  try {
    projects.value = await sendMessage<ProjectView[]>({ type: "list_projects" });
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err);
  }
}

async function signIn() {
  busy.value = true;
  error.value = null;
  try {
    await sendMessage({ type: "login", email: email.value, password: password.value });
    await load();
    status.value = "Signed in";
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err);
  } finally {
    busy.value = false;
  }
}

async function signOut() {
  await sendMessage({ type: "logout" });
  settings.value = { ...settings.value, token: null, user: null, defaultProjectId: null };
  projects.value = [];
}

async function saveServerUrl() {
  busy.value = true;
  error.value = null;
  try {
    await sendMessage({ type: "settings_save", patch: { serverUrl: settings.value.serverUrl.replace(/\/+$/, "") } });
    status.value = "Server URL saved";
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err);
  } finally {
    busy.value = false;
  }
}

async function saveDefaultProject() {
  await sendMessage({ type: "settings_save", patch: { defaultProjectId: settings.value.defaultProjectId } });
  status.value = "Default project saved";
}

onMounted(load);
</script>

<template>
  <div class="options">
    <h1>Live Testing Tool</h1>

    <section class="card">
      <h2>Server</h2>
      <label class="field">
        <span>Server URL</span>
        <input v-model="settings.serverUrl" type="url" placeholder="http://localhost:8001" />
      </label>
      <button type="button" :disabled="busy" @click="saveServerUrl">Save server URL</button>
    </section>

    <section class="card">
      <h2>Account</h2>
      <template v-if="settings.user">
        <p class="signed-in">
          Signed in as <strong>{{ settings.user.nickname }}</strong> ({{ settings.user.email }})
        </p>
        <button type="button" class="ghost" @click="signOut">Sign out</button>
      </template>
      <template v-else>
        <label class="field">
          <span>Email</span>
          <input v-model="email" type="email" autocomplete="username" />
        </label>
        <label class="field">
          <span>Password</span>
          <input v-model="password" type="password" autocomplete="current-password" @keyup.enter="signIn" />
        </label>
        <button type="button" :disabled="busy || !email || !password" @click="signIn">Sign in</button>
      </template>
    </section>

    <section v-if="settings.token" class="card">
      <h2>Default project</h2>
      <p class="hint">Reports captured with the extension are filed into this project.</p>
      <label class="field">
        <span>Project</span>
        <select v-model="settings.defaultProjectId" @change="saveDefaultProject">
          <option :value="null" disabled>Select a project…</option>
          <option v-for="project in projects" :key="project.id" :value="project.id">{{ project.name }}</option>
        </select>
      </label>
      <button type="button" class="ghost" @click="loadProjects">Refresh projects</button>
    </section>

    <p v-if="status" class="status">{{ status }}</p>
    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

<style>
:root {
  color-scheme: dark;
}
body {
  margin: 0;
  padding: 32px;
  background: #10121c;
  color: #c0caf5;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 14px;
}
.options {
  max-width: 560px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
h1 {
  font-size: 18px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
h2 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0 0 12px;
}
.card {
  padding: 16px;
  border: 2px solid #2f334d;
  background: #1a1c2b;
}
.field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
input,
select {
  padding: 6px 8px;
  border: 2px solid #2f334d;
  background: #10121c;
  color: #c0caf5;
  font: inherit;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}
input:focus,
select:focus {
  outline: none;
  border-color: #7aa2f7;
}
button {
  padding: 6px 14px;
  border: 2px solid #7aa2f7;
  background: #7aa2f7;
  color: #10121c;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  cursor: pointer;
}
button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
button.ghost {
  background: transparent;
  color: inherit;
  border-color: #2f334d;
}
.signed-in {
  margin: 0 0 12px;
}
.hint {
  font-size: 12px;
  opacity: 0.7;
  text-transform: none;
  letter-spacing: normal;
  margin: 0 0 12px;
}
.status {
  color: #9ece6a;
}
.error {
  color: #f7768e;
  white-space: pre-wrap;
}
</style>