mirror of https://github.com/wg-easy/wg-easy
Browse Source
* add tooltip info, extract strings * multi type toast * improve useSubmit, i18n * better login screen * improve * consistent folder casing * consistent casing * fix even more stuff * temp * fix type errors * remove armv6/7 support for now * add information to client page * optimize dockerfile * update base image in Dockerfile to use node:lts-alpine * fix build stagepull/1696/head
committed by
GitHub
117 changed files with 1107 additions and 1095 deletions
@ -1,3 +1,3 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
github: weejewel |
|||
github: [weejewel, kaaax0815] |
|||
|
@ -1,18 +1,10 @@ |
|||
<template> |
|||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ label }} |
|||
</Label> |
|||
<input |
|||
:id="id" |
|||
v-model.trim="data" |
|||
:name="id" |
|||
type="text" |
|||
v-model="data" |
|||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string }>(); |
|||
|
|||
const data = defineModel<string>(); |
|||
const data = defineModel<unknown>(); |
|||
</script> |
@ -0,0 +1,46 @@ |
|||
<template> |
|||
<ToastRoot |
|||
v-for="(e, i) in count" |
|||
:key="i" |
|||
:class="[ |
|||
`grid grid-cols-[auto_max-content] items-center gap-x-3 rounded-md p-3 text-neutral-200 shadow-lg [grid-template-areas:_'title_action'_'description_action'] data-[swipe=cancel]:translate-x-0 data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]`, |
|||
{ |
|||
'bg-green-800': e.type === 'success', |
|||
'bg-red-800': e.type === 'error', |
|||
}, |
|||
]" |
|||
> |
|||
<ToastTitle class="mb-1 text-sm font-medium [grid-area:_title]"> |
|||
{{ e.title }} |
|||
</ToastTitle> |
|||
<ToastDescription class="m-0 text-sm [grid-area:_description]">{{ |
|||
e.message |
|||
}}</ToastDescription> |
|||
<ToastAction as-child alt-text="toast" class="[grid-area:_action]"> |
|||
<slot /> |
|||
</ToastAction> |
|||
<ToastClose aria-label="Close"> |
|||
<span aria-hidden>×</span> |
|||
</ToastClose> |
|||
</ToastRoot> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { |
|||
ToastAction, |
|||
ToastClose, |
|||
ToastDescription, |
|||
ToastRoot, |
|||
ToastTitle, |
|||
} from 'radix-vue'; |
|||
|
|||
defineExpose({ |
|||
publish, |
|||
}); |
|||
|
|||
const count = reactive<ToastParams[]>([]); |
|||
|
|||
function publish(e: ToastParams) { |
|||
count.push({ type: e.type, title: e.title, message: e.message }); |
|||
} |
|||
</script> |
@ -0,0 +1,24 @@ |
|||
<template> |
|||
<TooltipProvider> |
|||
<TooltipRoot> |
|||
<TooltipTrigger |
|||
class="inline-flex h-8 w-8 items-center justify-center rounded-full text-gray-400 outline-none focus:shadow-sm focus:shadow-black" |
|||
> |
|||
<slot /> |
|||
</TooltipTrigger> |
|||
<TooltipPortal> |
|||
<TooltipContent |
|||
class="select-none rounded bg-gray-600 px-3 py-2 text-sm leading-none text-white shadow-lg will-change-[transform,opacity]" |
|||
:side-offset="5" |
|||
> |
|||
{{ text }} |
|||
<TooltipArrow class="fill-gray-600" :width="8" /> |
|||
</TooltipContent> |
|||
</TooltipPortal> |
|||
</TooltipRoot> |
|||
</TooltipProvider> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ text: string }>(); |
|||
</script> |
@ -1,38 +1,32 @@ |
|||
<template> |
|||
<button |
|||
class="inline-block rounded bg-gray-100 p-2 align-middle transition hover:bg-red-800 hover:text-white dark:bg-neutral-600 dark:text-neutral-300 dark:hover:bg-red-800 dark:hover:text-white" |
|||
:title="$t('OneTimeLink')" |
|||
@click="showOneTimeLink(client)" |
|||
:title="$t('client.otlDesc')" |
|||
@click="showOneTimeLink" |
|||
> |
|||
<svg |
|||
class="w-5" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961" |
|||
/> |
|||
</svg> |
|||
<IconsLink class="w-5" /> |
|||
</button> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
// TODO: improve |
|||
defineProps<{ client: LocalClient }>(); |
|||
const props = defineProps<{ client: LocalClient }>(); |
|||
|
|||
const clientsStore = useClientsStore(); |
|||
|
|||
function showOneTimeLink(client: LocalClient) { |
|||
// TODO: improve |
|||
$fetch(`/api/client/${client.id}/generateOneTimeLink`, { |
|||
const _showOneTimeLink = useSubmit( |
|||
`/api/client/${props.client.id}/generateOneTimeLink`, |
|||
{ |
|||
method: 'post', |
|||
}) |
|||
.catch((err) => alert(err.message || err.toString())) |
|||
.finally(() => clientsStore.refresh().catch(console.error)); |
|||
}, |
|||
{ |
|||
revert: async () => { |
|||
await clientsStore.refresh(); |
|||
}, |
|||
noSuccessToast: true, |
|||
} |
|||
); |
|||
|
|||
function showOneTimeLink() { |
|||
return _showOneTimeLink(undefined); |
|||
} |
|||
</script> |
|||
|
@ -1,8 +1,8 @@ |
|||
<template> |
|||
<ClientsCreateDialog> |
|||
<BaseButton> |
|||
<BaseButton as="span"> |
|||
<IconsPlus class="w-4 md:mr-2" /> |
|||
<span class="text-sm max-md:hidden">{{ $t('new') }}</span> |
|||
<span class="text-sm max-md:hidden">{{ $t('client.newShort') }}</span> |
|||
</BaseButton> |
|||
</ClientsCreateDialog> |
|||
</template> |
|||
|
@ -0,0 +1,25 @@ |
|||
<template> |
|||
<div class="flex items-center"> |
|||
<FormLabel :for="id"> |
|||
{{ label }} |
|||
</FormLabel> |
|||
<BaseTooltip v-if="description" :text="description"> |
|||
<IconsInfo class="size-4" /> |
|||
</BaseTooltip> |
|||
</div> |
|||
<BaseInput :id="id" v-model="data" :name="id" type="date" /> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string; description?: string }>(); |
|||
|
|||
const data = defineModel<string | null>({ |
|||
set(value) { |
|||
const temp = value?.trim() ?? null; |
|||
if (temp === '') { |
|||
return null; |
|||
} |
|||
return temp; |
|||
}, |
|||
}); |
|||
</script> |
@ -0,0 +1,12 @@ |
|||
<template> |
|||
<h4 class="col-span-full flex items-center py-6 text-2xl"> |
|||
<slot /> |
|||
<BaseTooltip v-if="description" :text="description"> |
|||
<IconsInfo class="size-4" /> |
|||
</BaseTooltip> |
|||
</h4> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ description?: string }>(); |
|||
</script> |
@ -0,0 +1,11 @@ |
|||
<template> |
|||
<RLabel :for="props.for" class="md:align-middle md:leading-10" |
|||
><slot |
|||
/></RLabel> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { Label as RLabel } from 'radix-vue'; |
|||
|
|||
const props = defineProps<{ for: string }>(); |
|||
</script> |
@ -0,0 +1,38 @@ |
|||
<template> |
|||
<div class="flex items-center"> |
|||
<FormLabel :for="id"> |
|||
{{ label }} |
|||
</FormLabel> |
|||
<BaseTooltip v-if="description" :text="description"> |
|||
<IconsInfo class="size-4" /> |
|||
</BaseTooltip> |
|||
</div> |
|||
<BaseInput |
|||
:id="id" |
|||
v-model.trim="data" |
|||
:name="id" |
|||
type="text" |
|||
:autcomplete="autocomplete" |
|||
:placeholder="placeholder" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ |
|||
id: string; |
|||
label: string; |
|||
description?: string; |
|||
autocomplete?: string; |
|||
placeholder?: string; |
|||
}>(); |
|||
|
|||
const data = defineModel<string | null>({ |
|||
set(value) { |
|||
const temp = value?.trim() ?? null; |
|||
if (temp === '') { |
|||
return null; |
|||
} |
|||
return temp; |
|||
}, |
|||
}); |
|||
</script> |
@ -0,0 +1,17 @@ |
|||
<template> |
|||
<div class="flex items-center"> |
|||
<FormLabel :for="id"> |
|||
{{ label }} |
|||
</FormLabel> |
|||
<BaseTooltip v-if="description" :text="description"> |
|||
<IconsInfo class="size-4" /> |
|||
</BaseTooltip> |
|||
</div> |
|||
<BaseInput :id="id" v-model.number="data" :name="id" type="number" /> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string; description?: string }>(); |
|||
|
|||
const data = defineModel<number>(); |
|||
</script> |
@ -0,0 +1,18 @@ |
|||
<template> |
|||
<FormLabel :for="id"> |
|||
{{ label }} |
|||
</FormLabel> |
|||
<BaseInput |
|||
:id="id" |
|||
v-model.trim="data" |
|||
:name="id" |
|||
type="password" |
|||
:autocomplete="autocomplete" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string; autocomplete: string }>(); |
|||
|
|||
const data = defineModel<string>(); |
|||
</script> |
@ -0,0 +1,16 @@ |
|||
<template> |
|||
<div class="flex items-center"> |
|||
<FormLabel :for="id"> |
|||
{{ label }} |
|||
</FormLabel> |
|||
<BaseTooltip v-if="description" :text="description"> |
|||
<IconsInfo class="size-4" /> |
|||
</BaseTooltip> |
|||
</div> |
|||
<BaseSwitch :id="id" v-model="data" /> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string; description?: string }>(); |
|||
const data = defineModel<boolean>(); |
|||
</script> |
@ -0,0 +1,28 @@ |
|||
<template> |
|||
<div class="flex items-center"> |
|||
<FormLabel :for="id"> |
|||
{{ label }} |
|||
</FormLabel> |
|||
<BaseTooltip v-if="description" :text="description"> |
|||
<IconsInfo class="size-4" /> |
|||
</BaseTooltip> |
|||
</div> |
|||
<BaseInput |
|||
:id="id" |
|||
v-model.trim="data" |
|||
:name="id" |
|||
type="text" |
|||
:autcomplete="autocomplete" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ |
|||
id: string; |
|||
label: string; |
|||
description?: string; |
|||
autocomplete?: string; |
|||
}>(); |
|||
|
|||
const data = defineModel<string>(); |
|||
</script> |
@ -1,21 +1,21 @@ |
|||
<template> |
|||
<div |
|||
v-if="globalStore.updateAvailable && globalStore.latestRelease" |
|||
v-if="globalStore.release?.updateAvailable" |
|||
class="font-small mb-10 rounded-md bg-red-800 p-4 text-sm text-white shadow-lg dark:bg-red-100 dark:text-red-600" |
|||
:title="`v${globalStore.currentRelease} → v${globalStore.latestRelease.version}`" |
|||
:title="`v${globalStore.release.currentRelease} → v${globalStore.release.latestRelease.version}`" |
|||
> |
|||
<div class="container mx-auto flex flex-auto flex-row items-center"> |
|||
<div class="flex-grow"> |
|||
<p class="font-bold">{{ $t('updateAvailable') }}</p> |
|||
<p>{{ globalStore.latestRelease.changelog }}</p> |
|||
<p class="font-bold">{{ $t('update.updateAvailable') }}</p> |
|||
<p>{{ globalStore.release.latestRelease.changelog }}</p> |
|||
</div> |
|||
|
|||
<a |
|||
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.latestRelease.version}`" |
|||
:href="`https://github.com/wg-easy/wg-easy/releases/tag/${globalStore.release.latestRelease.version}`" |
|||
target="_blank" |
|||
class="font-sm float-right flex-shrink-0 rounded-md border-2 border-red-800 bg-white p-3 font-semibold text-red-800 transition-all hover:border-white hover:bg-red-800 hover:text-white dark:border-red-600 dark:bg-red-100 dark:text-red-600 dark:hover:border-red-600 dark:hover:bg-red-600 dark:hover:text-red-100" |
|||
> |
|||
{{ $t('update') }} → |
|||
{{ $t('update.update') }} → |
|||
</a> |
|||
</div> |
|||
</div> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke-width="1.5" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -0,0 +1,15 @@ |
|||
<template> |
|||
<svg |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
fill="none" |
|||
viewBox="0 0 24 24" |
|||
stroke="currentColor" |
|||
> |
|||
<path |
|||
stroke-linecap="round" |
|||
stroke-linejoin="round" |
|||
stroke-width="2" |
|||
d="M13.213 9.787a3.391 3.391 0 0 0-4.795 0l-3.425 3.426a3.39 3.39 0 0 0 4.795 4.794l.321-.304m-.321-4.49a3.39 3.39 0 0 0 4.795 0l3.424-3.426a3.39 3.39 0 0 0-4.794-4.795l-1.028.961" |
|||
/> |
|||
</svg> |
|||
</template> |
@ -1,11 +1,11 @@ |
|||
<script setup lang="ts"> |
|||
const { text } = defineProps<{ |
|||
text: string; |
|||
}>(); |
|||
</script> |
|||
|
|||
<template> |
|||
<h2 class="flex-1 text-2xl font-medium"> |
|||
{{ text }} |
|||
</h2> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const { text } = defineProps<{ |
|||
text: string; |
|||
}>(); |
|||
</script> |
@ -1,45 +0,0 @@ |
|||
<script setup lang="ts"> |
|||
import { |
|||
ToastAction, |
|||
ToastClose, |
|||
ToastDescription, |
|||
ToastRoot, |
|||
ToastTitle, |
|||
} from 'radix-vue'; |
|||
|
|||
defineExpose({ |
|||
publish, |
|||
}); |
|||
|
|||
// TODO: support multiple types (info, success, error, warning) |
|||
|
|||
const count = reactive<{ title: string; message: string }[]>([]); |
|||
|
|||
function publish(e: { title: string; message: string }) { |
|||
count.push({ title: e.title, message: e.message }); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<ToastRoot |
|||
v-for="(e, i) in count" |
|||
:key="i" |
|||
class="data-[state=open]:animate-slideIn data-[state=closed]:animate-hide data-[swipe=end]:animate-swipeOut grid grid-cols-[auto_max-content] items-center gap-x-[15px] rounded-md bg-red-800 p-[15px] text-neutral-200 shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] [grid-template-areas:_'title_action'_'description_action'] data-[swipe=cancel]:translate-x-0 data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:transition-[transform_200ms_ease-out]" |
|||
> |
|||
<ToastTitle |
|||
class="text-slate12 mb-[5px] text-[15px] font-medium [grid-area:_title]" |
|||
> |
|||
{{ e.title }} |
|||
</ToastTitle> |
|||
<ToastDescription |
|||
class="text-slate11 m-0 text-[13px] leading-[1.3] [grid-area:_description]" |
|||
>{{ e.message }}</ToastDescription |
|||
> |
|||
<ToastAction as-child alt-text="toast" class="[grid-area:_action]"> |
|||
<slot /> |
|||
</ToastAction> |
|||
<ToastClose aria-label="Close"> |
|||
<span aria-hidden>×</span> |
|||
</ToastClose> |
|||
</ToastRoot> |
|||
</template> |
@ -1,26 +0,0 @@ |
|||
<template> |
|||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ label }} |
|||
</Label> |
|||
<input |
|||
:id="id" |
|||
v-model="data" |
|||
:name="id" |
|||
type="date" |
|||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string }>(); |
|||
|
|||
const data = defineModel<string | null>({ |
|||
set(value) { |
|||
const temp = value?.trim() ?? null; |
|||
if (temp === '') { |
|||
return null; |
|||
} |
|||
return temp; |
|||
}, |
|||
}); |
|||
</script> |
@ -1,5 +0,0 @@ |
|||
<template> |
|||
<h4 class="col-span-full py-6 text-2xl"> |
|||
<slot /> |
|||
</h4> |
|||
</template> |
@ -1,26 +0,0 @@ |
|||
<template> |
|||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ label }} |
|||
</Label> |
|||
<input |
|||
:id="id" |
|||
v-model.trim="data" |
|||
:name="id" |
|||
type="text" |
|||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string }>(); |
|||
|
|||
const data = defineModel<string | null>({ |
|||
set(value) { |
|||
const temp = value?.trim() ?? null; |
|||
if (temp === '') { |
|||
return null; |
|||
} |
|||
return temp; |
|||
}, |
|||
}); |
|||
</script> |
@ -1,18 +0,0 @@ |
|||
<template> |
|||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ label }} |
|||
</Label> |
|||
<input |
|||
:id="id" |
|||
v-model.number="data" |
|||
:name="id" |
|||
type="number" |
|||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string }>(); |
|||
|
|||
const data = defineModel<number>(); |
|||
</script> |
@ -1,19 +0,0 @@ |
|||
<template> |
|||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ label }} |
|||
</Label> |
|||
<input |
|||
:id="id" |
|||
v-model.trim="data" |
|||
:name="id" |
|||
type="password" |
|||
:autocomplete="autocomplete" |
|||
class="rounded-lg border-2 border-gray-100 text-gray-500 focus:border-red-800 focus:outline-0 focus:ring-0 dark:border-neutral-800 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400" |
|||
/> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string; autocomplete: string }>(); |
|||
|
|||
const data = defineModel<string>(); |
|||
</script> |
@ -1,11 +0,0 @@ |
|||
<template> |
|||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|||
{{ label }} |
|||
</Label> |
|||
<BaseSwitch :id="id" v-model="data" /> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
defineProps<{ id: string; label: string }>(); |
|||
const data = defineModel<boolean>(); |
|||
</script> |
@ -1,45 +0,0 @@ |
|||
<template> |
|||
<SelectRoot v-model="langProxy" :default-value="locale"> |
|||
<SelectTrigger |
|||
class="inline-flex h-[35px] min-w-[160px] items-center justify-between gap-[5px] rounded px-[15px] text-[13px] leading-none dark:bg-neutral-500 dark:text-white" |
|||
aria-label="Customize language" |
|||
> |
|||
<SelectValue :placeholder="$t('setup.chooseLang')" /> |
|||
<IconsArrowDown class="size-4" /> |
|||
</SelectTrigger> |
|||
|
|||
<SelectPortal> |
|||
<SelectContent |
|||
class="min-w-[160px] rounded bg-white dark:bg-neutral-500" |
|||
:side-offset="5" |
|||
> |
|||
<SelectViewport class="p-[5px]"> |
|||
<SelectItem |
|||
v-for="(option, index) in langs" |
|||
:key="index" |
|||
:value="option.code" |
|||
class="text-grass11 relative flex h-[25px] items-center rounded-[3px] pl-[25px] pr-[35px] text-[13px] leading-none hover:bg-red-800 hover:text-white dark:text-white" |
|||
> |
|||
<SelectItemText> |
|||
{{ option.name }} |
|||
</SelectItemText> |
|||
</SelectItem> |
|||
</SelectViewport> |
|||
</SelectContent> |
|||
</SelectPortal> |
|||
</SelectRoot> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
// TODO: improve |
|||
|
|||
const { locales, locale, setLocale } = useI18n(); |
|||
|
|||
const langProxy = ref(locale); |
|||
|
|||
watchEffect(() => { |
|||
setLocale(langProxy.value); |
|||
}); |
|||
|
|||
const langs = locales.value.sort((a, b) => a.code.localeCompare(b.code)); |
|||
</script> |
@ -0,0 +1,57 @@ |
|||
import type { NitroFetchRequest, NitroFetchOptions } from 'nitropack/types'; |
|||
import { FetchError } from 'ofetch'; |
|||
|
|||
type RevertFn = (success: boolean) => Promise<void>; |
|||
|
|||
type SubmitOpts = { |
|||
revert: RevertFn; |
|||
successMsg?: string; |
|||
errorMsg?: string; |
|||
noSuccessToast?: boolean; |
|||
}; |
|||
|
|||
export function useSubmit< |
|||
R extends NitroFetchRequest, |
|||
O extends NitroFetchOptions<R> & { body?: never }, |
|||
>(url: R, options: O, opts: SubmitOpts) { |
|||
const toast = useToast(); |
|||
const { t: $t } = useI18n(); |
|||
|
|||
return async (data: unknown) => { |
|||
try { |
|||
const res = await $fetch(url, { |
|||
...options, |
|||
body: data, |
|||
}); |
|||
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|||
if (!(res as any).success) { |
|||
throw new Error(opts.errorMsg || $t('toast.errored')); |
|||
} |
|||
|
|||
if (!opts.noSuccessToast) { |
|||
toast.showToast({ |
|||
type: 'success', |
|||
message: opts.successMsg, |
|||
}); |
|||
} |
|||
|
|||
await opts.revert(true); |
|||
} catch (e) { |
|||
if (e instanceof FetchError) { |
|||
toast.showToast({ |
|||
type: 'error', |
|||
message: e.data.message, |
|||
}); |
|||
} else if (e instanceof Error) { |
|||
toast.showToast({ |
|||
type: 'error', |
|||
message: e.message, |
|||
}); |
|||
} else { |
|||
console.error(e); |
|||
} |
|||
await opts.revert(false); |
|||
} |
|||
}; |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue