---
import Layout from '../layouts/Layout.astro';
export interface Props {
title?: string;
}
const { title = "Profile" } = Astro.props;
---
<Layout title={title} requireAuth={true}>
<!-- Profile Page Content -->
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<!-- Page header -->
<div class="md:flex md:items-center md:justify-between md:space-x-5">
<div class="flex items-start space-x-5">
<div class="flex-shrink-0">
<div class="relative">
<div class="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center"
x-show="!$store.auth.user?.avatar_url">
<span class="text-xl font-semibold text-primary-600" x-text="$store.auth.user?.first_name?.[0] || 'U'"></span>
</div>
<img x-show="$store.auth.user?.avatar_url"
:src="$store.auth.user?.avatar_url"
:alt="$store.auth.user?.full_name"
class="w-16 h-16 rounded-full object-cover">
</div>
</div>
<div class="pt-1.5">
<h1 class="text-2xl font-bold text-gray-900" x-text="$store.auth.user?.full_name"></h1>
<p class="text-sm font-medium text-gray-500" x-text="$store.auth.user?.email"></p>
</div>
</div>
</div>
<!-- Profile Form -->
<div class="mt-8">
<form x-data="{
firstName: $store.auth.user?.first_name || '',
lastName: $store.auth.user?.last_name || '',
displayName: $store.auth.user?.display_name || '',
email: $store.auth.user?.email || '',
phone: $store.auth.user?.phone || '',
timezone: $store.auth.user?.timezone || 'UTC',
language: $store.auth.user?.language || 'en',
saving: false,
async saveProfile() {
this.saving = true;
try {
const response = await fetch('/api/auth/profile', {
method: 'PUT',
headers: $store.auth.getAuthHeaders(),
body: JSON.stringify({
first_name: this.firstName,
last_name: this.lastName,
display_name: this.displayName,
phone: this.phone,
timezone: this.timezone,
language: this.language
})
});
if (response.ok) {
const updatedUser = await response.json();
$store.auth.user = updatedUser;
// Show success notification
if (window.Alpine && Alpine.store('notification')) {
Alpine.store('notification').show('success', 'Profile updated successfully');
}
} else {
const errorData = await response.json();
if (response.status === 401) {
alert('Authentication expired. Please log in again.');
$store.auth.logout();
return;
}
console.error('Profile update failed:', errorData);
if (window.Alpine && Alpine.store('notification')) {
Alpine.store('notification').show('error', errorData.detail || 'Failed to update profile');
}
}
} catch (error) {
console.error('Profile update error:', error);
if (window.Alpine && Alpine.store('notification')) {
Alpine.store('notification').show('error', 'Network error occurred');
}
} finally {
this.saving = false;
}
}
}"
@submit.prevent="saveProfile()"
class="space-y-6 bg-white shadow px-4 py-6 sm:p-6 sm:rounded-lg">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">Personal Information</h3>
<p class="mt-1 text-sm text-gray-500">Update your personal details and preferences.</p>
</div>
<div class="grid grid-cols-6 gap-6">
<!-- First Name -->
<div class="col-span-6 sm:col-span-3">
<label for="first-name" class="block text-sm font-medium text-gray-700">First name</label>
<input type="text"
name="first-name"
id="first-name"
x-model="firstName"
class="mt-1 form-input">
</div>
<!-- Last Name -->
<div class="col-span-6 sm:col-span-3">
<label for="last-name" class="block text-sm font-medium text-gray-700">Last name</label>
<input type="text"
name="last-name"
id="last-name"
x-model="lastName"
class="mt-1 form-input">
</div>
<!-- Display Name -->
<div class="col-span-6">
<label for="display-name" class="block text-sm font-medium text-gray-700">Display name</label>
<input type="text"
name="display-name"
id="display-name"
x-model="displayName"
placeholder="Optional - how others see your name"
class="mt-1 form-input">
</div>
<!-- Email -->
<div class="col-span-6 sm:col-span-4">
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
<input type="email"
name="email"
id="email"
x-model="email"
disabled
class="mt-1 form-input bg-gray-50 cursor-not-allowed">
<p class="mt-1 text-xs text-gray-500">Email cannot be changed. Contact support if needed.</p>
</div>
<!-- Phone -->
<div class="col-span-6 sm:col-span-3">
<label for="phone" class="block text-sm font-medium text-gray-700">Phone number</label>
<input type="tel"
name="phone"
id="phone"
x-model="phone"
placeholder="+1 (555) 123-4567"
class="mt-1 form-input">
</div>
<!-- Timezone -->
<div class="col-span-6 sm:col-span-3">
<label for="timezone" class="block text-sm font-medium text-gray-700">Timezone</label>
<select name="timezone"
id="timezone"
x-model="timezone"
class="mt-1 form-select">
<option value="UTC">UTC</option>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="Europe/London">London</option>
<option value="Europe/Paris">Paris</option>
<option value="Asia/Tokyo">Tokyo</option>
</select>
</div>
<!-- Language -->
<div class="col-span-6 sm:col-span-3">
<label for="language" class="block text-sm font-medium text-gray-700">Language</label>
<select name="language"
id="language"
x-model="language"
class="mt-1 form-select">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="ja">Japanese</option>
</select>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end">
<button type="submit"
:disabled="saving"
class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">
<span x-show="!saving">Save Changes</span>
<span x-show="saving">Saving...</span>
</button>
</div>
</form>
</div>
<!-- Connected Accounts Section -->
<div class="mt-8 bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">Connected Accounts</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>Manage your connected OAuth accounts and PassKeys.</p>
</div>
<div class="mt-5">
<div x-data="{
connectedAccounts: [],
passkeys: [],
loading: false,
async init() {
await this.loadConnectedAccounts();
},
async loadConnectedAccounts() {
this.loading = true;
try {
const [accountsResponse, passkeysResponse] = await Promise.all([
fetch('/api/auth/connected-accounts', {
headers: $store.auth.getAuthHeaders()
}),
fetch('/api/auth/passkeys', {
headers: $store.auth.getAuthHeaders()
})
]);
if (accountsResponse.ok) {
const accountsData = await accountsResponse.json();
this.connectedAccounts = accountsData.connected_accounts || [];
}
if (passkeysResponse.ok) {
const passkeysData = await passkeysResponse.json();
this.passkeys = passkeysData.passkeys || [];
}
} catch (error) {
console.error('Failed to load connected accounts:', error);
} finally {
this.loading = false;
}
}
}"
x-init="init()">
<!-- Connected Accounts List -->
<div x-show="connectedAccounts.length > 0" class="mb-6">
<h4 class="text-sm font-medium text-gray-900 mb-3">OAuth Accounts</h4>
<div class="space-y-2">
<template x-for="account in connectedAccounts" :key="account.id">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<span x-text="account.provider_type.charAt(0).toUpperCase() + account.provider_type.slice(1)"
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"></span>
</div>
<div>
<p class="text-sm font-medium text-gray-900" x-text="account.display_name || account.username"></p>
<p class="text-xs text-gray-500">Connected on <span x-text="new Date(account.created_at).toLocaleDateString()"></span></p>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- PassKeys List -->
<div x-show="passkeys.length > 0" class="mb-6">
<h4 class="text-sm font-medium text-gray-900 mb-3">PassKeys</h4>
<div class="space-y-2">
<template x-for="passkey in passkeys" :key="passkey.id">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="15 7a2 2 0 012 2m0 0a2 2 0 01-2 2m2-2h.01M9 12h6m-6 4h6M7 7V3a1 1 0 011-1h8a1 1 0 011 1v4M7 7H3a1 1 0 00-1 1v10a1 1 0 001 1h18a1 1 0 001-1V8a1 1 0 00-1-1h-4"/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-900" x-text="passkey.name || 'PassKey'"></p>
<p class="text-xs text-gray-500">
<span x-text="passkey.authenticator_type"></span> •
Created <span x-text="new Date(passkey.created_at).toLocaleDateString()"></span>
</p>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- Empty State -->
<div x-show="!loading && connectedAccounts.length === 0 && passkeys.length === 0" class="text-center py-6">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No connected accounts</h3>
<p class="mt-1 text-sm text-gray-500">Add OAuth accounts or PassKeys for enhanced security.</p>
</div>
<!-- Loading State -->
<div x-show="loading" class="text-center py-6">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-sm text-gray-500">Loading connected accounts...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>