feat: add event module pages (placeholders + full Team module) - 6 placeholder 'coming soon' pages: tasks, files, schedule, budget, guests, sponsors - Full Team module: add/remove members, change roles, role badges - Uses existing event_members DB table and API layer - i18n keys added for EN and ET (module placeholders + team) - svelte-check: 0 errors, vitest: 112/112 passed
This commit is contained in:
32
src/routes/[orgSlug]/events/[eventSlug]/budget/+page.svelte
Normal file
32
src/routes/[orgSlug]/events/[eventSlug]/budget/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
event: { name: string; slug: string };
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_budget()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40 p-6">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>account_balance_wallet</span
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">{m.events_mod_budget()}</h2>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.events_mod_budget_desc()}
|
||||
</p>
|
||||
<span
|
||||
class="mt-4 text-[11px] text-light/20 bg-light/5 px-3 py-1 rounded-full"
|
||||
>{m.module_coming_soon()}</span
|
||||
>
|
||||
</div>
|
||||
32
src/routes/[orgSlug]/events/[eventSlug]/files/+page.svelte
Normal file
32
src/routes/[orgSlug]/events/[eventSlug]/files/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
event: { name: string; slug: string };
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_files()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40 p-6">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>folder</span
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">{m.events_mod_files()}</h2>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.events_mod_files_desc()}
|
||||
</p>
|
||||
<span
|
||||
class="mt-4 text-[11px] text-light/20 bg-light/5 px-3 py-1 rounded-full"
|
||||
>{m.module_coming_soon()}</span
|
||||
>
|
||||
</div>
|
||||
32
src/routes/[orgSlug]/events/[eventSlug]/guests/+page.svelte
Normal file
32
src/routes/[orgSlug]/events/[eventSlug]/guests/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
event: { name: string; slug: string };
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_guests()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40 p-6">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>groups</span
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">{m.events_mod_guests()}</h2>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.events_mod_guests_desc()}
|
||||
</p>
|
||||
<span
|
||||
class="mt-4 text-[11px] text-light/20 bg-light/5 px-3 py-1 rounded-full"
|
||||
>{m.module_coming_soon()}</span
|
||||
>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
event: { name: string; slug: string };
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_schedule()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40 p-6">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>calendar_today</span
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">{m.events_mod_schedule()}</h2>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.events_mod_schedule_desc()}
|
||||
</p>
|
||||
<span
|
||||
class="mt-4 text-[11px] text-light/20 bg-light/5 px-3 py-1 rounded-full"
|
||||
>{m.module_coming_soon()}</span
|
||||
>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
event: { name: string; slug: string };
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_sponsors()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40 p-6">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>handshake</span
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">{m.events_mod_sponsors()}</h2>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.events_mod_sponsors_desc()}
|
||||
</p>
|
||||
<span
|
||||
class="mt-4 text-[11px] text-light/20 bg-light/5 px-3 py-1 rounded-full"
|
||||
>{m.module_coming_soon()}</span
|
||||
>
|
||||
</div>
|
||||
32
src/routes/[orgSlug]/events/[eventSlug]/tasks/+page.svelte
Normal file
32
src/routes/[orgSlug]/events/[eventSlug]/tasks/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
event: { name: string; slug: string };
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_tasks()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center h-full text-light/40 p-6">
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 64px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>task_alt</span
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">{m.events_mod_tasks()}</h2>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.events_mod_tasks_desc()}
|
||||
</p>
|
||||
<span
|
||||
class="mt-4 text-[11px] text-light/20 bg-light/5 px-3 py-1 rounded-full"
|
||||
>{m.module_coming_soon()}</span
|
||||
>
|
||||
</div>
|
||||
490
src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte
Normal file
490
src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte
Normal file
@@ -0,0 +1,490 @@
|
||||
<script lang="ts">
|
||||
import { Avatar, Button, Modal, Select } from "$lib/components/ui";
|
||||
import { getContext } from "svelte";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
import { toasts } from "$lib/stores/ui";
|
||||
import type { Event, EventMember } from "$lib/api/events";
|
||||
import * as m from "$lib/paraglide/messages";
|
||||
|
||||
interface OrgMember {
|
||||
id: string;
|
||||
user_id: string;
|
||||
role: string;
|
||||
profiles: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string | null;
|
||||
avatar_url: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
userRole: string;
|
||||
members: OrgMember[];
|
||||
event: Event;
|
||||
eventMembers: (EventMember & {
|
||||
profile?: {
|
||||
id: string;
|
||||
email: string;
|
||||
full_name: string | null;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
})[];
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const supabase = getContext<SupabaseClient<Database>>("supabase");
|
||||
|
||||
const isEditor = $derived(
|
||||
["owner", "admin", "editor"].includes(data.userRole),
|
||||
);
|
||||
|
||||
// Local mutable copy of event members
|
||||
let teamMembers = $state(data.eventMembers);
|
||||
|
||||
// Sync when data changes (e.g. after invalidation)
|
||||
$effect(() => {
|
||||
teamMembers = data.eventMembers;
|
||||
});
|
||||
|
||||
// Add member modal
|
||||
let showAddModal = $state(false);
|
||||
let selectedUserId = $state("");
|
||||
let selectedRole = $state<"lead" | "manager" | "member">("member");
|
||||
let adding = $state(false);
|
||||
|
||||
// Remove confirmation
|
||||
let memberToRemove = $state<(typeof teamMembers)[0] | null>(null);
|
||||
let removing = $state(false);
|
||||
|
||||
// Edit role
|
||||
let editingMember = $state<(typeof teamMembers)[0] | null>(null);
|
||||
let editRole = $state<"lead" | "manager" | "member">("member");
|
||||
let updatingRole = $state(false);
|
||||
|
||||
// Org members not yet on the team
|
||||
const availableMembers = $derived(
|
||||
data.members.filter(
|
||||
(om) => !teamMembers.some((tm) => tm.user_id === om.user_id),
|
||||
),
|
||||
);
|
||||
|
||||
const roleOptions = $derived([
|
||||
{ value: "lead", label: m.team_role_lead() },
|
||||
{ value: "manager", label: m.team_role_manager() },
|
||||
{ value: "member", label: m.team_role_member() },
|
||||
]);
|
||||
|
||||
function getRoleColor(role: string): string {
|
||||
const map: Record<string, string> = {
|
||||
lead: "text-amber-400 bg-amber-400/10",
|
||||
manager: "text-purple-400 bg-purple-400/10",
|
||||
member: "text-light/50 bg-light/5",
|
||||
};
|
||||
return map[role] ?? "text-light/50 bg-light/5";
|
||||
}
|
||||
|
||||
function getMemberName(member: (typeof teamMembers)[0]): string {
|
||||
return member.profile?.full_name || member.profile?.email || "Unknown";
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
if (!selectedUserId) return;
|
||||
adding = true;
|
||||
try {
|
||||
const { data: inserted, error } = await (supabase as any)
|
||||
.from("event_members")
|
||||
.upsert(
|
||||
{
|
||||
event_id: data.event.id,
|
||||
user_id: selectedUserId,
|
||||
role: selectedRole,
|
||||
},
|
||||
{ onConflict: "event_id,user_id" },
|
||||
)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Find profile from org members
|
||||
const orgMember = data.members.find(
|
||||
(m) => m.user_id === selectedUserId,
|
||||
);
|
||||
const profile = orgMember?.profiles ?? undefined;
|
||||
|
||||
teamMembers = [
|
||||
...teamMembers,
|
||||
{ ...inserted, profile },
|
||||
];
|
||||
|
||||
const name = profile?.full_name || profile?.email || "Member";
|
||||
toasts.success(m.team_added({ name }));
|
||||
showAddModal = false;
|
||||
selectedUserId = "";
|
||||
selectedRole = "member";
|
||||
} catch (e: any) {
|
||||
toasts.error(e.message || "Failed to add member");
|
||||
} finally {
|
||||
adding = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove() {
|
||||
if (!memberToRemove) return;
|
||||
removing = true;
|
||||
try {
|
||||
const { error } = await (supabase as any)
|
||||
.from("event_members")
|
||||
.delete()
|
||||
.eq("event_id", data.event.id)
|
||||
.eq("user_id", memberToRemove.user_id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const name = getMemberName(memberToRemove);
|
||||
teamMembers = teamMembers.filter(
|
||||
(m) => m.user_id !== memberToRemove!.user_id,
|
||||
);
|
||||
toasts.success(m.team_removed({ name }));
|
||||
memberToRemove = null;
|
||||
} catch (e: any) {
|
||||
toasts.error(e.message || "Failed to remove member");
|
||||
} finally {
|
||||
removing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRoleUpdate() {
|
||||
if (!editingMember) return;
|
||||
updatingRole = true;
|
||||
try {
|
||||
const { error } = await (supabase as any)
|
||||
.from("event_members")
|
||||
.update({ role: editRole })
|
||||
.eq("event_id", data.event.id)
|
||||
.eq("user_id", editingMember.user_id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
teamMembers = teamMembers.map((m) =>
|
||||
m.user_id === editingMember!.user_id
|
||||
? { ...m, role: editRole }
|
||||
: m,
|
||||
);
|
||||
toasts.success(m.team_updated());
|
||||
editingMember = null;
|
||||
} catch (e: any) {
|
||||
toasts.error(e.message || "Failed to update role");
|
||||
} finally {
|
||||
updatingRole = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openEditRole(member: (typeof teamMembers)[0]) {
|
||||
editingMember = member;
|
||||
editRole = member.role;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{m.events_mod_team()} | {data.event.name} | {data.org.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col h-full overflow-auto p-6">
|
||||
<div class="max-w-2xl w-full mx-auto flex flex-col gap-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="font-heading text-body text-white">{m.team_title()}</h2>
|
||||
<p class="text-body-sm text-light/40 mt-0.5">{m.team_subtitle()}</p>
|
||||
</div>
|
||||
{#if isEditor}
|
||||
<Button
|
||||
size="sm"
|
||||
icon="person_add"
|
||||
onclick={() => (showAddModal = true)}
|
||||
disabled={availableMembers.length === 0}
|
||||
>
|
||||
{m.team_add_member()}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Team List -->
|
||||
{#if teamMembers.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-16 text-light/40"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded mb-4"
|
||||
style="font-size: 56px; font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 48;"
|
||||
>badge</span
|
||||
>
|
||||
<p class="text-body-sm text-light/30 text-center max-w-sm">
|
||||
{m.team_empty()}
|
||||
</p>
|
||||
{#if isEditor && availableMembers.length > 0}
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
size="sm"
|
||||
icon="person_add"
|
||||
onclick={() => (showAddModal = true)}
|
||||
>
|
||||
{m.team_add_member()}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-dark/30 border border-light/5 rounded-xl overflow-hidden"
|
||||
>
|
||||
<div class="divide-y divide-light/5">
|
||||
{#each teamMembers as member}
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 hover:bg-light/5 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Avatar
|
||||
name={member.profile?.full_name ||
|
||||
member.profile?.email ||
|
||||
"?"}
|
||||
src={member.profile?.avatar_url}
|
||||
size="sm"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-body-sm text-white">
|
||||
{member.profile?.full_name ||
|
||||
member.profile?.email ||
|
||||
"Unknown"}
|
||||
</p>
|
||||
<p class="text-[11px] text-light/40">
|
||||
{member.profile?.email || ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="px-2 py-0.5 text-[10px] rounded-md capitalize font-body {getRoleColor(
|
||||
member.role,
|
||||
)}">{member.role}</span
|
||||
>
|
||||
{#if isEditor}
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||
onclick={() => openEditRole(member)}
|
||||
title="Change role"
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>swap_horiz</span
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 text-light/40 hover:text-error hover:bg-error/10 rounded-lg transition-colors"
|
||||
onclick={() =>
|
||||
(memberToRemove = member)}
|
||||
title={m.team_remove_btn()}
|
||||
>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>person_remove</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Member Modal -->
|
||||
<Modal
|
||||
isOpen={showAddModal}
|
||||
onClose={() => (showAddModal = false)}
|
||||
title={m.team_add_member()}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="team-member-select"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.team_select_member()}</label
|
||||
>
|
||||
<select
|
||||
id="team-member-select"
|
||||
bind:value={selectedUserId}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary"
|
||||
>
|
||||
<option value="" disabled>{m.team_select_member()}</option>
|
||||
{#each availableMembers as om}
|
||||
<option value={om.user_id}>
|
||||
{om.profiles?.full_name || om.profiles?.email || om.user_id}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="team-role-select"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.team_select_role()}</label
|
||||
>
|
||||
<select
|
||||
id="team-role-select"
|
||||
bind:value={selectedRole}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary"
|
||||
>
|
||||
{#each roleOptions as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 pt-2 border-t border-light/5">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||
onclick={() => {
|
||||
showAddModal = false;
|
||||
selectedUserId = "";
|
||||
selectedRole = "member";
|
||||
}}
|
||||
>
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selectedUserId || adding}
|
||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={handleAdd}
|
||||
>
|
||||
{adding ? "..." : m.team_add_member()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<!-- Edit Role Modal -->
|
||||
<Modal
|
||||
isOpen={!!editingMember}
|
||||
onClose={() => (editingMember = null)}
|
||||
title="Change Role"
|
||||
>
|
||||
{#if editingMember}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-3 p-3 bg-dark/30 rounded-xl">
|
||||
<Avatar
|
||||
name={editingMember.profile?.full_name ||
|
||||
editingMember.profile?.email ||
|
||||
"?"}
|
||||
src={editingMember.profile?.avatar_url}
|
||||
size="sm"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-body-sm text-white">
|
||||
{editingMember.profile?.full_name ||
|
||||
editingMember.profile?.email ||
|
||||
"Unknown"}
|
||||
</p>
|
||||
<p class="text-[11px] text-light/40 capitalize">
|
||||
{editingMember.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label
|
||||
for="edit-role-select"
|
||||
class="text-body-sm text-light/60 font-body"
|
||||
>{m.team_select_role()}</label
|
||||
>
|
||||
<select
|
||||
id="edit-role-select"
|
||||
bind:value={editRole}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary"
|
||||
>
|
||||
{#each roleOptions as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||
onclick={() => (editingMember = null)}
|
||||
>
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={updatingRole || editRole === editingMember.role}
|
||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick={handleRoleUpdate}
|
||||
>
|
||||
{updatingRole ? "..." : m.btn_save()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<!-- Remove Confirmation -->
|
||||
{#if memberToRemove}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
||||
<div
|
||||
class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
|
||||
onkeydown={(e) => e.key === "Escape" && (memberToRemove = null)}
|
||||
onclick={(e) => e.target === e.currentTarget && (memberToRemove = null)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={m.team_remove_btn()}
|
||||
>
|
||||
<div
|
||||
class="bg-night rounded-2xl w-full max-w-sm shadow-2xl border border-light/10 p-6"
|
||||
>
|
||||
<h2 class="text-h3 font-heading text-white mb-2">
|
||||
{m.team_remove_btn()}
|
||||
</h2>
|
||||
<p class="text-body-sm text-light/50 mb-6">
|
||||
{m.team_remove_confirm({ name: getMemberName(memberToRemove) })}
|
||||
</p>
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||
onclick={() => (memberToRemove = null)}
|
||||
>
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-error text-white rounded-xl text-body-sm hover:bg-error/80 transition-colors disabled:opacity-50"
|
||||
disabled={removing}
|
||||
onclick={handleRemove}
|
||||
>
|
||||
{removing ? "..." : m.team_remove_btn()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user