ui: overhaul org settings components (General, Members, Roles, Integrations) - SettingsGeneral: border-based cards, compact danger zone with error border - SettingsMembers: Avatar component, icon buttons, border-based list - SettingsRoles: icon buttons for edit/delete, smaller permission badges - SettingsIntegrations: compact integration cards, Material Symbols for coming-soon - Removed unused Card imports from all settings components - svelte-check: 0 errors, vitest: 112/112 passed
This commit is contained in:
@@ -124,93 +124,88 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
<div class="flex flex-col gap-6 max-w-2xl">
|
||||||
<!-- Organization Details -->
|
<!-- Organization Details -->
|
||||||
<h2 class="font-heading text-h2 text-white">Organization details</h2>
|
<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>
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
<!-- Avatar Upload -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-2">
|
||||||
<!-- Avatar Upload -->
|
<span class="font-body text-body-sm text-light/60">Avatar</span>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<span class="font-body text-body-sm text-light">Avatar</span>
|
<Avatar name={orgName || "?"} src={avatarUrl} size="lg" />
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex gap-2">
|
||||||
<Avatar name={orgName || "?"} src={avatarUrl} size="lg" />
|
<input
|
||||||
<div class="flex gap-2">
|
type="file"
|
||||||
<input
|
accept="image/*"
|
||||||
type="file"
|
class="hidden"
|
||||||
accept="image/*"
|
bind:this={avatarInput}
|
||||||
class="hidden"
|
onchange={handleAvatarUpload}
|
||||||
bind:this={avatarInput}
|
/>
|
||||||
onchange={handleAvatarUpload}
|
<Button
|
||||||
/>
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => avatarInput?.click()}
|
||||||
|
loading={isUploading}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
{#if avatarUrl}
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="tertiary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => avatarInput?.click()}
|
onclick={removeAvatar}
|
||||||
loading={isUploading}
|
|
||||||
>
|
>
|
||||||
Upload
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
{#if avatarUrl}
|
{/if}
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
onclick={removeAvatar}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
</div>
|
||||||
label="Name"
|
<Input
|
||||||
bind:value={orgName}
|
label="Name"
|
||||||
placeholder="Organization name"
|
bind:value={orgName}
|
||||||
/>
|
placeholder="Organization name"
|
||||||
<Input
|
/>
|
||||||
label="URL slug (yoursite.com/...)"
|
<Input
|
||||||
bind:value={orgSlug}
|
label="URL slug (yoursite.com/...)"
|
||||||
placeholder="my-org"
|
bind:value={orgSlug}
|
||||||
/>
|
placeholder="my-org"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button size="sm" onclick={saveGeneralSettings} loading={isSaving}
|
||||||
|
>Save Changes</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>
|
<div>
|
||||||
<Button onclick={saveGeneralSettings} loading={isSaving}
|
<Button variant="danger" size="sm" onclick={onDelete}
|
||||||
>Save Changes</Button
|
>Delete Organization</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Danger Zone -->
|
<!-- Leave Organization (non-owners) -->
|
||||||
{#if isOwner}
|
{#if !isOwner}
|
||||||
<div class="flex flex-col gap-4">
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-3">
|
||||||
<h4 class="font-heading text-h4 text-white">Danger Zone</h4>
|
<h4 class="font-heading text-body-sm text-white">Leave Organization</h4>
|
||||||
<p class="font-body text-body text-white">
|
<p class="font-body text-[11px] text-light/40">
|
||||||
Permanently delete this organization and all its data.
|
Leave this organization. You will need to be re-invited to rejoin.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<Button variant="danger" onclick={onDelete}
|
<Button variant="secondary" size="sm" onclick={onLeave}
|
||||||
>Delete Organization</Button
|
>Leave {org.name}</Button
|
||||||
>
|
>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
<!-- Leave Organization (non-owners) -->
|
|
||||||
{#if !isOwner}
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<h4 class="font-heading text-h4 text-white">
|
|
||||||
Leave Organization
|
|
||||||
</h4>
|
|
||||||
<p class="font-body text-body text-white">
|
|
||||||
Leave this organization. You will need to be re-invited to
|
|
||||||
rejoin.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<Button variant="secondary" onclick={onLeave}
|
|
||||||
>Leave {org.name}</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, Modal, Card, Input } from "$lib/components/ui";
|
import { Button, Modal, Input } from "$lib/components/ui";
|
||||||
import { toasts } from "$lib/stores/toast.svelte";
|
import { toasts } from "$lib/stores/toast.svelte";
|
||||||
import {
|
import {
|
||||||
extractCalendarId,
|
extractCalendarId,
|
||||||
@@ -108,184 +108,88 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6 max-w-2xl">
|
<div class="space-y-3 max-w-2xl">
|
||||||
<Card>
|
<!-- Google Calendar -->
|
||||||
<div class="p-6">
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-5">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div class="w-10 h-10 bg-white rounded-xl flex items-center justify-center shrink-0">
|
||||||
class="w-12 h-12 bg-white rounded-lg flex items-center justify-center"
|
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||||
>
|
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||||
<svg class="w-8 h-8" viewBox="0 0 24 24">
|
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||||
<path
|
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||||
fill="#4285F4"
|
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
</svg>
|
||||||
/>
|
</div>
|
||||||
<path
|
<div class="flex-1 min-w-0">
|
||||||
fill="#34A853"
|
<h3 class="font-heading text-body-sm text-white">Google Calendar</h3>
|
||||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
<p class="text-[11px] text-light/40 mt-0.5">
|
||||||
/>
|
Sync events between your organization and Google Calendar.
|
||||||
<path
|
</p>
|
||||||
fill="#FBBC05"
|
|
||||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="#EA4335"
|
|
||||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-lg font-semibold text-light">
|
|
||||||
Google Calendar
|
|
||||||
</h3>
|
|
||||||
<p class="text-sm text-light/50 mt-1">
|
|
||||||
Sync events between your organization and Google
|
|
||||||
Calendar.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{#if orgCalendar}
|
{#if orgCalendar}
|
||||||
<div
|
<div class="mt-3 p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||||
class="mt-4 p-3 bg-green-500/10 border border-green-500/20 rounded-lg"
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
>
|
<div class="min-w-0 flex-1">
|
||||||
<div
|
<p class="text-[11px] font-medium text-green-400">Connected</p>
|
||||||
class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 p-3 bg-green-500/10 rounded-lg"
|
<p class="text-body-sm text-white">{orgCalendar.calendar_name || "Google Calendar"}</p>
|
||||||
>
|
<p class="text-[10px] text-light/40 truncate" title={orgCalendar.calendar_id}>{orgCalendar.calendar_id}</p>
|
||||||
<div class="min-w-0 flex-1">
|
<p class="text-[10px] text-light/30 mt-1">Events sync both ways.</p>
|
||||||
<p
|
<a
|
||||||
class="text-sm font-medium text-green-400"
|
href="https://calendar.google.com/calendar/u/0/r?cid={encodeURIComponent(orgCalendar.calendar_id)}"
|
||||||
>
|
target="_blank"
|
||||||
Connected
|
rel="noopener noreferrer"
|
||||||
</p>
|
class="inline-flex items-center gap-1 text-[11px] text-blue-400 hover:text-blue-300 mt-1.5"
|
||||||
<p class="text-light font-medium">
|
|
||||||
{orgCalendar.calendar_name ||
|
|
||||||
"Google Calendar"}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="text-xs text-light/50 truncate"
|
|
||||||
title={orgCalendar.calendar_id}
|
|
||||||
>
|
|
||||||
{orgCalendar.calendar_id}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-light/40 mt-1">
|
|
||||||
Events sync both ways — create here or
|
|
||||||
in Google Calendar.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="https://calendar.google.com/calendar/u/0/r?cid={encodeURIComponent(
|
|
||||||
orgCalendar.calendar_id,
|
|
||||||
)}"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="inline-flex items-center gap-1.5 text-xs text-blue-400 hover:text-blue-300 mt-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
|
|
||||||
/>
|
|
||||||
<polyline points="15 3 21 3 21 9" />
|
|
||||||
<line
|
|
||||||
x1="10"
|
|
||||||
y1="14"
|
|
||||||
x2="21"
|
|
||||||
y2="3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Open in Google Calendar
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onclick={disconnectOrgCalendar}
|
|
||||||
>Disconnect</Button
|
|
||||||
>
|
>
|
||||||
|
<span class="material-symbols-rounded" style="font-size: 14px;">open_in_new</span>
|
||||||
|
Open in Google Calendar
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<Button variant="danger" size="sm" onclick={disconnectOrgCalendar}>Disconnect</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else if !serviceAccountEmail}
|
</div>
|
||||||
<div
|
{:else if !serviceAccountEmail}
|
||||||
class="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
<div class="mt-3 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||||
>
|
<p class="text-[11px] text-yellow-400 font-medium">Setup required</p>
|
||||||
<p class="text-sm text-yellow-400 font-medium">
|
<p class="text-[10px] text-light/40 mt-1">
|
||||||
Setup required
|
A server administrator needs to configure the <code class="bg-light/10 px-1 rounded">GOOGLE_SERVICE_ACCOUNT_KEY</code> environment variable.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-light/50 mt-1">
|
</div>
|
||||||
A server administrator needs to configure the <code
|
{:else}
|
||||||
class="bg-light/10 px-1 rounded"
|
<div class="mt-3">
|
||||||
>GOOGLE_SERVICE_ACCOUNT_KEY</code
|
<Button size="sm" onclick={() => (showConnectModal = true)}>Connect Google Calendar</Button>
|
||||||
> environment variable before calendars can be connected.
|
</div>
|
||||||
</p>
|
{/if}
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="mt-4">
|
|
||||||
<Button onclick={() => (showConnectModal = true)}
|
|
||||||
>Connect Google Calendar</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<!-- Discord (coming soon) -->
|
||||||
<div class="p-6 opacity-50">
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 opacity-40">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div class="w-10 h-10 bg-[#5865F2] rounded-xl flex items-center justify-center shrink-0">
|
||||||
class="w-12 h-12 bg-[#7289da] rounded-lg flex items-center justify-center"
|
<span class="material-symbols-rounded text-white" style="font-size: 22px;">forum</span>
|
||||||
>
|
</div>
|
||||||
<svg
|
<div class="flex-1">
|
||||||
class="w-7 h-7 text-white"
|
<h3 class="font-heading text-body-sm text-white">Discord</h3>
|
||||||
viewBox="0 0 24 24"
|
<p class="text-[11px] text-light/40 mt-0.5">Get notifications in your Discord server.</p>
|
||||||
fill="currentColor"
|
<p class="text-[10px] text-light/30 mt-1">Coming soon</p>
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-lg font-semibold text-light">Discord</h3>
|
|
||||||
<p class="text-sm text-light/50 mt-1">
|
|
||||||
Get notifications in your Discord server.
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-light/40 mt-2">Coming soon</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<!-- Slack (coming soon) -->
|
||||||
<div class="p-6 opacity-50">
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 opacity-40">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<div
|
<div class="w-10 h-10 bg-[#4A154B] rounded-xl flex items-center justify-center shrink-0">
|
||||||
class="w-12 h-12 bg-[#4A154B] rounded-lg flex items-center justify-center"
|
<span class="material-symbols-rounded text-white" style="font-size: 22px;">tag</span>
|
||||||
>
|
</div>
|
||||||
<svg
|
<div class="flex-1">
|
||||||
class="w-7 h-7 text-white"
|
<h3 class="font-heading text-body-sm text-white">Slack</h3>
|
||||||
viewBox="0 0 24 24"
|
<p class="text-[11px] text-light/40 mt-0.5">Get notifications in your Slack workspace.</p>
|
||||||
fill="currentColor"
|
<p class="text-[10px] text-light/30 mt-1">Coming soon</p>
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52a2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521a2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521a2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523a2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="text-lg font-semibold text-light">Slack</h3>
|
|
||||||
<p class="text-sm text-light/50 mt-1">
|
|
||||||
Get notifications in your Slack workspace.
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-light/40 mt-2">Coming soon</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connect Calendar Modal -->
|
<!-- Connect Calendar Modal -->
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
Card,
|
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -169,113 +168,97 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-4 max-w-2xl">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-semibold text-light">
|
<div>
|
||||||
{m.settings_members_title({
|
<h2 class="font-heading text-body text-white">
|
||||||
count: String(members.length),
|
{m.settings_members_title({
|
||||||
})}
|
count: String(members.length),
|
||||||
</h2>
|
})}
|
||||||
<Button onclick={() => (showInviteModal = true)}>
|
</h2>
|
||||||
<svg
|
</div>
|
||||||
class="w-4 h-4 mr-2"
|
<Button size="sm" icon="person_add" onclick={() => (showInviteModal = true)}>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle
|
|
||||||
cx="9"
|
|
||||||
cy="7"
|
|
||||||
r="4"
|
|
||||||
/><line x1="19" y1="8" x2="19" y2="14" /><line
|
|
||||||
x1="22"
|
|
||||||
y1="11"
|
|
||||||
x2="16"
|
|
||||||
y2="11"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{m.settings_members_invite()}
|
{m.settings_members_invite()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pending Invites -->
|
<!-- Pending Invites -->
|
||||||
{#if invites.length > 0}
|
{#if invites.length > 0}
|
||||||
<Card>
|
<div class="bg-dark/30 border border-light/5 rounded-xl p-4">
|
||||||
<div class="p-4">
|
<h3 class="text-body-sm font-heading text-light/60 mb-3">
|
||||||
<h3 class="text-sm font-medium text-light/70 mb-3">
|
{m.settings_members_pending()}
|
||||||
{m.settings_members_pending()}
|
</h3>
|
||||||
</h3>
|
<div class="space-y-2">
|
||||||
<div class="space-y-2">
|
{#each invites as invite}
|
||||||
{#each invites as invite}
|
<div
|
||||||
<div
|
class="flex items-center justify-between py-2 px-3 bg-light/5 rounded-lg"
|
||||||
class="flex items-center justify-between py-2 px-3 bg-light/5 rounded-lg"
|
>
|
||||||
>
|
<div>
|
||||||
<div>
|
<p class="text-body-sm text-white">{invite.email}</p>
|
||||||
<p class="text-light">{invite.email}</p>
|
<p class="text-[11px] text-light/40">
|
||||||
<p class="text-xs text-light/40">
|
Invited as {invite.role} • Expires {new Date(
|
||||||
Invited as {invite.role} • Expires {new Date(
|
invite.expires_at,
|
||||||
invite.expires_at,
|
).toLocaleDateString()}
|
||||||
).toLocaleDateString()}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
onclick={() =>
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
`${window.location.origin}/invite/${invite.token}`,
|
|
||||||
)}
|
|
||||||
>{m.settings_members_copy_link()}</Button
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => cancelInvite(invite.id)}
|
|
||||||
>Cancel</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
<div class="flex items-center gap-1.5">
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
|
onclick={() =>
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}/invite/${invite.token}`,
|
||||||
|
)}
|
||||||
|
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>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="p-1.5 text-light/40 hover:text-error hover:bg-error/10 rounded-lg transition-colors"
|
||||||
|
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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Members List -->
|
<!-- Members List -->
|
||||||
<Card>
|
<div class="bg-dark/30 border border-light/5 rounded-xl overflow-hidden">
|
||||||
<div class="divide-y divide-light/10">
|
<div class="divide-y divide-light/5">
|
||||||
{#each members as member}
|
{#each members as member}
|
||||||
{@const rawProfile = member.profiles}
|
{@const rawProfile = member.profiles}
|
||||||
{@const profile = Array.isArray(rawProfile)
|
{@const profile = Array.isArray(rawProfile)
|
||||||
? rawProfile[0]
|
? rawProfile[0]
|
||||||
: rawProfile}
|
: rawProfile}
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-4 hover:bg-light/5 transition-colors"
|
class="flex items-center justify-between px-4 py-3 hover:bg-light/5 transition-colors"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<Avatar
|
||||||
class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-medium"
|
name={profile?.full_name || profile?.email || "?"}
|
||||||
>
|
src={profile?.avatar_url}
|
||||||
{(profile?.full_name ||
|
size="sm"
|
||||||
profile?.email ||
|
/>
|
||||||
"?")[0].toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-light font-medium">
|
<p class="text-body-sm text-white">
|
||||||
{profile?.full_name ||
|
{profile?.full_name ||
|
||||||
profile?.email ||
|
profile?.email ||
|
||||||
"Unknown User"}
|
"Unknown User"}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-light/50">
|
<p class="text-[11px] text-light/40">
|
||||||
{profile?.email || "No email"}
|
{profile?.email || "No email"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="px-2 py-1 text-xs rounded-full capitalize"
|
class="px-2 py-0.5 text-[10px] rounded-md capitalize font-body"
|
||||||
style="background-color: {roles.find(
|
style="background-color: {roles.find(
|
||||||
(r) => r.name.toLowerCase() === member.role,
|
(r) => r.name.toLowerCase() === member.role,
|
||||||
)?.color ?? '#6366f1'}20; color: {roles.find(
|
)?.color ?? '#6366f1'}20; color: {roles.find(
|
||||||
@@ -283,18 +266,20 @@
|
|||||||
)?.color ?? '#6366f1'}">{member.role}</span
|
)?.color ?? '#6366f1'}">{member.role}</span
|
||||||
>
|
>
|
||||||
{#if member.user_id !== userId && member.role !== "owner"}
|
{#if member.user_id !== userId && member.role !== "owner"}
|
||||||
<Button
|
<button
|
||||||
variant="tertiary"
|
type="button"
|
||||||
size="sm"
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
onclick={() => openMemberModal(member)}
|
onclick={() => openMemberModal(member)}
|
||||||
>Edit</Button
|
title="Edit"
|
||||||
>
|
>
|
||||||
|
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Invite Member Modal -->
|
<!-- Invite Member Modal -->
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button, Modal, Card, Input } from "$lib/components/ui";
|
import { Button, Modal, Input } from "$lib/components/ui";
|
||||||
import { toasts } from "$lib/stores/toast.svelte";
|
import { toasts } from "$lib/stores/toast.svelte";
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from "$lib/supabase/types";
|
import type { Database } from "$lib/supabase/types";
|
||||||
@@ -188,86 +188,72 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-4 max-w-2xl">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-light">Roles</h2>
|
<h2 class="font-heading text-body text-white">Roles</h2>
|
||||||
<p class="text-sm text-light/50">
|
<p class="text-body-sm text-light/40 mt-0.5">
|
||||||
Create custom roles with specific permissions.
|
Create custom roles with specific permissions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onclick={() => openRoleModal()} icon="add">
|
<Button size="sm" onclick={() => openRoleModal()} icon="add">
|
||||||
Create Role
|
Create Role
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-4">
|
<div class="flex flex-col gap-2">
|
||||||
{#each roles as role}
|
{#each roles as role}
|
||||||
<Card>
|
<div class="bg-dark/30 border border-light/5 rounded-xl px-4 py-3 hover:border-light/10 transition-colors">
|
||||||
<div class="p-4">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center gap-3">
|
<div
|
||||||
<div
|
class="w-2.5 h-2.5 rounded-full shrink-0"
|
||||||
class="w-3 h-3 rounded-full"
|
style="background-color: {role.color}"
|
||||||
style="background-color: {role.color}"
|
></div>
|
||||||
></div>
|
<span class="text-body-sm font-medium text-white">{role.name}</span>
|
||||||
<span class="font-medium text-light"
|
{#if role.is_system}
|
||||||
>{role.name}</span
|
<span class="text-[10px] text-light/30 bg-light/5 px-1.5 py-0.5 rounded-md">System</span>
|
||||||
>
|
{/if}
|
||||||
{#if role.is_system}
|
{#if role.is_default}
|
||||||
<span
|
<span class="text-[10px] text-primary bg-primary/10 px-1.5 py-0.5 rounded-md">Default</span>
|
||||||
class="text-xs text-light/40 bg-light/10 px-2 py-0.5 rounded"
|
{/if}
|
||||||
>System</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if role.is_default}
|
|
||||||
<span
|
|
||||||
class="text-xs text-primary bg-primary/10 px-2 py-0.5 rounded"
|
|
||||||
>Default</span
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{#if !role.is_system || role.name !== "Owner"}
|
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => openRoleModal(role)}
|
|
||||||
>Edit</Button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if !role.is_system}
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => deleteRole(role)}
|
|
||||||
>Delete</Button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex items-center gap-1.5">
|
||||||
{#if role.permissions.includes("*")}
|
{#if !role.is_system || role.name !== "Owner"}
|
||||||
<span
|
<button
|
||||||
class="text-xs bg-light/10 text-light/70 px-2 py-1 rounded"
|
type="button"
|
||||||
>All Permissions</span
|
class="p-1.5 text-light/40 hover:text-white hover:bg-dark/50 rounded-lg transition-colors"
|
||||||
|
onclick={() => openRoleModal(role)}
|
||||||
|
title="Edit"
|
||||||
>
|
>
|
||||||
{:else}
|
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
|
||||||
{#each role.permissions.slice(0, 6) as perm}
|
</button>
|
||||||
<span
|
{/if}
|
||||||
class="text-xs bg-light/10 text-light/50 px-2 py-1 rounded"
|
{#if !role.is_system}
|
||||||
>{perm}</span
|
<button
|
||||||
>
|
type="button"
|
||||||
{/each}
|
class="p-1.5 text-light/40 hover:text-error hover:bg-error/10 rounded-lg transition-colors"
|
||||||
{#if role.permissions.length > 6}
|
onclick={() => deleteRole(role)}
|
||||||
<span class="text-xs text-light/40"
|
title="Delete"
|
||||||
>+{role.permissions.length - 6} more</span
|
>
|
||||||
>
|
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">delete</span>
|
||||||
{/if}
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
<div class="flex flex-wrap gap-1">
|
||||||
|
{#if role.permissions.includes("*")}
|
||||||
|
<span class="text-[10px] bg-light/5 text-light/50 px-1.5 py-0.5 rounded-md">All Permissions</span>
|
||||||
|
{:else}
|
||||||
|
{#each role.permissions.slice(0, 6) as perm}
|
||||||
|
<span class="text-[10px] bg-light/5 text-light/40 px-1.5 py-0.5 rounded-md">{perm}</span>
|
||||||
|
{/each}
|
||||||
|
{#if role.permissions.length > 6}
|
||||||
|
<span class="text-[10px] text-light/30">+{role.permissions.length - 6} more</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user