Files
root-org/src/lib/components/settings/SettingsGeneral.svelte
2026-02-28 17:06:03 +02:00

886 lines
24 KiB
Svelte

<script lang="ts">
import {
Button,
Input,
Avatar,
Select,
Textarea,
} from "$lib/components/ui";
import type { SupabaseClient } from "@supabase/supabase-js";
import * as m from "$lib/paraglide/messages";
import type { Database } from "$lib/supabase/types";
import { toasts } from "$lib/stores/toast.svelte";
import { invalidateAll } from "$app/navigation";
import {
SUPPORTED_CURRENCIES,
DATE_FORMAT_OPTIONS,
WEEK_START_OPTIONS,
CALENDAR_VIEW_OPTIONS,
TIMEZONE_OPTIONS,
EVENT_STATUS_OPTIONS,
DASHBOARD_LAYOUT_OPTIONS,
DEPT_MODULE_OPTIONS,
EVENT_COLOR_PRESETS,
formatCurrency,
} from "$lib/utils/currency";
interface OrgData {
id: string;
name: string;
slug: string;
avatar_url?: string | null;
description?: string | null;
theme_color?: string | null;
currency?: string;
date_format?: string;
timezone?: string;
week_start_day?: string;
default_calendar_view?: string;
default_event_color?: string;
default_event_status?: string;
default_dept_modules?: string[];
default_dept_layout?: string;
feature_chat?: boolean;
feature_sponsors?: boolean;
feature_contacts?: boolean;
feature_budget?: boolean;
social_links?: Record<string, string>;
}
interface Props {
supabase: SupabaseClient<Database>;
org: OrgData;
isOwner: boolean;
onLeave: () => void;
onDelete: () => void;
}
let { supabase, org, isOwner, onLeave, onDelete }: Props = $props();
// ── Organization details ──
// svelte-ignore state_referenced_locally
let orgName = $state(org.name);
// svelte-ignore state_referenced_locally
let orgSlug = $state(org.slug);
// svelte-ignore state_referenced_locally
let avatarUrl = $state(org.avatar_url ?? null);
// svelte-ignore state_referenced_locally
let orgDescription = $state(org.description ?? "");
// svelte-ignore state_referenced_locally
let themeColor = $state(org.theme_color ?? "#00a3e0");
let isSaving = $state(false);
let isUploading = $state(false);
let avatarInput = $state<HTMLInputElement | null>(null);
// ── Preferences ──
// svelte-ignore state_referenced_locally
let currency = $state(org.currency ?? "EUR");
// svelte-ignore state_referenced_locally
let dateFormat = $state(org.date_format ?? "DD/MM/YYYY");
// svelte-ignore state_referenced_locally
let timezone = $state(org.timezone ?? "Europe/Tallinn");
// svelte-ignore state_referenced_locally
let weekStartDay = $state(org.week_start_day ?? "monday");
// svelte-ignore state_referenced_locally
let defaultCalendarView = $state(org.default_calendar_view ?? "month");
let isSavingPrefs = $state(false);
// ── Event defaults ──
// svelte-ignore state_referenced_locally
let defaultEventColor = $state(org.default_event_color ?? "#7986cb");
// svelte-ignore state_referenced_locally
let defaultEventStatus = $state(org.default_event_status ?? "planning");
// svelte-ignore state_referenced_locally
let defaultDeptModules = $state<string[]>(
org.default_dept_modules ?? ["kanban", "files", "checklist"],
);
// svelte-ignore state_referenced_locally
let defaultDeptLayout = $state(org.default_dept_layout ?? "split");
let isSavingDefaults = $state(false);
// ── Social links ──
// svelte-ignore state_referenced_locally
let socialWebsite = $state(org.social_links?.website ?? "");
// svelte-ignore state_referenced_locally
let socialInstagram = $state(org.social_links?.instagram ?? "");
// svelte-ignore state_referenced_locally
let socialFacebook = $state(org.social_links?.facebook ?? "");
// svelte-ignore state_referenced_locally
let socialDiscord = $state(org.social_links?.discord ?? "");
// svelte-ignore state_referenced_locally
let socialLinkedin = $state(org.social_links?.linkedin ?? "");
// svelte-ignore state_referenced_locally
let socialX = $state(org.social_links?.x ?? "");
// svelte-ignore state_referenced_locally
let socialYoutube = $state(org.social_links?.youtube ?? "");
// svelte-ignore state_referenced_locally
let socialTiktok = $state(org.social_links?.tiktok ?? "");
// svelte-ignore state_referenced_locally
let socialFienta = $state(org.social_links?.fienta ?? "");
// svelte-ignore state_referenced_locally
let socialTwitch = $state(org.social_links?.twitch ?? "");
let isSavingSocial = $state(false);
// ── Feature toggles ──
// svelte-ignore state_referenced_locally
let featureChat = $state(org.feature_chat ?? true);
// svelte-ignore state_referenced_locally
let featureSponsors = $state(org.feature_sponsors ?? true);
// svelte-ignore state_referenced_locally
let featureContacts = $state(org.feature_contacts ?? true);
// svelte-ignore state_referenced_locally
let featureBudget = $state(org.feature_budget ?? true);
let isSavingFeatures = $state(false);
const currencyPreview = $derived(formatCurrency(1234.56, currency));
$effect(() => {
orgName = org.name;
orgSlug = org.slug;
avatarUrl = org.avatar_url ?? null;
orgDescription = org.description ?? "";
themeColor = org.theme_color ?? "#00a3e0";
currency = org.currency ?? "EUR";
dateFormat = org.date_format ?? "DD/MM/YYYY";
timezone = org.timezone ?? "Europe/Tallinn";
weekStartDay = org.week_start_day ?? "monday";
defaultCalendarView = org.default_calendar_view ?? "month";
defaultEventColor = org.default_event_color ?? "#7986cb";
defaultEventStatus = org.default_event_status ?? "planning";
defaultDeptModules = org.default_dept_modules ?? [
"kanban",
"files",
"checklist",
];
defaultDeptLayout = org.default_dept_layout ?? "split";
socialWebsite = org.social_links?.website ?? "";
socialInstagram = org.social_links?.instagram ?? "";
socialFacebook = org.social_links?.facebook ?? "";
socialDiscord = org.social_links?.discord ?? "";
socialLinkedin = org.social_links?.linkedin ?? "";
socialX = org.social_links?.x ?? "";
socialYoutube = org.social_links?.youtube ?? "";
socialTiktok = org.social_links?.tiktok ?? "";
socialFienta = org.social_links?.fienta ?? "";
socialTwitch = org.social_links?.twitch ?? "";
featureChat = org.feature_chat ?? true;
featureSponsors = org.feature_sponsors ?? true;
featureContacts = org.feature_contacts ?? true;
featureBudget = org.feature_budget ?? true;
});
async function handleAvatarUpload(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toasts.error(m.toast_error_select_image());
return;
}
if (file.size > 2 * 1024 * 1024) {
toasts.error(m.toast_error_image_too_large());
return;
}
isUploading = true;
try {
const ext = file.name.split(".").pop() || "png";
const path = `org-avatars/${org.id}.${ext}`;
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(path, file, { upsert: true });
if (uploadError) {
toasts.error(m.toast_error_upload_avatar());
return;
}
const { data: urlData } = supabase.storage
.from("avatars")
.getPublicUrl(path);
const publicUrl = `${urlData.publicUrl}?t=${Date.now()}`;
const { error: dbError } = await supabase
.from("organizations")
.update({ avatar_url: publicUrl })
.eq("id", org.id);
if (dbError) {
toasts.error(m.toast_error_save_avatar_url());
return;
}
avatarUrl = publicUrl;
await invalidateAll();
toasts.success(m.toast_success_avatar_updated());
} catch (err) {
toasts.error(m.toast_error_avatar_upload());
} finally {
isUploading = false;
input.value = "";
}
}
async function removeAvatar() {
isSaving = true;
const { error } = await supabase
.from("organizations")
.update({ avatar_url: null })
.eq("id", org.id);
if (error) {
toasts.error(m.toast_error_remove_avatar());
} else {
avatarUrl = null;
await invalidateAll();
toasts.success(m.toast_success_avatar_removed());
}
isSaving = false;
}
async function saveGeneralSettings() {
isSaving = true;
const { error } = await supabase
.from("organizations")
.update({
name: orgName,
slug: orgSlug,
description: orgDescription.trim(),
theme_color: themeColor,
})
.eq("id", org.id);
if (error) {
toasts.error(m.toast_error_save_settings());
} else if (orgSlug !== org.slug) {
window.location.href = `/${orgSlug}/settings`;
} else {
await invalidateAll();
toasts.success(m.toast_success_settings_saved());
}
isSaving = false;
}
async function savePreferences() {
isSavingPrefs = true;
const { error } = await supabase
.from("organizations")
.update({
currency,
date_format: dateFormat,
timezone,
week_start_day: weekStartDay,
default_calendar_view: defaultCalendarView,
})
.eq("id", org.id);
if (error) {
toasts.error(m.toast_error_save_preferences());
} else {
await invalidateAll();
toasts.success(m.toast_success_preferences_saved());
}
isSavingPrefs = false;
}
async function saveEventDefaults() {
isSavingDefaults = true;
const { error } = await supabase
.from("organizations")
.update({
default_event_color: defaultEventColor,
default_event_status: defaultEventStatus,
default_dept_modules: defaultDeptModules,
default_dept_layout: defaultDeptLayout,
})
.eq("id", org.id);
if (error) {
toasts.error(m.toast_error_save_event_defaults());
} else {
await invalidateAll();
toasts.success(m.toast_success_event_defaults_saved());
}
isSavingDefaults = false;
}
async function saveFeatureToggles() {
isSavingFeatures = true;
const { error } = await supabase
.from("organizations")
.update({
feature_chat: featureChat,
feature_sponsors: featureSponsors,
feature_contacts: featureContacts,
feature_budget: featureBudget,
})
.eq("id", org.id);
if (error) {
toasts.error(m.toast_error_save_features());
} else {
await invalidateAll();
toasts.success(m.toast_success_features_saved());
}
isSavingFeatures = false;
}
async function saveSocialLinks() {
isSavingSocial = true;
const links: Record<string, string> = {};
if (socialWebsite.trim()) links.website = socialWebsite.trim();
if (socialInstagram.trim()) links.instagram = socialInstagram.trim();
if (socialFacebook.trim()) links.facebook = socialFacebook.trim();
if (socialDiscord.trim()) links.discord = socialDiscord.trim();
if (socialLinkedin.trim()) links.linkedin = socialLinkedin.trim();
if (socialX.trim()) links.x = socialX.trim();
if (socialYoutube.trim()) links.youtube = socialYoutube.trim();
if (socialTiktok.trim()) links.tiktok = socialTiktok.trim();
if (socialFienta.trim()) links.fienta = socialFienta.trim();
if (socialTwitch.trim()) links.twitch = socialTwitch.trim();
const { error } = await (supabase as any)
.from("organizations")
.update({ social_links: links })
.eq("id", org.id);
if (error) {
toasts.error(m.toast_error_save_social());
} else {
await invalidateAll();
toasts.success(m.toast_success_social_saved());
}
isSavingSocial = false;
}
function toggleModule(mod: string) {
if (defaultDeptModules.includes(mod)) {
defaultDeptModules = defaultDeptModules.filter((m) => m !== mod);
} else {
defaultDeptModules = [...defaultDeptModules, mod];
}
}
</script>
<div class="flex flex-col gap-6 max-w-2xl">
<!-- Organization Details -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<h2 class="font-heading text-body text-white">Organization details</h2>
<!-- Avatar Upload -->
<div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60">Avatar</span>
<div class="flex items-center gap-4">
<Avatar name={orgName || "?"} src={avatarUrl} size="lg" />
<div class="flex gap-2">
<input
type="file"
accept="image/*"
class="hidden"
bind:this={avatarInput}
onchange={handleAvatarUpload}
/>
<Button
variant="secondary"
size="sm"
onclick={() => avatarInput?.click()}
loading={isUploading}
>
Upload
</Button>
{#if avatarUrl}
<Button
variant="ghost"
size="sm"
onclick={removeAvatar}
>
Remove
</Button>
{/if}
</div>
</div>
</div>
<Input
label="Name"
bind:value={orgName}
placeholder="Organization name"
/>
<Input
label="URL slug (yoursite.com/...)"
bind:value={orgSlug}
placeholder="my-org"
/>
<Textarea
variant="compact"
label="Description"
bind:value={orgDescription}
placeholder="What does your organization do?"
rows={2}
resize="none"
/>
<!-- Theme Color (hidden for now) -->
<!-- <div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60">Theme color</span>
<div class="flex items-center gap-3">
<label
class="w-8 h-8 rounded-lg cursor-pointer overflow-hidden border border-light/10"
style="background-color: {themeColor}"
>
<input type="color" bind:value={themeColor} class="opacity-0 w-0 h-0" />
</label>
<span class="text-[12px] text-light/40 font-mono">{themeColor}</span>
</div>
</div> -->
<div>
<Button size="sm" onclick={saveGeneralSettings} loading={isSaving}
>Save Changes</Button
>
</div>
</div>
<!-- Social & Links -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">
{m.settings_social_title()}
</h2>
<p class="text-[11px] text-light/40 mt-0.5">
{m.settings_social_desc()}
</p>
</div>
<Input
variant="compact"
type="url"
label={m.settings_social_website()}
bind:value={socialWebsite}
placeholder={m.settings_social_website_placeholder()}
icon="language"
/>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_instagram()}
bind:value={socialInstagram}
placeholder={m.settings_social_instagram_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_facebook()}
bind:value={socialFacebook}
placeholder={m.settings_social_facebook_placeholder()}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_discord()}
bind:value={socialDiscord}
placeholder={m.settings_social_discord_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_linkedin()}
bind:value={socialLinkedin}
placeholder={m.settings_social_linkedin_placeholder()}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_x()}
bind:value={socialX}
placeholder={m.settings_social_x_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_youtube()}
bind:value={socialYoutube}
placeholder={m.settings_social_youtube_placeholder()}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_tiktok()}
bind:value={socialTiktok}
placeholder={m.settings_social_tiktok_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_fienta()}
bind:value={socialFienta}
placeholder={m.settings_social_fienta_placeholder()}
/>
</div>
<Input
variant="compact"
type="url"
label={m.settings_social_twitch()}
bind:value={socialTwitch}
placeholder={m.settings_social_twitch_placeholder()}
/>
<div>
<Button size="sm" onclick={saveSocialLinks} loading={isSavingSocial}
>{m.settings_social_save()}</Button
>
</div>
</div>
<!-- Preferences -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">Preferences</h2>
<p class="text-[11px] text-light/40 mt-0.5">
Currency, date format, timezone, and calendar defaults.
</p>
</div>
<!-- Currency -->
<Select
variant="compact"
label="Currency"
bind:value={currency}
placeholder=""
options={SUPPORTED_CURRENCIES.map((c) => ({
value: c.code,
label: c.label,
}))}
hint={`Preview: ${currencyPreview}`}
/>
<!-- Date Format -->
<Select
variant="compact"
label="Date format"
bind:value={dateFormat}
placeholder=""
options={DATE_FORMAT_OPTIONS}
/>
<!-- Timezone -->
<Select
variant="compact"
label="Timezone"
bind:value={timezone}
placeholder=""
groups={TIMEZONE_OPTIONS.map((g) => ({
label: g.group,
options: g.zones.map((z) => ({
value: z,
label: z.replace(/_/g, " "),
})),
}))}
/>
<div class="grid grid-cols-2 gap-4">
<Select
variant="compact"
label="Week starts on"
bind:value={weekStartDay}
placeholder=""
options={WEEK_START_OPTIONS}
/>
<Select
variant="compact"
label="Default calendar view"
bind:value={defaultCalendarView}
placeholder=""
options={CALENDAR_VIEW_OPTIONS}
/>
</div>
<div>
<Button size="sm" onclick={savePreferences} loading={isSavingPrefs}
>Save Preferences</Button
>
</div>
</div>
<!-- Event Defaults -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">Event defaults</h2>
<p class="text-[11px] text-light/40 mt-0.5">
Defaults applied when creating new events and departments.
</p>
</div>
<!-- Default Event Color -->
<div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60"
>Default event color</span
>
<div class="flex flex-wrap gap-2">
{#each EVENT_COLOR_PRESETS as color}
<button
type="button"
class="w-7 h-7 rounded-lg border-2 transition-all {defaultEventColor ===
color
? 'border-white scale-110'
: 'border-transparent hover:border-light/30'}"
style="background-color: {color}"
onclick={() => (defaultEventColor = color)}
aria-label="Color {color}"
></button>
{/each}
<label
class="w-7 h-7 rounded-lg border-2 border-dashed border-light/20 hover:border-light/40 transition-all cursor-pointer flex items-center justify-center overflow-hidden"
title="Custom color"
>
<input
type="color"
class="opacity-0 absolute w-0 h-0"
bind:value={defaultEventColor}
/>
<span
class="material-symbols-rounded text-light/30"
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
>colorize</span
>
</label>
</div>
</div>
<!-- Default Event Status -->
<Select
variant="compact"
label="Default event status"
bind:value={defaultEventStatus}
placeholder=""
options={EVENT_STATUS_OPTIONS}
/>
<!-- Default Department Layout -->
<Select
variant="compact"
label="Default department layout"
bind:value={defaultDeptLayout}
placeholder=""
options={DASHBOARD_LAYOUT_OPTIONS}
/>
<!-- Default Department Modules -->
<div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60"
>Default department modules</span
>
<p class="text-[11px] text-light/30">
Modules auto-added when a new department is created.
</p>
<div class="grid grid-cols-2 gap-2">
{#each DEPT_MODULE_OPTIONS as mod}
<button
type="button"
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl border transition-all text-left {defaultDeptModules.includes(
mod.value,
)
? 'bg-primary/10 border-primary/30 text-white'
: 'bg-dark/30 border-light/5 text-light/40 hover:border-light/10'}"
onclick={() => toggleModule(mod.value)}
>
<span
class="material-symbols-rounded {defaultDeptModules.includes(
mod.value,
)
? 'text-primary'
: 'text-light/30'}"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>{mod.icon}</span
>
<span class="text-body-sm font-body">{mod.label}</span>
</button>
{/each}
</div>
</div>
<div>
<Button
size="sm"
onclick={saveEventDefaults}
loading={isSavingDefaults}>Save Event Defaults</Button
>
</div>
</div>
<!-- Feature Toggles -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">Features</h2>
<p class="text-[11px] text-light/40 mt-0.5">
Enable or disable features for this organization.
</p>
</div>
<div class="flex flex-col gap-3">
<!-- Chat toggle disabled until fully developed -->
<!-- <label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-purple-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>chat</span
>
<div>
<p class="text-body-sm text-white">Chat</p>
<p class="text-[11px] text-light/30">
Real-time messaging via Matrix
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureChat}
class="w-4 h-4 rounded accent-primary"
/>
</label> -->
<label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-emerald-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>account_balance</span
>
<div>
<p class="text-body-sm text-white">Budget & Finances</p>
<p class="text-[11px] text-light/30">
Income/expense tracking, planned vs actual budgets
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureBudget}
class="w-4 h-4 rounded accent-primary"
/>
</label>
<label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-indigo-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>handshake</span
>
<div>
<p class="text-body-sm text-white">Sponsors</p>
<p class="text-[11px] text-light/30">
Sponsor CRM with tiers, deliverables, and fund
tracking
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureSponsors}
class="w-4 h-4 rounded accent-primary"
/>
</label>
<label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-blue-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>contacts</span
>
<div>
<p class="text-body-sm text-white">Contacts</p>
<p class="text-[11px] text-light/30">
Vendor and contact directory per department
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureContacts}
class="w-4 h-4 rounded accent-primary"
/>
</label>
</div>
<div>
<Button
size="sm"
onclick={saveFeatureToggles}
loading={isSavingFeatures}>Save Features</Button
>
</div>
</div>
<!-- Danger Zone -->
{#if isOwner}
<div
class="bg-dark/30 border border-error/10 rounded-xl p-5 flex flex-col gap-3"
>
<h4 class="font-heading text-body-sm text-error">Danger Zone</h4>
<p class="font-body text-[11px] text-light/40">
Permanently delete this organization and all its data.
</p>
<div>
<Button variant="danger" size="sm" onclick={onDelete}
>Delete Organization</Button
>
</div>
</div>
{/if}
<!-- Leave Organization (non-owners) -->
{#if !isOwner}
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-3"
>
<h4 class="font-heading text-body-sm text-white">
Leave Organization
</h4>
<p class="font-body text-[11px] text-light/40">
Leave this organization. You will need to be re-invited to
rejoin.
</p>
<div>
<Button variant="secondary" size="sm" onclick={onLeave}
>Leave {org.name}</Button
>
</div>
</div>
{/if}
</div>