Files
collective-lists/apps/web/src/lib/components/Avatar.svelte
Oier Bravo Urtasun 7d91705fc2 feat(fase-1): auth, collective management, and DB migrations
- SQL migrations: users, collectives, collective_members, collective_invitations,
  full RLS policies, is_active_member/is_member/is_admin helpers,
  accept_invitation SECURITY DEFINER function, admin auto-promote trigger,
  avatars storage bucket
- Auth refactor: replace keycloak-js with Supabase OAuth (GoTrue OIDC proxy,
  Option B). signInWithOAuth({ provider: 'keycloak' }) + PKCE flow
- GoTrue config: GOTRUE_EXTERNAL_KEYCLOAK_URL + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI
  added to docker-compose.dev.yml; Keycloak realm updated with GoTrue callback URI
- New routes: /auth/callback, /invitation/[token], /(app)/collective/manage
- Functional onboarding: create collective or join via invite link
- Full settings page: display name (debounced), avatar (initials/emoji/upload),
  language switcher, Keycloak account console link
- Components: Avatar.svelte (initials/emoji/upload with fallback),
  ImageCropper.svelte (cropperjs, 1:1 crop, lazy-loaded)
- i18n: added all Fase 1 message keys (en + es); renamed 'delete' → 'action_delete'
  (JS reserved word); fixed plugin-m-function-matcher major-version URL format
- Database types: manually seeded packages/types/src/database.ts from migrations
- .env.development: committed public dev defaults for SvelteKit type checking
- CLAUDE.md: documented /etc/hosts requirement for keycloak hostname

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 15:29:29 +02:00

76 lines
1.8 KiB
Svelte

<script lang="ts">
interface Props {
/** Display name — used to generate initials and background colour. */
name: string;
/** Avatar type. Defaults to initials. */
type?: 'initials' | 'emoji' | 'upload';
/** Emoji character when type = 'emoji'. */
emoji?: string | null;
/** Image URL when type = 'upload'. */
src?: string | null;
/** Diameter in px. */
size?: number;
}
let { name, type = 'initials', emoji = null, src = null, size = 36 }: Props = $props();
let imgError = $state(false);
// Two initials from display name
const initials = $derived(
name
.split(/\s+/)
.slice(0, 2)
.map((w) => w[0]?.toUpperCase() ?? '')
.join('')
);
// Deterministic background colour from name (one of 8 slate-adjacent tones)
const COLOURS = [
'bg-slate-400',
'bg-blue-400',
'bg-violet-400',
'bg-emerald-400',
'bg-amber-400',
'bg-rose-400',
'bg-cyan-400',
'bg-pink-400'
];
const colourClass = $derived(
COLOURS[
name.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0) % COLOURS.length
]
);
const fontSize = $derived(Math.round(size * 0.38));
</script>
<div
class="flex flex-shrink-0 items-center justify-center overflow-hidden rounded-full"
style="width: {size}px; height: {size}px;"
>
{#if type === 'upload' && src && !imgError}
<img
{src}
alt={name}
class="h-full w-full object-cover"
onerror={() => (imgError = true)}
/>
{:else if type === 'emoji' && emoji}
<span
class="flex h-full w-full items-center justify-center bg-slate-100 dark:bg-slate-700"
style="font-size: {fontSize * 1.4}px;"
>
{emoji}
</span>
{:else}
<!-- Initials fallback (also used when upload fails) -->
<span
class="flex h-full w-full items-center justify-center font-semibold text-white {colourClass}"
style="font-size: {fontSize}px;"
>
{initials || '?'}
</span>
{/if}
</div>