diff --git a/package-lock.json b/package-lock.json index 4da4c5a..6fbf512 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "@eslint/js": "^9.29.0", "@inlang/paraglide-js": "2.2.0", "@internationalized/date": "^3.8.2", - "@lucide/svelte": "^0.515.0", + "@lucide/svelte": "^0.542.0", "@playwright/test": "^1.53.1", "@sveltejs/adapter-auto": "^6.0.1", "@sveltejs/kit": "^2.22.0", @@ -1619,9 +1619,9 @@ "license": "Apache-2.0" }, "node_modules/@lucide/svelte": { - "version": "0.515.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.515.0.tgz", - "integrity": "sha512-CEAyqcZmNBfYzVgaRmK2RFJP5tnbXxekRyDk0XX/eZQRfsJmkDvmQwXNX8C869BgNeryzmrRyjHhUL6g9ZOHNA==", + "version": "0.542.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.542.0.tgz", + "integrity": "sha512-NuWttxTVfMSURpOxcKiKvoCtma3JtEpcJWzF/0cO69saZfXlv6G8NYAvEEGLmk75YPl+I+ROe+F97WhddM8r2A==", "dev": true, "license": "ISC", "peerDependencies": { diff --git a/package.json b/package.json index 3fb41c6..de8ae7a 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@eslint/js": "^9.29.0", "@inlang/paraglide-js": "2.2.0", "@internationalized/date": "^3.8.2", - "@lucide/svelte": "^0.515.0", + "@lucide/svelte": "^0.542.0", "@playwright/test": "^1.53.1", "@sveltejs/adapter-auto": "^6.0.1", "@sveltejs/kit": "^2.22.0", diff --git a/project.inlang/messages/de.json b/project.inlang/messages/de.json index e155f42..fda6c22 100644 --- a/project.inlang/messages/de.json +++ b/project.inlang/messages/de.json @@ -92,5 +92,23 @@ "error": "Passkey konnte nicht hinzugefügt werden", "retry": "Erneut versuchen" } + }, + "nav": { + "home": "Übersicht", + "tenants": "Mandanten", + "calendar": "Kalender", + "staff": "Personal", + "agents": "Agenten", + "channels": "Kanäle", + "absences": "Abwesenheiten", + "account": "Konto", + "settings": "Einstellungen", + "documentation": "Dokumentation", + "addTenant": "Mandant hinzufügen", + "noTenantSelected": { + "title": "Kein Mandant ausgewählt", + "description": "Klicke zum Auswählen" + }, + "logout": "Ausloggen" } } diff --git a/project.inlang/messages/en.json b/project.inlang/messages/en.json index 43b650e..e703dbd 100644 --- a/project.inlang/messages/en.json +++ b/project.inlang/messages/en.json @@ -100,5 +100,23 @@ "error": "Passkey could not be added", "retry": "Retry" } + }, + "nav": { + "home": "Home", + "tenants": "Tenants", + "calendar": "Calendar", + "staff": "Staff", + "agents": "Agents", + "channels": "Channels", + "absences": "Absences", + "account": "Account", + "settings": "Settings", + "documentation": "Documentation", + "addTenant": "Add tenant", + "noTenantSelected": { + "title": "No tenant selected", + "description": "Click to select" + }, + "logout": "Log out" } } diff --git a/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte b/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte new file mode 100644 index 0000000..1fc0379 --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/components/app-sidebar.svelte @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte new file mode 100644 index 0000000..72def17 --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/components/nav-primary.svelte @@ -0,0 +1,70 @@ + + + + {#each items as item (item.title)} + {#if item.isTenatOnly === false || $auth.user?.tenantId} + + + {#snippet child({ props })} + + + {item.title} + + {/snippet} + + + {/if} + {/each} + diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte new file mode 100644 index 0000000..ddfd2dc --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/components/nav-secondary.svelte @@ -0,0 +1,57 @@ + + + + {#each items as item (item.title)} + {#if item.isTenatOnly === false || $auth.user?.tenantId} + + + {#snippet child({ props })} + + + {item.title} + {#if item.url.startsWith("http")} + + {/if} + + {/snippet} + + + {/if} + {/each} + diff --git a/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte b/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte new file mode 100644 index 0000000..b5c075c --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/components/nav-user.svelte @@ -0,0 +1,77 @@ + + + + + + + {#snippet child({ props })} + + + + {nameToAvatarFallback($auth.user?.name)} + + +
+ {$auth.user?.name} + {$auth.user?.email} +
+ +
+ {/snippet} +
+ + +
+ + + {nameToAvatarFallback($auth.user?.name)} + + +
+ {$auth.user?.name} + {$auth.user?.email} +
+
+
+ + + + goto(ROUTES.DASHBOARD.ACCOUNT)}> + + {m["nav.account"]()} + + + + goto(ROUTES.LOGOUT)}> + + {m["nav.logout"]()} + +
+
+
+
diff --git a/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte b/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte new file mode 100644 index 0000000..0e72784 --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/components/tenant-switcher.svelte @@ -0,0 +1,85 @@ + + + + + + + {#snippet child({ props })} + +
+ +
+
+ + {activeTenant?.name ?? m["nav.noTenantSelected.title"]()} + + + {activeTenant?.url ?? m["nav.noTenantSelected.description"]()} + +
+ +
+ {/snippet} +
+ + {m["nav.tenants"]()} + {#each tenants as tenant, index (tenant.id)} + (activeTenantId = tenant.id)} class="gap-2 p-2"> +
+ +
+ {tenant.name} +
+ {/each} + + +
+ +
+
{m["nav.addTenant"]()}
+
+
+
+
+
diff --git a/src/lib/components/layouts/sidebar-layout/index.ts b/src/lib/components/layouts/sidebar-layout/index.ts new file mode 100644 index 0000000..d34dd9a --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/index.ts @@ -0,0 +1,3 @@ +import SidebarLayout from "./root.svelte"; + +export { SidebarLayout }; diff --git a/src/lib/components/layouts/sidebar-layout/root.svelte b/src/lib/components/layouts/sidebar-layout/root.svelte new file mode 100644 index 0000000..e26df38 --- /dev/null +++ b/src/lib/components/layouts/sidebar-layout/root.svelte @@ -0,0 +1,56 @@ + + + sidebar.setOpen(open)}> + + + + +
+
+ + {#if breakcrumbs && breakcrumbs.length > 0} + + + + {#each breakcrumbs as crumb, index (`${crumb.href}-${index}`)} + {#if index === breakcrumbs.length - 1} + + + {crumb.label} + + + {:else} + + + + {/if} +
+
+ {@render children?.()} +
+
+
+
diff --git a/src/lib/components/templates/language-switch/language-switch.svelte b/src/lib/components/templates/language-switch/language-switch.svelte index 890ed83..7c7611e 100644 --- a/src/lib/components/templates/language-switch/language-switch.svelte +++ b/src/lib/components/templates/language-switch/language-switch.svelte @@ -3,12 +3,19 @@ import { getLocale, setLocale } from "$i18n/runtime.js"; import type { ButtonSize, ButtonVariant } from "$lib/components/ui/button"; import { ComboBox } from "$lib/components/ui/combobox"; + import { cn } from "$lib/utils"; let { class: className = "", + triggerClass = "", triggerSize = "default", triggerVariant = "ghost" - }: { triggerVariant?: ButtonVariant; triggerSize?: ButtonSize; class?: string } = $props(); + }: { + triggerVariant?: ButtonVariant; + triggerSize?: ButtonSize; + class?: string; + triggerClass?: string; + } = $props();
@@ -28,6 +35,7 @@ }} {triggerVariant} {triggerSize} - class="w-auto" + {triggerClass} + class={cn("w-auto")} />
diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..249d4a4 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..2bb9db4 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..e37214d --- /dev/null +++ b/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..d06457b --- /dev/null +++ b/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 0000000..a178cf5 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 0000000..1a84c4c --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 0000000..e6bc17d --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,31 @@ + + +{#if child} + {@render child({ props: attrs })} +{:else} + + {@render children?.()} + +{/if} diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 0000000..b5458fa --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,23 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte new file mode 100644 index 0000000..5fb6979 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 0000000..84106a1 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/src/lib/components/ui/breadcrumb/breadcrumb.svelte new file mode 100644 index 0000000..8f8a3e6 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/breadcrumb/index.ts b/src/lib/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..dc914ec --- /dev/null +++ b/src/lib/components/ui/breadcrumb/index.ts @@ -0,0 +1,25 @@ +import Root from "./breadcrumb.svelte"; +import Ellipsis from "./breadcrumb-ellipsis.svelte"; +import Item from "./breadcrumb-item.svelte"; +import Separator from "./breadcrumb-separator.svelte"; +import Link from "./breadcrumb-link.svelte"; +import List from "./breadcrumb-list.svelte"; +import Page from "./breadcrumb-page.svelte"; + +export { + Root, + Ellipsis, + Item, + Separator, + Link, + List, + Page, + // + Root as Breadcrumb, + Ellipsis as BreadcrumbEllipsis, + Item as BreadcrumbItem, + Separator as BreadcrumbSeparator, + Link as BreadcrumbLink, + List as BreadcrumbList, + Page as BreadcrumbPage, +}; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte index 46d9ec8..d5bf4bc 100644 --- a/src/lib/components/ui/button/button.svelte +++ b/src/lib/components/ui/button/button.svelte @@ -5,7 +5,7 @@ import { type VariantProps, tv } from "tailwind-variants"; export const buttonVariants = tv({ - base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 select-none", variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", diff --git a/src/lib/components/ui/collapsible/collapsible-content.svelte b/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..bdabb55 --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..ece7ad6 --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/collapsible/collapsible.svelte b/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..39cdd4e --- /dev/null +++ b/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/ui/collapsible/index.ts b/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..169b479 --- /dev/null +++ b/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Root from "./collapsible.svelte"; +import Trigger from "./collapsible-trigger.svelte"; +import Content from "./collapsible-content.svelte"; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/src/lib/components/ui/combobox/combobox.svelte b/src/lib/components/ui/combobox/combobox.svelte index 7286e41..9353567 100644 --- a/src/lib/components/ui/combobox/combobox.svelte +++ b/src/lib/components/ui/combobox/combobox.svelte @@ -14,7 +14,8 @@ onChange, class: className = "", triggerVariant = "outline", - triggerSize = "default" + triggerSize = "default", + triggerClass = "" }: { labels: { placeholder: string; @@ -25,6 +26,7 @@ onChange: (value: string) => void; triggerVariant?: ButtonVariant; triggerSize?: ButtonSize; + triggerClass?: string; class?: string; options?: { label: string; value: string; keywords?: string[] }[]; } = $props(); @@ -46,17 +48,20 @@ - - + + {#snippet child({ props })} + + {/snippet} diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..e03f949 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,41 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..907ef73 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..48d14a9 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..64bb283 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..f72e477 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..513170a --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,31 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..90f1b6f --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..6974947 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..10e14ca --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..f9b286a --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..1cf9f70 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,49 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +const Sub = DropdownMenuPrimitive.Sub; +const Root = DropdownMenuPrimitive.Root; + +export { + CheckboxItem, + Content, + Root as DropdownMenu, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..09d88f4 --- /dev/null +++ b/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/sheet/index.ts b/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..01d40c8 --- /dev/null +++ b/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,36 @@ +import { Dialog as SheetPrimitive } from "bits-ui"; +import Trigger from "./sheet-trigger.svelte"; +import Close from "./sheet-close.svelte"; +import Overlay from "./sheet-overlay.svelte"; +import Content from "./sheet-content.svelte"; +import Header from "./sheet-header.svelte"; +import Footer from "./sheet-footer.svelte"; +import Title from "./sheet-title.svelte"; +import Description from "./sheet-description.svelte"; + +const Root = SheetPrimitive.Root; +const Portal = SheetPrimitive.Portal; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription, +}; diff --git a/src/lib/components/ui/sheet/sheet-close.svelte b/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000..ae382c1 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-content.svelte b/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000..856922e --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,58 @@ + + + + + + + + {@render children?.()} + + + Close + + + diff --git a/src/lib/components/ui/sheet/sheet-description.svelte b/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000..333b17a --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-footer.svelte b/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000..dd9ed84 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sheet/sheet-header.svelte b/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000..757a6a5 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000..345e197 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-title.svelte b/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000..9fda327 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/ui/sheet/sheet-trigger.svelte b/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000..e266975 --- /dev/null +++ b/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/sidebar/constants.ts b/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..4de4435 --- /dev/null +++ b/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar:state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/src/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..15248ad --- /dev/null +++ b/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,81 @@ +import { IsMobile } from "$lib/hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current + ? (this.openMobile = !this.openMobile) + : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..318a341 --- /dev/null +++ b/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar, +}; diff --git a/src/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000..f121800 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000..6259cb9 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000..fb84e4a --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,36 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000..415255f --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000..e292945 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
    + {@render children?.()} +
    +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000..ec18a69 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000..a1b2db1 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000..19b3666 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000..d862761 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,24 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000..fa3fb0c --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000..69e5a3c --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,29 @@ + + +
    + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000..4bef683 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,103 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000..4db4453 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000..cc63b04 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000..987f104 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,43 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000..681d0f1 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000..8ab1111 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,25 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000..946ccce --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000..5665abe --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,50 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/src/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000..c180cf5 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000..5a7deda --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000..1825182 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000..3e9eba9 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,104 @@ + + +{#if collapsible === "none"} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} + {...restProps} + > + + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..313a7f0 --- /dev/null +++ b/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,21 @@ +import { Tooltip as TooltipPrimitive } from "bits-ui"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; + +const Root = TooltipPrimitive.Root; +const Provider = TooltipPrimitive.Provider; +const Portal = TooltipPrimitive.Portal; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..e495efe --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,47 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..1acdaa4 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/const/routes.ts b/src/lib/const/routes.ts index 2bbbffe..f82bf6f 100644 --- a/src/lib/const/routes.ts +++ b/src/lib/const/routes.ts @@ -8,6 +8,14 @@ export const ROUTES = { LOGIN: "/login", LOGOUT: "/logout", DASHBOARD: { - MAIN: "/dashboard" + MAIN: "/dashboard", + TENANTS: "/dashboard/tenants", + CALENDAR: "/dashboard/calendar", + STAFF: "/dashboard/staff", + AGENTS: "/dashboard/agents", + CHANNELS: "/dashboard/channels", + ABSENCES: "/dashboard/absences", + SETTINGS: "/dashboard/settings", + ACCOUNT: "/dashboard/account" } }; diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/src/lib/stores/auth.ts b/src/lib/stores/auth.ts new file mode 100644 index 0000000..907113b --- /dev/null +++ b/src/lib/stores/auth.ts @@ -0,0 +1,43 @@ +import { writable } from "svelte/store"; + +interface AuthState { + isAuthenticated: boolean; + isRefreshing: boolean; + user?: { + id: string; + email: string; + name: string; + role: string; + // The currently selected tenant + tenantId?: string | null; + }; +} + +function createAuthStore() { + const store = writable({ + isAuthenticated: false, + isRefreshing: false + }); + + return { + ...store, + setRefreshing: (isRefreshing: boolean) => { + store.update((state) => ({ ...state, isRefreshing })); + }, + setAuthenticated: (isAuthenticated: boolean) => { + store.update((state) => ({ ...state, isAuthenticated })); + }, + setUser: (user: AuthState["user"]) => { + store.update((state) => ({ ...state, isAuthenticated: true, user })); + }, + reset: () => { + store.set({ + isAuthenticated: false, + isRefreshing: false, + user: undefined + }); + } + }; +} + +export const auth = createAuthStore(); diff --git a/src/lib/stores/sidebar.ts b/src/lib/stores/sidebar.ts new file mode 100644 index 0000000..3cba045 --- /dev/null +++ b/src/lib/stores/sidebar.ts @@ -0,0 +1,25 @@ +import { browser } from "$app/environment"; +import { writable } from "svelte/store"; + +interface AuthState { + isOpen: boolean; +} + +const LOCAL_STORAGE_KEY = "sidebar-is-open"; + +function createSidebarStore() { + const storedValue = browser ? localStorage.getItem(LOCAL_STORAGE_KEY) : null; + const store = writable({ + isOpen: storedValue === "true" ? true : false + }); + + return { + ...store, + setOpen: (isOpen: boolean) => { + localStorage.setItem(LOCAL_STORAGE_KEY, `${isOpen}`); + store.update((state) => ({ ...state, isOpen })); + } + }; +} + +export const sidebar = createSidebarStore(); diff --git a/src/lib/types/tenant.ts b/src/lib/types/tenant.ts new file mode 100644 index 0000000..0b2419c --- /dev/null +++ b/src/lib/types/tenant.ts @@ -0,0 +1,6 @@ +export type TTenant = { + id: string; + name: string; + url: string; + logo?: string; +}; diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts new file mode 100644 index 0000000..2ee0868 --- /dev/null +++ b/src/lib/types/user.ts @@ -0,0 +1,9 @@ +import type { UserRole } from "$lib/server/auth/authorization-service"; + +export type TUser = { + id: string; + email: string; + name: string; + role: UserRole; + tenantId: string | null; +}; diff --git a/src/lib/utils/name.ts b/src/lib/utils/name.ts new file mode 100644 index 0000000..0801ddc --- /dev/null +++ b/src/lib/utils/name.ts @@ -0,0 +1,8 @@ +export const nameToAvatarFallback = (name?: string) => { + if (!name) return "NN"; + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase(); +}; diff --git a/src/lib/utils/routes.ts b/src/lib/utils/routes.ts new file mode 100644 index 0000000..ca4a9bc --- /dev/null +++ b/src/lib/utils/routes.ts @@ -0,0 +1,16 @@ +import { page } from "$app/state"; +import { ROUTES } from "$lib/const/routes"; + +export const isCurrentSection = (sectionPath: string): boolean => { + // Select home when on dashboard main + if (page.url.pathname === ROUTES.DASHBOARD.MAIN && sectionPath === ROUTES.DASHBOARD.MAIN) { + return true; + } + + // Not always select home + if (page.url.pathname.startsWith(sectionPath) && sectionPath === ROUTES.DASHBOARD.MAIN) { + return false; + } + + return page.url.pathname.startsWith(sectionPath); +}; diff --git a/src/lib/utils/session.ts b/src/lib/utils/session.ts new file mode 100644 index 0000000..8bd2d9d --- /dev/null +++ b/src/lib/utils/session.ts @@ -0,0 +1,48 @@ +import { goto } from "$app/navigation"; +import { ROUTES } from "$lib/const/routes"; +import { auth } from "$lib/stores/auth"; + +export const refreshSession = async () => { + auth.setRefreshing(true); + + try { + const response = await fetch("/api/auth/refresh", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + credentials: "same-origin" + }); + + if (!response.ok) { + // Token is invalid, clear and redirect to login + if (response.status === 401) { + auth.setRefreshing(false); + goto(ROUTES.LOGOUT); + return; + } + } + + refreshUserData(); + } finally { + auth.setRefreshing(false); + } +}; + +export const refreshUserData = async () => { + try { + const response = await fetch("/api/auth/session", { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + credentials: "same-origin" + }); + + const data = await response.json(); + console.log("refreshed user data", data); + // auth.setUser(data.user); + } catch (error) { + console.error("Failed to refresh user data", error); + } +}; diff --git a/src/routes/(pages)/dashboard/+layout.svelte b/src/routes/(pages)/dashboard/+layout.svelte new file mode 100644 index 0000000..0bfe2c8 --- /dev/null +++ b/src/routes/(pages)/dashboard/+layout.svelte @@ -0,0 +1,32 @@ + + +{@render children()} diff --git a/src/routes/(pages)/dashboard/+page.svelte b/src/routes/(pages)/dashboard/+page.svelte index f29a407..e73314b 100644 --- a/src/routes/(pages)/dashboard/+page.svelte +++ b/src/routes/(pages)/dashboard/+page.svelte @@ -1,2 +1,14 @@ -Hello from Dashboard
    -Logout + + + diff --git a/src/routes/(pages)/dashboard/absences/+page.svelte b/src/routes/(pages)/dashboard/absences/+page.svelte new file mode 100644 index 0000000..cffe1af --- /dev/null +++ b/src/routes/(pages)/dashboard/absences/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/dashboard/account/+page.svelte b/src/routes/(pages)/dashboard/account/+page.svelte new file mode 100644 index 0000000..bb69be5 --- /dev/null +++ b/src/routes/(pages)/dashboard/account/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/dashboard/agents/+page.svelte b/src/routes/(pages)/dashboard/agents/+page.svelte new file mode 100644 index 0000000..001ed86 --- /dev/null +++ b/src/routes/(pages)/dashboard/agents/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/dashboard/calendar/+page.svelte b/src/routes/(pages)/dashboard/calendar/+page.svelte new file mode 100644 index 0000000..144e228 --- /dev/null +++ b/src/routes/(pages)/dashboard/calendar/+page.svelte @@ -0,0 +1,8 @@ + + + diff --git a/src/routes/(pages)/dashboard/channels/+page.svelte b/src/routes/(pages)/dashboard/channels/+page.svelte new file mode 100644 index 0000000..ed8fa2a --- /dev/null +++ b/src/routes/(pages)/dashboard/channels/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/dashboard/settings/+page.svelte b/src/routes/(pages)/dashboard/settings/+page.svelte new file mode 100644 index 0000000..f705c4a --- /dev/null +++ b/src/routes/(pages)/dashboard/settings/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/dashboard/staff/+page.svelte b/src/routes/(pages)/dashboard/staff/+page.svelte new file mode 100644 index 0000000..4de5067 --- /dev/null +++ b/src/routes/(pages)/dashboard/staff/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/dashboard/tenants/+page.svelte b/src/routes/(pages)/dashboard/tenants/+page.svelte new file mode 100644 index 0000000..8fa19eb --- /dev/null +++ b/src/routes/(pages)/dashboard/tenants/+page.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/routes/(pages)/login/+page.server.ts b/src/routes/(pages)/login/+page.server.ts index b2699d0..c6926ce 100644 --- a/src/routes/(pages)/login/+page.server.ts +++ b/src/routes/(pages)/login/+page.server.ts @@ -4,6 +4,10 @@ import { zod } from "sveltekit-superforms/adapters"; import type { Actions, PageServerLoad } from "./$types"; import { formSchema } from "./schema"; import type { WebAuthnCredential } from "$lib/server/auth/webauthn-service"; +import type { TUser } from "$lib/types/user"; +import logger from "$lib/logger"; + +const log = logger.setContext("/login"); export const load: PageServerLoad = async () => { return { @@ -16,11 +20,10 @@ export const actions: Actions = { const form = await superValidate(event, zod(formSchema)); if (!form.valid) { - if (!form.valid) { - return fail(400, { - form: { ...form, data: { ...form.data, type: "passkey" } } - }); - } + log.error("Login form is not valid", { errors: form.errors }); + return fail(400, { + form: { ...form, data: { ...form.data, type: "passkey" } } + }); } let body: { credential?: WebAuthnCredential; email: string; passphrase?: string } = { @@ -52,8 +55,22 @@ export const actions: Actions = { body: JSON.stringify(body) }); - if (resp.status < 400) { - return { form }; + let user: TUser | null = null; + try { + const respJson = await resp.json(); + user = { + id: respJson.user.id, + email: respJson.user.email, + name: respJson.user.name, + role: respJson.user.role, + tenantId: respJson.user.tenantId + }; + } catch (error) { + log.error("Error parsing login response JSON", { error }); + } + + if (resp.status < 400 && user) { + return { form, user }; } else { return fail(400, { form diff --git a/src/routes/(pages)/login/login-form.svelte b/src/routes/(pages)/login/login-form.svelte index d3a5183..5da576c 100644 --- a/src/routes/(pages)/login/login-form.svelte +++ b/src/routes/(pages)/login/login-form.svelte @@ -17,6 +17,7 @@ import type { PasskeyState } from "$lib/components/ui/passkey/state.svelte"; import { arrayBufferToBase64, fetchChallenge, getCredential } from "$lib/utils/passkey"; import { Label } from "$lib/components/ui/label"; + import { auth } from "$lib/stores/auth"; let { data, @@ -34,6 +35,7 @@ }, onResult: async (event) => { if (event.result.type === "success") { + auth.setUser(event.result.data?.user); await goto(ROUTES.DASHBOARD.MAIN); } else { toast.error(m["login.error"]()); diff --git a/src/routes/(pages)/logout/+page.server.ts b/src/routes/(pages)/logout/+page.server.ts index 9a447df..81414c0 100644 --- a/src/routes/(pages)/logout/+page.server.ts +++ b/src/routes/(pages)/logout/+page.server.ts @@ -1,3 +1,4 @@ +import { auth } from "$lib/stores/auth"; import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async (event) => { @@ -13,5 +14,7 @@ export const load: PageServerLoad = async (event) => { return resp.status < 400; }); + auth.reset(); + return { streaming: { success } }; }; diff --git a/src/server-hooks/authGuard.ts b/src/server-hooks/authGuard.ts index 37bf404..5489e89 100644 --- a/src/server-hooks/authGuard.ts +++ b/src/server-hooks/authGuard.ts @@ -3,6 +3,7 @@ import { SessionService } from "$lib/server/auth/session-service"; import { AuthorizationService } from "$lib/server/auth/authorization-service"; import { getAccessToken } from "./utils/accessToken"; import { ROUTES } from "$lib/const/routes"; +import { auth } from "$lib/stores/auth"; export const authGuard: Handle = async ({ event, resolve }) => { const { url } = event; @@ -24,6 +25,15 @@ export const authGuard: Handle = async ({ event, resolve }) => { const sessionData = await SessionService.validateTokenWithDB(accessToken); if (!sessionData) redirect(302, ROUTES.LOGIN); + // Set user in auth store for SSR + auth.setUser({ + id: sessionData.user.id, + email: sessionData.user.email, + name: sessionData.user.name, + role: sessionData.user.role, + tenantId: sessionData.user.tenantId + }); + switch (true) { case isDashboardRoute && AuthorizationService.hasAnyRole(sessionData.user, [