Add OrgAvatar component and frontend-utils package
Some checks failed
CI / ci (push) Has been cancelled
Some checks failed
CI / ci (push) Has been cancelled
- Create @reviq/frontend-utils package for frontend-specific utilities - Add OrgAvatar component with size variants (xs, sm, md, lg, xl) - Display org initials with deterministic colors when no logo available - Add getOrgInitials and getOrgColor utility functions - Update org-switcher and all org display pages to use OrgAvatar - Add noNonNullAssertion lint rule as error in biome.jsonc Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"@orpc/contract": "^1.13.2",
|
||||
"@reviq/api-contract": "workspace:*",
|
||||
"@reviq/common": "workspace:*",
|
||||
"@reviq/frontend-utils": "workspace:*",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@tanstack/svelte-query": "^6.0.14",
|
||||
"@tanstack/svelte-query-devtools": "^6.0.3",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Check } from "@lucide/svelte";
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { getContext } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { OrgAvatar } from "$lib/components/org";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
@@ -18,9 +20,10 @@ const orgsQuery = createQuery(() => ({
|
||||
}));
|
||||
|
||||
const orgs = $derived(orgsQuery.data ?? []);
|
||||
const currentOrg = $derived(orgs.find((org) => org.slug === currentSlug));
|
||||
|
||||
function handleOrgSelect(slug: string) {
|
||||
goto(resolve(`/dashboard/${slug}`));
|
||||
goto(resolve("/dashboard/[slug]", { slug }));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,17 +33,24 @@ function handleOrgSelect(slug: string) {
|
||||
<button
|
||||
{...props}
|
||||
aria-label="Switch organization"
|
||||
class="group flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-b from-[#303035] to-[#26262c] shadow-sm transition-transform duration-200 hover:scale-105"
|
||||
class="group flex h-8 w-8 items-center justify-center transition-transform duration-200 hover:scale-105"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-white transition-transform duration-200 group-hover:scale-110"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
{#if currentOrg}
|
||||
<OrgAvatar org={currentOrg} size="md" />
|
||||
{:else}
|
||||
<!-- Default icon when no org is selected -->
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-b from-[#303035] to-[#26262c] shadow-sm">
|
||||
<svg
|
||||
class="h-4 w-4 text-white transition-transform duration-200 group-hover:scale-110"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
@@ -59,18 +69,10 @@ function handleOrgSelect(slug: string) {
|
||||
class={cn(isActive && "bg-accent")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if org.logoUrl}
|
||||
<img src={org.logoUrl} alt="" class="h-5 w-5 rounded" />
|
||||
{:else}
|
||||
<div class="flex h-5 w-5 items-center justify-center rounded bg-muted text-[10px] font-medium">
|
||||
{org.displayName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
{/if}
|
||||
<OrgAvatar {org} size="xs" />
|
||||
<span class="flex-1 truncate">{org.displayName}</span>
|
||||
{#if isActive}
|
||||
<svg class="h-4 w-4 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20,6 9,17 4,12" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<Check class="h-4 w-4 text-primary" />
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as ConfirmDialog } from "./confirm-dialog.svelte";
|
||||
export { default as OrgAvatar } from "./org-avatar.svelte";
|
||||
export { default as RoleBadge } from "./role-badge.svelte";
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getOrgColor,
|
||||
getOrgInitials,
|
||||
type OrgLike,
|
||||
} from "@reviq/frontend-utils";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
interface Props {
|
||||
org: OrgLike | null | undefined;
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { org, size = "md", class: className }: Props = $props();
|
||||
|
||||
const initials = $derived(getOrgInitials(org));
|
||||
const colorClass = $derived(getOrgColor(org));
|
||||
|
||||
const sizeClasses = {
|
||||
xs: "h-5 w-5 text-[10px] rounded",
|
||||
sm: "h-6 w-6 text-[10px] rounded",
|
||||
md: "h-8 w-8 text-xs rounded-lg",
|
||||
lg: "h-10 w-10 text-sm rounded-lg",
|
||||
xl: "h-16 w-16 text-xl rounded-xl",
|
||||
} as const;
|
||||
</script>
|
||||
|
||||
{#if org?.logoUrl}
|
||||
<img
|
||||
src={org.logoUrl}
|
||||
alt="{org.displayName} logo"
|
||||
class={cn(sizeClasses[size], "shrink-0 object-cover", className)}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
"flex shrink-0 items-center justify-center bg-gradient-to-br font-semibold text-white",
|
||||
sizeClasses[size],
|
||||
colorClass,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
Building2,
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
@@ -21,6 +20,7 @@ import { goto } from "$app/navigation";
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import { OrgAvatar } from "$lib/components/org";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
@@ -121,17 +121,7 @@ function isExpiringSoon(expiresAt: Date): boolean {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start gap-4">
|
||||
{#if invite.org.logoUrl}
|
||||
<img
|
||||
src={invite.org.logoUrl}
|
||||
alt="{invite.org.displayName} logo"
|
||||
class="h-16 w-16 rounded-xl object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-xl bg-gradient-to-br from-primary/20 to-primary/10">
|
||||
<Building2 class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
{/if}
|
||||
<OrgAvatar org={invite.org} size="xl" />
|
||||
<div class="flex-1">
|
||||
<CardTitle class="text-xl">{invite.org.displayName}</CardTitle>
|
||||
<CardDescription class="mt-1">
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Building,
|
||||
Globe,
|
||||
Loader2,
|
||||
Plus,
|
||||
@@ -17,7 +16,7 @@ import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { api } from "$lib/api/client";
|
||||
import { AdminLayout } from "$lib/components/layout";
|
||||
import { ConfirmDialog } from "$lib/components/org";
|
||||
import { ConfirmDialog, OrgAvatar } from "$lib/components/org";
|
||||
import { Alert, AlertDescription } from "$lib/components/ui/alert";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
@@ -259,19 +258,7 @@ async function executeConfirmAction() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex items-start gap-4">
|
||||
{#if org.logoUrl}
|
||||
<img
|
||||
src={org.logoUrl}
|
||||
alt="{org.displayName} logo"
|
||||
class="h-16 w-16 rounded-lg object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-16 w-16 items-center justify-center rounded-lg bg-muted"
|
||||
>
|
||||
<Building class="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
{/if}
|
||||
<OrgAvatar {org} size="xl" />
|
||||
<div class="flex-1">
|
||||
<CardTitle class="text-2xl">{org.displayName}</CardTitle>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createQuery } from "@tanstack/svelte-query";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { DashboardLayout } from "$lib/components/layout";
|
||||
import { OrgAvatar } from "$lib/components/org";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
@@ -70,17 +71,7 @@ $effect(() => {
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
{#if invite.org.logoUrl}
|
||||
<img
|
||||
src={invite.org.logoUrl}
|
||||
alt="{invite.org.displayName} logo"
|
||||
class="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/20">
|
||||
<Building2 class="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
{/if}
|
||||
<OrgAvatar org={invite.org} size="lg" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="truncate text-base">
|
||||
{invite.org.displayName}
|
||||
@@ -154,18 +145,7 @@ $effect(() => {
|
||||
<Card class="h-full transition-colors group-hover:border-primary/50">
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Logo or placeholder -->
|
||||
{#if org.logoUrl}
|
||||
<img
|
||||
src={org.logoUrl}
|
||||
alt="{org.displayName} logo"
|
||||
class="h-10 w-10 rounded-lg object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-primary/20 to-primary/10">
|
||||
<Building2 class="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
{/if}
|
||||
<OrgAvatar {org} size="lg" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<CardTitle class="truncate text-base">
|
||||
{org.displayName}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
AlertCircle,
|
||||
Building2,
|
||||
ChevronRight,
|
||||
Globe,
|
||||
Loader2,
|
||||
@@ -13,7 +12,7 @@ import { getContext } from "svelte";
|
||||
import { resolve } from "$app/paths";
|
||||
import { api } from "$lib/api/client";
|
||||
import { DashboardLayout } from "$lib/components/layout";
|
||||
import { RoleBadge } from "$lib/components/org";
|
||||
import { OrgAvatar, RoleBadge } from "$lib/components/org";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -98,17 +97,7 @@ const orgName = $derived(orgQuery.data?.displayName ?? slug);
|
||||
<!-- Header with org info -->
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
{#if orgQuery.data?.logoUrl}
|
||||
<img
|
||||
src={orgQuery.data.logoUrl}
|
||||
alt="{orgName} logo"
|
||||
class="h-16 w-16 rounded-xl object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-xl bg-gradient-to-br from-primary/20 to-primary/10">
|
||||
<Building2 class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
{/if}
|
||||
<OrgAvatar org={orgQuery.data} size="xl" />
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">{orgName}</h1>
|
||||
<p class="text-sm text-muted-foreground">{slug}</p>
|
||||
|
||||
Reference in New Issue
Block a user