mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 19:49:16 +01:00
chore: ⬆️ Upgrade to Nuxt 4
Some checks failed
Some checks failed
This commit is contained in:
parent
8debe97f63
commit
7f7cf20311
386 changed files with 2376 additions and 2332 deletions
73
app/components/preferences/emojis/batch-dropdown.vue
Normal file
73
app/components/preferences/emojis/batch-dropdown.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="min-w-48">
|
||||
<DropdownMenuItem @click="deleteAll" :disabled="!canEdit">
|
||||
<Delete />
|
||||
{{ m.tense_quick_cod_favor() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type CustomEmoji, RolePermission } from "@versia/client/schemas";
|
||||
import { Delete } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import type { z } from "zod";
|
||||
import { confirmModalService } from "~/components/modals/composable";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
const { emojis } = defineProps<{
|
||||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
}>();
|
||||
|
||||
const permissions = usePermissions();
|
||||
const canEdit =
|
||||
(!emojis.some((e) => e.global) &&
|
||||
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
|
||||
permissions.value.includes(RolePermission.ManageEmojis);
|
||||
|
||||
const deleteAll = async () => {
|
||||
if (!identity.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { confirmed } = await confirmModalService.confirm({
|
||||
title: m.tense_quick_cod_favor(),
|
||||
message: m.next_hour_jurgen_sprout({
|
||||
amount: emojis.length,
|
||||
}),
|
||||
confirmText: m.tense_quick_cod_favor(),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const id = toast.loading(
|
||||
m.equal_only_crow_file({
|
||||
amount: emojis.length,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await Promise.all(
|
||||
emojis.map((emoji) => client.value.deleteEmoji(emoji.id)),
|
||||
);
|
||||
toast.dismiss(id);
|
||||
toast.success("Emojis deleted");
|
||||
|
||||
identity.value.emojis = identity.value.emojis.filter(
|
||||
(e) => !emojis.some((emoji) => e.id === emoji.id),
|
||||
);
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
108
app/components/preferences/emojis/dropdown.vue
Normal file
108
app/components/preferences/emojis/dropdown.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="icon" title="Open menu" class="size-8 p-0">
|
||||
<MoreHorizontal class="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="min-w-48">
|
||||
<DropdownMenuItem @click="editName">
|
||||
<TextCursorInput />
|
||||
{{ m.cuddly_such_swallow_hush() }}
|
||||
</DropdownMenuItem>
|
||||
<!-- <DropdownMenuItem @click="editCaption">
|
||||
<Captions />
|
||||
<span>Add caption</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator /> -->
|
||||
<DropdownMenuItem @click="_delete">
|
||||
<Delete />
|
||||
{{ m.tense_quick_cod_favor() }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type CustomEmoji, RolePermission } from "@versia/client/schemas";
|
||||
import { Delete, MoreHorizontal, TextCursorInput } from "lucide-vue-next";
|
||||
import { toast } from "vue-sonner";
|
||||
import type { z } from "zod";
|
||||
import { confirmModalService } from "~/components/modals/composable";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import * as m from "~~/paraglide/messages.js";
|
||||
|
||||
const { emoji } = defineProps<{
|
||||
emoji: z.infer<typeof CustomEmoji>;
|
||||
}>();
|
||||
|
||||
const permissions = usePermissions();
|
||||
const canEdit =
|
||||
(!emoji.global &&
|
||||
permissions.value.includes(RolePermission.ManageOwnEmojis)) ||
|
||||
permissions.value.includes(RolePermission.ManageEmojis);
|
||||
|
||||
const editName = async () => {
|
||||
if (!identity.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await confirmModalService.confirm({
|
||||
title: m.slimy_awful_florian_sail(),
|
||||
defaultValue: emoji.shortcode,
|
||||
confirmText: m.teary_antsy_panda_aid(),
|
||||
inputType: "text",
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
const id = toast.loading(m.teary_tame_gull_bless());
|
||||
try {
|
||||
const { data } = await client.value.updateEmoji(emoji.id, {
|
||||
shortcode: result.value,
|
||||
});
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.gaudy_lime_bison_adore());
|
||||
|
||||
identity.value.emojis = identity.value.emojis.map((e) =>
|
||||
e.id === emoji.id ? data : e,
|
||||
);
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _delete = async () => {
|
||||
if (!identity.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { confirmed } = await confirmModalService.confirm({
|
||||
title: m.tense_quick_cod_favor(),
|
||||
message: m.honest_factual_carp_aspire(),
|
||||
confirmText: m.tense_quick_cod_favor(),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const id = toast.loading(m.weary_away_liger_zip());
|
||||
try {
|
||||
await client.value.deleteEmoji(emoji.id);
|
||||
toast.dismiss(id);
|
||||
toast.success(m.crisp_whole_canary_tear());
|
||||
|
||||
identity.value.emojis = identity.value.emojis.filter(
|
||||
(e) => e.id !== emoji.id,
|
||||
);
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
19
app/components/preferences/emojis/index.vue
Normal file
19
app/components/preferences/emojis/index.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div v-if="emojis.length > 0" class="grow">
|
||||
<Table :emojis="emojis" :can-upload="canUpload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import Table from "./table.vue";
|
||||
|
||||
const permissions = usePermissions();
|
||||
const canUpload = computed(
|
||||
() =>
|
||||
permissions.value.includes(RolePermission.ManageOwnEmojis) ||
|
||||
permissions.value.includes(RolePermission.ManageEmojis),
|
||||
);
|
||||
|
||||
const emojis = computed(() => identity.value?.emojis ?? []);
|
||||
</script>
|
||||
362
app/components/preferences/emojis/table.vue
Normal file
362
app/components/preferences/emojis/table.vue
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
<script setup lang="tsx">
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
ExpandedState,
|
||||
SortingState,
|
||||
Updater,
|
||||
VisibilityState,
|
||||
} from "@tanstack/vue-table";
|
||||
import {
|
||||
FlexRender,
|
||||
getCoreRowModel,
|
||||
getExpandedRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useVueTable,
|
||||
} from "@tanstack/vue-table";
|
||||
import type { CustomEmoji } from "@versia/client/schemas";
|
||||
import {
|
||||
ArrowDownAZ,
|
||||
ArrowUpAz,
|
||||
ArrowUpDown,
|
||||
ChevronDown,
|
||||
Ellipsis,
|
||||
Globe,
|
||||
Home,
|
||||
Plus,
|
||||
} from "lucide-vue-next";
|
||||
import { ref } from "vue";
|
||||
import type { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import BatchDropdown from "./batch-dropdown.vue";
|
||||
import Dropdown from "./dropdown.vue";
|
||||
import Uploader from "./uploader.vue";
|
||||
|
||||
// No destructuring props to avoid reactivity issues
|
||||
const props = defineProps<{
|
||||
emojis: z.infer<typeof CustomEmoji>[];
|
||||
canUpload: boolean;
|
||||
}>();
|
||||
|
||||
const emojisRef = computed(() => props.emojis);
|
||||
|
||||
const valueUpdater = <T extends Updater<any>>(updaterOrValue: T, ref: Ref) => {
|
||||
ref.value =
|
||||
typeof updaterOrValue === "function"
|
||||
? updaterOrValue(ref.value)
|
||||
: updaterOrValue;
|
||||
};
|
||||
|
||||
const columns: ColumnDef<z.infer<typeof CustomEmoji>>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
modelValue={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onUpdate:modelValue={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
modelValue={row.getIsSelected()}
|
||||
onUpdate:modelValue={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "url",
|
||||
header: "Image",
|
||||
cell: ({ row }) => (
|
||||
<img
|
||||
src={row.getValue("url")}
|
||||
alt={`:${row.getValue("shortcode")}:`}
|
||||
title={row.getValue("shortcode")}
|
||||
class="h-[1lh] align-middle inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "shortcode",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
class="!p-0 !h-auto"
|
||||
// @ts-expect-error types don't include onClick
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Shortcode
|
||||
{column.getIsSorted() === false ? (
|
||||
<ArrowUpDown class="ml-2 size-4" />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
<ArrowDownAZ class="ml-2 size-4" />
|
||||
) : (
|
||||
<ArrowUpAz class="ml-2 size-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div class="font-mono">{row.getValue("shortcode")}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "category",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
class="!p-0 !h-auto"
|
||||
// @ts-expect-error types don't include onClick
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Category
|
||||
{column.getIsSorted() === false ? (
|
||||
<ArrowUpDown class="ml-2 size-4" />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
<ArrowDownAZ class="ml-2 size-4" />
|
||||
) : (
|
||||
<ArrowUpAz class="ml-2 size-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div class="font-mono">
|
||||
{row.getValue("category") ?? "Uncategorized"}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "global",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="link"
|
||||
class="!p-0 !h-auto"
|
||||
// @ts-expect-error types don't include onClick
|
||||
onClick={() => {
|
||||
const filter = column.getFilterValue();
|
||||
|
||||
if (filter === undefined) {
|
||||
column.setFilterValue(false);
|
||||
} else if (filter === false) {
|
||||
column.setFilterValue(true);
|
||||
} else {
|
||||
column.setFilterValue(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Uploader
|
||||
{column.getFilterValue() === undefined ? (
|
||||
<Ellipsis class="ml-2 size-4" />
|
||||
) : column.getFilterValue() ? (
|
||||
<Globe class="ml-2 size-4" />
|
||||
) : (
|
||||
<Home class="ml-2 size-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div class="font-mono">
|
||||
{row.getValue("global") ? "Admin" : "You"}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
header: ({ table }) => {
|
||||
const selected = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((r) => r.original);
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<BatchDropdown emojis={selected}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
// @ts-expect-error types don't include title
|
||||
title="Open menu"
|
||||
disabled={selected.length === 0}
|
||||
>
|
||||
<Ellipsis class="size-4" />
|
||||
</Button>
|
||||
</BatchDropdown>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const emoji = row.original;
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<Dropdown emoji={emoji} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const sorting = ref<SortingState>([
|
||||
{
|
||||
id: "shortcode",
|
||||
desc: false,
|
||||
},
|
||||
]);
|
||||
const columnFilters = ref<ColumnFiltersState>([]);
|
||||
const columnVisibility = ref<VisibilityState>({});
|
||||
const rowSelection = ref({});
|
||||
const expanded = ref<ExpandedState>({});
|
||||
|
||||
const table = useVueTable({
|
||||
data: emojisRef,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
onSortingChange: (updaterOrValue) => valueUpdater(updaterOrValue, sorting),
|
||||
onColumnFiltersChange: (updaterOrValue) =>
|
||||
valueUpdater(updaterOrValue, columnFilters),
|
||||
onColumnVisibilityChange: (updaterOrValue) =>
|
||||
valueUpdater(updaterOrValue, columnVisibility),
|
||||
onRowSelectionChange: (updaterOrValue) =>
|
||||
valueUpdater(updaterOrValue, rowSelection),
|
||||
onExpandedChange: (updaterOrValue) =>
|
||||
valueUpdater(updaterOrValue, expanded),
|
||||
state: {
|
||||
get sorting() {
|
||||
return sorting.value;
|
||||
},
|
||||
get columnFilters() {
|
||||
return columnFilters.value;
|
||||
},
|
||||
get columnVisibility() {
|
||||
return columnVisibility.value;
|
||||
},
|
||||
get rowSelection() {
|
||||
return rowSelection.value;
|
||||
},
|
||||
get expanded() {
|
||||
return expanded.value;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="flex gap-2 items-center py-4">
|
||||
<Input class="max-w-52 mr-auto" placeholder="Filter emojis..."
|
||||
:model-value="(table.getColumn('shortcode')?.getFilterValue() as string)"
|
||||
@update:model-value="table.getColumn('shortcode')?.setFilterValue($event)" />
|
||||
<Uploader v-if="props.canUpload">
|
||||
<Button variant="outline" size="icon" title="Upload emoji">
|
||||
<Plus class="size-4" />
|
||||
</Button>
|
||||
</Uploader>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline">
|
||||
Columns
|
||||
<ChevronDown class="ml-2 size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuCheckboxItem
|
||||
v-for="column in table.getAllColumns().filter((column) => column.getCanHide())" :key="column.id"
|
||||
class="capitalize" :model-value="column.getIsVisible()" @update:model-value="(value) => {
|
||||
column.toggleVisibility(!!value)
|
||||
}">
|
||||
{{ column.id }}
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
<TableHead v-for="header in headerGroup.headers" :key="header.id" class="">
|
||||
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header"
|
||||
:props="header.getContext()" />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-if="table.getRowModel().rows?.length">
|
||||
<template v-for="row in table.getRowModel().rows" :key="row.id">
|
||||
<TableRow :data-state="row.getIsSelected() && 'selected'">
|
||||
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="row.getIsExpanded()">
|
||||
<TableCell :colspan="row.getAllCells().length">
|
||||
{{ JSON.stringify(row.original) }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<TableRow v-else>
|
||||
<TableCell :colspan="columns.length" class="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end space-x-2 py-4">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
{{ table.getFilteredSelectedRowModel().rows.length }} of
|
||||
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button variant="outline" size="sm" :disabled="!table.getCanPreviousPage()"
|
||||
@click="table.previousPage()">
|
||||
Previous
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" :disabled="!table.getCanNextPage()" @click="table.nextPage()">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
283
app/components/preferences/emojis/uploader.vue
Normal file
283
app/components/preferences/emojis/uploader.vue
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger>
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
{{ m.whole_icy_puffin_smile() }}
|
||||
</DialogTitle>
|
||||
<DialogDescription class="sr-only">
|
||||
{{ m.frail_great_marten_pet() }}
|
||||
</DialogDescription>
|
||||
<form class="grid gap-6" @submit="submit">
|
||||
<div
|
||||
v-if="values.image"
|
||||
class="flex items-center justify-around *:size-20 *:p-2 *:rounded *:border *:shadow"
|
||||
>
|
||||
<div class="bg-background">
|
||||
<img
|
||||
class="h-full object-cover"
|
||||
:src="createObjectURL(values.image as File)"
|
||||
:alt="values.alt"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-zinc-700">
|
||||
<img
|
||||
class="h-full object-cover"
|
||||
:src="createObjectURL(values.image as File)"
|
||||
:alt="values.alt"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-zinc-400">
|
||||
<img
|
||||
class="h-full object-cover"
|
||||
:src="createObjectURL(values.image as File)"
|
||||
:alt="values.alt"
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-foreground">
|
||||
<img
|
||||
class="h-full object-cover"
|
||||
:src="createObjectURL(values.image as File)"
|
||||
:alt="values.alt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField v-slot="{ handleChange, handleBlur }" name="image">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.active_direct_bear_compose() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="(e: any) => {
|
||||
handleChange(e);
|
||||
|
||||
if (!values.shortcode) {
|
||||
setFieldValue('shortcode', e.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
||||
}
|
||||
}"
|
||||
@blur="handleBlur"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.lime_late_millipede_urge() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="shortcode">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.happy_mild_fox_gleam() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.glad_day_kestrel_amaze() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="category">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.short_cute_jackdaw_comfort() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="alt">
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{{ m.watery_left_shrimp_bless() }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows="2"
|
||||
v-bind="componentField"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ m.weird_fun_jurgen_arise() }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ value, handleChange }"
|
||||
v-if="hasEmojiAdmin"
|
||||
name="global"
|
||||
as-child
|
||||
>
|
||||
<FormSwitch :title="m.pink_sharp_carp_work()" :description="m.dark_pretty_hyena_link()">
|
||||
<Switch
|
||||
:model-value="value"
|
||||
@update:model-value="handleChange"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</FormSwitch>
|
||||
</FormField>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose :as-child="true">
|
||||
<Button variant="outline" :disabled="isSubmitting">
|
||||
{{ m.soft_bold_ant_attend() }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
{{ m.flat_safe_haddock_gaze() }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from "@vee-validate/zod";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { useForm } from "vee-validate";
|
||||
import { toast } from "vue-sonner";
|
||||
import { z } from "zod";
|
||||
import FormSwitch from "~/components/form/switch.vue";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
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";
|
||||
|
||||
const open = ref(false);
|
||||
const permissions = usePermissions();
|
||||
const hasEmojiAdmin = permissions.value.includes(RolePermission.ManageEmojis);
|
||||
const createObjectURL = URL.createObjectURL;
|
||||
|
||||
const formSchema = toTypedSchema(
|
||||
z.object({
|
||||
image: z
|
||||
.instanceof(File, {
|
||||
message: m.sound_topical_gopher_offer(),
|
||||
})
|
||||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
(identity.value?.instance.configuration.emojis
|
||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY),
|
||||
m.orange_weird_parakeet_hug({
|
||||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
.emoji_size_limit ?? Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
),
|
||||
shortcode: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(
|
||||
identity.value?.instance.configuration.emojis
|
||||
.max_shortcode_characters ?? Number.POSITIVE_INFINITY,
|
||||
m.solid_inclusive_owl_hug({
|
||||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
.max_shortcode_characters ??
|
||||
Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
)
|
||||
.regex(emojiValidator),
|
||||
global: z.boolean().default(false),
|
||||
category: z
|
||||
.string()
|
||||
.max(
|
||||
64,
|
||||
m.home_cool_orangutan_hug({
|
||||
count: 64,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
alt: z
|
||||
.string()
|
||||
.max(
|
||||
identity.value?.instance.configuration.emojis
|
||||
.max_description_characters ?? Number.POSITIVE_INFINITY,
|
||||
m.key_ago_hound_emerge({
|
||||
count:
|
||||
identity.value?.instance.configuration.emojis
|
||||
.max_description_characters ??
|
||||
Number.POSITIVE_INFINITY,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
);
|
||||
const { isSubmitting, handleSubmit, values, setFieldValue } = useForm({
|
||||
validationSchema: formSchema,
|
||||
});
|
||||
|
||||
const submit = handleSubmit(async (values) => {
|
||||
if (!identity.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = toast.loading(m.factual_gray_mouse_believe());
|
||||
|
||||
try {
|
||||
const { data } = await client.value.uploadEmoji(
|
||||
values.shortcode,
|
||||
values.image,
|
||||
{
|
||||
alt: values.alt,
|
||||
category: values.category,
|
||||
global: values.global,
|
||||
},
|
||||
);
|
||||
|
||||
toast.dismiss(id);
|
||||
toast.success(m.cool_trite_gull_quiz());
|
||||
|
||||
identity.value.emojis = [...identity.value.emojis, data];
|
||||
open.value = false;
|
||||
} catch {
|
||||
toast.dismiss(id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue