Mega push vol 4

This commit is contained in:
AlacrisDevs
2026-02-06 16:08:40 +02:00
parent b517bb975c
commit d8bbfd9dc3
95 changed files with 8019 additions and 3946 deletions

View 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>

View File

@@ -0,0 +1 @@
export { default as SettingsGeneral } from './SettingsGeneral.svelte';