UI redesign vol1
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"app_name": "Root",
|
"app_name": "Root",
|
||||||
|
"nav_home": "Home",
|
||||||
"nav_files": "Files",
|
"nav_files": "Files",
|
||||||
|
"nav_storage": "Storage",
|
||||||
"nav_calendar": "Calendar",
|
"nav_calendar": "Calendar",
|
||||||
"nav_settings": "Settings",
|
"nav_settings": "Settings",
|
||||||
"nav_kanban": "Kanban",
|
"nav_kanban": "Kanban",
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"org_selector_slug_placeholder": "my-team",
|
"org_selector_slug_placeholder": "my-team",
|
||||||
"org_overview": "Organization Overview",
|
"org_overview": "Organization Overview",
|
||||||
"files_title": "Files",
|
"files_title": "Files",
|
||||||
|
"files_subtitle": "Organization files are stored here.",
|
||||||
"files_breadcrumb_home": "Home",
|
"files_breadcrumb_home": "Home",
|
||||||
"files_create_title": "Create New",
|
"files_create_title": "Create New",
|
||||||
"files_type_document": "Document",
|
"files_type_document": "Document",
|
||||||
@@ -82,6 +85,7 @@
|
|||||||
"kanban_go_to_files": "Go to Files",
|
"kanban_go_to_files": "Go to Files",
|
||||||
"kanban_select_board": "Select a board above",
|
"kanban_select_board": "Select a board above",
|
||||||
"calendar_title": "Calendar",
|
"calendar_title": "Calendar",
|
||||||
|
"calendar_subtitle": "Keep track of organization events here.",
|
||||||
"calendar_subscribe": "Subscribe to Calendar",
|
"calendar_subscribe": "Subscribe to Calendar",
|
||||||
"calendar_refresh": "Refresh Events",
|
"calendar_refresh": "Refresh Events",
|
||||||
"calendar_settings": "Calendar Settings",
|
"calendar_settings": "Calendar Settings",
|
||||||
@@ -1035,5 +1039,21 @@
|
|||||||
"settings_transfer_ownership": "Transfer ownership",
|
"settings_transfer_ownership": "Transfer ownership",
|
||||||
"settings_transfer_confirm": "Transfer ownership to {name}? You will be demoted to admin. This action is immediate.",
|
"settings_transfer_confirm": "Transfer ownership to {name}? You will be demoted to admin. This action is immediate.",
|
||||||
"toast_error_transfer_ownership": "Failed to transfer ownership",
|
"toast_error_transfer_ownership": "Failed to transfer ownership",
|
||||||
"toast_success_transfer_ownership": "Ownership transferred to {name}"
|
"toast_success_transfer_ownership": "Ownership transferred to {name}",
|
||||||
|
"home_nav_organizations": "Organizations",
|
||||||
|
"home_title": "Your Organizations",
|
||||||
|
"home_subtitle": "Select an Organization to get started.",
|
||||||
|
"home_empty_title": "No organizations yet",
|
||||||
|
"home_empty_desc": "Create your first organization to start collaborating",
|
||||||
|
"home_pending_invitations": "Pending Invitations",
|
||||||
|
"home_invite_click_accept": "Click to accept",
|
||||||
|
"home_invite_joining": "Joining...",
|
||||||
|
"home_create_org_title": "Create Organization",
|
||||||
|
"home_create_org_name_label": "Organization Name",
|
||||||
|
"home_create_org_name_placeholder": "e.g. Acme Inc",
|
||||||
|
"home_create_org_url_preview": "URL: /{slug}",
|
||||||
|
"account_settings_title": "Your Settings",
|
||||||
|
"account_settings_subtitle": "Manage your settings here.",
|
||||||
|
"user_settings_title": "User Settings",
|
||||||
|
"user_settings_subtitle": "Manage your personal account settings."
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"app_name": "Root",
|
"app_name": "Root",
|
||||||
|
"nav_home": "Avaleht",
|
||||||
"nav_files": "Failid",
|
"nav_files": "Failid",
|
||||||
|
"nav_storage": "Hoidla",
|
||||||
"nav_calendar": "Kalender",
|
"nav_calendar": "Kalender",
|
||||||
"nav_settings": "Seaded",
|
"nav_settings": "Seaded",
|
||||||
"nav_kanban": "Kanban",
|
"nav_kanban": "Kanban",
|
||||||
@@ -42,6 +44,7 @@
|
|||||||
"org_selector_slug_placeholder": "minu-meeskond",
|
"org_selector_slug_placeholder": "minu-meeskond",
|
||||||
"org_overview": "Organisatsiooni ülevaade",
|
"org_overview": "Organisatsiooni ülevaade",
|
||||||
"files_title": "Failid",
|
"files_title": "Failid",
|
||||||
|
"files_subtitle": "Organisatsiooni failid on siin.",
|
||||||
"files_breadcrumb_home": "Avaleht",
|
"files_breadcrumb_home": "Avaleht",
|
||||||
"files_create_title": "Loo uus",
|
"files_create_title": "Loo uus",
|
||||||
"files_type_document": "Dokument",
|
"files_type_document": "Dokument",
|
||||||
@@ -81,6 +84,7 @@
|
|||||||
"kanban_go_to_files": "Mine Failidesse",
|
"kanban_go_to_files": "Mine Failidesse",
|
||||||
"kanban_select_board": "Vali tahvel ülalt",
|
"kanban_select_board": "Vali tahvel ülalt",
|
||||||
"calendar_title": "Kalender",
|
"calendar_title": "Kalender",
|
||||||
|
"calendar_subtitle": "Jälgi organisatsiooni sündmusi siin.",
|
||||||
"calendar_subscribe": "Telli kalender",
|
"calendar_subscribe": "Telli kalender",
|
||||||
"calendar_refresh": "Värskenda sündmusi",
|
"calendar_refresh": "Värskenda sündmusi",
|
||||||
"calendar_settings": "Kalendri seaded",
|
"calendar_settings": "Kalendri seaded",
|
||||||
@@ -1035,5 +1039,21 @@
|
|||||||
"settings_transfer_ownership": "Kanna omanikõigused üle",
|
"settings_transfer_ownership": "Kanna omanikõigused üle",
|
||||||
"settings_transfer_confirm": "Kanna omanikõigused üle kasutajale {name}? Sind alandatakse adminiks. See toiming on kohene.",
|
"settings_transfer_confirm": "Kanna omanikõigused üle kasutajale {name}? Sind alandatakse adminiks. See toiming on kohene.",
|
||||||
"toast_error_transfer_ownership": "Omanikõiguste ülekandmine ebaõnnestus",
|
"toast_error_transfer_ownership": "Omanikõiguste ülekandmine ebaõnnestus",
|
||||||
"toast_success_transfer_ownership": "Omanikõigused kanti üle kasutajale {name}"
|
"toast_success_transfer_ownership": "Omanikõigused kanti üle kasutajale {name}",
|
||||||
|
"home_nav_organizations": "Organisatsioonid",
|
||||||
|
"home_title": "Sinu organisatsioonid",
|
||||||
|
"home_subtitle": "Vali organisatsioon alustamiseks.",
|
||||||
|
"home_empty_title": "Organisatsioone pole veel",
|
||||||
|
"home_empty_desc": "Loo oma esimene organisatsioon koostöö alustamiseks",
|
||||||
|
"home_pending_invitations": "Ootel kutsed",
|
||||||
|
"home_invite_click_accept": "Kliki nõustumiseks",
|
||||||
|
"home_invite_joining": "Liitun...",
|
||||||
|
"home_create_org_title": "Loo organisatsioon",
|
||||||
|
"home_create_org_name_label": "Organisatsiooni nimi",
|
||||||
|
"home_create_org_name_placeholder": "nt. Acme OÜ",
|
||||||
|
"home_create_org_url_preview": "URL: /{slug}",
|
||||||
|
"account_settings_title": "Sinu seaded",
|
||||||
|
"account_settings_subtitle": "Halda oma seadeid siin.",
|
||||||
|
"user_settings_title": "Kasutaja seaded",
|
||||||
|
"user_settings_subtitle": "Halda oma isiklikke konto seadeid."
|
||||||
}
|
}
|
||||||
@@ -141,10 +141,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDocColor(doc: Document): string {
|
function getDocColor(doc: Document): string {
|
||||||
if (doc.type === "folder") return "text-amber-400";
|
return "text-white";
|
||||||
if (doc.type === "kanban") return "text-purple-400";
|
|
||||||
if (doc.type === "file") return "text-pink-400";
|
|
||||||
return "text-light/50";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemClick(doc: Document) {
|
function handleItemClick(doc: Document) {
|
||||||
@@ -638,29 +635,27 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Toolbar: Breadcrumbs + Actions -->
|
<!-- Breadcrumb Bar -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-6 py-3 border-b border-light/5 shrink-0"
|
class="flex flex-wrap items-center justify-between gap-y-2 w-full shrink-0"
|
||||||
>
|
>
|
||||||
<!-- Breadcrumb Path -->
|
<!-- Breadcrumb Path -->
|
||||||
<nav class="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto">
|
<nav class="flex items-center flex-1 min-w-0 overflow-x-auto">
|
||||||
{#each breadcrumbPath as crumb, i}
|
{#each breadcrumbPath as crumb, i}
|
||||||
{#if i > 0}
|
{#if i > 0}
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/20 shrink-0"
|
class="material-symbols-rounded text-white"
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>chevron_forward</span
|
||||||
>
|
>
|
||||||
chevron_right
|
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<a
|
||||||
href={getFolderUrl(crumb.id)}
|
href={getFolderUrl(crumb.id)}
|
||||||
class="px-2 py-1 rounded-lg text-body-sm font-body whitespace-nowrap transition-colors
|
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity
|
||||||
{crumb.id === currentFolderId
|
|
||||||
? 'text-white bg-dark/30'
|
|
||||||
: 'text-light/50 hover:text-white hover:bg-dark/30'}
|
|
||||||
{dragOverBreadcrumb === (crumb.id ?? '__root__')
|
{dragOverBreadcrumb === (crumb.id ?? '__root__')
|
||||||
? 'ring-2 ring-primary bg-primary/10'
|
? 'ring-2 ring-primary bg-primary/10 rounded-lg'
|
||||||
: ''}"
|
: ''}"
|
||||||
ondragover={(e) => {
|
ondragover={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -689,54 +684,50 @@
|
|||||||
resetDragState();
|
resetDragState();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if i === 0}
|
<span class="flex items-center justify-center p-1">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded text-white"
|
||||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>home</span
|
>{i === 0 ? "cloud" : "folder"}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h3 text-white whitespace-nowrap"
|
||||||
|
>{crumb.name}</span
|
||||||
>
|
>
|
||||||
{:else}
|
|
||||||
{crumb.name}
|
|
||||||
{/if}
|
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
{#if fileUploading}
|
{#if fileUploading}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 px-3 py-1.5 bg-primary/5 rounded-lg"
|
class="flex items-center gap-2 px-3 py-1.5 bg-primary/10 rounded-[32px]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-primary animate-spin"
|
class="material-symbols-rounded text-primary animate-spin"
|
||||||
style="font-size: 14px;">progress_activity</span
|
style="font-size: 14px;">progress_activity</span
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text-[11px] text-primary truncate max-w-[200px]"
|
class="text-body-sm text-primary truncate max-w-[200px]"
|
||||||
>{fileUploadProgress}</span
|
>{fileUploadProgress}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
icon="upload"
|
|
||||||
onclick={() => fileUploadInput?.click()}>Upload</Button
|
|
||||||
>
|
|
||||||
<Button size="sm" icon="add" onclick={handleAdd}
|
|
||||||
>{m.btn_new()}</Button
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="p-1.5 rounded-lg text-light/40 hover:text-white hover:bg-dark/50 transition-colors"
|
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
|
||||||
title={m.files_toggle_view()}
|
title={m.files_toggle_view()}
|
||||||
onclick={toggleViewMode}
|
onclick={toggleViewMode}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded text-white"
|
||||||
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>{viewMode === "list" ? "grid_view" : "view_list"}</span
|
>{viewMode === "list" ? "grid_view" : "list"}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- File List/Grid -->
|
<!-- File List/Grid -->
|
||||||
<div class="flex-1 overflow-auto min-h-0 p-4">
|
<div class="flex-1 overflow-auto min-h-0 p-4">
|
||||||
@@ -819,28 +810,32 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<!-- Grid View -->
|
<!-- Grid View -->
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-2"
|
class="flex flex-wrap gap-8 items-start content-start"
|
||||||
ondragover={handleContainerDragOver}
|
ondragover={handleContainerDragOver}
|
||||||
ondrop={handleDropOnEmpty}
|
ondrop={handleDropOnEmpty}
|
||||||
role="list"
|
role="list"
|
||||||
>
|
>
|
||||||
{#if currentFolderItems.length === 0}
|
{#if currentFolderItems.length === 0}
|
||||||
<div
|
<div
|
||||||
class="col-span-full flex flex-col items-center justify-center text-light/40 py-16"
|
class="w-full flex flex-col items-center justify-center text-white/30 py-16"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded mb-3"
|
class="material-symbols-rounded mb-3"
|
||||||
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||||
>folder_open</span
|
>folder_open</span
|
||||||
>
|
>
|
||||||
<p class="text-body-sm">{m.files_empty()}</p>
|
<p class="font-body text-body-sm">
|
||||||
|
{m.files_empty()}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each currentFolderItems as item}
|
{#each currentFolderItems as item}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex flex-col items-center gap-2 p-3 rounded-xl border border-transparent transition-all hover:bg-dark/50 hover:border-light/5
|
class="flex flex-col gap-2 items-center justify-center w-[240px] min-w-[240px] h-[155px] min-h-[155px] p-4 rounded-[32px] overflow-clip transition-all cursor-pointer
|
||||||
{selectedDoc?.id === item.id ? 'bg-dark/50 border-primary/20' : ''}
|
{selectedDoc?.id === item.id
|
||||||
|
? 'bg-surface ring-2 ring-primary/50'
|
||||||
|
: 'hover:bg-surface/50'}
|
||||||
{draggedItem?.id === item.id ? 'opacity-50' : ''}
|
{draggedItem?.id === item.id ? 'opacity-50' : ''}
|
||||||
{dragOverFolder === item.id ? 'ring-2 ring-primary bg-primary/10' : ''}"
|
{dragOverFolder === item.id ? 'ring-2 ring-primary bg-primary/10' : ''}"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@@ -856,27 +851,20 @@
|
|||||||
handleContextMenu(e, item)}
|
handleContextMenu(e, item)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded {getDocColor(
|
class="flex items-center justify-center p-3"
|
||||||
item,
|
>
|
||||||
)}"
|
<span
|
||||||
style="font-size: 40px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 40;"
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 72px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||||
>
|
>
|
||||||
{getDocIcon(item)}
|
{getDocIcon(item)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
</span>
|
||||||
class="font-body text-[12px] text-white text-center truncate w-full"
|
<p
|
||||||
>{item.name}</span
|
class="font-body text-body text-white text-center truncate w-full min-w-full"
|
||||||
>
|
>
|
||||||
{#if item.type === "file"}
|
{item.name}
|
||||||
{@const fileMeta = getFileMetadata(item)}
|
</p>
|
||||||
{#if fileMeta}
|
|
||||||
<span class="text-[10px] text-light/25"
|
|
||||||
>{formatFileSize(
|
|
||||||
fileMeta.file_size,
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -961,7 +949,7 @@
|
|||||||
: m.files_doc_placeholder()}
|
: m.files_doc_placeholder()}
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<Button variant="tertiary" onclick={() => (showCreateModal = false)}
|
<Button variant="ghost" onclick={() => (showCreateModal = false)}
|
||||||
>{m.btn_cancel()}</Button
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
<Button onclick={handleCreate} disabled={!newDocName.trim()}
|
<Button onclick={handleCreate} disabled={!newDocName.trim()}
|
||||||
@@ -1092,7 +1080,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
showEditModal = false;
|
showEditModal = false;
|
||||||
editingDoc = null;
|
editingDoc = null;
|
||||||
|
|||||||
@@ -924,7 +924,7 @@
|
|||||||
<div></div>
|
<div></div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button variant="tertiary" onclick={onClose}
|
<Button variant="ghost" onclick={onClose}
|
||||||
>{m.btn_cancel()}</Button
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -167,7 +167,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-2 overflow-x-auto pb-4 h-full kanban-scroll"
|
class="flex gap-4 overflow-x-auto pb-4 h-full kanban-scroll"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
{#each columns as column, colIndex (column.id)}
|
{#each columns as column, colIndex (column.id)}
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
<!-- Add Card Button -->
|
<!-- Add Card Button -->
|
||||||
{#if canEdit}
|
{#if canEdit}
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
icon="add"
|
icon="add"
|
||||||
onclick={() => onAddCard?.(column.id)}
|
onclick={() => onAddCard?.(column.id)}
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
{#if canEdit}
|
{#if canEdit}
|
||||||
<div class="flex-shrink-0 w-[256px]">
|
<div class="flex-shrink-0 w-[256px]">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
fullWidth
|
fullWidth
|
||||||
icon="add"
|
icon="add"
|
||||||
onclick={() => onAddColumn?.()}
|
onclick={() => onAddColumn?.()}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-night/80 border border-light/5 hover:border-light/10 rounded-xl px-3 py-2.5 cursor-pointer transition-all group w-full text-left flex flex-col gap-1.5 relative"
|
class="bg-surface rounded-[16px] p-4 cursor-pointer transition-all group w-full text-left flex flex-col gap-2 relative hover:ring-1 hover:ring-white/10"
|
||||||
class:opacity-50={isDragging}
|
class:opacity-50={isDragging}
|
||||||
data-card-id={card.id}
|
data-card-id={card.id}
|
||||||
{draggable}
|
{draggable}
|
||||||
@@ -71,13 +71,13 @@
|
|||||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute top-1.5 right-1.5 p-0.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-error/10 transition-all z-10"
|
class="absolute top-2 right-2 p-0.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-error/10 transition-all z-10"
|
||||||
onclick={handleDelete}
|
onclick={handleDelete}
|
||||||
aria-label="Delete card"
|
aria-label="Delete card"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/30 hover:text-error"
|
class="material-symbols-rounded text-white/30 hover:text-error"
|
||||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||||
>
|
>
|
||||||
close
|
close
|
||||||
</span>
|
</span>
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
|
|
||||||
<!-- Tags / Chips -->
|
<!-- Tags / Chips -->
|
||||||
{#if card.tags && card.tags.length > 0}
|
{#if card.tags && card.tags.length > 0}
|
||||||
<div class="flex gap-1 items-start flex-wrap">
|
<div class="flex gap-1.5 items-start flex-wrap">
|
||||||
{#each card.tags as tag}
|
{#each card.tags as tag}
|
||||||
<span
|
<span
|
||||||
class="rounded-[4px] px-1.5 py-0.5 font-body font-bold text-[11px] text-night leading-none"
|
class="rounded-[32px] px-2.5 py-1 font-body font-bold text-body-sm text-night leading-none"
|
||||||
style="background-color: {tag.color || '#00A3E0'}"
|
style="background-color: {tag.color || '#00A3E0'}"
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
@@ -99,19 +99,19 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<p class="font-body text-body-sm text-white w-full leading-snug">
|
<p class="font-body text-body text-white w-full leading-snug">
|
||||||
{card.title}
|
{card.title}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Bottom row: details + avatar -->
|
<!-- Bottom row: details + avatar -->
|
||||||
{#if hasFooter}
|
{#if hasFooter}
|
||||||
<div class="flex items-center justify-between w-full mt-0.5">
|
<div class="flex items-center justify-between w-full mt-1">
|
||||||
<div class="flex gap-2 items-center text-[11px] text-light/40">
|
<div class="flex gap-3 items-center text-body-sm text-white/40">
|
||||||
{#if card.due_date}
|
{#if card.due_date}
|
||||||
<span class="flex items-center gap-0.5">
|
<span class="flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||||
>calendar_today</span
|
>calendar_today</span
|
||||||
>
|
>
|
||||||
{formatDueDate(card.due_date)}
|
{formatDueDate(card.due_date)}
|
||||||
@@ -119,10 +119,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (card.checklist_total ?? 0) > 0}
|
{#if (card.checklist_total ?? 0) > 0}
|
||||||
<span class="flex items-center gap-0.5">
|
<span class="flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
|
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
||||||
>check_box</span
|
>check_box</span
|
||||||
>
|
>
|
||||||
{card.checklist_done ?? 0}/{card.checklist_total}
|
{card.checklist_done ?? 0}/{card.checklist_total}
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
<Avatar
|
<Avatar
|
||||||
name={card.assignee_name || "?"}
|
name={card.assignee_name || "?"}
|
||||||
src={card.assignee_avatar}
|
src={card.assignee_avatar}
|
||||||
size="xs"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -429,7 +429,7 @@
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if hasMore}
|
{:else if hasMore}
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon="expand_more"
|
icon="expand_more"
|
||||||
onclick={() => loadEntries(false)}
|
onclick={() => loadEntries(false)}
|
||||||
|
|||||||
@@ -395,7 +395,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{#if avatarUrl}
|
{#if avatarUrl}
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={removeAvatar}
|
onclick={removeAvatar}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -298,7 +298,7 @@
|
|||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
onclick={copyServiceEmail}
|
onclick={copyServiceEmail}
|
||||||
>
|
>
|
||||||
{emailCopied ? "Copied!" : "Copy"}
|
{emailCopied ? "Copied!" : "Copy"}
|
||||||
@@ -330,9 +330,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<Button
|
<Button variant="ghost" onclick={() => (showConnectModal = false)}
|
||||||
variant="tertiary"
|
>Cancel</Button
|
||||||
onclick={() => (showConnectModal = false)}>Cancel</Button
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onclick={handleSaveOrgCalendar}
|
onclick={handleSaveOrgCalendar}
|
||||||
|
|||||||
63
src/lib/components/ui/Breadcrumb.svelte
Normal file
63
src/lib/components/ui/Breadcrumb.svelte
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: BreadcrumbItem[];
|
||||||
|
onToggleView?: () => void;
|
||||||
|
viewMode?: 'grid' | 'list';
|
||||||
|
}
|
||||||
|
|
||||||
|
let { items, onToggleView, viewMode = 'grid' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-y-2 w-full">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{#each items as item, i}
|
||||||
|
{#if i > 0}
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>chevron_forward</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if item.href}
|
||||||
|
<a href={item.href} class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity">
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>{item.icon}</span>
|
||||||
|
</span>
|
||||||
|
<span class="font-heading text-h3 text-white whitespace-nowrap">{item.label}</span>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>{item.icon}</span>
|
||||||
|
</span>
|
||||||
|
<span class="font-heading text-h3 text-white whitespace-nowrap">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if onToggleView}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
|
||||||
|
onclick={onToggleView}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>{viewMode === 'grid' ? 'list' : 'grid_view'}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -2,7 +2,13 @@
|
|||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variant?: "primary" | "secondary" | "tertiary" | "danger" | "success";
|
variant?:
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "outline"
|
||||||
|
| "danger"
|
||||||
|
| "success"
|
||||||
|
| "ghost";
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@@ -28,36 +34,26 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
"inline-flex items-center justify-center gap-2 font-heading rounded-[32px] overflow-clip transition-all cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed";
|
"inline-flex items-center justify-center gap-2 font-bold font-body rounded-[32px] overflow-clip transition-all cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed";
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary:
|
primary:
|
||||||
"btn-primary bg-primary text-night hover:btn-primary-hover active:btn-primary-active",
|
"bg-primary text-night hover:brightness-110 active:brightness-90",
|
||||||
secondary:
|
secondary: "bg-night text-white hover:bg-surface active:bg-dark",
|
||||||
"bg-transparent text-primary border-solid border-primary hover:bg-primary/10 active:bg-primary/20",
|
outline:
|
||||||
tertiary:
|
"bg-transparent text-primary border-2 border-primary hover:bg-primary/10 active:bg-primary/20",
|
||||||
"bg-primary/10 text-primary hover:bg-primary/20 active:bg-primary/30",
|
danger: "bg-error text-white hover:brightness-110 active:brightness-90",
|
||||||
danger: "btn-primary bg-error text-white hover:btn-primary-hover active:btn-primary-active",
|
|
||||||
success:
|
success:
|
||||||
"btn-primary bg-success text-night hover:btn-primary-hover active:btn-primary-active",
|
"bg-success text-night hover:brightness-110 active:brightness-90",
|
||||||
|
ghost: "bg-transparent text-white hover:bg-white/5 active:bg-white/10",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: "min-w-[36px] p-[10px] text-btn-sm",
|
sm: "min-w-[36px] px-[12px] py-[8px] text-btn-sm",
|
||||||
md: "min-w-[48px] p-[12px] text-btn-md",
|
md: "min-w-[48px] px-[14px] py-[10px] text-btn-md",
|
||||||
lg: "min-w-[56px] p-[16px] text-btn-lg",
|
lg: "min-w-[56px] px-[16px] py-[12px] text-btn-lg",
|
||||||
};
|
};
|
||||||
|
|
||||||
const borderClasses = {
|
|
||||||
sm: "border-2",
|
|
||||||
md: "border-3",
|
|
||||||
lg: "border-4",
|
|
||||||
};
|
|
||||||
|
|
||||||
const secondaryBorder = $derived(
|
|
||||||
variant === "secondary" ? borderClasses[size] : "",
|
|
||||||
);
|
|
||||||
|
|
||||||
const iconSize = $derived(size === "sm" ? 16 : size === "lg" ? 20 : 18);
|
const iconSize = $derived(size === "sm" ? 16 : size === "lg" ? 20 : 18);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -65,7 +61,7 @@
|
|||||||
{type}
|
{type}
|
||||||
class="{baseClasses} {variantClasses[variant]} {sizeClasses[
|
class="{baseClasses} {variantClasses[variant]} {sizeClasses[
|
||||||
size
|
size
|
||||||
]} {secondaryBorder} {className ?? ''}"
|
]} {className ?? ''}"
|
||||||
class:w-full={fullWidth}
|
class:w-full={fullWidth}
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
{onclick}
|
{onclick}
|
||||||
@@ -89,30 +85,3 @@
|
|||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
rgba(255, 255, 255, 0.2),
|
|
||||||
rgba(255, 255, 255, 0.2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.btn-primary-hover:not(:disabled) {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
rgba(255, 255, 255, 0.2),
|
|
||||||
rgba(255, 255, 255, 0.2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.btn-primary:active:not(:disabled) {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
rgba(14, 15, 25, 0.2),
|
|
||||||
rgba(14, 15, 25, 0.2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.btn-primary-active:not(:disabled) {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
rgba(14, 15, 25, 0.2),
|
|
||||||
rgba(14, 15, 25, 0.2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -4,35 +4,49 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
actionLabel?: string;
|
actionLabel?: string;
|
||||||
|
actionIcon?: string;
|
||||||
onAction?: () => void;
|
onAction?: () => void;
|
||||||
onMore?: () => void;
|
onMore?: () => void;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { title, actionLabel, onAction, onMore, children }: Props = $props();
|
let {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
actionLabel,
|
||||||
|
actionIcon,
|
||||||
|
onAction,
|
||||||
|
onMore,
|
||||||
|
children,
|
||||||
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 p-1 rounded-[32px] w-full">
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
<h1 class="font-heading text-h1 text-white truncate">{title}</h1>
|
<h2 class="font-heading text-h2 text-white">{title}</h2>
|
||||||
|
{#if subtitle}
|
||||||
|
<p class="font-body text-body text-white/50">{subtitle}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
{#if children}
|
{#if children}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
{#if actionLabel && onAction}
|
{#if actionLabel && onAction}
|
||||||
<Button variant="primary" onclick={onAction}>
|
<Button variant="primary" icon={actionIcon} onclick={onAction}>
|
||||||
{actionLabel}
|
{actionLabel}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if onMore}
|
{#if onMore}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="p-1 flex items-center justify-center hover:bg-dark/50 rounded-full transition-colors"
|
class="flex items-center justify-center p-1 rounded-full hover:bg-white/5 transition-colors"
|
||||||
onclick={onMore}
|
onclick={onMore}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light"
|
class="material-symbols-rounded text-white"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>
|
>
|
||||||
more_horiz
|
more_horiz
|
||||||
@@ -40,3 +54,4 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
value?: string;
|
value?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
description?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
value = $bindable(""),
|
value = $bindable(""),
|
||||||
placeholder = "",
|
placeholder = "",
|
||||||
label,
|
label,
|
||||||
|
description,
|
||||||
error,
|
error,
|
||||||
hint,
|
hint,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -55,16 +57,22 @@
|
|||||||
const inputType = $derived(isPassword && showPassword ? "text" : type);
|
const inputType = $derived(isPassword && showPassword ? "text" : type);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col {isCompact ? 'gap-1.5' : 'gap-3'} w-full">
|
<div class="flex flex-col gap-3 w-full">
|
||||||
{#if label}
|
{#if label}
|
||||||
|
<div class="flex flex-col gap-2 {isCompact ? '' : 'px-3'}">
|
||||||
<label
|
<label
|
||||||
for={inputId}
|
for={inputId}
|
||||||
class={isCompact
|
class={isCompact
|
||||||
? "font-body text-body-sm text-light/60"
|
? "font-body text-body-sm text-light/60"
|
||||||
: "px-3 font-bold font-body text-body text-white"}
|
: "font-bold font-body text-body text-white"}
|
||||||
>
|
>
|
||||||
{#if required}<span class="text-error">* </span>{/if}{label}
|
{#if required}<span class="text-error">* </span
|
||||||
|
>{/if}{label}
|
||||||
</label>
|
</label>
|
||||||
|
{#if description}
|
||||||
|
<p class="text-body-sm text-white/50">{description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center gap-3 w-full">
|
<div class="flex items-center gap-3 w-full">
|
||||||
@@ -97,23 +105,20 @@
|
|||||||
class="
|
class="
|
||||||
w-full {isCompact
|
w-full {isCompact
|
||||||
? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm'
|
? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm'
|
||||||
: 'p-3 bg-background rounded-[32px] font-medium font-input text-body'}
|
: 'px-3 py-2 bg-night rounded-[20px] font-medium font-input text-body'}
|
||||||
text-white placeholder:text-light/30
|
text-white placeholder:text-white/50
|
||||||
focus:outline-none {isCompact
|
focus:outline-none focus:ring-2 focus:ring-primary/50
|
||||||
? 'focus:border-primary'
|
|
||||||
: 'focus:ring-2 focus:ring-primary'}
|
|
||||||
disabled:opacity-30 disabled:cursor-not-allowed
|
disabled:opacity-30 disabled:cursor-not-allowed
|
||||||
transition-colors {className ?? ''}
|
transition-colors {className ?? ''}
|
||||||
"
|
"
|
||||||
class:ring-1={error && !isCompact}
|
class:ring-1={error}
|
||||||
class:ring-error={error && !isCompact}
|
class:ring-error={error}
|
||||||
class:border-error={error && isCompact}
|
|
||||||
class:pr-12={isPassword}
|
class:pr-12={isPassword}
|
||||||
/>
|
/>
|
||||||
{#if isPassword}
|
{#if isPassword}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-white/60 hover:text-white transition-colors"
|
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-full text-white/60 hover:text-white transition-colors"
|
||||||
onclick={() => (showPassword = !showPassword)}
|
onclick={() => (showPassword = !showPassword)}
|
||||||
aria-label={showPassword
|
aria-label={showPassword
|
||||||
? "Hide password"
|
? "Hide password"
|
||||||
@@ -121,7 +126,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>
|
>
|
||||||
{showPassword ? "visibility_off" : "visibility"}
|
{showPassword ? "visibility_off" : "visibility"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -19,10 +19,13 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-light/5">
|
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-light/5">
|
||||||
<div class="flex-1 flex items-center gap-2 min-w-0">
|
<div class="flex-1 flex items-center gap-2 min-w-0">
|
||||||
<span class="font-heading text-body-sm text-white truncate">{title}</span>
|
<span class="font-heading text-body-sm text-white truncate"
|
||||||
|
>{title}</span
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="text-[11px] text-light/40 bg-light/5 px-1.5 py-0.5 rounded-md shrink-0"
|
class="text-[11px] text-light/40 bg-light/5 px-1.5 py-0.5 rounded-md shrink-0"
|
||||||
>{count}</span>
|
>{count}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if onMore}
|
{#if onMore}
|
||||||
<button
|
<button
|
||||||
@@ -41,9 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cards container -->
|
<!-- Cards container -->
|
||||||
<div
|
<div class="flex-1 flex flex-col gap-1.5 p-2 overflow-y-auto min-h-0">
|
||||||
class="flex-1 flex flex-col gap-1.5 p-2 overflow-y-auto min-h-0"
|
|
||||||
>
|
|
||||||
{#if children}
|
{#if children}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -52,7 +53,13 @@
|
|||||||
<!-- Add button -->
|
<!-- Add button -->
|
||||||
{#if onAddCard}
|
{#if onAddCard}
|
||||||
<div class="px-2 pb-2">
|
<div class="px-2 pb-2">
|
||||||
<Button variant="tertiary" fullWidth size="sm" icon="add" onclick={onAddCard}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
fullWidth
|
||||||
|
size="sm"
|
||||||
|
icon="add"
|
||||||
|
onclick={onAddCard}
|
||||||
|
>
|
||||||
Add card
|
Add card
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
src/lib/components/ui/ListCard.svelte
Normal file
55
src/lib/components/ui/ListCard.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
label: string;
|
||||||
|
type: 'folder' | 'document' | 'kanban';
|
||||||
|
href?: string;
|
||||||
|
onclick?: () => void;
|
||||||
|
ondblclick?: () => void;
|
||||||
|
onauxclick?: (e: MouseEvent) => void;
|
||||||
|
selected?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { label, type, href, onclick, ondblclick, onauxclick, selected = false }: Props = $props();
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
folder: 'folder',
|
||||||
|
document: 'description',
|
||||||
|
kanban: 'view_kanban',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
{href}
|
||||||
|
class="flex flex-col gap-2 items-center justify-center w-[240px] min-w-[240px] h-[155px] min-h-[155px] p-4 rounded-[32px] overflow-clip transition-all cursor-pointer
|
||||||
|
{selected ? 'bg-surface ring-2 ring-primary/50' : 'hover:bg-surface/50'}"
|
||||||
|
{onclick}
|
||||||
|
{ondblclick}
|
||||||
|
{onauxclick}
|
||||||
|
>
|
||||||
|
<span class="flex items-center justify-center p-3">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 72px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||||
|
>{iconMap[type]}</span>
|
||||||
|
</span>
|
||||||
|
<p class="font-body text-body text-white text-center truncate w-full min-w-full">{label}</p>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex flex-col gap-2 items-center justify-center w-[240px] min-w-[240px] h-[155px] min-h-[155px] p-4 rounded-[32px] overflow-clip transition-all cursor-pointer
|
||||||
|
{selected ? 'bg-surface ring-2 ring-primary/50' : 'hover:bg-surface/50'}"
|
||||||
|
{onclick}
|
||||||
|
{ondblclick}
|
||||||
|
{onauxclick}
|
||||||
|
>
|
||||||
|
<span class="flex items-center justify-center p-3">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 72px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||||
|
>{iconMap[type]}</span>
|
||||||
|
</span>
|
||||||
|
<p class="font-body text-body text-white text-center truncate w-full min-w-full">{label}</p>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header class="flex items-center justify-between shrink-0 {className}">
|
||||||
class="flex items-center justify-between px-6 py-5 border-b border-light/5 shrink-0 {className}"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<span
|
<span
|
||||||
@@ -31,10 +29,10 @@
|
|||||||
>{icon}</span
|
>{icon}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="min-w-0">
|
<div class="min-w-0 flex flex-col gap-2">
|
||||||
<h1 class="text-h1 font-heading text-white truncate">{title}</h1>
|
<h2 class="text-h2 font-heading text-white truncate">{title}</h2>
|
||||||
{#if subtitle}
|
{#if subtitle}
|
||||||
<p class="text-body-sm text-light/50 mt-0.5">{subtitle}</p>
|
<p class="text-body text-white/50 font-body">{subtitle}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,13 +24,11 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="bg-surface rounded-[32px] {paddingClasses[padding]} {className}">
|
||||||
class="bg-dark/30 border border-light/5 rounded-xl {paddingClasses[padding]} {className}"
|
|
||||||
>
|
|
||||||
{#if title || titleRight}
|
{#if title || titleRight}
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
{#if title}
|
{#if title}
|
||||||
<h2 class="text-body font-heading text-white">{title}</h2>
|
<h3 class="text-h4 font-heading text-white">{title}</h3>
|
||||||
{/if}
|
{/if}
|
||||||
{#if titleRight}
|
{#if titleRight}
|
||||||
{@render titleRight()}
|
{@render titleRight()}
|
||||||
|
|||||||
@@ -66,11 +66,9 @@
|
|||||||
{onchange}
|
{onchange}
|
||||||
class="w-full {isCompact
|
class="w-full {isCompact
|
||||||
? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm'
|
? 'px-3 py-2 bg-dark border border-light/10 rounded-xl text-body-sm'
|
||||||
: 'p-3 bg-background rounded-[32px] font-medium font-input text-body'}
|
: 'px-3 py-2 bg-night rounded-[20px] font-medium font-input text-body'}
|
||||||
text-white
|
text-white
|
||||||
focus:outline-none {isCompact
|
focus:outline-none focus:ring-2 focus:ring-primary/50
|
||||||
? 'focus:border-primary'
|
|
||||||
: 'focus:ring-2 focus:ring-primary'}
|
|
||||||
disabled:opacity-30 disabled:cursor-not-allowed
|
disabled:opacity-30 disabled:cursor-not-allowed
|
||||||
transition-colors appearance-none cursor-pointer {className ?? ''}"
|
transition-colors appearance-none cursor-pointer {className ?? ''}"
|
||||||
class:ring-1={error && !isCompact}
|
class:ring-1={error && !isCompact}
|
||||||
|
|||||||
@@ -21,38 +21,36 @@
|
|||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
class="bg-dark/30 border border-light/5 hover:border-light/10 rounded-xl p-4 flex items-center gap-3 transition-all group"
|
class="bg-surface rounded-[32px] p-5 flex items-center gap-4 transition-all hover:ring-1 hover:ring-white/10 group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 rounded-xl {bg} flex items-center justify-center shrink-0"
|
class="w-12 h-12 rounded-full {bg} flex items-center justify-center shrink-0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded {color}"
|
class="material-symbols-rounded {color}"
|
||||||
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>{icon}</span
|
>{icon}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<p class="text-xl font-bold text-white leading-none">{value}</p>
|
<p class="text-h2 font-heading text-white leading-none">{value}</p>
|
||||||
<p class="text-[12px] text-light/40 mt-0.5">{label}</p>
|
<p class="text-body font-body text-white/50">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="bg-surface rounded-[32px] p-5 flex items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class="bg-dark/30 border border-light/5 rounded-xl p-4 flex items-center gap-3"
|
class="w-12 h-12 rounded-full {bg} flex items-center justify-center shrink-0"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-10 h-10 rounded-xl {bg} flex items-center justify-center shrink-0"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded {color}"
|
class="material-symbols-rounded {color}"
|
||||||
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>{icon}</span
|
>{icon}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<p class="text-xl font-bold text-white leading-none">{value}</p>
|
<p class="text-h2 font-heading text-white leading-none">{value}</p>
|
||||||
<p class="text-[12px] text-light/40 mt-0.5">{label}</p>
|
<p class="text-body font-body text-white/50">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -43,3 +43,5 @@ export { default as Twemoji } from './Twemoji.svelte';
|
|||||||
export { default as EmojiPicker } from './EmojiPicker.svelte';
|
export { default as EmojiPicker } from './EmojiPicker.svelte';
|
||||||
export { default as VirtualList } from './VirtualList.svelte';
|
export { default as VirtualList } from './VirtualList.svelte';
|
||||||
export { default as PersonContactModal } from './PersonContactModal.svelte';
|
export { default as PersonContactModal } from './PersonContactModal.svelte';
|
||||||
|
export { default as Breadcrumb } from './Breadcrumb.svelte';
|
||||||
|
export { default as ListCard } from './ListCard.svelte';
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ ${dump}
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-[80px] font-heading text-primary">{$page.status}</p>
|
<p class="text-[80px] font-heading text-primary">{$page.status}</p>
|
||||||
<h1 class="text-2xl font-heading text-white">
|
<h1 class="text-2xl font-heading text-white">
|
||||||
{$page.status === 404 ? "Page not found" : "Something went wrong"}
|
{$page.status === 404
|
||||||
|
? "Page not found"
|
||||||
|
: "Something went wrong"}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-light/60 text-base">
|
<p class="text-light/60 text-base">
|
||||||
{$page.error?.message || "An unexpected error occurred."}
|
{$page.error?.message || "An unexpected error occurred."}
|
||||||
@@ -54,10 +56,10 @@ ${dump}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center flex-wrap">
|
<div class="flex gap-3 justify-center flex-wrap">
|
||||||
<Button onclick={() => window.location.href = "/"}>
|
<Button onclick={() => (window.location.href = "/")}>
|
||||||
Go Home
|
Go Home
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="tertiary" onclick={() => window.location.reload()}>
|
<Button variant="ghost" onclick={() => window.location.reload()}>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" onclick={handleCopyLogs}>
|
<Button variant="secondary" onclick={handleCopyLogs}>
|
||||||
@@ -75,7 +77,9 @@ ${dump}
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if showLogs}
|
{#if showLogs}
|
||||||
<pre class="mt-4 p-4 bg-dark rounded-[16px] text-left text-xs text-light/70 overflow-auto max-h-[300px] font-mono whitespace-pre-wrap">{logDump || "No recent logs."}</pre>
|
<pre
|
||||||
|
class="mt-4 p-4 bg-dark rounded-[16px] text-left text-xs text-light/70 overflow-auto max-h-[300px] font-mono whitespace-pre-wrap">{logDump ||
|
||||||
|
"No recent logs."}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,8 +41,21 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
org: (inv as Record<string, unknown>).organizations as { id: string; name: string; slug: string } | null,
|
org: (inv as Record<string, unknown>).organizations as { id: string; name: string; slug: string } | null,
|
||||||
})).filter((inv) => inv.org !== null);
|
})).filter((inv) => inv.org !== null);
|
||||||
|
|
||||||
|
// Fetch user profile for sidebar
|
||||||
|
const { data: profile } = await locals.supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('full_name, avatar_url')
|
||||||
|
.eq('id', user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
organizations,
|
organizations,
|
||||||
pendingInvites,
|
pendingInvites,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
fullName: profile?.full_name ?? user.user_metadata?.full_name ?? null,
|
||||||
|
avatarUrl: profile?.avatar_url ?? null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { Modal, Logo, Badge } from "$lib/components/ui";
|
import { page } from "$app/stores";
|
||||||
|
import { Modal, Logo, Avatar, Button, Input } from "$lib/components/ui";
|
||||||
|
import * as m from "$lib/paraglide/messages";
|
||||||
import { createOrganization, generateSlug } from "$lib/api/organizations";
|
import { createOrganization, generateSlug } from "$lib/api/organizations";
|
||||||
import { toasts } from "$lib/stores/toast.svelte";
|
import { toasts } from "$lib/stores/toast.svelte";
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
@@ -27,7 +29,12 @@
|
|||||||
data: {
|
data: {
|
||||||
organizations: OrgWithRole[];
|
organizations: OrgWithRole[];
|
||||||
pendingInvites: PendingInvite[];
|
pendingInvites: PendingInvite[];
|
||||||
user: any;
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string | undefined;
|
||||||
|
fullName: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,250 +129,292 @@
|
|||||||
<title>Organizations | root</title>
|
<title>Organizations | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen bg-background">
|
<div class="h-screen bg-background flex gap-4 items-start p-4">
|
||||||
<!-- Header -->
|
<!-- Sidebar -->
|
||||||
<header class="border-b border-light/5">
|
<aside
|
||||||
<div
|
class="bg-night flex flex-col gap-4 h-full items-start min-w-[256px] overflow-clip px-4 py-5 rounded-[32px] shrink-0 w-[256px]"
|
||||||
class="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2.5">
|
<!-- Logo -->
|
||||||
<Logo size="sm" />
|
<Logo size="sm" />
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
<!-- Nav list -->
|
||||||
{#if pendingInvites.length > 0}
|
<nav class="flex flex-1 flex-col gap-1 items-start w-full min-h-0">
|
||||||
|
{#each [{ href: "/", icon: "apartment", label: m.home_nav_organizations() }, { href: "/settings", icon: "settings", label: m.nav_settings() }] as item}
|
||||||
|
{@const active =
|
||||||
|
item.href === "/"
|
||||||
|
? $page.url.pathname === "/"
|
||||||
|
: $page.url.pathname.startsWith(item.href)}
|
||||||
<a
|
<a
|
||||||
href="#invites"
|
href={item.href}
|
||||||
class="relative p-2 text-light/40 hover:text-white hover:bg-dark/50 rounded-xl transition-colors"
|
class="flex gap-2 items-center w-full overflow-clip pl-1 pr-2 py-1 rounded-[32px] transition-colors {active
|
||||||
title="{pendingInvites.length} pending invite{pendingInvites.length >
|
? 'bg-primary'
|
||||||
1
|
: 'hover:bg-white/5'}"
|
||||||
? 's'
|
|
||||||
: ''}"
|
|
||||||
>
|
>
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded {active
|
||||||
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
|
? 'text-background'
|
||||||
>notifications</span
|
: 'text-white'}"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>{item.icon}</span
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="absolute -top-0.5 -right-0.5 w-5 h-5 bg-primary text-background text-[10px] font-bold rounded-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
{pendingInvites.length}
|
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex-1 font-body text-body truncate {active
|
||||||
|
? 'text-background'
|
||||||
|
: 'text-white'}">{item.label}</span
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Profile footer -->
|
||||||
|
<div class="flex gap-4 items-center w-full">
|
||||||
|
<div class="flex flex-1 gap-4 items-center min-w-0">
|
||||||
|
<Avatar
|
||||||
|
name={data.user.fullName || data.user.email || "User"}
|
||||||
|
src={data.user.avatarUrl}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-1 flex-col items-start min-w-0">
|
||||||
|
<p class="font-heading text-h5 text-white w-full truncate">
|
||||||
|
{data.user.fullName || data.user.email || "User"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form method="POST" action="/auth/logout">
|
<form method="POST" action="/auth/logout">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="px-3 py-1.5 text-body-sm text-light/40 hover:text-white hover:bg-dark/50 rounded-xl transition-colors"
|
class="flex items-center justify-center p-1 rounded-[32px] text-white/60 hover:text-white transition-colors"
|
||||||
>Sign Out</button
|
title="Sign out"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>exit_to_app</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</header>
|
|
||||||
|
<!-- Content -->
|
||||||
|
<main
|
||||||
|
class="bg-night flex flex-1 flex-col gap-8 h-full items-start min-h-0 min-w-0 overflow-auto p-8 rounded-[32px]"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
|
<h2 class="font-heading text-h2 text-white">
|
||||||
|
{m.home_title()}
|
||||||
|
</h2>
|
||||||
|
<p class="font-body text-body text-white/50">
|
||||||
|
{m.home_subtitle()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button icon="add" onclick={() => (showCreateModal = true)}>
|
||||||
|
{m.btn_new()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="max-w-5xl mx-auto px-6 py-8">
|
|
||||||
<!-- Pending Invites -->
|
<!-- Pending Invites -->
|
||||||
{#if pendingInvites.length > 0}
|
{#if pendingInvites.length > 0}
|
||||||
<section id="invites" class="mb-8">
|
<section id="invites" class="w-full shrink-0">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-primary"
|
class="material-symbols-rounded text-primary"
|
||||||
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
|
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
|
||||||
>mail</span
|
>mail</span
|
||||||
>
|
>
|
||||||
<h2 class="font-heading text-body text-white">
|
<h3 class="font-heading text-h3 text-white">
|
||||||
Pending Invitations
|
{m.home_pending_invitations()}
|
||||||
</h2>
|
</h3>
|
||||||
<span
|
<span
|
||||||
class="text-[11px] px-2 py-0.5 bg-primary/10 text-primary rounded-lg font-body"
|
class="text-body-sm px-2 py-0.5 bg-surface text-primary rounded-[8px] font-body"
|
||||||
>
|
>
|
||||||
{pendingInvites.length}
|
{pendingInvites.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex flex-wrap gap-8 items-start">
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"
|
|
||||||
>
|
|
||||||
{#each pendingInvites as invite}
|
{#each pendingInvites as invite}
|
||||||
<div
|
<a
|
||||||
class="bg-primary/5 border border-primary/20 rounded-2xl p-5 transition-all"
|
href="/{invite.org.slug}"
|
||||||
|
class="bg-background flex flex-col items-start overflow-clip rounded-[16px] w-[256px] cursor-pointer hover:ring-2 hover:ring-primary/30 transition-all"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
acceptInvite(invite);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between mb-3">
|
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 bg-primary/10 rounded-xl flex items-center justify-center text-primary font-heading text-body"
|
class="h-[80px] w-full bg-primary/10 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
{invite.org.name.charAt(0).toUpperCase()}
|
<span
|
||||||
|
class="material-symbols-rounded text-primary"
|
||||||
|
style="font-size: 32px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 32;"
|
||||||
|
>mail</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-4 items-start justify-center px-4 py-5 w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-start justify-between w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 rounded-full bg-primary/20 flex items-center justify-center text-primary font-heading text-h5"
|
||||||
|
>
|
||||||
|
{invite.org.name
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="text-[10px] px-2 py-0.5 bg-primary/10 rounded-lg text-primary capitalize font-body"
|
class="bg-surface flex items-center justify-center p-1 rounded-[8px] font-body text-body-md text-white capitalize"
|
||||||
>{invite.role}</span
|
>{invite.role}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h3
|
<div class="flex flex-col gap-1 w-full">
|
||||||
class="font-heading text-body-sm text-white mb-1"
|
<p class="font-heading text-h5 text-white">
|
||||||
>
|
|
||||||
{invite.org.name}
|
{invite.org.name}
|
||||||
</h3>
|
|
||||||
<p class="text-[11px] text-light/30 mb-4 font-body">
|
|
||||||
You've been invited to join this organization
|
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2">
|
<p
|
||||||
<button
|
class="font-body text-body-md text-white/50"
|
||||||
type="button"
|
|
||||||
disabled={acceptingInviteId === invite.id}
|
|
||||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary text-background rounded-xl text-body-sm font-body hover:bg-primary-hover transition-colors disabled:opacity-50"
|
|
||||||
onclick={() => acceptInvite(invite)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="material-symbols-rounded"
|
|
||||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
|
||||||
>{acceptingInviteId === invite.id
|
|
||||||
? "hourglass_empty"
|
|
||||||
: "check"}</span
|
|
||||||
>
|
>
|
||||||
{acceptingInviteId === invite.id
|
{acceptingInviteId === invite.id
|
||||||
? "Joining..."
|
? m.home_invite_joining()
|
||||||
: "Accept"}
|
: m.home_invite_click_accept()}
|
||||||
</button>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
<!-- Org Card Grid -->
|
||||||
<div>
|
<div
|
||||||
<h2 class="font-heading text-h3 text-white">
|
class="flex flex-1 flex-wrap gap-y-8 gap-x-8 items-start w-full content-start"
|
||||||
Your Organizations
|
|
||||||
</h2>
|
|
||||||
<p class="text-body-sm text-light/40 mt-1">
|
|
||||||
Select an organization to get started
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-1.5 px-3 py-2 bg-primary text-background rounded-xl text-body-sm font-body hover:bg-primary-hover transition-colors"
|
|
||||||
onclick={() => (showCreateModal = true)}
|
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="material-symbols-rounded"
|
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
|
||||||
>add</span
|
|
||||||
>
|
|
||||||
New Organization
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if organizations.length === 0}
|
{#if organizations.length === 0}
|
||||||
<div
|
<div
|
||||||
class="bg-dark/30 border border-light/5 rounded-2xl p-12 text-center"
|
class="bg-background rounded-[16px] p-12 text-center w-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-14 h-14 mx-auto mb-4 rounded-2xl bg-light/5 flex items-center justify-center"
|
class="w-14 h-14 mx-auto mb-4 rounded-2xl bg-white/5 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/20"
|
class="material-symbols-rounded text-white/20"
|
||||||
style="font-size: 28px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 28;"
|
style="font-size: 28px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 28;"
|
||||||
>groups</span
|
>groups</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-heading text-body text-white mb-1">
|
<h3 class="font-heading text-h5 text-white mb-1">
|
||||||
No organizations yet
|
{m.home_empty_title()}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-body-sm text-light/40 mb-6">
|
<p class="font-body text-body-md text-white/50 mb-6">
|
||||||
Create your first organization to start collaborating
|
{m.home_empty_desc()}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button onclick={() => (showCreateModal = true)}>
|
||||||
class="px-4 py-2 bg-primary text-background rounded-xl text-body-sm font-body hover:bg-primary-hover transition-colors"
|
{m.org_selector_create()}
|
||||||
onclick={() => (showCreateModal = true)}
|
</Button>
|
||||||
>Create Organization</button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
||||||
{#each organizations as org}
|
{#each organizations as org}
|
||||||
<a href="/{org.slug}" class="block group">
|
<a
|
||||||
<div
|
href="/{org.slug}"
|
||||||
class="bg-dark/30 border border-light/5 hover:border-primary/30 rounded-2xl p-5 transition-all h-full"
|
class="bg-background flex flex-col items-start overflow-clip rounded-[16px] w-[256px] cursor-pointer hover:ring-2 hover:ring-primary/30 transition-all"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between mb-3">
|
<!-- Card image area -->
|
||||||
|
<div class="h-[144px] w-full relative bg-surface">
|
||||||
{#if org.avatar_url}
|
{#if org.avatar_url}
|
||||||
<img
|
<img
|
||||||
src={org.avatar_url}
|
src={org.avatar_url}
|
||||||
alt={org.name}
|
alt={org.name}
|
||||||
class="w-10 h-10 rounded-xl object-cover"
|
class="absolute inset-0 w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 bg-primary/10 rounded-xl flex items-center justify-center text-primary font-heading text-body"
|
class="w-full h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h1 text-white/10"
|
||||||
|
>
|
||||||
|
{org.name.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- Card info -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-4 items-start justify-center px-4 py-5 w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-start justify-between w-full"
|
||||||
|
>
|
||||||
|
{#if org.avatar_url}
|
||||||
|
<img
|
||||||
|
src={org.avatar_url}
|
||||||
|
alt={org.name}
|
||||||
|
class="w-12 h-12 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="w-12 h-12 rounded-full bg-primary flex items-center justify-center text-night font-heading text-h5"
|
||||||
>
|
>
|
||||||
{org.name.charAt(0).toUpperCase()}
|
{org.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="text-[10px] px-2 py-0.5 bg-light/5 rounded-lg text-light/40 capitalize font-body"
|
class="bg-surface flex items-center justify-center p-1 rounded-[8px] font-body text-body-md text-white capitalize"
|
||||||
>{org.role}</span
|
>{org.role}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h3
|
<div class="flex flex-col gap-1 w-full">
|
||||||
class="font-heading text-body-sm text-white group-hover:text-primary transition-colors"
|
<p class="font-heading text-h5 text-white">
|
||||||
>
|
|
||||||
{org.name}
|
{org.name}
|
||||||
</h3>
|
</p>
|
||||||
<p
|
<p class="font-body text-body-md text-white/50">
|
||||||
class="text-[11px] text-light/30 mt-0.5 font-body"
|
|
||||||
>
|
|
||||||
/{org.slug}
|
/{org.slug}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showCreateModal}
|
isOpen={showCreateModal}
|
||||||
onClose={() => (showCreateModal = false)}
|
onClose={() => (showCreateModal = false)}
|
||||||
title="Create Organization"
|
title={m.home_create_org_title()}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="flex flex-col gap-1.5">
|
<Input
|
||||||
<label for="org-name" class="text-body-sm text-light/60 font-body"
|
label={m.home_create_org_name_label()}
|
||||||
>Organization Name</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="org-name"
|
|
||||||
type="text"
|
|
||||||
bind:value={newOrgName}
|
bind:value={newOrgName}
|
||||||
placeholder="e.g. Acme Inc"
|
placeholder={m.home_create_org_name_placeholder()}
|
||||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white placeholder:text-light/30 focus:outline-none focus:border-primary"
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{#if newOrgName}
|
{#if newOrgName}
|
||||||
<p class="text-body-sm text-light/40">
|
<p class="font-body text-body-md text-white/50 px-3">
|
||||||
URL: <span class="text-white font-body"
|
URL: <span class="text-white font-body"
|
||||||
>/{generateSlug(newOrgName)}</span
|
>/{generateSlug(newOrgName)}</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div class="flex items-center justify-end gap-3 pt-2">
|
||||||
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
|
<Button variant="ghost" onclick={() => (showCreateModal = false)}
|
||||||
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
|
||||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
|
||||||
onclick={() => (showCreateModal = false)}>Cancel</button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!newOrgName.trim() || creating}
|
disabled={!newOrgName.trim() || creating}
|
||||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
onclick={handleCreateOrg}
|
onclick={handleCreateOrg}
|
||||||
|
loading={creating}
|
||||||
>
|
>
|
||||||
{creating ? "Creating..." : "Create"}
|
{m.btn_create()}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import { on } from "svelte/events";
|
|
||||||
import { Avatar, Logo } from "$lib/components/ui";
|
import { Avatar, Logo } from "$lib/components/ui";
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from "$lib/supabase/types";
|
import type { Database } from "$lib/supabase/types";
|
||||||
@@ -75,55 +74,25 @@
|
|||||||
hasPermission(data.userRole, data.userPermissions, permission);
|
hasPermission(data.userRole, data.userPermissions, permission);
|
||||||
setContext("canAccess", canAccess);
|
setContext("canAccess", canAccess);
|
||||||
|
|
||||||
// Sidebar is always expanded (collapse removed)
|
|
||||||
const sidebarCollapsed = false;
|
|
||||||
|
|
||||||
// User dropdown
|
|
||||||
let showUserMenu = $state(false);
|
|
||||||
let menuContainerEl = $state<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
// Attach click-outside and Escape listeners only while menu is open.
|
|
||||||
// Uses svelte/events 'on' to respect Svelte 5 event delegation order.
|
|
||||||
$effect(() => {
|
|
||||||
if (!showUserMenu) return;
|
|
||||||
|
|
||||||
// Defer so the opening click doesn't immediately close the menu
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
cleanupClick = on(document, "click", (e: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
menuContainerEl &&
|
|
||||||
!menuContainerEl.contains(e.target as Node)
|
|
||||||
) {
|
|
||||||
showUserMenu = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const cleanupKey = on(document, "keydown", (e: Event) => {
|
|
||||||
if ((e as KeyboardEvent).key === "Escape") showUserMenu = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
let cleanupClick: (() => void) | undefined;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
cleanupClick?.();
|
|
||||||
cleanupKey();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
goto("/");
|
goto("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = $derived([
|
const navItems = $derived([
|
||||||
|
{
|
||||||
|
href: `/${data.org.slug}`,
|
||||||
|
label: m.nav_home(),
|
||||||
|
icon: "home",
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
...(canAccess("documents.view")
|
...(canAccess("documents.view")
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
href: `/${data.org.slug}/documents`,
|
href: `/${data.org.slug}/documents`,
|
||||||
label: m.nav_files(),
|
label: m.nav_storage(),
|
||||||
icon: "cloud",
|
icon: "cloud",
|
||||||
|
exact: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -133,6 +102,7 @@
|
|||||||
href: `/${data.org.slug}/calendar`,
|
href: `/${data.org.slug}/calendar`,
|
||||||
label: m.nav_calendar(),
|
label: m.nav_calendar(),
|
||||||
icon: "calendar_today",
|
icon: "calendar_today",
|
||||||
|
exact: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -140,6 +110,7 @@
|
|||||||
href: `/${data.org.slug}/events`,
|
href: `/${data.org.slug}/events`,
|
||||||
label: m.nav_events(),
|
label: m.nav_events(),
|
||||||
icon: "celebration",
|
icon: "celebration",
|
||||||
|
exact: false,
|
||||||
},
|
},
|
||||||
// Chat disabled until fully developed
|
// Chat disabled until fully developed
|
||||||
// ...(data.org.feature_chat
|
// ...(data.org.feature_chat
|
||||||
@@ -157,22 +128,23 @@
|
|||||||
// },
|
// },
|
||||||
// ]
|
// ]
|
||||||
// : []),
|
// : []),
|
||||||
// Settings requires settings.view or admin role
|
|
||||||
...(canAccess("settings.view")
|
...(canAccess("settings.view")
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
href: `/${data.org.slug}/settings`,
|
href: `/${data.org.slug}/settings`,
|
||||||
label: m.nav_settings(),
|
label: m.nav_settings(),
|
||||||
icon: "settings",
|
icon: "settings",
|
||||||
|
exact: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isActive(href: string): boolean {
|
function isActive(href: string, exact = false): boolean {
|
||||||
const navTo = $navigating?.to?.url.pathname;
|
const navTo = $navigating?.to?.url.pathname;
|
||||||
if (navTo) return navTo.startsWith(href);
|
const current = navTo || $page.url.pathname;
|
||||||
return $page.url.pathname.startsWith(href);
|
if (exact) return current === href || current === href + "/";
|
||||||
|
return current.startsWith(href);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNavigatingTo(href: string): boolean {
|
function isNavigatingTo(href: string): boolean {
|
||||||
@@ -189,60 +161,62 @@
|
|||||||
<div class="flex h-screen bg-background p-4 gap-4">
|
<div class="flex h-screen bg-background p-4 gap-4">
|
||||||
<!-- Organization Module -->
|
<!-- Organization Module -->
|
||||||
<aside
|
<aside
|
||||||
class="w-64 bg-night rounded-[32px] flex flex-col px-4 py-5 gap-4 overflow-hidden shrink-0"
|
class="w-[256px] min-w-[256px] bg-night rounded-[32px] flex flex-col px-4 py-5 gap-4 overflow-hidden shrink-0 h-full"
|
||||||
>
|
>
|
||||||
|
<!-- Logo -->
|
||||||
|
<Logo size="sm" />
|
||||||
|
|
||||||
<!-- Org Header -->
|
<!-- Org Header -->
|
||||||
<a
|
<a
|
||||||
href="/{data.org.slug}"
|
href="/{data.org.slug}"
|
||||||
class="flex items-center gap-2 p-1 rounded-[32px] hover:bg-dark transition-colors"
|
class="flex items-center gap-4 w-full hover:opacity-80 transition-opacity"
|
||||||
>
|
>
|
||||||
<div class="shrink-0 w-12 h-12">
|
<div class="shrink-0">
|
||||||
<Avatar
|
<Avatar
|
||||||
name={data.org.name}
|
name={data.org.name}
|
||||||
src={data.org.avatar_url}
|
src={data.org.avatar_url}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1 overflow-hidden">
|
<div class="min-w-0 flex-1 flex flex-col gap-1">
|
||||||
<h1
|
<p class="font-heading text-h4 text-white truncate w-full">
|
||||||
class="font-heading text-h3 text-white truncate whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{data.org.name}
|
{data.org.name}
|
||||||
</h1>
|
</p>
|
||||||
<p
|
<p class="font-body text-body text-white capitalize w-full">
|
||||||
class="text-body-sm text-white font-body capitalize whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{data.userRole}
|
{data.userRole}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Nav Items -->
|
<!-- Nav Items -->
|
||||||
<nav class="flex-1 flex flex-col gap-1">
|
<nav class="flex-1 flex flex-col gap-1 min-h-0">
|
||||||
{#each navItems as item}
|
{#each navItems as item}
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
class="flex items-center gap-2 h-10 pl-1 pr-2 py-1 rounded-[32px] transition-colors {isActive(
|
class="flex items-center gap-2 pl-1 pr-2 py-1 rounded-[32px] transition-colors overflow-clip {isActive(
|
||||||
item.href,
|
item.href,
|
||||||
|
item.exact,
|
||||||
)
|
)
|
||||||
? 'bg-primary'
|
? 'bg-primary'
|
||||||
: 'hover:bg-dark'}"
|
: 'hover:bg-white/5'}"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-8 h-8 flex items-center justify-center p-1 shrink-0"
|
|
||||||
>
|
>
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded {isActive(item.href)
|
class="material-symbols-rounded {isActive(
|
||||||
|
item.href,
|
||||||
|
item.exact,
|
||||||
|
)
|
||||||
? 'text-background'
|
? 'text-background'
|
||||||
: 'text-light'}"
|
: 'text-white'}"
|
||||||
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="font-body text-body truncate whitespace-nowrap {isActive(
|
class="flex-1 font-body text-body truncate {isActive(
|
||||||
item.href,
|
item.href,
|
||||||
|
item.exact,
|
||||||
)
|
)
|
||||||
? 'text-background'
|
? 'text-background'
|
||||||
: 'text-white'}">{item.label}</span
|
: 'text-white'}">{item.label}</span
|
||||||
@@ -258,98 +232,38 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- User Section + Logo at bottom -->
|
<!-- Profile footer -->
|
||||||
<div class="mt-auto flex flex-col gap-3">
|
<div class="flex gap-4 items-center w-full">
|
||||||
<!-- User Avatar + Quick Menu -->
|
<a
|
||||||
<div
|
href="/{data.org.slug}/account"
|
||||||
class="relative user-menu-container"
|
class="flex flex-1 gap-4 items-center min-w-0 hover:opacity-80 transition-opacity"
|
||||||
bind:this={menuContainerEl}
|
|
||||||
>
|
>
|
||||||
<button
|
<div class="shrink-0">
|
||||||
type="button"
|
|
||||||
class="flex items-center gap-2 p-1 rounded-[32px] hover:bg-dark transition-colors w-full"
|
|
||||||
onclick={() => (showUserMenu = !showUserMenu)}
|
|
||||||
aria-expanded={showUserMenu}
|
|
||||||
aria-haspopup="true"
|
|
||||||
>
|
|
||||||
<div class="shrink-0 w-10 h-10">
|
|
||||||
<Avatar
|
<Avatar
|
||||||
name={data.profile.full_name || data.profile.email}
|
name={data.profile.full_name || data.profile.email}
|
||||||
src={data.profile.avatar_url}
|
src={data.profile.avatar_url}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1 overflow-hidden text-left">
|
<div class="flex flex-1 flex-col items-start min-w-0">
|
||||||
<p
|
<p class="font-heading text-h5 text-white w-full truncate">
|
||||||
class="font-body text-body-sm text-white truncate whitespace-nowrap leading-tight"
|
|
||||||
>
|
|
||||||
{data.profile.full_name || "User"}
|
{data.profile.full_name || "User"}
|
||||||
</p>
|
</p>
|
||||||
<p
|
|
||||||
class="font-body text-[11px] text-light/50 truncate whitespace-nowrap leading-tight"
|
|
||||||
>
|
|
||||||
{data.profile.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if showUserMenu}
|
|
||||||
<div
|
|
||||||
class="absolute bottom-full left-0 mb-2 py-1 bg-dark border border-light/10 rounded-xl shadow-xl min-w-[200px] z-50"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/{data.org.slug}/account"
|
|
||||||
class="flex items-center gap-3 px-3 py-2 text-sm text-light hover:bg-light/5 transition-colors"
|
|
||||||
onclick={() => (showUserMenu = false)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="material-symbols-rounded text-light/50"
|
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
|
||||||
>
|
|
||||||
person
|
|
||||||
</span>
|
|
||||||
<span>{m.user_menu_account_settings()}</span>
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
class="flex items-center gap-3 px-3 py-2 text-sm text-light hover:bg-light/5 transition-colors"
|
|
||||||
onclick={() => (showUserMenu = false)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="material-symbols-rounded text-light/50"
|
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
|
||||||
>
|
|
||||||
swap_horiz
|
|
||||||
</span>
|
|
||||||
<span>{m.user_menu_switch_org()}</span>
|
|
||||||
</a>
|
|
||||||
<div class="border-t border-light/10 my-1"></div>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm text-error hover:bg-error/10 transition-colors"
|
class="flex items-center justify-center p-1 rounded-[32px] shrink-0 text-white/60 hover:text-white transition-colors"
|
||||||
onclick={handleLogout}
|
onclick={handleLogout}
|
||||||
|
title="Sign out"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-error/60"
|
class="material-symbols-rounded"
|
||||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>exit_to_app</span
|
||||||
>
|
>
|
||||||
logout
|
|
||||||
</span>
|
|
||||||
<span>{m.user_menu_logout()}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Logo -->
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
title="Back to organizations"
|
|
||||||
class="flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<Logo size="md" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
|
|||||||
@@ -79,13 +79,13 @@
|
|||||||
<title>{data.org.name} | root</title>
|
<title>{data.org.name} | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex flex-col h-full overflow-auto">
|
<div class="flex flex-col gap-8 h-full p-8 overflow-auto">
|
||||||
<PageHeader title={data.org.name} subtitle={m.overview_subtitle()}>
|
<PageHeader title={data.org.name} subtitle={m.overview_subtitle()}>
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
{#if isEditor}
|
{#if isEditor}
|
||||||
<a
|
<a
|
||||||
href="/{data.org.slug}/events"
|
href="/{data.org.slug}/events"
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors"
|
class="flex items-center gap-2 px-5 py-2.5 bg-primary text-background rounded-[32px] font-body text-body hover:bg-primary-hover transition-colors"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
@@ -98,9 +98,9 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 overflow-auto">
|
||||||
<!-- Stats Grid -->
|
<!-- Stats Grid -->
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
{#await data.stats}
|
{#await data.stats}
|
||||||
{#each Array(4) as _}
|
{#each Array(4) as _}
|
||||||
<Skeleton
|
<Skeleton
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
<!-- Left Column: Upcoming Events + Activity -->
|
<!-- Left Column: Upcoming Events + Activity -->
|
||||||
<div class="lg:col-span-2 flex flex-col gap-6">
|
<div class="lg:col-span-2 flex flex-col gap-6">
|
||||||
<!-- Upcoming Events -->
|
<!-- Upcoming Events -->
|
||||||
@@ -260,24 +260,24 @@
|
|||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<a
|
<a
|
||||||
href="/{data.org.slug}/settings"
|
href="/{data.org.slug}/settings"
|
||||||
class="flex items-center gap-3 bg-dark/30 border border-light/5 hover:border-light/10 rounded-xl p-4 transition-all group"
|
class="flex items-center gap-4 bg-surface rounded-[32px] p-5 transition-all hover:ring-1 hover:ring-white/10 group"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 rounded-xl bg-light/5 flex items-center justify-center"
|
class="w-12 h-12 rounded-full bg-white/5 flex items-center justify-center shrink-0"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/40 group-hover:text-white transition-colors"
|
class="material-symbols-rounded text-white/50 group-hover:text-white transition-colors"
|
||||||
style="font-size: 22px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 22;"
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
>settings</span
|
>settings</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<p
|
<p
|
||||||
class="text-body-sm text-white group-hover:text-primary transition-colors"
|
class="text-body font-heading text-white group-hover:text-primary transition-colors"
|
||||||
>
|
>
|
||||||
{m.nav_settings()}
|
{m.nav_settings()}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[11px] text-light/30">
|
<p class="text-body-sm font-body text-white/40">
|
||||||
{m.settings_general_title()}
|
{m.settings_general_title()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -277,28 +277,50 @@
|
|||||||
<title>Account Settings | root</title>
|
<title>Account Settings | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 flex flex-col gap-8 h-full overflow-auto p-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
<!-- Header -->
|
||||||
<!-- Profile Section -->
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
<div
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5"
|
<h2 class="font-heading text-h2 text-white">
|
||||||
>
|
{m.account_settings_title?.() ?? "Your Settings"}
|
||||||
<h2 class="font-heading text-body text-white">
|
|
||||||
{m.account_profile()}
|
|
||||||
</h2>
|
</h2>
|
||||||
|
<p class="font-body text-body text-white/50">
|
||||||
|
{m.account_settings_subtitle?.() ??
|
||||||
|
"Manage your settings here."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Settings panels -->
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-1 gap-8 items-start min-h-0 overflow-clip w-full">
|
||||||
<span class="font-body text-body-sm text-light/60"
|
<!-- Details panel -->
|
||||||
>{m.account_photo()}</span
|
<div
|
||||||
|
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-4">
|
<!-- Panel header -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 rounded-[32px] shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center min-w-0">
|
||||||
|
<h3
|
||||||
|
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{m.account_profile()}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel content -->
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
|
||||||
|
>
|
||||||
|
<!-- Avatar -->
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
name={fullName || data.profile.email}
|
name={fullName || data.profile.email}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
size="xl"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -315,17 +337,9 @@
|
|||||||
>
|
>
|
||||||
{m.btn_upload()}
|
{m.btn_upload()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
onclick={syncGoogleAvatar}
|
|
||||||
>
|
|
||||||
{m.account_sync_google()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{#if avatarUrl}
|
{#if avatarUrl}
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="danger"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={removeAvatar}
|
onclick={removeAvatar}
|
||||||
>
|
>
|
||||||
@@ -334,13 +348,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Name -->
|
<!-- Name -->
|
||||||
<Input
|
<Input
|
||||||
label={m.account_display_name()}
|
label={m.account_display_name()}
|
||||||
bind:value={fullName}
|
bind:value={fullName}
|
||||||
placeholder={m.account_display_name_placeholder()}
|
placeholder={m.account_display_name_placeholder()}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Email (read-only) -->
|
<!-- Email (read-only) -->
|
||||||
@@ -348,22 +362,72 @@
|
|||||||
label={m.account_email()}
|
label={m.account_email()}
|
||||||
value={data.profile.email}
|
value={data.profile.email}
|
||||||
disabled
|
disabled
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<!-- Password -->
|
||||||
<Button onclick={saveProfile} loading={isSaving}>
|
<Input
|
||||||
|
type="password"
|
||||||
|
label={m.account_password()}
|
||||||
|
value="••••••••••"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
const { error } =
|
||||||
|
await supabase.auth.resetPasswordForEmail(
|
||||||
|
data.profile.email,
|
||||||
|
{
|
||||||
|
redirectTo: `${window.location.origin}/${data.org.slug}/account`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error) toasts.error(m.toast_error_reset_email());
|
||||||
|
else toasts.success(m.toast_success_reset_email());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{m.account_send_reset()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel CTA -->
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<Button fullWidth onclick={saveProfile} loading={isSaving}>
|
||||||
{m.account_save_profile()}
|
{m.account_save_profile()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact & Sizing Section -->
|
<!-- Member Information panel -->
|
||||||
<div
|
<div
|
||||||
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5"
|
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
|
||||||
|
>
|
||||||
|
<!-- Panel header -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 rounded-[32px] shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center min-w-0">
|
||||||
|
<h3
|
||||||
|
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<h2 class="font-heading text-body text-white">
|
|
||||||
{m.account_contact_info()}
|
{m.account_contact_info()}
|
||||||
</h2>
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel content -->
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label={m.account_discord()}
|
||||||
|
bind:value={discordHandle}
|
||||||
|
placeholder={m.account_discord_placeholder()}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label={m.account_phone()}
|
label={m.account_phone()}
|
||||||
@@ -371,22 +435,14 @@
|
|||||||
placeholder={m.account_phone_placeholder()}
|
placeholder={m.account_phone_placeholder()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
|
||||||
label={m.account_discord()}
|
|
||||||
bind:value={discordHandle}
|
|
||||||
placeholder={m.account_discord_placeholder()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
|
||||||
<Select
|
<Select
|
||||||
variant="compact"
|
|
||||||
label={m.account_shirt_size()}
|
label={m.account_shirt_size()}
|
||||||
bind:value={shirtSize}
|
bind:value={shirtSize}
|
||||||
placeholder={m.account_size_placeholder()}
|
placeholder={m.account_size_placeholder()}
|
||||||
options={clothingSizes.map((s) => ({ value: s, label: s }))}
|
options={clothingSizes.map((s) => ({ value: s, label: s }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
variant="compact"
|
|
||||||
label={m.account_hoodie_size()}
|
label={m.account_hoodie_size()}
|
||||||
bind:value={hoodieSize}
|
bind:value={hoodieSize}
|
||||||
placeholder={m.account_size_placeholder()}
|
placeholder={m.account_size_placeholder()}
|
||||||
@@ -394,21 +450,37 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- Panel CTA -->
|
||||||
<Button onclick={saveProfile} loading={isSaving}>
|
<div
|
||||||
|
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<Button fullWidth onclick={saveProfile} loading={isSaving}>
|
||||||
{m.account_save_profile()}
|
{m.account_save_profile()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Appearance Section -->
|
<!-- Appearance panel -->
|
||||||
<div
|
<div
|
||||||
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5"
|
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
|
||||||
|
>
|
||||||
|
<!-- Panel header -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 rounded-[32px] shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center min-w-0">
|
||||||
|
<h3
|
||||||
|
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<h2 class="font-heading text-body text-white">
|
|
||||||
{m.account_appearance()}
|
{m.account_appearance()}
|
||||||
</h2>
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel content -->
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
|
||||||
|
>
|
||||||
<!-- Theme -->
|
<!-- Theme -->
|
||||||
<Select
|
<Select
|
||||||
label={m.account_theme()}
|
label={m.account_theme()}
|
||||||
@@ -422,93 +494,32 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<div class="flex flex-col gap-2">
|
<Select
|
||||||
<span class="font-body text-body-sm text-light/60"
|
label={m.account_language()}
|
||||||
>{m.account_language()}</span
|
bind:value={currentLocale}
|
||||||
>
|
placeholder=""
|
||||||
<p class="font-body text-[11px] text-light/40">
|
options={locales.map((l) => ({
|
||||||
{m.account_language_desc()}
|
value: l,
|
||||||
</p>
|
label: localeLabels[l] ?? l,
|
||||||
<div class="flex gap-2 mt-1">
|
}))}
|
||||||
{#each locales as locale}
|
onchange={(e) => {
|
||||||
<button
|
const val = (e.target as HTMLSelectElement)?.value;
|
||||||
type="button"
|
if (val)
|
||||||
class="px-3 py-1.5 rounded-lg text-[12px] font-medium transition-colors {currentLocale ===
|
handleLanguageChange(
|
||||||
locale
|
val as (typeof locales)[number],
|
||||||
? 'bg-primary text-background'
|
);
|
||||||
: 'bg-light/5 text-light/50 hover:bg-light/10'}"
|
}}
|
||||||
onclick={() => handleLanguageChange(locale)}
|
/>
|
||||||
>
|
|
||||||
{localeLabels[locale] ?? locale}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- Panel CTA -->
|
||||||
<Button onclick={savePreferences} loading={isSaving}>
|
<div
|
||||||
|
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<Button fullWidth onclick={savePreferences} loading={isSaving}>
|
||||||
{m.account_save_preferences()}
|
{m.account_save_preferences()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Security & Sessions Section -->
|
|
||||||
<div
|
|
||||||
class="bg-dark/30 border border-light/5 rounded-2xl p-5 flex flex-col gap-5"
|
|
||||||
>
|
|
||||||
<h2 class="font-heading text-body text-white">
|
|
||||||
{m.account_security()}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<p class="font-body text-body-sm text-white">
|
|
||||||
{m.account_password()}
|
|
||||||
</p>
|
|
||||||
<p class="font-body text-[11px] text-light/40">
|
|
||||||
{m.account_password_desc()}
|
|
||||||
</p>
|
|
||||||
<div class="mt-2">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onclick={async () => {
|
|
||||||
const { error } =
|
|
||||||
await supabase.auth.resetPasswordForEmail(
|
|
||||||
data.profile.email,
|
|
||||||
{
|
|
||||||
redirectTo: `${window.location.origin}/${data.org.slug}/account`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (error)
|
|
||||||
toasts.error(m.toast_error_reset_email());
|
|
||||||
else toasts.success(m.toast_success_reset_email());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{m.account_send_reset()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-t border-light/5 pt-4 flex flex-col gap-2">
|
|
||||||
<p class="font-body text-body-sm text-white">
|
|
||||||
{m.account_active_sessions()}
|
|
||||||
</p>
|
|
||||||
<p class="font-body text-[11px] text-light/40">
|
|
||||||
{m.account_sessions_desc()}
|
|
||||||
</p>
|
|
||||||
<div class="mt-2">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onclick={async () => {
|
|
||||||
await supabase.auth.signOut({ scope: "others" });
|
|
||||||
toasts.success(m.toast_success_signout_others());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{m.account_signout_others()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -462,13 +462,19 @@
|
|||||||
<title>{m.calendar_title()} - {data.org.name} | root</title>
|
<title>{m.calendar_title()} - {data.org.name} | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col gap-8 h-full p-8 overflow-hidden">
|
||||||
<!-- Toolbar -->
|
<!-- Header -->
|
||||||
<div
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
class="flex items-center gap-2 px-6 py-3 border-b border-light/5 shrink-0"
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
>
|
<h2 class="font-heading text-h2 text-white">
|
||||||
<div class="flex-1"></div>
|
{m.calendar_title()}
|
||||||
<Button size="sm" onclick={() => handleDateClick(new Date())}
|
</h2>
|
||||||
|
<p class="font-body text-body text-white/50">
|
||||||
|
{m.calendar_subtitle()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<Button icon="add" onclick={() => handleDateClick(new Date())}
|
||||||
>{m.btn_new()}</Button
|
>{m.btn_new()}</Button
|
||||||
>
|
>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
@@ -509,9 +515,10 @@
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Calendar Grid -->
|
<!-- Calendar Grid -->
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 overflow-auto min-h-0">
|
||||||
<Calendar
|
<Calendar
|
||||||
events={allEvents}
|
events={allEvents}
|
||||||
onDateClick={handleDateClick}
|
onDateClick={handleDateClick}
|
||||||
@@ -791,9 +798,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<Button
|
<Button variant="ghost" onclick={() => (showEventFormModal = false)}
|
||||||
variant="tertiary"
|
|
||||||
onclick={() => (showEventFormModal = false)}
|
|
||||||
>{m.btn_cancel()}</Button
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FileBrowser } from "$lib/components/documents";
|
import { FileBrowser } from "$lib/components/documents";
|
||||||
|
import { ContentHeader } from "$lib/components/ui";
|
||||||
import type { Document } from "$lib/supabase/types";
|
import type { Document } from "$lib/supabase/types";
|
||||||
|
import * as m from "$lib/paraglide/messages";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: {
|
data: {
|
||||||
@@ -17,13 +19,24 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
documents = data.documents;
|
documents = data.documents;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let fileBrowserRef:
|
||||||
|
| { handleAdd: () => void; handleUpload: () => void }
|
||||||
|
| undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Files - {data.org.name} | root</title>
|
<title>{m.nav_storage()} - {data.org.name} | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="h-full p-6">
|
<div class="flex flex-col gap-8 h-full p-8 overflow-auto">
|
||||||
|
<ContentHeader
|
||||||
|
title={m.nav_storage()}
|
||||||
|
subtitle={m.files_subtitle()}
|
||||||
|
actionLabel={m.btn_new()}
|
||||||
|
actionIcon="add"
|
||||||
|
onAction={() => fileBrowserRef?.handleAdd()}
|
||||||
|
/>
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
org={data.org}
|
org={data.org}
|
||||||
bind:documents
|
bind:documents
|
||||||
|
|||||||
@@ -664,9 +664,9 @@
|
|||||||
<title>{data.document.name} - {data.org.name} | root</title>
|
<title>{data.document.name} - {data.org.name} | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex flex-col h-full p-4 lg:p-5 gap-4">
|
<div class="flex flex-col h-full p-8 gap-8">
|
||||||
{#if data.isKanban}
|
{#if data.isKanban}
|
||||||
<!-- Kanban: needs its own header since DocumentViewer is for documents -->
|
<!-- Kanban header -->
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
@@ -674,10 +674,15 @@
|
|||||||
bind:this={fileInput}
|
bind:this={fileInput}
|
||||||
onchange={handleJsonImport}
|
onchange={handleJsonImport}
|
||||||
/>
|
/>
|
||||||
<header class="flex items-center gap-2 p-1">
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
<h1 class="flex-1 font-heading text-h1 text-white truncate">
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
|
<h2 class="font-heading text-h2 text-white truncate w-full">
|
||||||
{data.document.name}
|
{data.document.name}
|
||||||
</h1>
|
</h2>
|
||||||
|
<p class="font-body text-body text-white/50">
|
||||||
|
{data.document.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
@@ -698,7 +703,61 @@
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-between gap-y-2 w-full shrink-0"
|
||||||
|
>
|
||||||
|
<nav class="flex items-center">
|
||||||
|
<a
|
||||||
|
href="/{data.org.slug}/documents"
|
||||||
|
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>cloud</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h3 text-white whitespace-nowrap"
|
||||||
|
>Home</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>chevron_forward</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>view_kanban</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h3 text-white whitespace-nowrap"
|
||||||
|
>{data.document.name}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>list</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-auto min-h-0">
|
<div class="flex-1 overflow-auto min-h-0">
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
@@ -718,19 +777,124 @@
|
|||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex items-center justify-center h-full">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded text-light/30 animate-spin mb-4"
|
class="material-symbols-rounded text-white/30 animate-spin mb-4"
|
||||||
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;"
|
style="font-size: 48px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 48;"
|
||||||
>
|
>
|
||||||
progress_activity
|
progress_activity
|
||||||
</span>
|
</span>
|
||||||
<p class="text-light/50">Loading board...</p>
|
<p class="font-body text-body text-white/50">
|
||||||
|
Loading board...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Document Editor: use shared DocumentViewer component -->
|
<!-- Document header -->
|
||||||
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
|
<h2 class="font-heading text-h2 text-white truncate w-full">
|
||||||
|
{data.document.name}
|
||||||
|
</h2>
|
||||||
|
<p class="font-body text-body text-white/50">
|
||||||
|
{data.document.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-center p-1 rounded-full hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>more_horiz</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-between gap-y-2 w-full shrink-0"
|
||||||
|
>
|
||||||
|
<nav class="flex items-center">
|
||||||
|
<a
|
||||||
|
href="/{data.org.slug}/documents"
|
||||||
|
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>cloud</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h3 text-white whitespace-nowrap"
|
||||||
|
>Home</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{#if data.document.parent_id}
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>chevron_forward</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="/{data.org.slug}/documents/folder/{data.document
|
||||||
|
.parent_id}"
|
||||||
|
class="flex items-center gap-2 shrink-0 hover:opacity-80 transition-opacity"
|
||||||
|
>
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>folder</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h3 text-white whitespace-nowrap"
|
||||||
|
>...</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>chevron_forward</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<span class="flex items-center justify-center p-1">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>description</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-heading text-h3 text-white whitespace-nowrap"
|
||||||
|
>{data.document.name}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-center p-1 shrink-0 hover:bg-white/5 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded text-white"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>list</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Document Editor -->
|
||||||
|
<div class="flex-1 min-h-0 overflow-auto">
|
||||||
<DocumentViewer
|
<DocumentViewer
|
||||||
document={data.document}
|
document={data.document}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
@@ -738,11 +902,12 @@
|
|||||||
locked={lockInfo.isLocked && !lockInfo.isOwnLock}
|
locked={lockInfo.isLocked && !lockInfo.isOwnLock}
|
||||||
lockedByName={lockInfo.lockedByName}
|
lockedByName={lockInfo.lockedByName}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
{#if isSaving}
|
{#if isSaving}
|
||||||
<div class="text-body-sm text-light/50">Saving...</div>
|
<div class="font-body text-body-sm text-white/50">Saving...</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -803,7 +968,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
onclick={() => (showAddColumnModal = false)}
|
onclick={() => (showAddColumnModal = false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FileBrowser } from "$lib/components/documents";
|
import { FileBrowser } from "$lib/components/documents";
|
||||||
|
import { ContentHeader } from "$lib/components/ui";
|
||||||
import type { Document } from "$lib/supabase/types";
|
import type { Document } from "$lib/supabase/types";
|
||||||
|
import * as m from "$lib/paraglide/messages";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: {
|
data: {
|
||||||
@@ -25,7 +27,13 @@
|
|||||||
<title>{data.folder.name} - {data.org.name} | root</title>
|
<title>{data.folder.name} - {data.org.name} | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="h-full p-4 lg:p-5">
|
<div class="flex flex-col gap-8 h-full p-8 overflow-auto">
|
||||||
|
<ContentHeader
|
||||||
|
title={m.nav_storage()}
|
||||||
|
subtitle={m.files_subtitle()}
|
||||||
|
actionLabel={m.btn_new()}
|
||||||
|
actionIcon="add"
|
||||||
|
/>
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
org={data.org}
|
org={data.org}
|
||||||
bind:documents
|
bind:documents
|
||||||
|
|||||||
@@ -176,43 +176,39 @@
|
|||||||
<title>{m.events_title()} | {data.org.name}</title>
|
<title>{m.events_title()} | {data.org.name}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col gap-8 h-full p-8 overflow-hidden">
|
||||||
<!-- Toolbar: Status Tabs + Create Button -->
|
<!-- Header -->
|
||||||
<div
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
class="flex items-center justify-between px-6 py-3 border-b border-light/5 shrink-0"
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
>
|
<h2 class="font-heading text-h2 text-white">{m.events_title()}</h2>
|
||||||
<div class="flex items-center gap-1">
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
{#if isEditor}
|
||||||
|
<Button icon="add" onclick={() => (showCreateModal = true)}>
|
||||||
|
{m.events_new()}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Tabs -->
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
{#each statusTabs as tab}
|
{#each statusTabs as tab}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-body-sm font-body transition-colors {data.statusFilter ===
|
class="flex items-center gap-1.5 px-4 py-2 rounded-[32px] text-body font-body transition-colors {data.statusFilter ===
|
||||||
tab.value
|
tab.value
|
||||||
? 'bg-primary text-background'
|
? 'bg-surface text-white'
|
||||||
: 'text-light/50 hover:text-white hover:bg-dark/50'}"
|
: 'text-white/50 hover:text-white hover:bg-white/5'}"
|
||||||
onclick={() => switchStatus(tab.value)}
|
onclick={() => switchStatus(tab.value)}
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="material-symbols-rounded"
|
|
||||||
style="font-size: 16px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 16;"
|
|
||||||
>{tab.icon}</span
|
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if isEditor}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
icon="add"
|
|
||||||
onclick={() => (showCreateModal = true)}
|
|
||||||
>
|
|
||||||
{m.events_new()}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Events Grid -->
|
<!-- Events Grid -->
|
||||||
<div class="flex-1 overflow-auto p-6">
|
<div class="flex-1 overflow-auto min-h-0">
|
||||||
{#if data.events.length === 0}
|
{#if data.events.length === 0}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col items-center justify-center h-full text-light/40"
|
class="flex flex-col items-center justify-center h-full text-light/40"
|
||||||
|
|||||||
@@ -679,7 +679,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
onclick={() => (showCreateBoardModal = false)}
|
onclick={() => (showCreateBoardModal = false)}
|
||||||
>{m.btn_cancel()}</Button
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
@@ -712,7 +712,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="ghost"
|
||||||
onclick={() => (showEditBoardModal = false)}
|
onclick={() => (showEditBoardModal = false)}
|
||||||
>{m.btn_cancel()}</Button
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
@@ -737,9 +737,7 @@
|
|||||||
placeholder={m.kanban_column_name_placeholder()}
|
placeholder={m.kanban_column_name_placeholder()}
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button
|
<Button variant="ghost" onclick={() => (showAddColumnModal = false)}
|
||||||
variant="tertiary"
|
|
||||||
onclick={() => (showAddColumnModal = false)}
|
|
||||||
>{m.btn_cancel()}</Button
|
>{m.btn_cancel()}</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -204,11 +204,7 @@
|
|||||||
>
|
>
|
||||||
{m.onboarding_save()}
|
{m.onboarding_save()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button fullWidth variant="ghost" onclick={skipOnboarding}>
|
||||||
fullWidth
|
|
||||||
variant="tertiary"
|
|
||||||
onclick={skipOnboarding}
|
|
||||||
>
|
|
||||||
{m.onboarding_skip()}
|
{m.onboarding_skip()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,44 +6,43 @@
|
|||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Colors - Figma Design System */
|
/* Colors - Figma Design System (exact exports) */
|
||||||
--color-background: #05090f;
|
--color-background: rgba(5, 9, 15, 1);
|
||||||
--color-night: #0A121F;
|
--color-night: rgba(10, 18, 31, 1);
|
||||||
--color-dark: #14243E;
|
--color-dark: #14243E;
|
||||||
--color-surface: #0A121F;
|
--color-surface: rgba(15, 28, 46, 1);
|
||||||
--color-light: #E5E6F0;
|
--color-light: rgba(255, 255, 255, 1);
|
||||||
--color-text: #FFFFFF;
|
--color-text: rgba(255, 255, 255, 1);
|
||||||
--color-text-muted: rgba(229, 230, 240, 0.5);
|
--color-text-muted: rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
/* Brand - Primary */
|
/* Brand - Primary */
|
||||||
--color-primary: #00A3E0;
|
--color-primary: rgba(0, 163, 224, 1);
|
||||||
--color-primary-hover: #33b5e6;
|
--color-primary-hover: #33b5e6;
|
||||||
|
|
||||||
/* Status Colors */
|
/* Status Colors */
|
||||||
--color-success: #33E000;
|
--color-success: rgba(51, 224, 0, 1);
|
||||||
--color-warning: #FFAB00;
|
--color-warning: rgba(255, 171, 0, 1);
|
||||||
--color-error: #E03D00;
|
--color-error: rgba(224, 61, 0, 1);
|
||||||
--color-info: #00A3E0;
|
--color-info: rgba(0, 163, 224, 1);
|
||||||
|
|
||||||
/* Typography - Figma Fonts */
|
/* Typography - Figma Fonts */
|
||||||
--font-heading: 'Tilt Warp', sans-serif;
|
--font-heading: 'Tilt Warp', sans-serif;
|
||||||
--font-body: 'Work Sans', sans-serif;
|
--font-body: 'Work Sans', sans-serif;
|
||||||
--font-input: 'Inter', sans-serif;
|
--font-input: 'Work Sans', sans-serif;
|
||||||
--font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
|
--font-sans: 'Work Sans', system-ui, -apple-system, sans-serif;
|
||||||
|
|
||||||
/* Font Sizes - Figma Text Styles (--text-* → text-* utilities) */
|
/* Font Sizes - Figma Text Styles: headings-buttons */
|
||||||
/* Headings (heading font) */
|
--text-h1: 28px;
|
||||||
--text-h1: 32px;
|
--text-h2: 24px;
|
||||||
--text-h2: 28px;
|
--text-h3: 20px;
|
||||||
--text-h3: 24px;
|
--text-h4: 18px;
|
||||||
--text-h4: 20px;
|
|
||||||
--text-h5: 16px;
|
--text-h5: 16px;
|
||||||
--text-h6: 14px;
|
--text-h6: 14px;
|
||||||
/* Button text (heading font) */
|
/* Font Sizes - Figma Text Styles: body btn */
|
||||||
--text-btn-lg: 20px;
|
--text-btn-lg: 18px;
|
||||||
--text-btn-md: 16px;
|
--text-btn-md: 16px;
|
||||||
--text-btn-sm: 14px;
|
--text-btn-sm: 14px;
|
||||||
/* Body text (body font) */
|
/* Font Sizes - Figma Text Styles: body */
|
||||||
--text-body: 16px;
|
--text-body: 16px;
|
||||||
--text-body-md: 14px;
|
--text-body-md: 14px;
|
||||||
--text-body-sm: 12px;
|
--text-body-sm: 12px;
|
||||||
|
|||||||
@@ -97,19 +97,24 @@
|
|||||||
<title>{mode === "login" ? "Log In" : "Sign Up"} | root</title>
|
<title>{mode === "login" ? "Log In" : "Sign Up"} | root</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen bg-background flex items-center justify-center p-4">
|
<div class="h-screen bg-background flex items-start p-4">
|
||||||
<div class="w-full max-w-sm">
|
<div
|
||||||
<div class="text-center mb-8">
|
class="bg-night flex-1 flex flex-col gap-4 min-h-0 h-full items-center justify-center overflow-auto p-8 rounded-[32px]"
|
||||||
<div class="flex justify-center mb-4">
|
>
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="flex justify-center">
|
||||||
<Logo size="lg" />
|
<Logo size="lg" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-heading-sm font-heading text-white mb-1">
|
|
||||||
{m.app_name()}
|
|
||||||
</h1>
|
|
||||||
<p class="text-body-sm text-light/40">{m.login_subtitle()}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-surface rounded-2xl border border-light/5 p-6">
|
<!-- Tagline -->
|
||||||
|
<p class="font-bold font-body text-h4 text-white text-center">
|
||||||
|
{m.login_subtitle()}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Form card -->
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-col gap-8 items-center overflow-clip p-4 rounded-[32px] w-full max-w-[384px]"
|
||||||
|
>
|
||||||
{#if signupSuccess}
|
{#if signupSuccess}
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<div
|
<div
|
||||||
@@ -129,7 +134,7 @@
|
|||||||
{m.login_signup_success_text({ email })}
|
{m.login_signup_success_text({ email })}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="tertiary"
|
variant="secondary"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
signupSuccess = false;
|
signupSuccess = false;
|
||||||
mode = "login";
|
mode = "login";
|
||||||
@@ -140,23 +145,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Tab switcher -->
|
<!-- Tab switcher -->
|
||||||
<div
|
<div class="flex gap-4 items-start w-full">
|
||||||
class="flex items-center gap-1 bg-dark/50 rounded-xl p-1 mb-6"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="flex-1 py-2 rounded-lg text-body-sm font-body transition-colors {mode ===
|
class="flex-1 flex items-center justify-center gap-2 min-w-[36px] overflow-clip px-3 py-2 rounded-[32px] font-bold font-body text-btn-sm transition-colors {mode ===
|
||||||
'login'
|
'login'
|
||||||
? 'bg-primary text-background'
|
? 'bg-primary text-night'
|
||||||
: 'text-light/40 hover:text-white'}"
|
: 'bg-night text-white hover:bg-surface'}"
|
||||||
onclick={() => (mode = "login")}
|
onclick={() => (mode = "login")}
|
||||||
>
|
>
|
||||||
{m.login_tab_login()}
|
{m.login_tab_login()}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex-1 py-2 rounded-lg text-body-sm font-body transition-colors {mode ===
|
class="flex-1 flex items-center justify-center gap-2 min-w-[36px] overflow-clip px-3 py-2 rounded-[32px] font-bold font-body text-btn-sm transition-colors {mode ===
|
||||||
'signup'
|
'signup'
|
||||||
? 'bg-primary text-background'
|
? 'bg-primary text-night'
|
||||||
: 'text-light/40 hover:text-white'}"
|
: 'bg-night text-white hover:bg-surface'}"
|
||||||
onclick={() => (mode = "signup")}
|
onclick={() => (mode = "signup")}
|
||||||
>
|
>
|
||||||
{m.login_tab_signup()}
|
{m.login_tab_signup()}
|
||||||
@@ -165,7 +168,7 @@
|
|||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div
|
<div
|
||||||
class="mb-4 p-3 bg-error/10 border border-error/20 rounded-xl text-error text-body-sm flex items-center gap-2"
|
class="w-full p-3 bg-error/10 border border-error/20 rounded-[20px] text-error text-body-sm flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="material-symbols-rounded"
|
class="material-symbols-rounded"
|
||||||
@@ -181,16 +184,8 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
}}
|
}}
|
||||||
class="flex flex-col gap-4"
|
class="flex flex-col gap-8 w-full"
|
||||||
>
|
>
|
||||||
<Input
|
|
||||||
type="email"
|
|
||||||
label={m.login_email_label()}
|
|
||||||
placeholder={m.login_email_placeholder()}
|
|
||||||
bind:value={email}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if mode === "signup"}
|
{#if mode === "signup"}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -201,6 +196,14 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
label={m.login_email_label()}
|
||||||
|
placeholder={m.login_email_placeholder()}
|
||||||
|
bind:value={email}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
label={m.login_password_label()}
|
label={m.login_password_label()}
|
||||||
@@ -209,46 +212,39 @@
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" fullWidth loading={isLoading}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
fullWidth
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
{mode === "login"
|
{mode === "login"
|
||||||
? m.login_btn_login()
|
? m.login_btn_login()
|
||||||
: m.login_btn_signup()}
|
: m.login_btn_signup()}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="my-5 flex items-center gap-3">
|
<!-- Separator -->
|
||||||
<div class="flex-1 h-px bg-light/10"></div>
|
<div
|
||||||
|
class="flex gap-1 items-center justify-center opacity-50 overflow-clip w-full"
|
||||||
|
>
|
||||||
|
<div class="flex-1 h-px bg-white/30"></div>
|
||||||
<span
|
<span
|
||||||
class="text-light/30 text-[11px] uppercase tracking-wider"
|
class="font-body text-body-md text-white text-center px-2"
|
||||||
>{m.login_or_continue()}</span
|
>{m.login_or_continue()}</span
|
||||||
>
|
>
|
||||||
<div class="flex-1 h-px bg-light/10"></div>
|
<div class="flex-1 h-px bg-white/30"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<!-- OAuth -->
|
||||||
class="w-full flex items-center justify-center gap-2.5 px-4 py-2.5 rounded-xl border border-light/10 hover:border-light/20 hover:bg-light/5 transition-all text-body-sm text-white"
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
fullWidth
|
||||||
onclick={() => handleOAuth("google")}
|
onclick={() => handleOAuth("google")}
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
fill="#4285F4"
|
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#34A853"
|
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{m.login_google()}
|
{m.login_google()}
|
||||||
</button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
29
src/routes/settings/+page.server.ts
Normal file
29
src/routes/settings/+page.server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
const { session, user } = await locals.safeGetSession();
|
||||||
|
|
||||||
|
if (!session || !user) {
|
||||||
|
redirect(303, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: profile } = await locals.supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('full_name, avatar_url, phone, discord_handle, shirt_size, hoodie_size')
|
||||||
|
.eq('id', user.id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
fullName: profile?.full_name ?? user.user_metadata?.full_name ?? null,
|
||||||
|
avatarUrl: profile?.avatar_url ?? null,
|
||||||
|
phone: profile?.phone ?? null,
|
||||||
|
discordHandle: profile?.discord_handle ?? null,
|
||||||
|
shirtSize: profile?.shirt_size ?? null,
|
||||||
|
hoodieSize: profile?.hoodie_size ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
437
src/routes/settings/+page.svelte
Normal file
437
src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { Logo, Avatar, Button, Input, Select } from "$lib/components/ui";
|
||||||
|
import { toasts } from "$lib/stores/toast.svelte";
|
||||||
|
import * as m from "$lib/paraglide/messages";
|
||||||
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import type { Database } from "$lib/supabase/types";
|
||||||
|
import { getLocale, setLocale, locales } from "$lib/paraglide/runtime.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string | undefined;
|
||||||
|
fullName: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
discordHandle: string | null;
|
||||||
|
shirtSize: string | null;
|
||||||
|
hoodieSize: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
const supabase = getContext<SupabaseClient<Database>>("supabase");
|
||||||
|
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let fullName = $state(data.user.fullName ?? "");
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let avatarUrl = $state(data.user.avatarUrl);
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let phone = $state(data.user.phone ?? "");
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let discordHandle = $state(data.user.discordHandle ?? "");
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let shirtSize = $state(data.user.shirtSize ?? "");
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
let hoodieSize = $state(data.user.hoodieSize ?? "");
|
||||||
|
|
||||||
|
let isSaving = $state(false);
|
||||||
|
let isUploading = $state(false);
|
||||||
|
let avatarInput = $state<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
fullName = data.user.fullName ?? "";
|
||||||
|
avatarUrl = data.user.avatarUrl;
|
||||||
|
phone = data.user.phone ?? "";
|
||||||
|
discordHandle = data.user.discordHandle ?? "";
|
||||||
|
shirtSize = data.user.shirtSize ?? "";
|
||||||
|
hoodieSize = data.user.hoodieSize ?? "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const clothingSizes = ["XS", "S", "M", "L", "XL", "XXL", "3XL"];
|
||||||
|
|
||||||
|
let currentLocale = $state<(typeof locales)[number]>(getLocale());
|
||||||
|
const localeLabels: Record<string, string> = {
|
||||||
|
en: "English",
|
||||||
|
et: "Eesti",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function saveProfile() {
|
||||||
|
isSaving = true;
|
||||||
|
try {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.update({
|
||||||
|
full_name: fullName || null,
|
||||||
|
phone: phone || null,
|
||||||
|
discord_handle: discordHandle || null,
|
||||||
|
shirt_size: shirtSize || null,
|
||||||
|
hoodie_size: hoodieSize || null,
|
||||||
|
})
|
||||||
|
.eq("id", data.user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
toasts.success(m.account_save_profile());
|
||||||
|
} catch (e) {
|
||||||
|
toasts.error("Failed to save profile.");
|
||||||
|
} finally {
|
||||||
|
isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAvatarUpload(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
isUploading = true;
|
||||||
|
try {
|
||||||
|
const ext = file.name.split(".").pop();
|
||||||
|
const path = `avatars/${data.user.id}.${ext}`;
|
||||||
|
|
||||||
|
const { error: uploadError } = await supabase.storage
|
||||||
|
.from("avatars")
|
||||||
|
.upload(path, file, { upsert: true });
|
||||||
|
|
||||||
|
if (uploadError) throw uploadError;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { publicUrl },
|
||||||
|
} = supabase.storage.from("avatars").getPublicUrl(path);
|
||||||
|
|
||||||
|
const url = `${publicUrl}?t=${Date.now()}`;
|
||||||
|
await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.update({ avatar_url: url })
|
||||||
|
.eq("id", data.user.id);
|
||||||
|
|
||||||
|
avatarUrl = url;
|
||||||
|
toasts.success("Avatar updated.");
|
||||||
|
} catch (e) {
|
||||||
|
toasts.error("Failed to upload avatar.");
|
||||||
|
} finally {
|
||||||
|
isUploading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAvatar() {
|
||||||
|
await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.update({ avatar_url: null })
|
||||||
|
.eq("id", data.user.id);
|
||||||
|
avatarUrl = null;
|
||||||
|
toasts.success(m.account_remove_photo());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLanguageChange(newLocale: (typeof locales)[number]) {
|
||||||
|
currentLocale = newLocale;
|
||||||
|
setLocale(newLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
goto("/login");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{m.user_settings_title()} | root</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="h-screen bg-background flex gap-4 items-start p-4">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside
|
||||||
|
class="bg-night flex flex-col gap-4 h-full items-start min-w-[256px] overflow-clip px-4 py-5 rounded-[32px] shrink-0 w-[256px]"
|
||||||
|
>
|
||||||
|
<Logo size="sm" />
|
||||||
|
|
||||||
|
<nav class="flex flex-1 flex-col gap-1 items-start w-full min-h-0">
|
||||||
|
{#each [{ href: "/", icon: "apartment", label: m.home_nav_organizations() }, { href: "/settings", icon: "settings", label: m.nav_settings() }] as item}
|
||||||
|
{@const active =
|
||||||
|
item.href === "/"
|
||||||
|
? $page.url.pathname === "/"
|
||||||
|
: $page.url.pathname.startsWith(item.href)}
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
class="flex gap-2 items-center w-full overflow-clip pl-1 pr-2 py-1 rounded-[32px] transition-colors {active
|
||||||
|
? 'bg-primary'
|
||||||
|
: 'hover:bg-white/5'}"
|
||||||
|
>
|
||||||
|
<span class="flex items-center p-1 shrink-0">
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded {active
|
||||||
|
? 'text-background'
|
||||||
|
: 'text-white'}"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>{item.icon}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex-1 font-body text-body truncate {active
|
||||||
|
? 'text-background'
|
||||||
|
: 'text-white'}">{item.label}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Profile footer -->
|
||||||
|
<div class="flex gap-4 items-center w-full">
|
||||||
|
<div class="flex flex-1 gap-4 items-center min-w-0">
|
||||||
|
<Avatar
|
||||||
|
name={data.user.fullName || data.user.email || "User"}
|
||||||
|
src={data.user.avatarUrl}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-1 flex-col items-start min-w-0">
|
||||||
|
<p class="font-heading text-h5 text-white w-full truncate">
|
||||||
|
{data.user.fullName || data.user.email || "User"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-center p-1 rounded-[32px] text-white/60 hover:text-white transition-colors"
|
||||||
|
title="Sign out"
|
||||||
|
onclick={handleLogout}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style="font-size: 24px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;"
|
||||||
|
>exit_to_app</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<main
|
||||||
|
class="bg-night flex flex-1 flex-col gap-8 h-full items-start min-h-0 min-w-0 overflow-auto p-8 rounded-[32px]"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between w-full shrink-0">
|
||||||
|
<div class="flex flex-1 flex-col gap-2 items-start min-w-0">
|
||||||
|
<h2 class="font-heading text-h2 text-white">
|
||||||
|
{m.user_settings_title()}
|
||||||
|
</h2>
|
||||||
|
<p class="font-body text-body text-white/50">
|
||||||
|
{m.user_settings_subtitle()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings panels -->
|
||||||
|
<div class="flex flex-1 gap-8 items-start min-h-0 overflow-clip w-full">
|
||||||
|
<!-- Details panel -->
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{m.account_profile()}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
|
||||||
|
>
|
||||||
|
<!-- Avatar -->
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<Avatar
|
||||||
|
name={fullName || data.user.email || "User"}
|
||||||
|
src={avatarUrl}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
bind:this={avatarInput}
|
||||||
|
onchange={handleAvatarUpload}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => avatarInput?.click()}
|
||||||
|
loading={isUploading}
|
||||||
|
>
|
||||||
|
{m.btn_upload()}
|
||||||
|
</Button>
|
||||||
|
{#if avatarUrl}
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onclick={removeAvatar}
|
||||||
|
>
|
||||||
|
{m.account_remove_photo()}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={m.account_display_name()}
|
||||||
|
bind:value={fullName}
|
||||||
|
placeholder={m.account_display_name_placeholder()}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={m.account_email()}
|
||||||
|
value={data.user.email ?? ""}
|
||||||
|
disabled
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label={m.account_password()}
|
||||||
|
value="••••••••••"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={async () => {
|
||||||
|
const { error } =
|
||||||
|
await supabase.auth.resetPasswordForEmail(
|
||||||
|
data.user.email ?? "",
|
||||||
|
{
|
||||||
|
redirectTo: `${window.location.origin}/settings`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (error)
|
||||||
|
toasts.error(m.toast_error_reset_email());
|
||||||
|
else toasts.success(m.toast_success_reset_email());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{m.account_send_reset()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<Button fullWidth onclick={saveProfile} loading={isSaving}>
|
||||||
|
{m.account_save_profile()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Member Information panel -->
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{m.account_contact_info()}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label={m.account_discord()}
|
||||||
|
bind:value={discordHandle}
|
||||||
|
placeholder={m.account_discord_placeholder()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={m.account_phone()}
|
||||||
|
bind:value={phone}
|
||||||
|
placeholder={m.account_phone_placeholder()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={m.account_shirt_size()}
|
||||||
|
bind:value={shirtSize}
|
||||||
|
placeholder={m.account_size_placeholder()}
|
||||||
|
options={clothingSizes.map((s) => ({
|
||||||
|
value: s,
|
||||||
|
label: s,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={m.account_hoodie_size()}
|
||||||
|
bind:value={hoodieSize}
|
||||||
|
placeholder={m.account_size_placeholder()}
|
||||||
|
options={clothingSizes.map((s) => ({
|
||||||
|
value: s,
|
||||||
|
label: s,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<Button fullWidth onclick={saveProfile} loading={isSaving}>
|
||||||
|
{m.account_save_profile()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Appearance panel -->
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-1 flex-col h-full items-start min-h-0 min-w-0 overflow-clip rounded-[32px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-2 items-center pb-4 pt-5 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="font-heading text-h3 text-white overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{m.account_appearance()}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-1 flex-col gap-8 items-start min-h-0 overflow-auto p-4 w-full"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
label={m.account_language()}
|
||||||
|
bind:value={currentLocale}
|
||||||
|
placeholder=""
|
||||||
|
options={locales.map((l) => ({
|
||||||
|
value: l,
|
||||||
|
label: localeLabels[l] ?? l,
|
||||||
|
}))}
|
||||||
|
onchange={(e) => {
|
||||||
|
const val = (e.target as HTMLSelectElement)?.value;
|
||||||
|
if (val)
|
||||||
|
handleLanguageChange(
|
||||||
|
val as (typeof locales)[number],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-background flex flex-col items-start pb-5 pt-4 px-4 shrink-0 w-full"
|
||||||
|
>
|
||||||
|
<Button fullWidth onclick={saveProfile} loading={isSaving}>
|
||||||
|
{m.account_save_preferences()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user