- 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>
76 lines
1.8 KiB
Svelte
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>
|