1143 lines
40 KiB
Plaintext
1143 lines
40 KiB
Plaintext
package views
|
|
|
|
import (
|
|
"nebula/layouts"
|
|
"nebula/models"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
templ SettingsPage(data models.SettingsData, activeTab string) {
|
|
@layouts.DashboardLayout("Settings - Sonr", layouts.WalletUser{Name: "Sonr Wallet", Address: "sonr1x9f...7k2m"}, "settings") {
|
|
@settingsStyles()
|
|
<header class="page-header">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-xl">Settings</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Manage your account preferences and security</span>
|
|
</div>
|
|
</header>
|
|
<div class="settings-container">
|
|
<wa-tab-group class="settings-tabs">
|
|
<wa-tab panel="profile" active?={ activeTab == "" || activeTab == "profile" }>
|
|
<wa-icon name="user"></wa-icon>
|
|
Profile
|
|
</wa-tab>
|
|
<wa-tab panel="devices" active?={ activeTab == "devices" }>
|
|
<wa-icon name="laptop"></wa-icon>
|
|
Devices
|
|
</wa-tab>
|
|
<wa-tab panel="oauth" active?={ activeTab == "oauth" }>
|
|
<wa-icon name="key"></wa-icon>
|
|
OAuth
|
|
</wa-tab>
|
|
<wa-tab panel="notifications" active?={ activeTab == "notifications" }>
|
|
<wa-icon name="bell"></wa-icon>
|
|
Notifications
|
|
</wa-tab>
|
|
<wa-tab panel="emails" active?={ activeTab == "emails" }>
|
|
<wa-icon name="envelope"></wa-icon>
|
|
Emails
|
|
</wa-tab>
|
|
<wa-tab panel="phones" active?={ activeTab == "phones" }>
|
|
<wa-icon name="phone"></wa-icon>
|
|
Phone Numbers
|
|
</wa-tab>
|
|
<wa-tab panel="developer" active?={ activeTab == "developer" }>
|
|
<wa-icon name="code"></wa-icon>
|
|
Developer
|
|
</wa-tab>
|
|
<wa-tab-panel name="profile">
|
|
@ProfileTab(data.Profile)
|
|
</wa-tab-panel>
|
|
<wa-tab-panel name="devices">
|
|
@DevicesTab(data.Devices)
|
|
</wa-tab-panel>
|
|
<wa-tab-panel name="oauth">
|
|
@OAuthTab(data.OAuth)
|
|
</wa-tab-panel>
|
|
<wa-tab-panel name="notifications">
|
|
@NotificationsTab(data.Notifications)
|
|
</wa-tab-panel>
|
|
<wa-tab-panel name="emails">
|
|
@EmailsTab(data.Emails)
|
|
</wa-tab-panel>
|
|
<wa-tab-panel name="phones">
|
|
@PhonesTab(data.Phones, data.SMSSettings)
|
|
</wa-tab-panel>
|
|
<wa-tab-panel name="developer">
|
|
@DeveloperTab(data.Developer)
|
|
</wa-tab-panel>
|
|
</wa-tab-group>
|
|
</div>
|
|
@AddEmailDialog()
|
|
@AddPhoneDialog()
|
|
@AddDeviceDialog()
|
|
@GenerateKeyDialog()
|
|
@AddWebhookDialog()
|
|
@settingsScripts()
|
|
}
|
|
}
|
|
|
|
templ ProfileTab(profile models.ProfileSettings) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Avatar</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Your public profile image</p>
|
|
</div>
|
|
<div class="avatar-upload">
|
|
<wa-avatar initials={ profile.Initials } style="background: linear-gradient(135deg, #17c2ff, #0090ff);"></wa-avatar>
|
|
<div class="wa-stack wa-gap-s">
|
|
<div class="wa-cluster wa-gap-s">
|
|
<wa-button size="small" variant="neutral" appearance="outlined">
|
|
<wa-icon slot="start" name="upload"></wa-icon>
|
|
Upload
|
|
</wa-button>
|
|
<wa-button size="small" variant="neutral" appearance="plain">Remove</wa-button>
|
|
</div>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">JPG, PNG or GIF. Max 2MB.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Display Name</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Shown on your public profile</p>
|
|
</div>
|
|
<wa-input value={ profile.DisplayName } placeholder="Enter display name" name="profile-display-name"></wa-input>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Username</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Your unique identifier</p>
|
|
</div>
|
|
<wa-input value={ profile.Username } placeholder="Enter username" name="profile-username">
|
|
<span slot="prefix">{ "@" }</span>
|
|
</wa-input>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Bio</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Brief description for your profile</p>
|
|
</div>
|
|
<wa-textarea rows="3" placeholder="Tell us about yourself..." value={ profile.Bio } name="profile-bio"></wa-textarea>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Website</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Your personal or project website</p>
|
|
</div>
|
|
<wa-input type="url" value={ profile.Website } placeholder="https://example.com" name="profile-website">
|
|
<wa-icon slot="prefix" name="globe"></wa-icon>
|
|
</wa-input>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Social Links</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Connect your social accounts</p>
|
|
</div>
|
|
<div class="wa-stack wa-gap-m">
|
|
<wa-input placeholder="twitter.com/username" value={ profile.SocialLinks.Twitter } name="profile-twitter">
|
|
<wa-icon slot="prefix" name="x-twitter" variant="brand"></wa-icon>
|
|
</wa-input>
|
|
<wa-input placeholder="github.com/username" value={ profile.SocialLinks.GitHub } name="profile-github">
|
|
<wa-icon slot="prefix" name="github" variant="brand"></wa-icon>
|
|
</wa-input>
|
|
<wa-input placeholder="discord.gg/invite" value={ profile.SocialLinks.Discord } name="profile-discord">
|
|
<wa-icon slot="prefix" name="discord" variant="brand"></wa-icon>
|
|
</wa-input>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Profile Visibility</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Control who can see your profile</p>
|
|
</div>
|
|
<wa-radio-group value={ profile.Visibility } name="profile-visibility">
|
|
<wa-radio value="public">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>Public</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Anyone can view your profile</span>
|
|
</div>
|
|
</wa-radio>
|
|
<wa-radio value="connections">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>Connections Only</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Only connected apps and users</span>
|
|
</div>
|
|
</wa-radio>
|
|
<wa-radio value="private">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>Private</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Only you can see your profile</span>
|
|
</div>
|
|
</wa-radio>
|
|
</wa-radio-group>
|
|
</div>
|
|
</div>
|
|
<div class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
|
<wa-button variant="neutral" appearance="outlined">Cancel</wa-button>
|
|
<wa-button variant="brand" hx-post="/api/settings/profile" hx-include="[name^='profile-']">Save Changes</wa-button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ DevicesTab(devices []models.Device) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="wa-flank">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-m">Linked Devices</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Passkeys and security keys registered with your account</span>
|
|
</div>
|
|
<wa-button size="small" variant="brand" id="add-device-btn">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Add Device
|
|
</wa-button>
|
|
</div>
|
|
<div class="wa-stack wa-gap-m" id="devices-list">
|
|
for _, device := range devices {
|
|
@DeviceItem(device)
|
|
}
|
|
</div>
|
|
<wa-callout variant="neutral">
|
|
<wa-icon slot="icon" name="shield-check"></wa-icon>
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-xs">Security Recommendation</span>
|
|
<span class="wa-caption-s">Register at least 2 devices to ensure account recovery if one is lost.</span>
|
|
</div>
|
|
</wa-callout>
|
|
</div>
|
|
}
|
|
|
|
templ DeviceItem(device models.Device) {
|
|
<div class="device-item" id={ "device-" + device.ID }>
|
|
<div class="device-icon">
|
|
<wa-icon name={ deviceIcon(device.Type) } style="font-size: 24px;"></wa-icon>
|
|
</div>
|
|
<div class="device-info">
|
|
<div class="wa-stack wa-gap-0">
|
|
<div class="wa-cluster wa-gap-xs">
|
|
<span class="wa-heading-s">{ device.Name }</span>
|
|
if device.IsCurrent {
|
|
<wa-badge variant="success" pill>Current</wa-badge>
|
|
}
|
|
</div>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
|
if device.Browser != "" {
|
|
{ device.Browser } on { device.OS } -
|
|
}
|
|
{ device.AuthType }
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="wa-stack wa-gap-0 wa-align-items-end">
|
|
<span class="wa-caption-s">Added { formatDate(device.AddedAt) }</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Last used: { formatRelativeTime(device.LastUsed) }</span>
|
|
</div>
|
|
<wa-dropdown>
|
|
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="Options"></wa-icon-button>
|
|
<wa-dropdown-item>
|
|
<wa-icon slot="icon" name="pen"></wa-icon>
|
|
Rename
|
|
</wa-dropdown-item>
|
|
<wa-divider></wa-divider>
|
|
<wa-dropdown-item
|
|
style="color: var(--wa-color-danger);"
|
|
hx-delete={ "/api/devices/" + device.ID }
|
|
hx-target={ "#device-" + device.ID }
|
|
hx-swap="delete"
|
|
hx-confirm="Remove this device?"
|
|
>
|
|
<wa-icon slot="icon" name="trash"></wa-icon>
|
|
Remove
|
|
</wa-dropdown-item>
|
|
</wa-dropdown>
|
|
</div>
|
|
}
|
|
|
|
func deviceIcon(deviceType string) string {
|
|
switch deviceType {
|
|
case "laptop":
|
|
return "laptop"
|
|
case "mobile":
|
|
return "mobile"
|
|
case "key":
|
|
return "key"
|
|
case "desktop":
|
|
return "desktop"
|
|
default:
|
|
return "laptop"
|
|
}
|
|
}
|
|
|
|
templ OAuthTab(oauth models.OAuthSettings) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="wa-flank">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-m">OAuth Settings</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Configure your identity provider settings</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Default Scopes</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">Permissions granted by default</p>
|
|
</div>
|
|
<div class="wa-stack wa-gap-s">
|
|
for _, scope := range oauth.DefaultScopes {
|
|
<wa-checkbox checked?={ scope.Enabled } disabled?={ scope.Required }>
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>{ scope.Name }</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ scope.Description }</span>
|
|
</div>
|
|
</wa-checkbox>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Session Duration</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">How long OAuth sessions remain valid</p>
|
|
</div>
|
|
<wa-select value={ oauth.SessionDuration }>
|
|
<wa-option value="1h">1 hour</wa-option>
|
|
<wa-option value="24h">24 hours</wa-option>
|
|
<wa-option value="7d">7 days</wa-option>
|
|
<wa-option value="30d">30 days</wa-option>
|
|
<wa-option value="never">Never expire</wa-option>
|
|
</wa-select>
|
|
</div>
|
|
</div>
|
|
<div class="form-section">
|
|
<div class="form-row">
|
|
<div class="form-label">
|
|
<span class="wa-heading-s">Consent Prompt</span>
|
|
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500); margin: var(--wa-space-2xs) 0 0;">When to show permission dialogs</p>
|
|
</div>
|
|
<wa-radio-group value={ oauth.ConsentPrompt }>
|
|
<wa-radio value="always">Always prompt for consent</wa-radio>
|
|
<wa-radio value="first">Only on first connection</wa-radio>
|
|
<wa-radio value="scope">When new scopes requested</wa-radio>
|
|
</wa-radio-group>
|
|
</div>
|
|
</div>
|
|
<wa-divider></wa-divider>
|
|
<div class="wa-stack wa-gap-m">
|
|
<div class="wa-flank">
|
|
<span class="wa-heading-m">Authorized Clients</span>
|
|
<wa-button size="small" variant="neutral" appearance="outlined">View All</wa-button>
|
|
</div>
|
|
<div class="wa-stack wa-gap-s">
|
|
for _, client := range oauth.Clients {
|
|
@AuthorizedClientCard(client)
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
|
<wa-button variant="neutral" appearance="outlined">Cancel</wa-button>
|
|
<wa-button variant="brand">Save Changes</wa-button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ AuthorizedClientCard(client models.AuthorizedClient) {
|
|
<div class="oauth-client-card">
|
|
<div class="wa-flank">
|
|
<div class="wa-cluster wa-gap-m">
|
|
<wa-avatar initials={ client.Initials } style={ "--size: 40px; background: " + client.Color + ";" }></wa-avatar>
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-s">{ client.Name }</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ client.Domain }</span>
|
|
</div>
|
|
</div>
|
|
<div class="wa-cluster wa-gap-s">
|
|
if client.Status == "active" {
|
|
<wa-badge variant="success" pill>Active</wa-badge>
|
|
} else {
|
|
<wa-badge variant="neutral" pill>Idle</wa-badge>
|
|
}
|
|
<wa-button size="small" variant="danger" appearance="plain" hx-post={ "/api/oauth/clients/" + client.ID + "/revoke" } hx-swap="none">Revoke</wa-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ NotificationsTab(prefs models.NotificationPrefs) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-m">Notification Preferences</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Choose how and when you receive notifications</span>
|
|
</div>
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">Security Alerts</span>
|
|
<div class="wa-stack">
|
|
for _, item := range prefs.SecurityAlerts {
|
|
@NotificationRow(item)
|
|
}
|
|
</div>
|
|
</wa-card>
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">Transaction Notifications</span>
|
|
<div class="wa-stack">
|
|
for _, item := range prefs.Transactions {
|
|
@NotificationRow(item)
|
|
}
|
|
</div>
|
|
</wa-card>
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">App Notifications</span>
|
|
<div class="wa-stack">
|
|
for _, item := range prefs.Apps {
|
|
@NotificationRow(item)
|
|
}
|
|
</div>
|
|
</wa-card>
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">Marketing & Updates</span>
|
|
<div class="wa-stack">
|
|
for _, item := range prefs.Marketing {
|
|
@NotificationRow(item)
|
|
}
|
|
</div>
|
|
</wa-card>
|
|
<div class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
|
<wa-button variant="brand" hx-post="/api/settings/notifications" hx-swap="none">Save Preferences</wa-button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ NotificationRow(item models.NotificationItem) {
|
|
<div class="notification-row">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">{ item.Title }</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">{ item.Description }</span>
|
|
</div>
|
|
if item.Threshold > 0 {
|
|
<div class="wa-cluster wa-gap-s">
|
|
<wa-input type="number" value={ itoa(item.Threshold) } size="small" style="width: 100px;">
|
|
<span slot="suffix">USD</span>
|
|
</wa-input>
|
|
<wa-switch checked?={ item.Enabled } hx-post="/api/settings/notifications" hx-vals={ `{"key":"` + item.Key + `"}` } hx-swap="none"></wa-switch>
|
|
</div>
|
|
} else {
|
|
<wa-switch checked?={ item.Enabled } hx-post="/api/settings/notifications" hx-vals={ `{"key":"` + item.Key + `"}` } hx-swap="none"></wa-switch>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
templ EmailsTab(emails []models.Email) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="wa-flank">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-m">Email Addresses</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Manage email addresses linked to your account</span>
|
|
</div>
|
|
<wa-button size="small" variant="brand" id="add-email-btn">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Add Email
|
|
</wa-button>
|
|
</div>
|
|
<div class="wa-stack wa-gap-m" id="emails-list">
|
|
for _, email := range emails {
|
|
@EmailItem(email)
|
|
}
|
|
</div>
|
|
<wa-callout variant="neutral">
|
|
<wa-icon slot="icon" name="circle-info"></wa-icon>
|
|
<span class="wa-caption-s">Your primary email is used for account recovery and important notifications. You must have at least one verified email.</span>
|
|
</wa-callout>
|
|
</div>
|
|
}
|
|
|
|
templ EmailItem(email models.Email) {
|
|
<div class="contact-item" id={ "email-" + email.ID }>
|
|
if email.IsVerified {
|
|
<wa-icon name="envelope" style="font-size: 20px; color: var(--wa-color-neutral-500);"></wa-icon>
|
|
} else {
|
|
<wa-icon name="envelope" style="font-size: 20px; color: var(--wa-color-warning);"></wa-icon>
|
|
}
|
|
<div class="wa-stack wa-gap-0" style="flex: 1;">
|
|
<div class="wa-cluster wa-gap-xs">
|
|
<span class="wa-heading-s">{ email.Address }</span>
|
|
if email.IsPrimary {
|
|
<wa-badge variant="success" pill>Primary</wa-badge>
|
|
}
|
|
if email.IsVerified {
|
|
<wa-badge variant="brand" pill>Verified</wa-badge>
|
|
} else {
|
|
<wa-badge variant="warning" pill>Pending</wa-badge>
|
|
}
|
|
</div>
|
|
if email.IsVerified {
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Added { formatDate(email.AddedAt) }</span>
|
|
} else {
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Verification sent - check your inbox</span>
|
|
}
|
|
</div>
|
|
<wa-dropdown>
|
|
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="Options"></wa-icon-button>
|
|
<wa-dropdown-item disabled?={ email.IsPrimary }>
|
|
<wa-icon slot="icon" name="star"></wa-icon>
|
|
Set as Primary
|
|
</wa-dropdown-item>
|
|
<wa-dropdown-item hx-post={ "/api/settings/emails/" + email.ID + "/resend" } hx-swap="none">
|
|
<wa-icon slot="icon" name="paper-plane"></wa-icon>
|
|
Resend Verification
|
|
</wa-dropdown-item>
|
|
<wa-divider></wa-divider>
|
|
<wa-dropdown-item
|
|
disabled?={ email.IsPrimary }
|
|
style="color: var(--wa-color-danger);"
|
|
hx-delete={ "/api/settings/emails/" + email.ID }
|
|
hx-target={ "#email-" + email.ID }
|
|
hx-swap="delete"
|
|
>
|
|
<wa-icon slot="icon" name="trash"></wa-icon>
|
|
Remove
|
|
</wa-dropdown-item>
|
|
</wa-dropdown>
|
|
</div>
|
|
}
|
|
|
|
templ PhonesTab(phones []models.Phone, smsSettings models.SMSSettings) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="wa-flank">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-m">Phone Numbers</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Manage phone numbers for 2FA and recovery</span>
|
|
</div>
|
|
<wa-button size="small" variant="brand" id="add-phone-btn">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Add Phone
|
|
</wa-button>
|
|
</div>
|
|
<div class="wa-stack wa-gap-m" id="phones-list">
|
|
for _, phone := range phones {
|
|
@PhoneItem(phone)
|
|
}
|
|
</div>
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">SMS Authentication</span>
|
|
<div class="wa-stack">
|
|
<div class="notification-row">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">Enable SMS 2FA</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Receive codes via SMS for high-risk actions</span>
|
|
</div>
|
|
<wa-switch checked?={ smsSettings.Enabled }></wa-switch>
|
|
</div>
|
|
<div class="notification-row">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">SMS recovery codes</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Allow account recovery via SMS</span>
|
|
</div>
|
|
<wa-switch checked?={ smsSettings.RecoveryCodes }></wa-switch>
|
|
</div>
|
|
</div>
|
|
</wa-card>
|
|
<wa-callout variant="warning">
|
|
<wa-icon slot="icon" name="triangle-alert"></wa-icon>
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-xs">SMS Security Notice</span>
|
|
<span class="wa-caption-s">SMS-based authentication is less secure than passkeys. We recommend using passkeys as your primary authentication method.</span>
|
|
</div>
|
|
</wa-callout>
|
|
</div>
|
|
}
|
|
|
|
templ PhoneItem(phone models.Phone) {
|
|
<div class="contact-item" id={ "phone-" + phone.ID }>
|
|
<wa-icon name="phone" style="font-size: 20px; color: var(--wa-color-neutral-500);"></wa-icon>
|
|
<div class="wa-stack wa-gap-0" style="flex: 1;">
|
|
<div class="wa-cluster wa-gap-xs">
|
|
<span class="wa-heading-s">{ phone.Number }</span>
|
|
if phone.IsPrimary {
|
|
<wa-badge variant="success" pill>Primary</wa-badge>
|
|
}
|
|
if phone.IsVerified {
|
|
<wa-badge variant="brand" pill>Verified</wa-badge>
|
|
}
|
|
</div>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
|
Added { formatDate(phone.AddedAt) }
|
|
if phone.Use2FA {
|
|
- Used for 2FA
|
|
}
|
|
</span>
|
|
</div>
|
|
<wa-dropdown>
|
|
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="Options"></wa-icon-button>
|
|
<wa-dropdown-item disabled?={ phone.IsPrimary }>
|
|
<wa-icon slot="icon" name="star"></wa-icon>
|
|
Set as Primary
|
|
</wa-dropdown-item>
|
|
<wa-dropdown-item hx-post={ "/api/settings/phones/" + phone.ID + "/test" } hx-swap="none">
|
|
<wa-icon slot="icon" name="message"></wa-icon>
|
|
Send Test SMS
|
|
</wa-dropdown-item>
|
|
<wa-divider></wa-divider>
|
|
<wa-dropdown-item
|
|
disabled?={ phone.IsPrimary }
|
|
style="color: var(--wa-color-danger);"
|
|
hx-delete={ "/api/settings/phones/" + phone.ID }
|
|
hx-target={ "#phone-" + phone.ID }
|
|
hx-swap="delete"
|
|
>
|
|
<wa-icon slot="icon" name="trash"></wa-icon>
|
|
Remove
|
|
</wa-dropdown-item>
|
|
</wa-dropdown>
|
|
</div>
|
|
}
|
|
|
|
templ DeveloperTab(dev models.DeveloperSettings) {
|
|
<div class="wa-stack wa-gap-xl">
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-m">Developer Settings</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">API access and integration options</span>
|
|
</div>
|
|
<wa-card>
|
|
<div slot="header" class="wa-flank">
|
|
<span class="wa-heading-s">API Keys</span>
|
|
<wa-button size="small" variant="neutral" appearance="outlined" id="generate-key-btn">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Generate Key
|
|
</wa-button>
|
|
</div>
|
|
<div class="wa-stack wa-gap-m" id="api-keys-list">
|
|
for _, key := range dev.APIKeys {
|
|
@APIKeyCard(key)
|
|
}
|
|
</div>
|
|
</wa-card>
|
|
<wa-card>
|
|
<div slot="header" class="wa-flank">
|
|
<span class="wa-heading-s">Webhooks</span>
|
|
<wa-button size="small" variant="neutral" appearance="outlined" id="add-webhook-btn">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Add Endpoint
|
|
</wa-button>
|
|
</div>
|
|
<div class="wa-stack wa-gap-m" id="webhooks-list">
|
|
for _, wh := range dev.Webhooks {
|
|
@WebhookCard(wh)
|
|
}
|
|
</div>
|
|
</wa-card>
|
|
@OAuthAppConfigCard(dev.OAuthApp)
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">Developer Mode</span>
|
|
<div class="wa-stack">
|
|
<div class="notification-row">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">Enable debug logging</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Log detailed API requests and responses</span>
|
|
</div>
|
|
<wa-switch checked?={ dev.DebugMode }></wa-switch>
|
|
</div>
|
|
<div class="notification-row">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">Test mode</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Use testnet for all transactions</span>
|
|
</div>
|
|
<wa-switch checked?={ dev.TestMode }></wa-switch>
|
|
</div>
|
|
<div class="notification-row">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">Show raw responses</span>
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Display JSON in UI for debugging</span>
|
|
</div>
|
|
<wa-switch checked?={ dev.ShowRaw }></wa-switch>
|
|
</div>
|
|
</div>
|
|
</wa-card>
|
|
<wa-callout variant="neutral">
|
|
<wa-icon slot="icon" name="book"></wa-icon>
|
|
<div class="wa-stack wa-gap-2xs">
|
|
<span class="wa-heading-xs">API Documentation</span>
|
|
<span class="wa-caption-s">
|
|
View our <a href="#" style="color: var(--wa-color-primary);">API reference</a> and
|
|
<a href="#" style="color: var(--wa-color-primary);">integration guides</a> to get started.
|
|
</span>
|
|
</div>
|
|
</wa-callout>
|
|
<div class="wa-cluster wa-gap-s" style="justify-content: flex-end;">
|
|
<wa-button variant="neutral" appearance="outlined">Cancel</wa-button>
|
|
<wa-button variant="brand">Save Changes</wa-button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ APIKeyCard(key models.APIKey) {
|
|
<div class="wa-stack wa-gap-s">
|
|
<div class="wa-flank">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span class="wa-heading-xs">{ key.Name }</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Created { formatDate(key.CreatedAt) }</span>
|
|
</div>
|
|
if key.Environment == "live" {
|
|
<wa-badge variant="success" pill>Active</wa-badge>
|
|
} else {
|
|
<wa-badge variant="neutral" pill>Test Mode</wa-badge>
|
|
}
|
|
</div>
|
|
<div class="api-key-display">
|
|
<span>{ key.KeyPreview }</span>
|
|
<wa-copy-button value={ key.KeyFull }></wa-copy-button>
|
|
<wa-tooltip content="Reveal key">
|
|
<wa-icon-button name="eye" label="Reveal"></wa-icon-button>
|
|
</wa-tooltip>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ WebhookCard(wh models.Webhook) {
|
|
<div class="webhook-item">
|
|
<div class="wa-flank">
|
|
<div class="wa-stack wa-gap-0">
|
|
<div class="wa-cluster wa-gap-xs">
|
|
<span class="wa-heading-xs">{ wh.URL }</span>
|
|
if wh.Status == "active" {
|
|
<wa-badge variant="success" pill>Active</wa-badge>
|
|
} else {
|
|
<wa-badge variant="warning" pill>Failing</wa-badge>
|
|
}
|
|
</div>
|
|
if wh.Status == "failing" {
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-danger);">{ formatEvents(wh.Events) } events - { itoa(wh.FailureCount) } failures in last hour</span>
|
|
} else {
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">{ formatEvents(wh.Events) } events - Last triggered { formatRelativeTime(wh.LastTriggered) }</span>
|
|
}
|
|
</div>
|
|
<wa-dropdown>
|
|
<wa-icon-button slot="trigger" name="ellipsis-vertical" label="Options"></wa-icon-button>
|
|
<wa-dropdown-item>
|
|
<wa-icon slot="icon" name="pen"></wa-icon>
|
|
Edit
|
|
</wa-dropdown-item>
|
|
<wa-dropdown-item hx-post={ "/api/webhooks/" + wh.ID + "/test" } hx-swap="none">
|
|
<wa-icon slot="icon" name="paper-plane"></wa-icon>
|
|
Send Test
|
|
</wa-dropdown-item>
|
|
<wa-dropdown-item>
|
|
<wa-icon slot="icon" name="clock-rotate-left"></wa-icon>
|
|
View Logs
|
|
</wa-dropdown-item>
|
|
<wa-divider></wa-divider>
|
|
<wa-dropdown-item style="color: var(--wa-color-danger);" hx-delete={ "/api/webhooks/" + wh.ID } hx-target="closest .webhook-item" hx-swap="delete">
|
|
<wa-icon slot="icon" name="trash"></wa-icon>
|
|
Delete
|
|
</wa-dropdown-item>
|
|
</wa-dropdown>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ OAuthAppConfigCard(config models.OAuthAppConfig) {
|
|
<wa-card>
|
|
<span slot="header" class="wa-heading-s">OAuth Application</span>
|
|
<div class="wa-stack wa-gap-l">
|
|
<div class="form-row" style="grid-template-columns: 140px 1fr;">
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Client ID</span>
|
|
<div class="wa-cluster wa-gap-s">
|
|
<code style="font-family: var(--wa-font-mono); font-size: var(--wa-font-size-s);">{ config.ClientID }</code>
|
|
<wa-copy-button value={ config.ClientID }></wa-copy-button>
|
|
</div>
|
|
</div>
|
|
<div class="form-row" style="grid-template-columns: 140px 1fr;">
|
|
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Client Secret</span>
|
|
<div class="wa-cluster wa-gap-s">
|
|
<code style="font-family: var(--wa-font-mono); font-size: var(--wa-font-size-s);">••••••••••••••••</code>
|
|
<wa-copy-button value={ config.ClientSecret }></wa-copy-button>
|
|
<wa-button size="small" appearance="plain">Regenerate</wa-button>
|
|
</div>
|
|
</div>
|
|
<wa-divider></wa-divider>
|
|
<div class="wa-stack wa-gap-s">
|
|
<span class="wa-heading-xs">Redirect URIs</span>
|
|
for _, uri := range config.RedirectURIs {
|
|
<wa-input value={ uri } placeholder="https://example.com/callback">
|
|
<wa-icon-button slot="suffix" name="trash" label="Remove"></wa-icon-button>
|
|
</wa-input>
|
|
}
|
|
<wa-button size="small" appearance="plain">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Add URI
|
|
</wa-button>
|
|
</div>
|
|
</div>
|
|
</wa-card>
|
|
}
|
|
|
|
templ AddEmailDialog() {
|
|
<wa-dialog label="Add Email Address" id="add-email-dialog" style="--width: 420px;">
|
|
<div class="wa-stack wa-gap-l">
|
|
<wa-input type="email" label="Email Address" placeholder="you@example.com" autofocus name="email">
|
|
<wa-icon slot="prefix" name="envelope"></wa-icon>
|
|
</wa-input>
|
|
<wa-callout variant="neutral">
|
|
<wa-icon slot="icon" name="circle-info"></wa-icon>
|
|
<span class="wa-caption-s">We'll send a verification link to this address.</span>
|
|
</wa-callout>
|
|
</div>
|
|
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end; width: 100%;">
|
|
<wa-button variant="neutral" appearance="outlined" data-dialog="close">Cancel</wa-button>
|
|
<wa-button variant="brand" hx-post="/api/settings/emails" hx-target="#emails-list" hx-swap="beforeend">Send Verification</wa-button>
|
|
</div>
|
|
</wa-dialog>
|
|
}
|
|
|
|
templ AddPhoneDialog() {
|
|
<wa-dialog label="Add Phone Number" id="add-phone-dialog" style="--width: 420px;">
|
|
<div class="wa-stack wa-gap-l">
|
|
<div class="wa-cluster wa-gap-s">
|
|
<wa-select value="+1" style="width: 100px;" name="country-code">
|
|
<wa-option value="+1">+1</wa-option>
|
|
<wa-option value="+44">+44</wa-option>
|
|
<wa-option value="+49">+49</wa-option>
|
|
<wa-option value="+33">+33</wa-option>
|
|
<wa-option value="+81">+81</wa-option>
|
|
</wa-select>
|
|
<wa-input type="tel" label="Phone Number" placeholder="(555) 123-4567" style="flex: 1;" name="phone">
|
|
<wa-icon slot="prefix" name="phone"></wa-icon>
|
|
</wa-input>
|
|
</div>
|
|
<wa-callout variant="neutral">
|
|
<wa-icon slot="icon" name="circle-info"></wa-icon>
|
|
<span class="wa-caption-s">We'll send a verification code via SMS.</span>
|
|
</wa-callout>
|
|
</div>
|
|
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end; width: 100%;">
|
|
<wa-button variant="neutral" appearance="outlined" data-dialog="close">Cancel</wa-button>
|
|
<wa-button variant="brand" hx-post="/api/settings/phones" hx-target="#phones-list" hx-swap="beforeend">Send Code</wa-button>
|
|
</div>
|
|
</wa-dialog>
|
|
}
|
|
|
|
templ AddDeviceDialog() {
|
|
<wa-dialog label="Add Device" id="add-device-dialog" style="--width: 480px;">
|
|
<div class="wa-stack wa-gap-l" style="text-align: center;">
|
|
<wa-icon name="fingerprint" style="font-size: 64px; color: var(--wa-color-primary);"></wa-icon>
|
|
<div class="wa-stack wa-gap-xs">
|
|
<span class="wa-heading-m">Register a Passkey</span>
|
|
<span class="wa-caption-m" style="color: var(--wa-color-neutral-500);">
|
|
Use your device's built-in authenticator (Face ID, Touch ID, Windows Hello) or a security key.
|
|
</span>
|
|
</div>
|
|
<wa-button variant="brand" size="large" style="width: 100%;" id="register-passkey-btn">
|
|
<wa-icon slot="start" name="plus"></wa-icon>
|
|
Register Device
|
|
</wa-button>
|
|
<wa-divider>or</wa-divider>
|
|
<wa-button variant="neutral" appearance="outlined" style="width: 100%;" id="register-security-key-btn">
|
|
<wa-icon slot="start" name="key"></wa-icon>
|
|
Use Security Key
|
|
</wa-button>
|
|
</div>
|
|
<div slot="footer" class="wa-cluster" style="justify-content: center; width: 100%;">
|
|
<wa-button variant="neutral" appearance="plain" data-dialog="close">Cancel</wa-button>
|
|
</div>
|
|
</wa-dialog>
|
|
}
|
|
|
|
templ GenerateKeyDialog() {
|
|
<wa-dialog label="Generate API Key" id="generate-key-dialog" style="--width: 420px;">
|
|
<div class="wa-stack wa-gap-l">
|
|
<wa-input label="Key Name" placeholder="e.g., Production Server" autofocus name="key-name">
|
|
<wa-icon slot="prefix" name="tag"></wa-icon>
|
|
</wa-input>
|
|
<wa-select label="Environment" value="test" name="key-env">
|
|
<wa-option value="test">Test Mode</wa-option>
|
|
<wa-option value="live">Production</wa-option>
|
|
</wa-select>
|
|
<wa-select label="Permissions" value="full" name="key-perms">
|
|
<wa-option value="full">Full Access</wa-option>
|
|
<wa-option value="read">Read Only</wa-option>
|
|
<wa-option value="write">Write Only</wa-option>
|
|
</wa-select>
|
|
<wa-callout variant="warning">
|
|
<wa-icon slot="icon" name="triangle-alert"></wa-icon>
|
|
<span class="wa-caption-s">Your secret key will only be shown once. Store it securely.</span>
|
|
</wa-callout>
|
|
</div>
|
|
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end; width: 100%;">
|
|
<wa-button variant="neutral" appearance="outlined" data-dialog="close">Cancel</wa-button>
|
|
<wa-button variant="brand" hx-post="/api/settings/api-keys" hx-target="#api-keys-list" hx-swap="beforeend">Generate Key</wa-button>
|
|
</div>
|
|
</wa-dialog>
|
|
}
|
|
|
|
templ AddWebhookDialog() {
|
|
<wa-dialog label="Add Webhook Endpoint" id="add-webhook-dialog" style="--width: 480px;">
|
|
<div class="wa-stack wa-gap-l">
|
|
<wa-input label="Endpoint URL" placeholder="https://api.example.com/webhooks" type="url" name="webhook-url">
|
|
<wa-icon slot="prefix" name="globe"></wa-icon>
|
|
</wa-input>
|
|
<wa-input label="Description" placeholder="Optional description" name="webhook-desc">
|
|
<wa-icon slot="prefix" name="align-left"></wa-icon>
|
|
</wa-input>
|
|
<div class="wa-stack wa-gap-s">
|
|
<span class="wa-heading-xs">Events to Subscribe</span>
|
|
<wa-checkbox checked name="webhook-event-tx">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>transaction.*</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">All transaction events</span>
|
|
</div>
|
|
</wa-checkbox>
|
|
<wa-checkbox checked name="webhook-event-conn">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>connection.*</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">App connection events</span>
|
|
</div>
|
|
</wa-checkbox>
|
|
<wa-checkbox name="webhook-event-sig">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>signature.*</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Signature request events</span>
|
|
</div>
|
|
</wa-checkbox>
|
|
<wa-checkbox name="webhook-event-acct">
|
|
<div class="wa-stack wa-gap-0">
|
|
<span>account.*</span>
|
|
<span class="wa-caption-xs" style="color: var(--wa-color-neutral-500);">Account update events</span>
|
|
</div>
|
|
</wa-checkbox>
|
|
</div>
|
|
</div>
|
|
<div slot="footer" class="wa-cluster wa-gap-s" style="justify-content: flex-end; width: 100%;">
|
|
<wa-button variant="neutral" appearance="outlined" data-dialog="close">Cancel</wa-button>
|
|
<wa-button variant="brand" hx-post="/api/settings/webhooks" hx-target="#webhooks-list" hx-swap="beforeend">Create Webhook</wa-button>
|
|
</div>
|
|
</wa-dialog>
|
|
}
|
|
|
|
func itoa(i int) string {
|
|
return strconv.Itoa(i)
|
|
}
|
|
|
|
func formatDate(t time.Time) string {
|
|
return t.Format("Jan 2, 2006")
|
|
}
|
|
|
|
func formatRelativeTime(t time.Time) string {
|
|
diff := time.Since(t)
|
|
if diff < time.Minute {
|
|
return "Just now"
|
|
}
|
|
if diff < time.Hour {
|
|
mins := int(diff.Minutes())
|
|
if mins == 1 {
|
|
return "1 minute ago"
|
|
}
|
|
return strconv.Itoa(mins) + " minutes ago"
|
|
}
|
|
if diff < 24*time.Hour {
|
|
hours := int(diff.Hours())
|
|
if hours == 1 {
|
|
return "1 hour ago"
|
|
}
|
|
return strconv.Itoa(hours) + " hours ago"
|
|
}
|
|
days := int(diff.Hours() / 24)
|
|
if days == 1 {
|
|
return "1 day ago"
|
|
}
|
|
if days < 14 {
|
|
return strconv.Itoa(days) + " days ago"
|
|
}
|
|
weeks := days / 7
|
|
if weeks == 1 {
|
|
return "1 week ago"
|
|
}
|
|
return strconv.Itoa(weeks) + " weeks ago"
|
|
}
|
|
|
|
func formatEvents(events []string) string {
|
|
if len(events) == 1 && events[0] == "*" {
|
|
return "All"
|
|
}
|
|
return strings.Join(events, ", ")
|
|
}
|
|
|
|
templ settingsStyles() {
|
|
<style>
|
|
.page-header {
|
|
margin-bottom: var(--wa-space-xl);
|
|
}
|
|
.settings-container {
|
|
background: var(--wa-color-surface);
|
|
border-radius: var(--wa-radius-l);
|
|
overflow: hidden;
|
|
}
|
|
.settings-tabs {
|
|
--indicator-color: var(--wa-color-primary);
|
|
}
|
|
.settings-tabs wa-tab-panel {
|
|
padding: var(--wa-space-xl);
|
|
}
|
|
.settings-tabs wa-tab-group::part(base) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.settings-tabs wa-tab-group::part(nav) {
|
|
background: var(--wa-color-surface);
|
|
border-bottom: 1px solid var(--wa-color-neutral-200);
|
|
padding: 0 var(--wa-space-m);
|
|
}
|
|
.form-section {
|
|
padding: var(--wa-space-l) 0;
|
|
border-bottom: 1px solid var(--wa-color-neutral-100);
|
|
}
|
|
.form-section:first-child {
|
|
padding-top: 0;
|
|
}
|
|
.form-section:last-child {
|
|
border-bottom: none;
|
|
padding-bottom: 0;
|
|
}
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 200px 1fr;
|
|
gap: var(--wa-space-xl);
|
|
align-items: start;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
gap: var(--wa-space-s);
|
|
}
|
|
}
|
|
.form-label {
|
|
padding-top: var(--wa-space-xs);
|
|
}
|
|
.avatar-upload {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--wa-space-l);
|
|
}
|
|
.avatar-upload wa-avatar {
|
|
--size: 80px;
|
|
}
|
|
.device-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--wa-space-m);
|
|
padding: var(--wa-space-m);
|
|
background: var(--wa-color-surface-alt);
|
|
border-radius: var(--wa-radius-m);
|
|
}
|
|
.device-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: var(--wa-radius-m);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--wa-color-surface);
|
|
border: 1px solid var(--wa-color-neutral-200);
|
|
}
|
|
.device-info {
|
|
flex: 1;
|
|
}
|
|
.contact-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--wa-space-m);
|
|
padding: var(--wa-space-m);
|
|
background: var(--wa-color-surface-alt);
|
|
border-radius: var(--wa-radius-m);
|
|
}
|
|
.api-key-display {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--wa-space-s);
|
|
padding: var(--wa-space-m);
|
|
background: var(--wa-color-neutral-900);
|
|
color: var(--wa-color-neutral-100);
|
|
border-radius: var(--wa-radius-m);
|
|
font-family: var(--wa-font-mono);
|
|
font-size: var(--wa-font-size-s);
|
|
}
|
|
.api-key-display span {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.webhook-item {
|
|
padding: var(--wa-space-m);
|
|
background: var(--wa-color-surface-alt);
|
|
border-radius: var(--wa-radius-m);
|
|
}
|
|
.notification-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--wa-space-m) 0;
|
|
border-bottom: 1px solid var(--wa-color-neutral-100);
|
|
}
|
|
.notification-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.oauth-client-card {
|
|
padding: var(--wa-space-m);
|
|
background: var(--wa-color-surface-alt);
|
|
border-radius: var(--wa-radius-m);
|
|
}
|
|
</style>
|
|
}
|
|
|
|
templ settingsScripts() {
|
|
<script>
|
|
(function() {
|
|
const addEmailDialog = document.getElementById('add-email-dialog');
|
|
const addPhoneDialog = document.getElementById('add-phone-dialog');
|
|
const addDeviceDialog = document.getElementById('add-device-dialog');
|
|
const generateKeyDialog = document.getElementById('generate-key-dialog');
|
|
const addWebhookDialog = document.getElementById('add-webhook-dialog');
|
|
|
|
document.getElementById('add-email-btn')?.addEventListener('click', () => {
|
|
addEmailDialog.open = true;
|
|
});
|
|
|
|
document.getElementById('add-phone-btn')?.addEventListener('click', () => {
|
|
addPhoneDialog.open = true;
|
|
});
|
|
|
|
document.getElementById('add-device-btn')?.addEventListener('click', () => {
|
|
addDeviceDialog.open = true;
|
|
});
|
|
|
|
document.getElementById('generate-key-btn')?.addEventListener('click', () => {
|
|
generateKeyDialog.open = true;
|
|
});
|
|
|
|
document.getElementById('add-webhook-btn')?.addEventListener('click', () => {
|
|
addWebhookDialog.open = true;
|
|
});
|
|
|
|
document.querySelectorAll('[data-dialog="close"]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
btn.closest('wa-dialog').open = false;
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
}
|