feat(fase-10): CU-H07 rename collective + change emoji
Admin gets an editable name input + emoji picker at the top of /collective/manage. Enter / blur / Save button all flush the PostgREST UPDATE; Esc reverts to the persisted value. The response payload feeds both $currentCollective and $userCollectives so the sidebar updates without reload. Members and guests see no editing affordances at all (the section is admin-gated; UPDATE policies were already in place from Fase 2a so no migration is needed). MC-01a: Ana renames + picks a new emoji, reload preserves both, DB reflects the change. MC-01b: Borja never sees the editable fields. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { getSupabase } from '$lib/supabase';
|
||||
import { currentUser } from '$lib/stores/auth';
|
||||
import { currentCollective, collectiveMembers } from '$lib/stores/collective';
|
||||
import { currentCollective, collectiveMembers, userCollectives } from '$lib/stores/collective';
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
@@ -143,6 +143,75 @@
|
||||
if (role === 'member') return m.role_member();
|
||||
return m.role_guest();
|
||||
}
|
||||
|
||||
// ── Fase 10.2: rename + change emoji (admin only) ───────────────────────
|
||||
let nameDraft = $state('');
|
||||
let nameSaving = $state(false);
|
||||
let nameSaved = $state(false);
|
||||
let emojiPickerOpen = $state(false);
|
||||
|
||||
const EMOJI_OPTIONS = [
|
||||
'🏠', '🏡', '🏢', '🏣', '🏤', '🏥', '🏦', '🏨',
|
||||
'🏩', '🏪', '🏫', '🏬', '🏭', '🏯', '🏰', '🗼',
|
||||
'🌆', '🌇', '🌃', '🌉', '🏙️', '⛺', '🌴', '🌵'
|
||||
];
|
||||
|
||||
// Keep nameDraft in sync with $currentCollective.
|
||||
$effect(() => {
|
||||
if ($currentCollective) nameDraft = $currentCollective.name;
|
||||
});
|
||||
|
||||
async function saveCollectiveName() {
|
||||
if (!$currentCollective || !isAdmin) return;
|
||||
const trimmed = nameDraft.trim();
|
||||
if (!trimmed || trimmed === $currentCollective.name) return;
|
||||
nameSaving = true;
|
||||
const { data, error: updErr } = await getSupabase()
|
||||
.from('collectives')
|
||||
.update({ name: trimmed })
|
||||
.eq('id', $currentCollective.id)
|
||||
.select('id, name, emoji, created_at')
|
||||
.single();
|
||||
nameSaving = false;
|
||||
if (updErr || !data) {
|
||||
error = updErr?.message ?? 'Failed to save name.';
|
||||
return;
|
||||
}
|
||||
applyCollectivePatch(data as { id: string; name: string; emoji: string; created_at: string });
|
||||
nameSaved = true;
|
||||
setTimeout(() => (nameSaved = false), 1500);
|
||||
}
|
||||
|
||||
function onNameKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void saveCollectiveName();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if ($currentCollective) nameDraft = $currentCollective.name;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectEmoji(emoji: string) {
|
||||
emojiPickerOpen = false;
|
||||
if (!$currentCollective || !isAdmin || emoji === $currentCollective.emoji) return;
|
||||
const { data, error: updErr } = await getSupabase()
|
||||
.from('collectives')
|
||||
.update({ emoji })
|
||||
.eq('id', $currentCollective.id)
|
||||
.select('id, name, emoji, created_at')
|
||||
.single();
|
||||
if (updErr || !data) {
|
||||
error = updErr?.message ?? 'Failed to save emoji.';
|
||||
return;
|
||||
}
|
||||
applyCollectivePatch(data as { id: string; name: string; emoji: string; created_at: string });
|
||||
}
|
||||
|
||||
function applyCollectivePatch(c: { id: string; name: string; emoji: string; created_at: string }) {
|
||||
currentCollective.set(c);
|
||||
userCollectives.update((list) => list.map((it) => (it.id === c.id ? c : it)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
@@ -161,6 +230,72 @@
|
||||
<p class="mb-4 text-sm text-red-600">{error}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Collective identity (Fase 10.2) — admin only -->
|
||||
{#if isAdmin && $currentCollective}
|
||||
<section class="mb-8">
|
||||
<div class="relative flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="collective-emoji-toggle"
|
||||
onclick={() => (emojiPickerOpen = !emojiPickerOpen)}
|
||||
class="flex h-12 w-12 items-center justify-center rounded-lg bg-surface-container-low text-3xl
|
||||
hover:bg-black/5 dark:bg-surface-raised dark:hover:bg-white/5"
|
||||
aria-label={m.manage_collective_emoji_label()}
|
||||
>
|
||||
{$currentCollective.emoji}
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<label for="collective-name-edit" class="mb-1 block text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||
{m.manage_collective_name_label()}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="collective-name-edit"
|
||||
data-testid="collective-name-edit"
|
||||
type="text"
|
||||
bind:value={nameDraft}
|
||||
onkeydown={onNameKeydown}
|
||||
onblur={() => void saveCollectiveName()}
|
||||
maxlength="60"
|
||||
class="w-full rounded-lg border border-slate-300 bg-surface px-3 py-2 text-sm
|
||||
focus:border-slate-500 focus:outline-none dark:border-slate-600 dark:bg-slate-800"
|
||||
/>
|
||||
{#if nameSaving}
|
||||
<span class="absolute right-3 top-2 text-xs text-text-secondary">{m.settings_saving()}</span>
|
||||
{:else if nameSaved}
|
||||
<span class="absolute right-3 top-2 text-xs text-emerald-600">✓</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="collective-name-save"
|
||||
onclick={() => void saveCollectiveName()}
|
||||
class="self-end rounded-md bg-slate-900 px-3 py-2 text-xs font-semibold text-white hover:opacity-90
|
||||
dark:bg-slate-50 dark:text-slate-900"
|
||||
>
|
||||
{m.manage_collective_save()}
|
||||
</button>
|
||||
</div>
|
||||
{#if emojiPickerOpen}
|
||||
<div class="mt-3 grid grid-cols-8 gap-1 rounded-lg bg-surface-container-low p-3 dark:bg-surface-raised">
|
||||
{#each EMOJI_OPTIONS as e}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="collective-emoji-option-{e}"
|
||||
class="flex h-9 w-9 items-center justify-center rounded-md text-xl transition-colors
|
||||
{$currentCollective.emoji === e
|
||||
? 'bg-slate-900 dark:bg-slate-100'
|
||||
: 'hover:bg-slate-100 dark:hover:bg-slate-700'}"
|
||||
onclick={() => void selectEmoji(e)}
|
||||
aria-label={e}
|
||||
>{e}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Members list — no dividers, vertical padding creates separation -->
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||
|
||||
Reference in New Issue
Block a user