mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-14 03:59:15 +01:00
refactor: ♻️ Finish rewrite and delete old settings backend
This commit is contained in:
parent
3ce71dd4df
commit
34ce25cc1d
50 changed files with 472 additions and 1538 deletions
|
|
@ -1,419 +0,0 @@
|
|||
<template>
|
||||
<Card v-if="identity" class="w-full max-h-full block overflow-y-auto">
|
||||
<form class="p-4 grid gap-6" ref="formRef" @submit="handleSubmit">
|
||||
<FormField v-slot="{ handleChange, handleBlur }" name="banner">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.bright_late_osprey_renew() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="handleChange"
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.great_level_lamb_sway() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ setValue }" name="avatar">
|
||||
<FormItem class="grid gap-1">
|
||||
<FormLabel>
|
||||
{{ m.safe_icy_bulldog_quell() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<ImageUploader
|
||||
v-model:image="identity.account.avatar"
|
||||
@submit-file="(file) => setValue(file)"
|
||||
@submit-url="(url) => setValue(url)"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.mild_known_mallard_jolt() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.lime_dry_skunk_loop() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="username">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.neat_silly_dog_prosper() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.petty_plane_tadpole_earn() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="bio">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.next_caring_ladybug_hack() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea rows="10" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.stale_just_anaconda_earn() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ value, handleChange }" name="fields">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.aqua_mealy_toucan_pride() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<div class="grid gap-4">
|
||||
<div
|
||||
v-for="(field, index) in value"
|
||||
:key="index"
|
||||
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))] gap-2"
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
@click="
|
||||
handleChange([
|
||||
...value.slice(0, index),
|
||||
...value.slice(index + 1),
|
||||
])
|
||||
"
|
||||
>
|
||||
<Trash />
|
||||
</Button>
|
||||
<Input
|
||||
v-model="field.name"
|
||||
placeholder="Name"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
handleChange([
|
||||
...value.slice(0, index),
|
||||
{ name: e, value: field.value },
|
||||
...value.slice(index + 1),
|
||||
]);
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
v-model="field.value"
|
||||
placeholder="Value"
|
||||
class="col-span-2"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
handleChange([
|
||||
...value.slice(0, index),
|
||||
{ name: field.name, value: e },
|
||||
...value.slice(index + 1),
|
||||
]);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
@click="
|
||||
handleChange([
|
||||
...value,
|
||||
{ name: '', value: '' },
|
||||
])
|
||||
"
|
||||
>
|
||||
{{ m.front_north_eel_gulp() }}
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField, value, handleChange }"
|
||||
name="bot"
|
||||
:as="Card"
|
||||
class="block"
|
||||
>
|
||||
<FormItem
|
||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
||||
>
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<FormLabel :as="CardTitle">
|
||||
{{ m.gaudy_each_opossum_play() }}
|
||||
</FormLabel>
|
||||
<CardDescription>
|
||||
{{ m.grassy_acidic_gadfly_cure() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:checked="value"
|
||||
@update:checked="handleChange"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField, value, handleChange }"
|
||||
name="locked"
|
||||
:as="Card"
|
||||
class="block"
|
||||
>
|
||||
<FormItem
|
||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
||||
>
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<FormLabel :as="CardTitle">
|
||||
{{ m.dirty_moving_shark_emerge() }}
|
||||
</FormLabel>
|
||||
<CardDescription>
|
||||
{{ m.bright_fun_mouse_boil() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:checked="value"
|
||||
@update:checked="handleChange"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField, value, handleChange }"
|
||||
name="discoverable"
|
||||
:as="Card"
|
||||
class="block"
|
||||
>
|
||||
<FormItem
|
||||
class="grid grid-cols-[1fr_auto] items-center gap-2"
|
||||
>
|
||||
<CardHeader class="space-y-0.5 p-0">
|
||||
<FormLabel :as="CardTitle">
|
||||
{{ m.red_vivid_cuckoo_spark() }}
|
||||
</FormLabel>
|
||||
<CardDescription>
|
||||
{{ m.plain_zany_donkey_dart() }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:checked="value"
|
||||
@update:checked="handleChange"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</form>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import type { ResponseError } from "@versia/client";
|
||||
import { Trash } from "lucide-vue-next";
|
||||
import { useForm } from "vee-validate";
|
||||
import { toast } from "vue-sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
import ImageUploader from "./image-uploader.vue";
|
||||
|
||||
if (!identity.value) {
|
||||
throw new Error("Identity not found.");
|
||||
}
|
||||
|
||||
const account = ref(identity.value.account);
|
||||
|
||||
const formSchema = toTypedSchema(
|
||||
z.object({
|
||||
banner: z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
(identity.value?.instance.configuration.accounts
|
||||
.header_size_limit ?? Number.POSITIVE_INFINITY),
|
||||
m.civil_icy_ant_mend({
|
||||
size: identity.value?.instance.configuration.accounts
|
||||
.header_size_limit,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
avatar: z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
(identity.value?.instance.configuration.accounts
|
||||
.avatar_size_limit ?? Number.POSITIVE_INFINITY),
|
||||
m.zippy_caring_raven_edit({
|
||||
size: identity.value?.instance.configuration.accounts
|
||||
.avatar_size_limit,
|
||||
}),
|
||||
)
|
||||
.or(z.string().url())
|
||||
.optional(),
|
||||
name: z
|
||||
.string()
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_displayname_characters,
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9_-]+$/, m.still_upper_otter_dine())
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_username_characters,
|
||||
),
|
||||
bio: z
|
||||
.string()
|
||||
.max(
|
||||
identity.value.instance.configuration.accounts
|
||||
.max_note_characters,
|
||||
),
|
||||
bot: z.boolean(),
|
||||
locked: z.boolean(),
|
||||
discoverable: z.boolean(),
|
||||
fields: z.array(z.object({ name: z.string(), value: z.string() })),
|
||||
}),
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
initialValues: {
|
||||
bio: account.value.source?.note ?? "",
|
||||
bot: account.value.bot ?? false,
|
||||
locked: account.value.locked ?? false,
|
||||
discoverable: account.value.discoverable ?? true,
|
||||
username: account.value.username,
|
||||
name: account.value.display_name,
|
||||
fields: account.value.source?.fields ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
const id = toast.loading(m.jolly_noble_sloth_breathe());
|
||||
|
||||
const changedData = {
|
||||
display_name:
|
||||
values.name === account.value.display_name
|
||||
? undefined
|
||||
: values.name,
|
||||
username:
|
||||
values.username === account.value.username
|
||||
? undefined
|
||||
: values.username,
|
||||
note:
|
||||
values.bio === account.value.source?.note ? undefined : values.bio,
|
||||
bot: values.bot === account.value.bot ? undefined : values.bot,
|
||||
locked:
|
||||
values.locked === account.value.locked ? undefined : values.locked,
|
||||
discoverable:
|
||||
values.discoverable === account.value.discoverable
|
||||
? undefined
|
||||
: values.discoverable,
|
||||
// Can't compare two arrays directly in JS, so we need to check if all fields are the same
|
||||
fields_attributes: values.fields.every((field) =>
|
||||
account.value.source?.fields?.some(
|
||||
(f) => f.name === field.name && f.value === field.value,
|
||||
),
|
||||
)
|
||||
? undefined
|
||||
: values.fields,
|
||||
header: values.banner ? values.banner : undefined,
|
||||
avatar: values.avatar ? values.avatar : undefined,
|
||||
};
|
||||
|
||||
if (
|
||||
Object.values(changedData).filter((v) => v !== undefined).length === 0
|
||||
) {
|
||||
toast.dismiss(id);
|
||||
toast.error(m.tough_alive_niklas_promise());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await client.value.updateCredentials(
|
||||
Object.fromEntries(
|
||||
Object.entries(changedData).filter(([, v]) => v !== undefined),
|
||||
),
|
||||
);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.spry_honest_kestrel_arrive());
|
||||
|
||||
if (identity.value) {
|
||||
identity.value.account = data;
|
||||
}
|
||||
|
||||
account.value = data;
|
||||
form.resetForm({
|
||||
values: {
|
||||
...form.values,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e as ResponseError<{ error: string }>;
|
||||
|
||||
toast.dismiss(id);
|
||||
}
|
||||
});
|
||||
|
||||
const formRef = ref<HTMLFormElement | null>(null);
|
||||
|
||||
defineExpose({
|
||||
submitForm: () => handleSubmit(),
|
||||
dirty: computed(() => form.meta.value.dirty),
|
||||
});
|
||||
</script>
|
||||
104
components/preferences/profile/fields.vue
Normal file
104
components/preferences/profile/fields.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ title }}
|
||||
<Button type="button" variant="secondary" size="icon" class="ml-auto" @click="addField()" :title="m.front_north_eel_gulp()">
|
||||
<Plus />
|
||||
</Button>
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<VueDraggable class="grid gap-4" v-model="list" :animation="200" handle=".drag-handle">
|
||||
<div v-for="(field, index) in list" :key="field.id"
|
||||
class="grid items-center grid-cols-[auto_repeat(3,minmax(0,1fr))_auto] gap-2">
|
||||
<Button as="span" variant="ghost" size="icon" class="drag-handle cursor-grab">
|
||||
<GripVertical />
|
||||
</Button>
|
||||
<Input :model-value="field.name" placeholder="Name" @update:model-value="
|
||||
(e) => updateKey(index, String(e))
|
||||
" />
|
||||
<Input :model-value="field.value" placeholder="Value" class="col-span-2" @update:model-value="
|
||||
(e) => updateValue(index, String(e))
|
||||
" />
|
||||
<Button type="button" variant="secondary" size="icon" @click="removeField(index)">
|
||||
<Trash />
|
||||
</Button>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
<FormMessage />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { GripVertical, Plus, Trash } from "lucide-vue-next";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
FormControl,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "~/components/ui/form";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import * as m from "~/paraglide/messages.js";
|
||||
|
||||
const { title } = defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
|
||||
const value = defineModel<{ name: string; value: string }[]>("value", {
|
||||
default: [],
|
||||
});
|
||||
|
||||
const list = ref<
|
||||
{
|
||||
id: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}[]
|
||||
>(
|
||||
value.value.map((item, index) => ({
|
||||
id: String(index),
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
);
|
||||
|
||||
watch(
|
||||
list,
|
||||
(newList) => {
|
||||
value.value = newList.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
}));
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const updateKey = (index: number, key: string) => {
|
||||
if (!list.value[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.value[index].name = key;
|
||||
};
|
||||
|
||||
const updateValue = (index: number, val: string) => {
|
||||
if (!list.value[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.value[index].value = val;
|
||||
};
|
||||
|
||||
const removeField = (index: number) => {
|
||||
list.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const addField = () => {
|
||||
list.value.push({ name: "", value: "", id: String(list.value.length) });
|
||||
};
|
||||
</script>
|
||||
|
|
@ -21,10 +21,10 @@
|
|||
<DialogDescription class="sr-only">
|
||||
{{ m.suave_broad_albatross_drop() }}
|
||||
</DialogDescription>
|
||||
<form class="p-4 grid gap-6" @submit="submit">
|
||||
<form class="grid gap-6" @submit="submit">
|
||||
<Tabs
|
||||
default-value="upload"
|
||||
class="mt-2 data-[component=tabpanel]:*:mt-6"
|
||||
class="mt-2 *:data-[slot=tabs-content]:mt-2"
|
||||
>
|
||||
<TabsList class="w-full *:w-full">
|
||||
<TabsTrigger value="upload">
|
||||
|
|
@ -222,7 +222,7 @@ const emailToGravatar = async (email: string) => {
|
|||
const open = ref(false);
|
||||
const gravatarUrl = ref<string | undefined>(undefined);
|
||||
|
||||
const { handleSubmit, isSubmitting, values } = useForm({
|
||||
const { handleSubmit, isSubmitting } = useForm({
|
||||
validationSchema: schema,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue