Browse Source

refactor dialog (#1665)

pull/1686/head
Bernd Storath 6 months ago
committed by GitHub
parent
commit
7aefc341db
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      src/app/components/ClientCard/OneTimeLinkBtn.vue
  2. 17
      src/app/components/ClientCard/QRCode.vue
  3. 177
      src/app/components/Clients/CreateDialog.vue
  4. 53
      src/app/components/Clients/DeleteDialog.vue
  5. 21
      src/app/components/Clients/Empty.vue
  6. 20
      src/app/components/Clients/New.vue
  7. 30
      src/app/components/Clients/QRCodeDialog.vue
  8. 1
      src/app/components/Clients/Sort.vue
  9. 55
      src/app/components/admin/CidrDialog.vue
  10. 2
      src/app/components/base/Button.vue
  11. 31
      src/app/components/base/Dialog.vue
  12. 3
      src/app/middleware/auth.global.ts
  13. 4
      src/app/pages/admin/config.vue
  14. 3
      src/app/pages/index.vue
  15. 30
      src/app/stores/modal.ts

1
src/app/components/ClientCard/OneTimeLinkBtn.vue

@ -22,6 +22,7 @@
</template>
<script setup lang="ts">
// TODO: improve
defineProps<{ client: LocalClient }>();
const clientsStore = useClientsStore();

17
src/app/components/ClientCard/QRCode.vue

@ -1,17 +1,16 @@
<template>
<button
class="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('showQR')"
@click="modalStore.qrcode = `./api/client/${client.id}/qrcode.svg`"
>
<IconsQRCode class="w-5" />
</button>
<ClientsQRCodeDialog :qr-code="`./api/client/${client.id}/qrcode.svg`">
<button
class="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('showQR')"
>
<IconsQRCode class="w-5" />
</button>
</ClientsQRCodeDialog>
</template>
<script setup lang="ts">
defineProps<{
client: LocalClient;
}>();
const modalStore = useModalStore();
</script>

177
src/app/components/Clients/CreateDialog.vue

@ -1,132 +1,59 @@
<template>
<!-- Create Dialog -->
<div
v-if="modalStore.clientCreate"
class="fixed inset-0 z-10 overflow-y-auto"
>
<div
class="flex min-h-screen items-center justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0"
>
<!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
<BaseDialog :trigger-class="triggerClass">
<template #trigger>
<slot />
</template>
<template #title>
{{ $t('newClient') }}
</template>
<template #description>
<div class="flex flex-col">
<FormTextField id="name" v-model="name" label="Name" />
<FormDateField id="expiresAt" v-model="expiresAt" label="Expire Date" />
</div>
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="createClient">{{ $t('create') }}</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<!-- This element is to trick the browser into centering the modal contents. -->
<span
class="hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>&#8203;</span
>
<!--
Modal panel, show/hide based on modal state.
<script lang="ts" setup>
import { FetchError } from 'ofetch';
Entering: "ease-out duration-300"
From: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
To: "opacity-100 tranneutral-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 tranneutral-y-0 sm:scale-100"
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
-->
<div
class="inline-block w-full transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:max-w-lg sm:align-middle dark:bg-neutral-700"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4 dark:bg-neutral-700">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-800 sm:mx-0 sm:h-10 sm:w-10"
>
<IconsPlus class="h-6 w-6 text-white" />
</div>
<div
class="mt-3 flex-grow text-center sm:ml-4 sm:mt-0 sm:text-left"
>
<h3
id="modal-headline"
class="text-lg font-medium leading-6 text-gray-900 dark:text-neutral-200"
>
{{ $t('newClient') }}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
<input
v-model.trim="modalStore.clientCreateName"
class="w-full rounded border-2 border-gray-100 p-2 outline-none focus:border-gray-200 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400 focus:dark:border-neutral-500"
type="text"
:placeholder="$t('name')"
/>
</p>
</div>
<div class="mt-2">
<p class="text-sm text-gray-500">
<label
class="mb-2 block text-sm font-bold text-gray-900 dark:text-neutral-200"
for="expireDate"
>
{{ $t('ExpireDate') }}
</label>
<input
v-model.trim="modalStore.clientExpireDate"
class="w-full rounded border-2 border-gray-100 p-2 outline-none focus:border-gray-200 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-400 focus:dark:border-neutral-500"
type="date"
:placeholder="$t('ExpireDate')"
name="expireDate"
/>
</p>
</div>
</div>
</div>
</div>
<div
class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-neutral-700"
>
<button
v-if="modalStore.clientCreateName.length"
type="button"
class="inline-flex w-full justify-center rounded-md border border-transparent bg-red-800 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm"
@click="
modalStore.createClient();
modalStore.clientCreate = null;
"
>
{{ $t('create') }}
</button>
<button
v-else
type="button"
class="inline-flex w-full cursor-not-allowed justify-center rounded-md border border-transparent bg-gray-200 px-4 py-2 text-base font-medium text-white shadow-sm sm:ml-3 sm:w-auto sm:text-sm dark:bg-neutral-400 dark:text-neutral-300"
>
{{ $t('create') }}
</button>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none sm:ml-3 sm:mt-0 sm:w-auto sm:text-sm dark:border-neutral-500 dark:bg-neutral-500 dark:text-neutral-50 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
@click="modalStore.clientCreate = null"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</div>
</template>
const name = ref<string>('');
const expiresAt = ref<string | null>(null);
const toast = useToast();
const clientsStore = useClientsStore();
<script setup lang="ts">
const modalStore = useModalStore();
defineProps<{ triggerClass?: string }>();
// TODO: use radix
async function createClient() {
try {
await $fetch('/api/client', {
method: 'post',
body: { name: name.value, expiresAt: expiresAt.value },
});
toast.showToast({
type: 'success',
title: 'Success',
message: 'Client created',
});
await clientsStore.refresh();
} catch (e) {
if (e instanceof FetchError) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.data.message,
});
}
// TODO: handle errors better
}
}
</script>

53
src/app/components/Clients/DeleteDialog.vue

@ -1,38 +1,23 @@
<template>
<DialogRoot :modal="true">
<DialogTrigger :class="triggerClass"><slot /></DialogTrigger>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
<DialogContent
class="data-[state=open]:animate-contentShow fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
>
<DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
>
{{ $t('deleteClient') }}
</DialogTitle>
<DialogDescription
class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300"
>
{{ $t('deleteDialog1') }}
<strong>{{ 'test' }}</strong
>? {{ $t('deleteDialog2') }}
</DialogDescription>
<div class="mt-6 flex justify-end gap-2">
<DialogClose as-child>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('delete')">{{
$t('deleteClient')
}}</BaseButton>
</DialogClose>
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
<BaseDialog>
<template #trigger><slot /></template>
<template #title>{{ $t('deleteClient') }}</template>
<template #description>
{{ $t('deleteDialog1') }}
<strong>{{ 'test' }}</strong
>? {{ $t('deleteDialog2') }}
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('delete')">{{
$t('deleteClient')
}}</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>

21
src/app/components/Clients/Empty.vue

@ -1,20 +1,11 @@
<template>
<p class="m-10 text-center text-sm text-gray-400 dark:text-neutral-400">
{{ $t('noClients') }}<br /><br />
<button
class="inline-flex items-center rounded border-2 border-none bg-red-800 px-4 py-2 text-white transition hover:bg-red-700"
@click="
modalStore.clientCreate = true;
modalStore.clientCreateName = '';
modalStore.clientExpireDate = '';
"
>
<IconsPlus class="mr-2 w-4" />
<span class="text-sm">{{ $t('newClient') }}</span>
</button>
<ClientsCreateDialog>
<BaseButton>
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm">{{ $t('new') }}</span>
</BaseButton>
</ClientsCreateDialog>
</p>
</template>
<script setup lang="ts">
const modalStore = useModalStore();
</script>

20
src/app/components/Clients/New.vue

@ -1,16 +1,8 @@
<template>
<BaseButton
@click="
modalStore.clientCreate = true;
modalStore.clientCreateName = '';
modalStore.clientExpireDate = '';
"
>
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden">{{ $t('new') }}</span>
</BaseButton>
<ClientsCreateDialog>
<BaseButton>
<IconsPlus class="w-4 md:mr-2" />
<span class="text-sm max-md:hidden">{{ $t('new') }}</span>
</BaseButton>
</ClientsCreateDialog>
</template>
<script setup lang="ts">
const modalStore = useModalStore();
</script>

30
src/app/components/Clients/QRCodeDialog.vue

@ -1,21 +1,19 @@
<template>
<div v-if="modalStore.qrcode">
<div
class="fixed bottom-0 left-0 right-0 top-0 z-20 flex items-center justify-center bg-black bg-opacity-50"
>
<div class="relative rounded-md bg-white p-8 shadow-lg">
<button
class="absolute right-4 top-4 text-gray-600 hover:text-gray-800 dark:text-neutral-500 dark:hover:text-neutral-700"
@click="modalStore.qrcode = null"
>
<IconsClose class="w-8" />
</button>
<img :src="modalStore.qrcode" />
</div>
</div>
</div>
<BaseDialog>
<template #trigger>
<slot />
</template>
<template #description>
<img :src="qrCode" />
</template>
<template #actions>
<DialogClose>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script setup lang="ts">
const modalStore = useModalStore();
defineProps<{ qrCode: string }>();
</script>

1
src/app/components/Clients/Sort.vue

@ -46,6 +46,7 @@
</template>
<script setup lang="ts">
// TODO: improve
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();

55
src/app/components/admin/CidrDialog.vue

@ -1,39 +1,24 @@
<template>
<DialogRoot :modal="true">
<DialogTrigger :class="triggerClass"><slot /></DialogTrigger>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
<DialogContent
class="data-[state=open]:animate-contentShow fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
>
<DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
>
Change CIDR
</DialogTitle>
<DialogDescription
class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300"
>
<FormGroup>
<FormTextField id="ipv4Cidr" v-model="ipv4Cidr" label="IPv4" />
<FormTextField id="ipv6Cidr" v-model="ipv6Cidr" label="IPv6" />
</FormGroup>
</DialogDescription>
<div class="mt-6 flex justify-end gap-2">
<DialogClose as-child>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('change', ipv4Cidr, ipv6Cidr)"
>Change</BaseButton
>
</DialogClose>
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
<BaseDialog>
<template #trigger><slot /></template>
<template #title>Change CIDR</template>
<template #description>
<FormGroup>
<FormTextField id="ipv4Cidr" v-model="ipv4Cidr" label="IPv4" />
<FormTextField id="ipv6Cidr" v-model="ipv6Cidr" label="IPv6" />
</FormGroup>
</template>
<template #actions>
<DialogClose as-child>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('change', ipv4Cidr, ipv6Cidr)">
Change
</BaseButton>
</DialogClose>
</template>
</BaseDialog>
</template>
<script lang="ts" setup>

2
src/app/components/base/Button.vue

@ -2,7 +2,7 @@
<component
:is="elementType"
role="button"
class="inline-flex items-center rounded border-2 border-gray-100 py-2 text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white max-md:rounded-full max-md:border-x-0 md:px-4 dark:border-neutral-600 dark:text-neutral-200"
class="inline-flex items-center rounded border-2 border-gray-100 px-4 py-2 text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white dark:border-neutral-600 dark:text-neutral-200"
v-bind="attrs"
>
<slot />

31
src/app/components/base/Dialog.vue

@ -0,0 +1,31 @@
<template>
<DialogRoot :modal="true">
<DialogTrigger :class="triggerClass"><slot name="trigger" /></DialogTrigger>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
<DialogContent
class="data-[state=open]:animate-contentShow fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
>
<DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
>
<slot name="title" />
</DialogTitle>
<DialogDescription
class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300"
>
<slot name="description" />
</DialogDescription>
<div class="mt-6 flex justify-end gap-2">
<slot name="actions" />
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
<script lang="ts" setup>
defineProps<{ triggerClass?: string }>();
</script>

3
src/app/middleware/auth.global.ts

@ -14,6 +14,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
}
return;
}
// Require auth for every page other than Login
if (!userData?.username) {
return navigateTo('/login', { redirectCode: 302 });
@ -21,7 +22,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
// Check for admin access
if (to.path.startsWith('/admin')) {
if (userData && hasPermissions(userData, 'admin', 'any')) {
if (!hasPermissions(userData, 'admin', 'any')) {
return abortNavigation('Not allowed to access Admin Panel');
}
}

4
src/app/pages/admin/config.vue

@ -62,7 +62,8 @@ async function submit() {
if (!res.success) {
throw new Error('Failed to save');
}
await refreshNuxtData();
// TODO: avoid refreshNuxtData
await revert();
} catch (e) {
if (e instanceof Error) {
toast.showToast({
@ -71,6 +72,7 @@ async function submit() {
message: e.message,
});
}
await revert();
}
}

3
src/app/pages/index.vue

@ -24,9 +24,6 @@
<IconsLoading class="mx-auto w-5 animate-spin" />
</div>
</Panel>
<ClientsQRCodeDialog />
<ClientsCreateDialog />
</main>
</template>

30
src/app/stores/modal.ts

@ -1,30 +0,0 @@
import { defineStore } from 'pinia';
export const useModalStore = defineStore('Modal', () => {
const clientsStore = useClientsStore();
const clientCreate = ref<null | boolean>(null);
const clientCreateName = ref<string>('');
const clientExpireDate = ref<string>('');
const qrcode = ref<null | string>(null);
function createClient() {
const name = clientCreateName.value;
const expiresAt = clientExpireDate.value || null;
if (!name) return;
$fetch('/api/client', {
method: 'post',
body: { name, expiresAt },
})
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
}
return {
clientCreate,
clientCreateName,
clientExpireDate,
qrcode,
createClient,
};
});
Loading…
Cancel
Save