<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>