···
canceled_at: number | null;
+
type SettingsPage = "account" | "sessions" | "passkeys" | "billing" | "notifications" | "danger";
@customElement("user-settings")
export class UserSettings extends LitElement {
···
@state() passkeySupported = false;
@state() addingPasskey = false;
+
@state() emailNotificationsEnabled = true;
+
@state() deletingAccount = false;
static override styles = css`
···
+
border: 2px solid rgba(220, 38, 38, 0.8);
+
justify-content: space-between;
+
border: 1px solid var(--secondary);
+
background-color: var(--secondary);
+
.toggle-slider:before {
+
background-color: white;
+
.toggle input:checked + .toggle-slider {
+
background-color: var(--primary);
+
.toggle input:checked + .toggle-slider:before {
+
transform: translateX(24px);
@media (max-width: 768px) {
···
private isValidTab(tab: string): boolean {
+
return ["account", "sessions", "passkeys", "billing", "notifications", "danger"].includes(tab);
private setTab(tab: SettingsPage) {
+
this.error = ""; // Clear errors when switching tabs
// Update URL without reloading page
const url = new URL(window.location.href);
url.searchParams.set("tab", tab);
···
+
const data = await response.json();
+
this.emailNotificationsEnabled = data.email_notifications_enabled ?? true;
···
const response = await fetch(`/api/passkeys/${passkeyId}`, {
+
const data = await response.json();
+
this.error = data.error || "Failed to delete passkey";
await this.loadPasskeys();
+
this.error = err instanceof Error ? err.message : "Failed to delete passkey";
+
const response = await fetch("/api/auth/logout", { method: "POST" });
+
const data = await response.json();
+
this.error = data.error || "Failed to logout";
window.location.href = "/";
+
this.error = err instanceof Error ? err.message : "Failed to logout";
async handleDeleteAccount() {
+
this.deletingAccount = true;
+
document.body.style.cursor = "wait";
const response = await fetch("/api/user", {
+
const data = await response.json();
+
this.error = data.error || "Failed to delete account";
···
this.error = "Failed to delete account";
+
this.deletingAccount = false;
this.showDeleteConfirm = false;
+
document.body.style.cursor = "";
async handleUpdateEmail() {
this.error = "Email required";
···
async handleUpdatePassword() {
this.error = "Password required";
···
async handleUpdateName() {
this.error = "Name required";
···
async handleUpdateAvatar() {
this.error = "Avatar required";
···
async handleKillSession(sessionId: string) {
const response = await fetch(`/api/sessions`, {
···
<div class="content-inner">
+
<div class="error-banner">
<h2 class="section-title">Profile Information</h2>
···
<div class="content-inner">
+
<div class="error-banner">
<h2 class="section-title">Active Sessions</h2>
···
<div class="content-inner">
+
<div class="error-banner">
<h2 class="section-title">Subscription</h2>
···
Reactivate your subscription to unlock unlimited transcriptions.
···
if (hasActiveSubscription) {
<div class="content-inner">
+
<div class="error-banner">
<h2 class="section-title">Subscription</h2>
···
Opens the customer portal where you can update payment methods, view invoices, and manage your subscription.
···
<div class="content-inner">
+
<div class="error-banner">
<h2 class="section-title">Billing & Subscription</h2>
<p class="field-description" style="margin-bottom: 1.5rem;">
···
${this.loading ? "Loading..." : "Activate Your Subscription"}
···
<div class="content-inner">
+
<div class="error-banner">
<div class="section danger-section">
<h2 class="section-title">Delete Account</h2>
···
+
renderNotificationsPage() {
+
<div class="content-inner">
+
<div class="error-banner">
+
<h2 class="section-title">Email Notifications</h2>
+
<p style="color: var(--text); margin-bottom: 1rem;">
+
Control which emails you receive from Thistle.
+
<div class="setting-row">
+
<div class="setting-info">
+
<strong>Transcription Complete</strong>
+
<p style="color: var(--paynes-gray); font-size: 0.875rem; margin: 0.25rem 0 0 0;">
+
Get notified when your transcription is ready
+
.checked=${this.emailNotificationsEnabled}
+
@change=${async (e: Event) => {
+
const target = e.target as HTMLInputElement;
+
this.emailNotificationsEnabled = target.checked;
+
const response = await fetch("/api/user/notifications", {
+
headers: { "Content-Type": "application/json" },
+
email_notifications_enabled: this.emailNotificationsEnabled,
+
const data = await response.json();
+
throw new Error(data.error || "Failed to update notification settings");
+
this.emailNotificationsEnabled = !target.checked;
+
target.checked = !target.checked;
+
this.error = err instanceof Error ? err.message : "Failed to update notification settings";
+
<span class="toggle-slider"></span>
return html`<div class="loading">Loading...</div>`;
···
+
class="tab ${this.currentPage === "notifications" ? "active" : ""}"
+
this.setTab("notifications");
class="tab ${this.currentPage === "danger" ? "active" : ""}"
···
${this.currentPage === "account" ? this.renderAccountPage() : ""}
${this.currentPage === "sessions" ? this.renderSessionsPage() : ""}
${this.currentPage === "billing" ? this.renderBillingPage() : ""}
+
${this.currentPage === "notifications" ? this.renderNotificationsPage() : ""}
${this.currentPage === "danger" ? this.renderDangerPage() : ""}
···
<div class="modal-actions">
+
class="btn btn-rejection"
+
@click=${this.handleDeleteAccount}
+
?disabled=${this.deletingAccount}
+
${this.deletingAccount ? "Deleting..." : "Yes, Delete My Account"}
this.showDeleteConfirm = false;
+
?disabled=${this.deletingAccount}