···
canceled_at: number | null;
39
-
type SettingsPage = "account" | "sessions" | "passkeys" | "billing" | "danger";
39
+
type SettingsPage = "account" | "sessions" | "passkeys" | "billing" | "notifications" | "danger";
@customElement("user-settings")
export class UserSettings extends LitElement {
···
@state() passkeySupported = false;
@state() addingPasskey = false;
62
+
@state() emailNotificationsEnabled = true;
63
+
@state() deletingAccount = false;
static override styles = css`
···
424
+
background: #fecaca;
425
+
border: 2px solid rgba(220, 38, 38, 0.8);
426
+
border-radius: 6px;
428
+
margin-bottom: 1.5rem;
441
+
align-items: center;
442
+
justify-content: space-between;
444
+
border: 1px solid var(--secondary);
445
+
border-radius: 6px;
454
+
position: relative;
455
+
display: inline-block;
467
+
position: absolute;
473
+
background-color: var(--secondary);
475
+
border-radius: 24px;
478
+
.toggle-slider:before {
479
+
position: absolute;
485
+
background-color: white;
487
+
border-radius: 50%;
490
+
.toggle input:checked + .toggle-slider {
491
+
background-color: var(--primary);
494
+
.toggle input:checked + .toggle-slider:before {
495
+
transform: translateX(24px);
@media (max-width: 768px) {
···
private isValidTab(tab: string): boolean {
466
-
return ["account", "sessions", "passkeys", "billing", "danger"].includes(tab);
537
+
return ["account", "sessions", "passkeys", "billing", "notifications", "danger"].includes(tab);
private setTab(tab: SettingsPage) {
542
+
this.error = ""; // Clear errors when switching tabs
// Update URL without reloading page
const url = new URL(window.location.href);
url.searchParams.set("tab", tab);
···
486
-
this.user = await response.json();
558
+
const data = await response.json();
560
+
this.emailNotificationsEnabled = data.email_notifications_enabled ?? true;
···
const response = await fetch(`/api/passkeys/${passkeyId}`, {
567
-
const error = await response.json();
568
-
this.error = error.error || "Failed to delete passkey";
642
+
const data = await response.json();
643
+
this.error = data.error || "Failed to delete passkey";
await this.loadPasskeys();
575
-
this.error = "Failed to delete passkey";
650
+
this.error = err instanceof Error ? err.message : "Failed to delete passkey";
581
-
await fetch("/api/auth/logout", { method: "POST" });
657
+
const response = await fetch("/api/auth/logout", { method: "POST" });
659
+
if (!response.ok) {
660
+
const data = await response.json();
661
+
this.error = data.error || "Failed to logout";
window.location.href = "/";
584
-
this.error = "Failed to logout";
667
+
this.error = err instanceof Error ? err.message : "Failed to logout";
async handleDeleteAccount() {
672
+
this.deletingAccount = true;
674
+
document.body.style.cursor = "wait";
const response = await fetch("/api/user", {
595
-
this.error = "Failed to delete account";
682
+
const data = await response.json();
683
+
this.error = data.error || "Failed to delete account";
···
this.error = "Failed to delete account";
691
+
this.deletingAccount = false;
this.showDeleteConfirm = false;
693
+
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">
993
+
${this.error ? html`
994
+
<div class="error-banner">
<h2 class="section-title">Profile Information</h2>
···
<div class="content-inner">
1206
+
${this.error ? html`
1207
+
<div class="error-banner">
<h2 class="section-title">Active Sessions</h2>
···
<div class="content-inner">
1301
+
${this.error ? html`
1302
+
<div class="error-banner">
<h2 class="section-title">Subscription</h2>
···
Reactivate your subscription to unlock unlimited transcriptions.
1239
-
${this.error ? html`<p class="error" style="margin-top: 1rem;">${this.error}</p>` : ""}
···
if (hasActiveSubscription) {
<div class="content-inner">
1356
+
${this.error ? html`
1357
+
<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.
1297
-
${this.error ? html`<p class="error" style="margin-top: 1rem;">${this.error}</p>` : ""}
···
<div class="content-inner">
1416
+
${this.error ? html`
1417
+
<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"}
1317
-
${this.error ? html`<p class="error" style="margin-top: 1rem;">${this.error}</p>` : ""}
···
<div class="content-inner">
1441
+
${this.error ? html`
1442
+
<div class="error-banner">
<div class="section danger-section">
<h2 class="section-title">Delete Account</h2>
···
1465
+
renderNotificationsPage() {
1467
+
<div class="content-inner">
1468
+
${this.error ? html`
1469
+
<div class="error-banner">
1473
+
<div class="section">
1474
+
<h2 class="section-title">Email Notifications</h2>
1475
+
<p style="color: var(--text); margin-bottom: 1rem;">
1476
+
Control which emails you receive from Thistle.
1479
+
<div class="setting-row">
1480
+
<div class="setting-info">
1481
+
<strong>Transcription Complete</strong>
1482
+
<p style="color: var(--paynes-gray); font-size: 0.875rem; margin: 0.25rem 0 0 0;">
1483
+
Get notified when your transcription is ready
1486
+
<label class="toggle">
1489
+
.checked=${this.emailNotificationsEnabled}
1490
+
@change=${async (e: Event) => {
1491
+
const target = e.target as HTMLInputElement;
1492
+
this.emailNotificationsEnabled = target.checked;
1496
+
const response = await fetch("/api/user/notifications", {
1498
+
headers: { "Content-Type": "application/json" },
1499
+
body: JSON.stringify({
1500
+
email_notifications_enabled: this.emailNotificationsEnabled,
1504
+
if (!response.ok) {
1505
+
const data = await response.json();
1506
+
throw new Error(data.error || "Failed to update notification settings");
1509
+
// Revert on error
1510
+
this.emailNotificationsEnabled = !target.checked;
1511
+
target.checked = !target.checked;
1512
+
this.error = err instanceof Error ? err.message : "Failed to update notification settings";
1516
+
<span class="toggle-slider"></span>
return html`<div class="loading">Loading...</div>`;
1351
-
return html`<div class="error">${this.error}</div>`;
···
1563
+
class="tab ${this.currentPage === "notifications" ? "active" : ""}"
1565
+
this.setTab("notifications");
class="tab ${this.currentPage === "danger" ? "active" : ""}"
···
${this.currentPage === "account" ? this.renderAccountPage() : ""}
${this.currentPage === "sessions" ? this.renderSessionsPage() : ""}
${this.currentPage === "billing" ? this.renderBillingPage() : ""}
1583
+
${this.currentPage === "notifications" ? this.renderNotificationsPage() : ""}
${this.currentPage === "danger" ? this.renderDangerPage() : ""}
···
<div class="modal-actions">
1419
-
<button class="btn btn-rejection" @click=${this.handleDeleteAccount}>
1420
-
Yes, Delete My Account
1604
+
class="btn btn-rejection"
1605
+
@click=${this.handleDeleteAccount}
1606
+
?disabled=${this.deletingAccount}
1608
+
${this.deletingAccount ? "Deleting..." : "Yes, Delete My Account"}
this.showDeleteConfirm = false;
1615
+
?disabled=${this.deletingAccount}