@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.
@@ -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
+ }
@@ -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 "@tailwindcss/forms" {
14
+ @plugin '@tailwindcss/forms' {
13
15
  strategy: 'base';
14
16
  }
15
17
 
16
- @plugin "@tailwindcss/typography";
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 { $dayjs } = useNuxtApp()
20
- const { locale, t } = useI18n()
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
- const cookieTimezone = useCookie(TIMEZONE_COOKIE_NAME, {
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
- showToast({ title: t('donationUrlCopySuccess') })
29
+ toast.success(t('donationUrlCopySuccess'))
30
30
  } catch (error: unknown) {
31
31
  console.error(error)
32
32
  alert(t('donationUrlCopyError'))
@@ -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 useFireError = () => {
3
+ export const useAlertError = () => {
6
4
  const { t } = useI18n({ useScope: 'global' })
7
5
 
8
- return ({ error }: { error: Error }, api?: Ref<{ errors: Error[] }>) => {
9
- Swal.fire({
10
- icon: 'error',
11
- title: t('globalStatusError'),
12
- text: error.message,
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,5 @@
1
+ import { provideSSRWidth } from '@vueuse/core'
2
+
3
+ export default defineNuxtPlugin((nuxtApp) => {
4
+ provideSSRWidth(1024, nuxtApp.vueApp)
5
+ })
@@ -0,0 +1 @@
1
+ export type ColorMode = 'dark' | 'light' | 'system'
@@ -0,0 +1 @@
1
+ export { toast } from 'vue-sonner'
@@ -0,0 +1,7 @@
1
+ import type { ClassValue } from 'clsx'
2
+ import { clsx } from 'clsx'
3
+ import { twMerge } from 'tailwind-merge'
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "globalError": "Fehler",
2
3
  "globalSeoOgImageAlt": "Social Preview Image für eine Vio Webseite.",
3
4
  "globalPlaceholderEmailAddress": "e-mail{'@'}adres.se",
4
5
  "globalPlaceholderUrl": "https://websei.te",
@@ -1,4 +1,5 @@
1
1
  {
2
+ "globalError": "Error",
2
3
  "globalSeoOgImageAlt": "Social Preview Image for a Vio webpage.",
3
4
  "globalPlaceholderEmailAddress": "email{'@'}addre.ss",
4
5
  "globalPlaceholderUrl": "https://websi.te",
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
- "dayjs": "2.0.0-alpha.4",
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
- "sweetalert2": "11.26.3",
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": "19.1.0"
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
- "'unsafe-inline'", // TODO: replace with "'nonce-{{nonce}}'" once Sweetalert supports it
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
+ })
@@ -1,9 +0,0 @@
1
- import type { Dayjs } from 'dayjs'
2
-
3
- export const useDateTime = () => {
4
- const { $dayjs } = useNuxtApp()
5
- const timezone = getTimezone()
6
-
7
- return (dateTime?: string | number | Dayjs | Date | null) =>
8
- $dayjs(dateTime).tz(timezone)
9
- }
@@ -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
- }
@@ -1,5 +0,0 @@
1
- export default defineNuxtPlugin((nuxtApp) => {
2
- nuxtApp.hook('i18n:localeSwitched', ({ newLocale }) => {
3
- nuxtApp.vueApp.$nuxt.$dayjs.locale(newLocale)
4
- })
5
- })
@@ -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
- })