Mega push vol 4
This commit is contained in:
216
src/lib/components/settings/SettingsGeneral.svelte
Normal file
216
src/lib/components/settings/SettingsGeneral.svelte
Normal file
@@ -0,0 +1,216 @@
|
||||
<script lang="ts">
|
||||
import { Button, Input, Avatar } from "$lib/components/ui";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
import { toasts } from "$lib/stores/toast.svelte";
|
||||
import { invalidateAll } from "$app/navigation";
|
||||
|
||||
interface Props {
|
||||
supabase: SupabaseClient<Database>;
|
||||
org: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
avatar_url?: string | null;
|
||||
};
|
||||
isOwner: boolean;
|
||||
onLeave: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
let { supabase, org, isOwner, onLeave, onDelete }: Props = $props();
|
||||
|
||||
let orgName = $state(org.name);
|
||||
let orgSlug = $state(org.slug);
|
||||
let avatarUrl = $state(org.avatar_url ?? null);
|
||||
let isSaving = $state(false);
|
||||
let isUploading = $state(false);
|
||||
let avatarInput = $state<HTMLInputElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
orgName = org.name;
|
||||
orgSlug = org.slug;
|
||||
avatarUrl = org.avatar_url ?? null;
|
||||
});
|
||||
|
||||
async function handleAvatarUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toasts.error("Please select an image file.");
|
||||
return;
|
||||
}
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
toasts.error("Image must be under 2MB.");
|
||||
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("Failed to 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("Failed to save avatar URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
avatarUrl = publicUrl;
|
||||
await invalidateAll();
|
||||
toasts.success("Avatar updated.");
|
||||
} catch (err) {
|
||||
toasts.error("Avatar upload failed.");
|
||||
} 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("Failed to remove avatar.");
|
||||
} else {
|
||||
avatarUrl = null;
|
||||
await invalidateAll();
|
||||
toasts.success("Avatar removed.");
|
||||
}
|
||||
isSaving = false;
|
||||
}
|
||||
|
||||
async function saveGeneralSettings() {
|
||||
isSaving = true;
|
||||
const { error } = await supabase
|
||||
.from("organizations")
|
||||
.update({ name: orgName, slug: orgSlug })
|
||||
.eq("id", org.id);
|
||||
|
||||
if (error) {
|
||||
toasts.error("Failed to save settings.");
|
||||
} else if (orgSlug !== org.slug) {
|
||||
window.location.href = `/${orgSlug}/settings`;
|
||||
} else {
|
||||
toasts.success("Settings saved.");
|
||||
}
|
||||
isSaving = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Organization Details -->
|
||||
<h2 class="font-heading text-h2 text-white">Organization details</h2>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Avatar Upload -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-body text-body-sm text-light">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="tertiary"
|
||||
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"
|
||||
/>
|
||||
<div>
|
||||
<Button onclick={saveGeneralSettings} loading={isSaving}
|
||||
>Save Changes</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
{#if isOwner}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h4 class="font-heading text-h4 text-white">Danger Zone</h4>
|
||||
<p class="font-body text-body text-white">
|
||||
Permanently delete this organization and all its data.
|
||||
</p>
|
||||
<div>
|
||||
<Button variant="danger" onclick={onDelete}
|
||||
>Delete Organization</Button
|
||||
>
|
||||
</div>
|
||||
</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>
|
||||
Reference in New Issue
Block a user