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>
This commit is contained in:
92
apps/web/src/lib/components/ImageCropper.svelte
Normal file
92
apps/web/src/lib/components/ImageCropper.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user