@dargmuesli/nuxt-vio 19.1.0 → 20.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/assets/css/shadcn.css +119 -0
- package/app/assets/css/vio.css +5 -3
- package/app/components/scn/sonner/Sonner.vue +49 -0
- package/app/components/scn/sonner/index.ts +1 -0
- package/app/components/vio/_/VioApp.vue +11 -12
- package/app/components/vio/_/VioSonner.vue +16 -0
- package/app/components/vio/_/VioTime.vue +34 -0
- package/app/components/vio/button/VioButtonShare.vue +1 -1
- package/app/composables/alert.ts +19 -10
- package/app/composables/timeZone.ts +10 -0
- package/app/plugins/ssr-width.ts +5 -0
- package/app/types/color-mode.d.ts +1 -0
- package/app/utils/notification.ts +1 -0
- package/app/utils/shadcn.ts +7 -0
- package/i18n/locales/de.json +1 -0
- package/i18n/locales/en.json +1 -0
- package/nuxt.config.ts +11 -1
- package/package.json +11 -3
- package/shared/utils/constants.ts +1 -1
- package/shared/utils/dateTime.ts +118 -0
- package/app/composables/dateTime.ts +0 -9
- package/app/plugins/dayjs.ts +0 -30
- package/app/plugins/i18n.ts +0 -5
- package/shared/utils/modal.ts +0 -16
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* stylelint-disable at-rule-no-unknown */
|
|
2
|
+
/* stylelint-disable plugin/no-unsupported-browser-features */
|
|
3
|
+
|
|
4
|
+
.dark {
|
|
5
|
+
--background: oklch(14.5% 0 0deg);
|
|
6
|
+
--foreground: oklch(98.5% 0 0deg);
|
|
7
|
+
--card: oklch(20.5% 0 0deg);
|
|
8
|
+
--card-foreground: oklch(98.5% 0 0deg);
|
|
9
|
+
--popover: oklch(20.5% 0 0deg);
|
|
10
|
+
--popover-foreground: oklch(98.5% 0 0deg);
|
|
11
|
+
--primary: oklch(92.2% 0 0deg);
|
|
12
|
+
--primary-foreground: oklch(20.5% 0 0deg);
|
|
13
|
+
--secondary: oklch(26.9% 0 0deg);
|
|
14
|
+
--secondary-foreground: oklch(98.5% 0 0deg);
|
|
15
|
+
--muted: oklch(26.9% 0 0deg);
|
|
16
|
+
--muted-foreground: oklch(70.8% 0 0deg);
|
|
17
|
+
--accent: oklch(26.9% 0 0deg);
|
|
18
|
+
--accent-foreground: oklch(98.5% 0 0deg);
|
|
19
|
+
--destructive: oklch(70.4% 0.191 22.216deg);
|
|
20
|
+
--border: oklch(100% 0 0deg / 10%);
|
|
21
|
+
--input: oklch(100% 0 0deg / 15%);
|
|
22
|
+
--ring: oklch(55.6% 0 0deg);
|
|
23
|
+
--chart-1: oklch(48.8% 0.243 264.376deg);
|
|
24
|
+
--chart-2: oklch(69.6% 0.17 162.48deg);
|
|
25
|
+
--chart-3: oklch(76.9% 0.188 70.08deg);
|
|
26
|
+
--chart-4: oklch(62.7% 0.265 303.9deg);
|
|
27
|
+
--chart-5: oklch(64.5% 0.246 16.439deg);
|
|
28
|
+
--sidebar: oklch(20.5% 0 0deg);
|
|
29
|
+
--sidebar-foreground: oklch(98.5% 0 0deg);
|
|
30
|
+
--sidebar-primary: oklch(48.8% 0.243 264.37deg);
|
|
31
|
+
--sidebar-primary-foreground: oklch(98.5% 0 0deg);
|
|
32
|
+
--sidebar-accent: oklch(26.9% 0 0deg);
|
|
33
|
+
--sidebar-accent-foreground: oklch(98.5% 0 0deg);
|
|
34
|
+
--sidebar-border: oklch(100% 0 0deg / 10%);
|
|
35
|
+
--sidebar-ring: oklch(55.6% 0 0deg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@theme inline {
|
|
39
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
40
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
41
|
+
--radius-lg: var(--radius);
|
|
42
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
43
|
+
--color-background: var(--background);
|
|
44
|
+
--color-foreground: var(--foreground);
|
|
45
|
+
--color-card: var(--card);
|
|
46
|
+
--color-card-foreground: var(--card-foreground);
|
|
47
|
+
--color-popover: var(--popover);
|
|
48
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
49
|
+
--color-primary: var(--primary);
|
|
50
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
51
|
+
--color-secondary: var(--secondary);
|
|
52
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
53
|
+
--color-muted: var(--muted);
|
|
54
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
55
|
+
--color-accent: var(--accent);
|
|
56
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
57
|
+
--color-destructive: var(--destructive);
|
|
58
|
+
--color-border: var(--border);
|
|
59
|
+
--color-input: var(--input);
|
|
60
|
+
--color-ring: var(--ring);
|
|
61
|
+
--color-chart-1: var(--chart-1);
|
|
62
|
+
--color-chart-2: var(--chart-2);
|
|
63
|
+
--color-chart-3: var(--chart-3);
|
|
64
|
+
--color-chart-4: var(--chart-4);
|
|
65
|
+
--color-chart-5: var(--chart-5);
|
|
66
|
+
--color-sidebar: var(--sidebar);
|
|
67
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
68
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
69
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
70
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
71
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
72
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
73
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:root {
|
|
77
|
+
--radius: 0.625rem;
|
|
78
|
+
--background: oklch(100% 0 0deg);
|
|
79
|
+
--foreground: oklch(14.5% 0 0deg);
|
|
80
|
+
--card: oklch(100% 0 0deg);
|
|
81
|
+
--card-foreground: oklch(14.5% 0 0deg);
|
|
82
|
+
--popover: oklch(100% 0 0deg);
|
|
83
|
+
--popover-foreground: oklch(14.5% 0 0deg);
|
|
84
|
+
--primary: oklch(20.5% 0 0deg);
|
|
85
|
+
--primary-foreground: oklch(98.5% 0 0deg);
|
|
86
|
+
--secondary: oklch(97% 0 0deg);
|
|
87
|
+
--secondary-foreground: oklch(20.5% 0 0deg);
|
|
88
|
+
--muted: oklch(97% 0 0deg);
|
|
89
|
+
--muted-foreground: oklch(55.6% 0 0deg);
|
|
90
|
+
--accent: oklch(97% 0 0deg);
|
|
91
|
+
--accent-foreground: oklch(20.5% 0 0deg);
|
|
92
|
+
--destructive: oklch(57.7% 0.245 27.32deg);
|
|
93
|
+
--border: oklch(92.2% 0 0deg);
|
|
94
|
+
--input: oklch(92.2% 0 0deg);
|
|
95
|
+
--ring: oklch(70.8% 0 0deg);
|
|
96
|
+
--chart-1: oklch(64.6% 0.222 41.11deg);
|
|
97
|
+
--chart-2: oklch(60% 0.118 184.7deg);
|
|
98
|
+
--chart-3: oklch(39.8% 0.07 227.39deg);
|
|
99
|
+
--chart-4: oklch(82.8% 0.189 84.42deg);
|
|
100
|
+
--chart-5: oklch(76.9% 0.188 70deg);
|
|
101
|
+
--sidebar: oklch(98.5% 0 0deg);
|
|
102
|
+
--sidebar-foreground: oklch(14.5% 0 0deg);
|
|
103
|
+
--sidebar-primary: oklch(20.5% 0 0deg);
|
|
104
|
+
--sidebar-primary-foreground: oklch(98.5% 0 0deg);
|
|
105
|
+
--sidebar-accent: oklch(97% 0 0deg);
|
|
106
|
+
--sidebar-accent-foreground: oklch(20.5% 0 0deg);
|
|
107
|
+
--sidebar-border: oklch(92.2% 0 0deg);
|
|
108
|
+
--sidebar-ring: oklch(70.8% 0 0deg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@layer base {
|
|
112
|
+
* {
|
|
113
|
+
@apply border-border outline-ring/50;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
body {
|
|
117
|
+
@apply bg-background text-foreground;
|
|
118
|
+
}
|
|
119
|
+
}
|
package/app/assets/css/vio.css
CHANGED
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
/* stylelint-disable plugin/no-unsupported-browser-features */
|
|
6
6
|
|
|
7
7
|
@import 'tailwindcss';
|
|
8
|
+
@import 'tw-animate-css';
|
|
8
9
|
@import './forms.css';
|
|
10
|
+
@import './shadcn.css';
|
|
9
11
|
|
|
10
|
-
@source
|
|
12
|
+
@source '../../';
|
|
11
13
|
|
|
12
|
-
@plugin
|
|
14
|
+
@plugin '@tailwindcss/forms' {
|
|
13
15
|
strategy: 'base';
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
@plugin
|
|
18
|
+
@plugin '@tailwindcss/typography';
|
|
17
19
|
|
|
18
20
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
19
21
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type { ToasterProps } from 'vue-sonner'
|
|
3
|
+
import {
|
|
4
|
+
CircleCheckIcon,
|
|
5
|
+
InfoIcon,
|
|
6
|
+
Loader2Icon,
|
|
7
|
+
OctagonXIcon,
|
|
8
|
+
TriangleAlertIcon,
|
|
9
|
+
XIcon,
|
|
10
|
+
} from 'lucide-vue-next'
|
|
11
|
+
import { Toaster as Sonner } from 'vue-sonner'
|
|
12
|
+
import { cn } from '@/utils/shadcn'
|
|
13
|
+
|
|
14
|
+
const props = defineProps<ToasterProps>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<Sonner
|
|
19
|
+
:class="cn('toaster group', props.class)"
|
|
20
|
+
:style="{
|
|
21
|
+
'--normal-bg': 'var(--popover)',
|
|
22
|
+
'--normal-text': 'var(--popover-foreground)',
|
|
23
|
+
'--normal-border': 'var(--border)',
|
|
24
|
+
'--border-radius': 'var(--radius)',
|
|
25
|
+
}"
|
|
26
|
+
v-bind="props"
|
|
27
|
+
>
|
|
28
|
+
<template #success-icon>
|
|
29
|
+
<CircleCheckIcon class="size-4" />
|
|
30
|
+
</template>
|
|
31
|
+
<template #info-icon>
|
|
32
|
+
<InfoIcon class="size-4" />
|
|
33
|
+
</template>
|
|
34
|
+
<template #warning-icon>
|
|
35
|
+
<TriangleAlertIcon class="size-4" />
|
|
36
|
+
</template>
|
|
37
|
+
<template #error-icon>
|
|
38
|
+
<OctagonXIcon class="size-4" />
|
|
39
|
+
</template>
|
|
40
|
+
<template #loading-icon>
|
|
41
|
+
<div>
|
|
42
|
+
<Loader2Icon class="size-4 animate-spin" />
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
<template #close-icon>
|
|
46
|
+
<XIcon class="size-4" />
|
|
47
|
+
</template>
|
|
48
|
+
</Sonner>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Toaster } from './Sonner.vue'
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<!-- `NuxtLayout` can't have mulitple child nodes (https://github.com/nuxt/nuxt/issues/21759) -->
|
|
6
6
|
<NuxtPage />
|
|
7
7
|
</NuxtLayout>
|
|
8
|
+
<VioSonner />
|
|
8
9
|
</div>
|
|
9
10
|
</template>
|
|
10
11
|
|
|
@@ -16,26 +17,24 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
16
17
|
ogImageComponent: undefined,
|
|
17
18
|
})
|
|
18
19
|
|
|
19
|
-
const {
|
|
20
|
-
const
|
|
20
|
+
const { t } = useI18n()
|
|
21
|
+
const timeZone = useTimeZone()
|
|
21
22
|
|
|
22
23
|
const { loadingIds, indicateLoadingDone } = useLoadingDoneIndicator('app')
|
|
23
24
|
|
|
24
25
|
// methods
|
|
25
26
|
const initialize = () => {
|
|
26
|
-
$dayjs.locale(locale.value)
|
|
27
|
-
|
|
28
27
|
if (import.meta.client) {
|
|
29
|
-
|
|
30
|
-
// default: () => undefined, // setting `default` on the client side only does not write the cookie
|
|
31
|
-
httpOnly: false,
|
|
32
|
-
sameSite: 'strict',
|
|
33
|
-
secure: true,
|
|
34
|
-
})
|
|
35
|
-
// @ts-expect-error `tz` should be part of `$dayjs` (https://github.com/iamkun/dayjs/issues/2106)
|
|
36
|
-
cookieTimezone.value = $dayjs.tz.guess()
|
|
28
|
+
saveTimeZoneAsCookie()
|
|
37
29
|
}
|
|
38
30
|
}
|
|
31
|
+
const saveTimeZoneAsCookie = () =>
|
|
32
|
+
(useCookie(TIMEZONE_COOKIE_NAME, {
|
|
33
|
+
// default: () => undefined, // setting `default` on the client side only does not write the cookie
|
|
34
|
+
httpOnly: false,
|
|
35
|
+
sameSite: 'strict',
|
|
36
|
+
secure: true,
|
|
37
|
+
}).value = timeZone)
|
|
39
38
|
|
|
40
39
|
// computations
|
|
41
40
|
const isLoading = computed(() => !!loadingIds.value.length)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- TODO: add `class="pointer-events-auto"` -->
|
|
3
|
+
<Toaster rich-colors :theme="colorModePreference" />
|
|
4
|
+
</template>
|
|
5
|
+
|
|
6
|
+
<script setup lang="ts">
|
|
7
|
+
import type { ColorMode } from '~/types/color-mode'
|
|
8
|
+
|
|
9
|
+
const colorMode = useColorMode()
|
|
10
|
+
|
|
11
|
+
const colorModePreference = colorMode.preference as ColorMode
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
@import url('vue-sonner/style.css');
|
|
16
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NuxtTime
|
|
3
|
+
v-bind="forwardedProps"
|
|
4
|
+
:locale="props.locale || defaultLocale"
|
|
5
|
+
:time-zone="props.timeZone || defaultTimeZone"
|
|
6
|
+
/>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { reactiveOmit } from '@vueuse/core'
|
|
11
|
+
import { useForwardProps } from 'reka-ui'
|
|
12
|
+
|
|
13
|
+
import type { NuxtTimeProps } from '#app'
|
|
14
|
+
|
|
15
|
+
const { locale: defaultLocale } = useI18n()
|
|
16
|
+
const defaultTimeZone = useTimeZone()
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<NuxtTimeProps>(), {
|
|
19
|
+
// ...dateTimeFormatOptions, TODO: use shared options
|
|
20
|
+
day: 'numeric',
|
|
21
|
+
hour: 'numeric',
|
|
22
|
+
locale: undefined,
|
|
23
|
+
minute: 'numeric',
|
|
24
|
+
month: 'short',
|
|
25
|
+
relative: undefined,
|
|
26
|
+
timeZone: undefined,
|
|
27
|
+
timeZoneName: undefined,
|
|
28
|
+
weekday: undefined,
|
|
29
|
+
year: 'numeric',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const delegatedProps = reactiveOmit(props, 'locale', 'timeZone')
|
|
33
|
+
const forwardedProps = useForwardProps(delegatedProps)
|
|
34
|
+
</script>
|
|
@@ -26,7 +26,7 @@ const copy = async (string: string) => {
|
|
|
26
26
|
|
|
27
27
|
try {
|
|
28
28
|
await copyText(string)
|
|
29
|
-
|
|
29
|
+
toast.success(t('donationUrlCopySuccess'))
|
|
30
30
|
} catch (error: unknown) {
|
|
31
31
|
console.error(error)
|
|
32
32
|
alert(t('donationUrlCopyError'))
|
package/app/composables/alert.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { consola } from 'consola'
|
|
2
|
-
import Swal from 'sweetalert2'
|
|
3
|
-
import type { Ref } from 'vue'
|
|
4
2
|
|
|
5
|
-
export const
|
|
3
|
+
export const useAlertError = () => {
|
|
6
4
|
const { t } = useI18n({ useScope: 'global' })
|
|
7
5
|
|
|
8
|
-
return (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
return (
|
|
7
|
+
options:
|
|
8
|
+
| string
|
|
9
|
+
| {
|
|
10
|
+
error?: Error
|
|
11
|
+
messageI18n: string
|
|
12
|
+
toastOptions?: Parameters<typeof toast>[1]
|
|
13
|
+
},
|
|
14
|
+
) => {
|
|
15
|
+
const error =
|
|
16
|
+
typeof options === 'string' ? new Error(options) : options.error
|
|
17
|
+
const errorMessage =
|
|
18
|
+
typeof options === 'string' ? options : options.messageI18n
|
|
19
|
+
|
|
20
|
+
consola.error({ errorMessage, ...(error ? { error } : {}) })
|
|
21
|
+
toast.error(t('globalError'), {
|
|
22
|
+
...(typeof options !== 'string' ? options.toastOptions || {} : {}),
|
|
23
|
+
description: errorMessage,
|
|
13
24
|
})
|
|
14
|
-
api?.value.errors.push(error)
|
|
15
|
-
consola.error(error)
|
|
16
25
|
}
|
|
17
26
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const useTimeZone = () =>
|
|
2
|
+
useNuxtApp().ssrContext?.event.context.$timeZone ||
|
|
3
|
+
useCookie(TIMEZONE_COOKIE_NAME, {
|
|
4
|
+
httpOnly: false,
|
|
5
|
+
sameSite: 'strict',
|
|
6
|
+
secure: true,
|
|
7
|
+
}).value ||
|
|
8
|
+
(import.meta.client
|
|
9
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
10
|
+
: undefined)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ColorMode = 'dark' | 'light' | 'system'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toast } from 'vue-sonner'
|
package/i18n/locales/de.json
CHANGED
package/i18n/locales/en.json
CHANGED
package/nuxt.config.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
1
4
|
import tailwindcss from '@tailwindcss/vite'
|
|
2
5
|
import { defu } from 'defu'
|
|
3
6
|
|
|
@@ -10,6 +13,8 @@ import {
|
|
|
10
13
|
} from './shared/utils/constants'
|
|
11
14
|
import { VIO_NUXT_BASE_CONFIG } from './shared/utils/nuxt'
|
|
12
15
|
|
|
16
|
+
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
|
|
13
18
|
export default defineNuxtConfig(
|
|
14
19
|
defu(
|
|
15
20
|
{
|
|
@@ -50,6 +55,7 @@ export default defineNuxtConfig(
|
|
|
50
55
|
'@nuxtjs/seo',
|
|
51
56
|
'@pinia/nuxt',
|
|
52
57
|
'nuxt-gtag',
|
|
58
|
+
'shadcn-nuxt',
|
|
53
59
|
(_options, nuxt) => {
|
|
54
60
|
if (nuxt.options.nitro.static) {
|
|
55
61
|
nuxt.options.features.inlineStyles = false
|
|
@@ -237,6 +243,10 @@ export default defineNuxtConfig(
|
|
|
237
243
|
},
|
|
238
244
|
strict: true,
|
|
239
245
|
},
|
|
246
|
+
shadcn: {
|
|
247
|
+
prefix: '',
|
|
248
|
+
componentDir: join(currentDir, './app/components/scn'),
|
|
249
|
+
},
|
|
240
250
|
site: {
|
|
241
251
|
url: SITE_URL,
|
|
242
252
|
},
|
|
@@ -266,7 +276,6 @@ export default defineNuxtConfig(
|
|
|
266
276
|
// modules
|
|
267
277
|
security: {
|
|
268
278
|
headers: {
|
|
269
|
-
crossOriginEmbedderPolicy: 'unsafe-none',
|
|
270
279
|
strictTransportSecurity: false, // prevent endless reload in Chrome
|
|
271
280
|
},
|
|
272
281
|
},
|
|
@@ -291,6 +300,7 @@ export default defineNuxtConfig(
|
|
|
291
300
|
},
|
|
292
301
|
security: {
|
|
293
302
|
headers: {
|
|
303
|
+
crossOriginEmbedderPolicy: 'require-corp', // breaks nuxt devtools
|
|
294
304
|
strictTransportSecurity: {
|
|
295
305
|
maxAge: 31536000,
|
|
296
306
|
preload: true,
|
package/package.json
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"@urql/vue": "2.0.0",
|
|
22
22
|
"@vuelidate/core": "2.0.3",
|
|
23
23
|
"@vuelidate/validators": "2.0.4",
|
|
24
|
+
"@vueuse/core": "14.0.0",
|
|
25
|
+
"class-variance-authority": "0.7.1",
|
|
24
26
|
"clipboardy": "5.0.1",
|
|
25
|
-
"
|
|
27
|
+
"clsx": "2.1.1",
|
|
26
28
|
"eslint": "9.39.1",
|
|
27
29
|
"eslint-config-prettier": "10.1.8",
|
|
28
30
|
"eslint-plugin-compat": "6.0.2",
|
|
@@ -31,15 +33,20 @@
|
|
|
31
33
|
"globals": "16.5.0",
|
|
32
34
|
"jiti": "2.6.1",
|
|
33
35
|
"jose": "6.1.2",
|
|
36
|
+
"lucide-vue-next": "0.554.0",
|
|
34
37
|
"nuxt-gtag": "4.1.0",
|
|
35
38
|
"nuxt-security": "2.5.0",
|
|
36
|
-
"
|
|
39
|
+
"shadcn-nuxt": "2.3.3",
|
|
40
|
+
"tailwind-merge": "3.4.0",
|
|
41
|
+
"tw-animate-css": "1.4.0",
|
|
42
|
+
"vue-sonner": "2.0.9",
|
|
37
43
|
"vue-tsc": "3.1.5"
|
|
38
44
|
},
|
|
39
45
|
"devDependencies": {
|
|
40
46
|
"@types/node": "24.10.1",
|
|
41
47
|
"@urql/devtools": "2.0.3",
|
|
42
48
|
"@urql/exchange-graphcache": "8.1.0",
|
|
49
|
+
"@vueuse/core": "14.0.0",
|
|
43
50
|
"consola": "3.4.2",
|
|
44
51
|
"defu": "6.1.4",
|
|
45
52
|
"h3": "1.15.4",
|
|
@@ -48,6 +55,7 @@
|
|
|
48
55
|
"pinia": "3.0.4",
|
|
49
56
|
"prettier": "3.6.2",
|
|
50
57
|
"prettier-plugin-tailwindcss": "0.7.1",
|
|
58
|
+
"reka-ui": "2.6.0",
|
|
51
59
|
"serve": "14.2.5",
|
|
52
60
|
"sharp": "0.34.5",
|
|
53
61
|
"stylelint": "16.26.0",
|
|
@@ -106,5 +114,5 @@
|
|
|
106
114
|
"start:static": "serve playground/.output/public --ssl-cert ./.config/certificates/ssl.crt --ssl-key ./.config/certificates/ssl.key"
|
|
107
115
|
},
|
|
108
116
|
"type": "module",
|
|
109
|
-
"version": "
|
|
117
|
+
"version": "20.0.0-beta.2"
|
|
110
118
|
}
|
|
@@ -123,7 +123,7 @@ export const VIO_GET_CSP = ({ siteUrl }: { siteUrl: URL }) =>
|
|
|
123
123
|
`${siteUrl}_nuxt/`, // bundle
|
|
124
124
|
],
|
|
125
125
|
'style-src': [
|
|
126
|
-
"'
|
|
126
|
+
"'nonce-{{nonce}}'",
|
|
127
127
|
"'self'", // TODO: `${siteUrl}_nuxt/`, // bundle
|
|
128
128
|
], // TODO: use `style-src-elem` once Playwright WebKit supports it
|
|
129
129
|
},
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the duration between two dates in terms of years, months, weeks, days, hours, and minutes.
|
|
3
|
+
*
|
|
4
|
+
* This utility is useful for displaying human-readable differences between two timestamps.
|
|
5
|
+
* The returned object represents the absolute difference, regardless of the order of `from` and `to`.
|
|
6
|
+
*
|
|
7
|
+
* @param {Date} from - The starting date of the interval.
|
|
8
|
+
* @param {Date} to - The ending date of the interval.
|
|
9
|
+
* @returns {{ years: number, months: number, weeks: number, days: number, hours: number, minutes: number }}
|
|
10
|
+
* An object representing the time difference between the two dates:
|
|
11
|
+
* - `years`: Number of full years
|
|
12
|
+
* - `months`: Remaining full months after accounting for years
|
|
13
|
+
* - `weeks`: Remaining full weeks after accounting for months
|
|
14
|
+
* - `days`: Remaining days after accounting for weeks
|
|
15
|
+
* - `hours`: Remaining hours
|
|
16
|
+
* - `minutes`: Remaining minutes
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* getDuration(new Date('2020-01-01'), new Date('2023-04-10'))
|
|
20
|
+
* // returns: { years: 3, months: 3, weeks: 1, days: 2, hours: 0, minutes: 0 }
|
|
21
|
+
*/
|
|
22
|
+
export const getDuration = (
|
|
23
|
+
from: Date,
|
|
24
|
+
to: Date,
|
|
25
|
+
): {
|
|
26
|
+
days: number
|
|
27
|
+
hours: number
|
|
28
|
+
minutes: number
|
|
29
|
+
months: number
|
|
30
|
+
weeks: number
|
|
31
|
+
years: number
|
|
32
|
+
} => {
|
|
33
|
+
const start = from > to ? to : from
|
|
34
|
+
const end = from > to ? from : to
|
|
35
|
+
|
|
36
|
+
let years = end.getUTCFullYear() - start.getUTCFullYear()
|
|
37
|
+
let months = end.getUTCMonth() - start.getUTCMonth()
|
|
38
|
+
let days = end.getUTCDate() - start.getUTCDate()
|
|
39
|
+
let hours = end.getUTCHours() - start.getUTCHours()
|
|
40
|
+
let minutes = end.getUTCMinutes() - start.getUTCMinutes()
|
|
41
|
+
|
|
42
|
+
if (minutes < 0) {
|
|
43
|
+
minutes += 60
|
|
44
|
+
hours--
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (hours < 0) {
|
|
48
|
+
hours += 24
|
|
49
|
+
days--
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (days < 0) {
|
|
53
|
+
const prevMonth = new Date(
|
|
54
|
+
Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), 0),
|
|
55
|
+
)
|
|
56
|
+
days += prevMonth.getUTCDate()
|
|
57
|
+
months--
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (months < 0) {
|
|
61
|
+
months += 12
|
|
62
|
+
years--
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const weeks = Math.floor(days / 7)
|
|
66
|
+
days = days % 7
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
days,
|
|
70
|
+
hours,
|
|
71
|
+
minutes,
|
|
72
|
+
months,
|
|
73
|
+
weeks,
|
|
74
|
+
years,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default date-time formatting options used for displaying timestamps in a concise and readable format.
|
|
80
|
+
*
|
|
81
|
+
* Format includes:
|
|
82
|
+
* - Day (numeric, e.g., "15")
|
|
83
|
+
* - Month (short name, e.g., "Jul")
|
|
84
|
+
* - Year (numeric, e.g., "2025")
|
|
85
|
+
* - Hour and minute (numeric, 12/24-hour based on locale, e.g., "6:00 PM")
|
|
86
|
+
* - Time zone name (short, e.g., "UTC", "PST")
|
|
87
|
+
*
|
|
88
|
+
* These options are intended to be used with `Intl.DateTimeFormat`.
|
|
89
|
+
*/
|
|
90
|
+
export const dateTimeFormatOptions: Intl.DateTimeFormatOptions = {
|
|
91
|
+
day: 'numeric',
|
|
92
|
+
hour: 'numeric',
|
|
93
|
+
minute: 'numeric',
|
|
94
|
+
month: 'short',
|
|
95
|
+
timeZoneName: 'short',
|
|
96
|
+
year: 'numeric',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns a date-time formatter function for use in emails which don't support targeted locales yet.
|
|
101
|
+
*
|
|
102
|
+
* The formatter uses the specified locale and a fixed UTC time zone.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} locale - A BCP 47 language tag (e.g., "en-US", "fr-FR") used to format the date/time.
|
|
105
|
+
* @returns {Intl.DateTimeFormat} A formatter instance that formats dates in the specified locale and UTC time zone.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* const format = getEmailDateTimeFormatter('en-US')
|
|
109
|
+
* format.format(new Date('2025-07-15T18:00:00Z'))
|
|
110
|
+
* // → "Jul 15, 2025, 6:00 PM UTC"
|
|
111
|
+
*/
|
|
112
|
+
export const getEmailDateTimeFormatter = (
|
|
113
|
+
locale: string,
|
|
114
|
+
): Intl.DateTimeFormat =>
|
|
115
|
+
Intl.DateTimeFormat(locale, {
|
|
116
|
+
...dateTimeFormatOptions,
|
|
117
|
+
timeZone: 'UTC',
|
|
118
|
+
})
|
package/app/plugins/dayjs.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import dayjs from 'dayjs'
|
|
2
|
-
import type { DayjsFn } from 'dayjs'
|
|
3
|
-
|
|
4
|
-
import 'dayjs/locale/de'
|
|
5
|
-
|
|
6
|
-
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
|
7
|
-
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
|
8
|
-
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
9
|
-
import timezone from 'dayjs/plugin/timezone'
|
|
10
|
-
import utc from 'dayjs/plugin/utc'
|
|
11
|
-
|
|
12
|
-
export default defineNuxtPlugin((_nuxtApp) => {
|
|
13
|
-
dayjs.extend(isSameOrBefore)
|
|
14
|
-
dayjs.extend(localizedFormat)
|
|
15
|
-
dayjs.extend(relativeTime)
|
|
16
|
-
dayjs.extend(timezone)
|
|
17
|
-
dayjs.extend(utc)
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
provide: {
|
|
21
|
-
dayjs,
|
|
22
|
-
},
|
|
23
|
-
}
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
declare module '#app' {
|
|
27
|
-
interface NuxtApp {
|
|
28
|
-
$dayjs: DayjsFn
|
|
29
|
-
}
|
|
30
|
-
}
|
package/app/plugins/i18n.ts
DELETED
package/shared/utils/modal.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import Swal from 'sweetalert2'
|
|
2
|
-
|
|
3
|
-
export const showToast = ({ title }: { title: string }) =>
|
|
4
|
-
Swal.fire({
|
|
5
|
-
didOpen: (toast) => {
|
|
6
|
-
toast.addEventListener('mouseenter', Swal.stopTimer)
|
|
7
|
-
toast.addEventListener('mouseleave', Swal.resumeTimer)
|
|
8
|
-
},
|
|
9
|
-
icon: 'success',
|
|
10
|
-
position: 'bottom-right',
|
|
11
|
-
showConfirmButton: false,
|
|
12
|
-
timer: 3000,
|
|
13
|
-
timerProgressBar: true,
|
|
14
|
-
title,
|
|
15
|
-
toast: true,
|
|
16
|
-
})
|