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 |
# These are supported funding model platforms |
||||
|
|
||||
github: weejewel |
github: [weejewel, kaaax0815] |
||||
|
@ -1,18 +1,10 @@ |
|||||
<template> |
<template> |
||||
<Label :for="id" class="font-semibold md:align-middle md:leading-10"> |
|
||||
{{ label }} |
|
||||
</Label> |
|
||||
<input |
<input |
||||
:id="id" |
v-model="data" |
||||
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" |
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> |
</template> |
||||
|
|
||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||
defineProps<{ id: string; label: string }>(); |
const data = defineModel<unknown>(); |
||||
|
|
||||
const data = defineModel<string>(); |
|
||||
</script> |
</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> |
<template> |
||||
<button |
<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" |
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')" |
:title="$t('client.otlDesc')" |
||||
@click="showOneTimeLink(client)" |
@click="showOneTimeLink" |
||||
> |
> |
||||
<svg |
<IconsLink class="w-5" /> |
||||
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> |
|
||||
</button> |
</button> |
||||
</template> |
</template> |
||||
|
|
||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||
// TODO: improve |
const props = defineProps<{ client: LocalClient }>(); |
||||
defineProps<{ client: LocalClient }>(); |
|
||||
|
|
||||
const clientsStore = useClientsStore(); |
const clientsStore = useClientsStore(); |
||||
|
|
||||
function showOneTimeLink(client: LocalClient) { |
const _showOneTimeLink = useSubmit( |
||||
// TODO: improve |
`/api/client/${props.client.id}/generateOneTimeLink`, |
||||
$fetch(`/api/client/${client.id}/generateOneTimeLink`, { |
{ |
||||
method: 'post', |
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> |
</script> |
||||
|
@ -1,8 +1,8 @@ |
|||||
<template> |
<template> |
||||
<ClientsCreateDialog> |
<ClientsCreateDialog> |
||||
<BaseButton> |
<BaseButton as="span"> |
||||
<IconsPlus class="w-4 md:mr-2" /> |
<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> |
</BaseButton> |
||||
</ClientsCreateDialog> |
</ClientsCreateDialog> |
||||
</template> |
</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> |
<template> |
||||
<div |
<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" |
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="container mx-auto flex flex-auto flex-row items-center"> |
||||
<div class="flex-grow"> |
<div class="flex-grow"> |
||||
<p class="font-bold">{{ $t('updateAvailable') }}</p> |
<p class="font-bold">{{ $t('update.updateAvailable') }}</p> |
||||
<p>{{ globalStore.latestRelease.changelog }}</p> |
<p>{{ globalStore.release.latestRelease.changelog }}</p> |
||||
</div> |
</div> |
||||
|
|
||||
<a |
<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" |
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" |
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> |
</a> |
||||
</div> |
</div> |
||||
</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> |
<template> |
||||
<h2 class="flex-1 text-2xl font-medium"> |
<h2 class="flex-1 text-2xl font-medium"> |
||||
{{ text }} |
{{ text }} |
||||
</h2> |
</h2> |
||||
</template> |
</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