- 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>
93 lines
2.5 KiB
Svelte
93 lines
2.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
|
|
interface Props {
|
|
file: File;
|
|
onCrop: (blob: Blob) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
let { file, onCrop, onCancel }: Props = $props();
|
|
|
|
let canvasEl: HTMLCanvasElement;
|
|
let imgEl: HTMLImageElement;
|
|
let cropperInstance: unknown = null;
|
|
let objectUrl = $state('');
|
|
|
|
onMount(async () => {
|
|
// Lazy-load Cropper.js only when the component mounts
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const CropperModule = await import('cropperjs') as any;
|
|
const Cropper = CropperModule.default ?? CropperModule;
|
|
|
|
objectUrl = URL.createObjectURL(file);
|
|
imgEl.src = objectUrl;
|
|
|
|
imgEl.onload = () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
cropperInstance = new (Cropper as any)(imgEl, {
|
|
aspectRatio: 1,
|
|
viewMode: 1,
|
|
autoCropArea: 0.8,
|
|
movable: true,
|
|
zoomable: true,
|
|
rotatable: false,
|
|
scalable: false
|
|
});
|
|
};
|
|
});
|
|
|
|
onDestroy(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(cropperInstance as any)?.destroy();
|
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
});
|
|
|
|
function crop() {
|
|
if (!cropperInstance) return;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const canvas: HTMLCanvasElement = (cropperInstance as any).getCroppedCanvas({ width: 256, height: 256 });
|
|
canvas.toBlob(
|
|
(blob) => {
|
|
if (blob) onCrop(blob);
|
|
},
|
|
'image/webp',
|
|
0.85
|
|
);
|
|
}
|
|
</script>
|
|
|
|
<!-- Import Cropper.js CSS via cdn-style bundled import -->
|
|
<svelte:head>
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.css"
|
|
/>
|
|
</svelte:head>
|
|
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
|
<div class="w-full max-w-sm rounded-xl bg-white p-4 dark:bg-slate-900">
|
|
<p class="mb-3 text-sm font-semibold text-slate-900 dark:text-slate-50">Crop photo</p>
|
|
<div class="mb-4 max-h-72 overflow-hidden rounded-lg">
|
|
<!-- svelte-ignore a11y_missing_attribute -->
|
|
<img bind:this={imgEl} class="block max-w-full" />
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={onCancel}
|
|
class="flex-1 rounded-lg border border-slate-300 px-4 py-2 text-sm text-slate-700
|
|
hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onclick={crop}
|
|
class="flex-1 rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white
|
|
hover:opacity-90 dark:bg-slate-50 dark:text-slate-900"
|
|
>
|
|
Use photo
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|