feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata
This commit is contained in:
@@ -1,18 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { Button, Input, Avatar } from "$lib/components/ui";
|
||||
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: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
avatar_url?: string | null;
|
||||
};
|
||||
org: OrgData;
|
||||
isOwner: boolean;
|
||||
onLeave: () => void;
|
||||
onDelete: () => void;
|
||||
@@ -20,20 +57,116 @@
|
||||
|
||||
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) {
|
||||
@@ -41,7 +174,6 @@
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toasts.error("Please select an image file.");
|
||||
return;
|
||||
@@ -113,7 +245,12 @@
|
||||
isSaving = true;
|
||||
const { error } = await supabase
|
||||
.from("organizations")
|
||||
.update({ name: orgName, slug: orgSlug })
|
||||
.update({
|
||||
name: orgName,
|
||||
slug: orgSlug,
|
||||
description: orgDescription.trim(),
|
||||
theme_color: themeColor,
|
||||
})
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
@@ -121,15 +258,118 @@
|
||||
} else if (orgSlug !== org.slug) {
|
||||
window.location.href = `/${orgSlug}/settings`;
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.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("Failed to save preferences.");
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.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("Failed to save event defaults.");
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.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("Failed to save feature settings.");
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Feature settings 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("Failed to save social links.");
|
||||
} else {
|
||||
await invalidateAll();
|
||||
toasts.success("Social links 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">
|
||||
<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 -->
|
||||
@@ -175,6 +415,29 @@
|
||||
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
|
||||
@@ -182,9 +445,401 @@
|
||||
</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)}
|
||||
></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 hidden for now -->
|
||||
<!-- <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;">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">
|
||||
<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.
|
||||
@@ -199,10 +854,15 @@
|
||||
|
||||
<!-- 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>
|
||||
<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.
|
||||
Leave this organization. You will need to be re-invited to
|
||||
rejoin.
|
||||
</p>
|
||||
<div>
|
||||
<Button variant="secondary" size="sm" onclick={onLeave}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Input,
|
||||
Select,
|
||||
Avatar,
|
||||
} from "$lib/components/ui";
|
||||
import { Button, Modal, Input, Select, Avatar } from "$lib/components/ui";
|
||||
import { toasts } from "$lib/stores/toast.svelte";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
@@ -177,7 +171,11 @@
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
<Button size="sm" icon="person_add" onclick={() => (showInviteModal = true)}>
|
||||
<Button
|
||||
size="sm"
|
||||
icon="person_add"
|
||||
onclick={() => (showInviteModal = true)}
|
||||
>
|
||||
{m.settings_members_invite()}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -194,7 +192,9 @@
|
||||
class="flex items-center justify-between py-2 px-3 bg-light/5 rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<p class="text-body-sm text-white">{invite.email}</p>
|
||||
<p class="text-body-sm text-white">
|
||||
{invite.email}
|
||||
</p>
|
||||
<p class="text-[11px] text-light/40">
|
||||
Invited as {invite.role} • Expires {new Date(
|
||||
invite.expires_at,
|
||||
@@ -211,7 +211,11 @@
|
||||
)}
|
||||
title={m.settings_members_copy_link()}
|
||||
>
|
||||
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">content_copy</span>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>content_copy</span
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -219,7 +223,11 @@
|
||||
onclick={() => cancelInvite(invite.id)}
|
||||
title="Cancel invite"
|
||||
>
|
||||
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">close</span>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>close</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,7 +280,11 @@
|
||||
onclick={() => openMemberModal(member)}
|
||||
title="Edit"
|
||||
>
|
||||
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
|
||||
<span
|
||||
class="material-symbols-rounded"
|
||||
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
|
||||
>edit</span
|
||||
>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -289,31 +301,42 @@
|
||||
title="Invite Member"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="invite-email" class="text-body-sm text-light/60 font-body">Email address</label>
|
||||
<input
|
||||
id="invite-email"
|
||||
type="email"
|
||||
bind:value={inviteEmail}
|
||||
placeholder="colleague@example.com"
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white placeholder:text-light/30 focus:outline-none focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="invite-role" class="text-body-sm text-light/60 font-body">Role</label>
|
||||
<select
|
||||
id="invite-role"
|
||||
bind:value={inviteRole}
|
||||
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary"
|
||||
<Input
|
||||
variant="compact"
|
||||
type="email"
|
||||
label="Email address"
|
||||
bind:value={inviteEmail}
|
||||
placeholder="colleague@example.com"
|
||||
/>
|
||||
<Select
|
||||
variant="compact"
|
||||
label="Role"
|
||||
bind:value={inviteRole}
|
||||
placeholder=""
|
||||
options={[
|
||||
{ value: "viewer", label: "Viewer - Can view content" },
|
||||
{
|
||||
value: "commenter",
|
||||
label: "Commenter - Can view and comment",
|
||||
},
|
||||
{
|
||||
value: "editor",
|
||||
label: "Editor - Can create and edit content",
|
||||
},
|
||||
{
|
||||
value: "admin",
|
||||
label: "Admin - Can manage members and settings",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<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={() => (showInviteModal = false)}>Cancel</button
|
||||
>
|
||||
<option value="viewer">Viewer - Can view content</option>
|
||||
<option value="commenter">Commenter - Can view and comment</option>
|
||||
<option value="editor">Editor - Can create and edit content</option>
|
||||
<option value="admin">Admin - Can manage members and settings</option>
|
||||
</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={() => (showInviteModal = false)}>Cancel</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!inviteEmail.trim() || isSendingInvite}
|
||||
@@ -338,7 +361,9 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-3 p-3 bg-dark/50 rounded-xl">
|
||||
<Avatar
|
||||
name={memberProfile?.full_name || memberProfile?.email || "?"}
|
||||
name={memberProfile?.full_name ||
|
||||
memberProfile?.email ||
|
||||
"?"}
|
||||
src={memberProfile?.avatar_url}
|
||||
size="sm"
|
||||
/>
|
||||
@@ -351,29 +376,38 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label for="member-role" class="text-body-sm text-light/60 font-body">Role</label>
|
||||
<select
|
||||
id="member-role"
|
||||
bind:value={selectedMemberRole}
|
||||
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="viewer">Viewer</option>
|
||||
<option value="commenter">Commenter</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="text-[11px] text-error hover:underline self-start" onclick={removeMember}>
|
||||
<Select
|
||||
variant="compact"
|
||||
label="Role"
|
||||
bind:value={selectedMemberRole}
|
||||
placeholder=""
|
||||
options={[
|
||||
{ value: "viewer", label: "Viewer" },
|
||||
{ value: "commenter", label: "Commenter" },
|
||||
{ value: "editor", label: "Editor" },
|
||||
{ value: "admin", label: "Admin" },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="text-[11px] text-error hover:underline self-start"
|
||||
onclick={removeMember}
|
||||
>
|
||||
Remove from organization
|
||||
</button>
|
||||
<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={() => (showMemberModal = false)}>Cancel</button>
|
||||
<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={() => (showMemberModal = false)}>Cancel</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors"
|
||||
onclick={updateMemberRole}
|
||||
>Save</button>
|
||||
onclick={updateMemberRole}>Save</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user