@dargmuesli/nuxt-vio 20.0.0-beta.1 → 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/.config/lint.js CHANGED
@@ -36,13 +36,6 @@ export const VIO_ESLINT_CONFIG = [
36
36
  },
37
37
  },
38
38
  rules: {
39
- '@intlify/vue-i18n/no-missing-keys': 'error',
40
- '@intlify/vue-i18n/no-raw-text': 'error',
41
- '@intlify/vue-i18n/no-deprecated-i18n-component': 'error', // TODO: do not specify below rules manually, but have them included in `recommended` https://github.com/intlify/eslint-plugin-vue-i18n/issues/275
42
- '@intlify/vue-i18n/no-deprecated-i18n-place-attr': 'error',
43
- '@intlify/vue-i18n/no-deprecated-i18n-places-prop': 'error',
44
- '@intlify/vue-i18n/no-i18n-t-path-prop': 'error',
45
- '@intlify/vue-i18n/valid-message-syntax': 'error',
46
39
  '@intlify/vue-i18n/key-format-style': 'error',
47
40
  '@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
48
41
  '@intlify/vue-i18n/no-dynamic-keys': 'error',
@@ -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
 
@@ -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>
@@ -10,7 +10,6 @@
10
10
  import { reactiveOmit } from '@vueuse/core'
11
11
  import { useForwardProps } from 'reka-ui'
12
12
 
13
- // TODO: use imported type (https://github.com/nuxt/nuxt/issues/29757)
14
13
  import type { NuxtTimeProps } from '#app'
15
14
 
16
15
  const { locale: defaultLocale } = useI18n()
@@ -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,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
@@ -65,11 +71,6 @@ export default defineNuxtConfig(
65
71
  ) {
66
72
  if (nuxt.options.nitro.static) {
67
73
  nuxtConfigSecurityHeaders.contentSecurityPolicy = defu(
68
- {
69
- 'script-src-elem': [
70
- "'unsafe-inline'", // TODO: remove (https://github.com/Baroshem/nuxt-security/pull/659)
71
- ],
72
- },
73
74
  VIO_GET_CSP({ siteUrl: new URL(SITE_URL) }),
74
75
  nuxtConfigSecurityHeaders.contentSecurityPolicy,
75
76
  )
@@ -242,6 +243,10 @@ export default defineNuxtConfig(
242
243
  },
243
244
  strict: true,
244
245
  },
246
+ shadcn: {
247
+ prefix: '',
248
+ componentDir: join(currentDir, './app/components/scn'),
249
+ },
245
250
  site: {
246
251
  url: SITE_URL,
247
252
  },
@@ -256,7 +261,8 @@ export default defineNuxtConfig(
256
261
  },
257
262
  nitro: {
258
263
  experimental: {
259
- openAPI: false, // TODO: set to true (https://github.com/nuxt/content/issues/2839)
264
+ asyncContext: true,
265
+ openAPI: true,
260
266
  },
261
267
  },
262
268
  runtimeConfig: {
@@ -270,7 +276,6 @@ export default defineNuxtConfig(
270
276
  // modules
271
277
  security: {
272
278
  headers: {
273
- crossOriginEmbedderPolicy: 'unsafe-none',
274
279
  strictTransportSecurity: false, // prevent endless reload in Chrome
275
280
  },
276
281
  },
@@ -295,6 +300,7 @@ export default defineNuxtConfig(
295
300
  },
296
301
  security: {
297
302
  headers: {
303
+ crossOriginEmbedderPolicy: 'require-corp', // breaks nuxt devtools
298
304
  strictTransportSecurity: {
299
305
  maxAge: 31536000,
300
306
  preload: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
- "@dargmuesli/nuxt-cookie-control": "9.1.7",
4
- "@eslint/compat": "1.4.1",
3
+ "@dargmuesli/nuxt-cookie-control": "9.1.8",
4
+ "@eslint/compat": "2.0.0",
5
5
  "@heroicons/vue": "2.2.0",
6
6
  "@http-util/status-i18n": "0.9.0",
7
7
  "@intlify/eslint-plugin-vue-i18n": "4.1.0",
@@ -10,7 +10,7 @@
10
10
  "@nuxt/image": "2.0.0",
11
11
  "@nuxtjs/color-mode": "4.0.0",
12
12
  "@nuxtjs/html-validator": "2.1.0",
13
- "@nuxtjs/i18n": "10.2.0",
13
+ "@nuxtjs/i18n": "10.2.1",
14
14
  "@nuxtjs/seo": "3.2.2",
15
15
  "@pinia/nuxt": "0.11.3",
16
16
  "@tailwindcss/forms": "0.5.10",
@@ -21,7 +21,10 @@
21
21
  "@urql/vue": "2.0.0",
22
22
  "@vuelidate/core": "2.0.3",
23
23
  "@vuelidate/validators": "2.0.4",
24
- "clipboardy": "5.0.0",
24
+ "@vueuse/core": "14.0.0",
25
+ "class-variance-authority": "0.7.1",
26
+ "clipboardy": "5.0.1",
27
+ "clsx": "2.1.1",
25
28
  "eslint": "9.39.1",
26
29
  "eslint-config-prettier": "10.1.8",
27
30
  "eslint-plugin-compat": "6.0.2",
@@ -29,14 +32,18 @@
29
32
  "eslint-plugin-yml": "1.19.0",
30
33
  "globals": "16.5.0",
31
34
  "jiti": "2.6.1",
32
- "jose": "6.1.1",
35
+ "jose": "6.1.2",
36
+ "lucide-vue-next": "0.554.0",
33
37
  "nuxt-gtag": "4.1.0",
34
- "nuxt-security": "2.4.0",
35
- "sweetalert2": "11.26.3",
36
- "vue-tsc": "3.1.3"
38
+ "nuxt-security": "2.5.0",
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",
43
+ "vue-tsc": "3.1.5"
37
44
  },
38
45
  "devDependencies": {
39
- "@types/node": "24.10.0",
46
+ "@types/node": "24.10.1",
40
47
  "@urql/devtools": "2.0.3",
41
48
  "@urql/exchange-graphcache": "8.1.0",
42
49
  "@vueuse/core": "14.0.0",
@@ -51,12 +58,12 @@
51
58
  "reka-ui": "2.6.0",
52
59
  "serve": "14.2.5",
53
60
  "sharp": "0.34.5",
54
- "stylelint": "16.25.0",
61
+ "stylelint": "16.26.0",
55
62
  "stylelint-config-recommended-vue": "1.6.1",
56
63
  "stylelint-config-standard": "39.0.1",
57
64
  "stylelint-no-unsupported-browser-features": "8.0.5",
58
65
  "tailwindcss": "4.1.17",
59
- "vue": "3.5.24",
66
+ "vue": "3.5.25",
60
67
  "vue-router": "4.6.3"
61
68
  },
62
69
  "engines": {
@@ -107,5 +114,5 @@
107
114
  "start:static": "serve playground/.output/public --ssl-cert ./.config/certificates/ssl.crt --ssl-key ./.config/certificates/ssl.key"
108
115
  },
109
116
  "type": "module",
110
- "version": "20.0.0-beta.1"
117
+ "version": "20.0.0-beta.2"
111
118
  }
@@ -1,5 +1,5 @@
1
1
  export default defineEventHandler(async (event) => {
2
- event.context.$timezone = await getTimezoneServer(event)
2
+ event.context.$timezone = await getTimezone(event)
3
3
  })
4
4
 
5
5
  declare module 'h3' {
@@ -1,10 +1,9 @@
1
1
  import { defu } from 'defu'
2
- import type { NuxtOptions } from 'nuxt/schema'
2
+ import type { ModuleOptions } from 'nuxt-security'
3
3
 
4
4
  // remove invalid `'none'`s and duplicates
5
5
  export const cleanupCsp = (
6
- // @ts-expect-error https://github.com/Baroshem/nuxt-security/pull/661
7
- nuxtSecurityConfiguration: Partial<NuxtOptions['security']>,
6
+ nuxtSecurityConfiguration: Partial<ModuleOptions>,
8
7
  ) => {
9
8
  if (
10
9
  nuxtSecurityConfiguration.headers &&
@@ -28,7 +27,6 @@ export const cleanupCsp = (
28
27
  }
29
28
 
30
29
  export default defineNitroPlugin((nitroApp) => {
31
- // @ts-expect-error https://github.com/Baroshem/nuxt-security/pull/661
32
30
  nitroApp.hooks.hook('nuxt-security:routeRules', async (routeRules) => {
33
31
  const { siteUrlTyped: siteUrl } = useSiteUrl()
34
32
 
@@ -1,7 +1,6 @@
1
1
  import type { H3Event } from 'h3'
2
2
 
3
- // TODO: rename to `getTimezone` (https://github.com/nuxt/cli/issues/266)
4
- export const getTimezoneServer = async (event: H3Event) => {
3
+ export const getTimezone = async (event: H3Event) => {
5
4
  const timezoneBySsr = event.context.$timezone
6
5
 
7
6
  if (timezoneBySsr) return timezoneBySsr
@@ -113,14 +113,6 @@ export const VIO_GET_CSP = ({ siteUrl }: { siteUrl: URL }) =>
113
113
  : {}),
114
114
  'connect-src': [
115
115
  "'self'", // e.g. `/_nuxt/builds/meta/`, `/_payload.json`, `/privacy-policy/_payload.json`
116
- // ...(process.env.NODE_ENV === 'development'
117
- // ? [
118
- // 'http://localhost:3000/_nuxt/', // hot reload
119
- // 'https://localhost:3000/_nuxt/', // hot reload
120
- // 'ws://localhost:3000/_nuxt/', // hot reload
121
- // 'wss://localhost:3000/_nuxt/', // hot reload
122
- // ] // TODO: generalize for different ports
123
- // : []),
124
116
  ],
125
117
  'img-src': [
126
118
  "'self'", // e.g. favicon
@@ -131,7 +123,7 @@ export const VIO_GET_CSP = ({ siteUrl }: { siteUrl: URL }) =>
131
123
  `${siteUrl}_nuxt/`, // bundle
132
124
  ],
133
125
  'style-src': [
134
- "'unsafe-inline'", // TODO: replace with "'nonce-{{nonce}}'" once Sweetalert supports it
126
+ "'nonce-{{nonce}}'",
135
127
  "'self'", // TODO: `${siteUrl}_nuxt/`, // bundle
136
128
  ], // TODO: use `style-src-elem` once Playwright WebKit supports it
137
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
+ })
@@ -103,9 +103,7 @@ export const getServiceHref = ({
103
103
 
104
104
  if (stagingHost) {
105
105
  return `https://${nameSubdomainString}${stagingHost}`
106
- // TODO: remove disable below (https://github.com/nuxt/nuxt/issues/25323)
107
- // eslint-disable-next-line nuxt/prefer-import-meta
108
- } else if (isSsr && process.server) {
106
+ } else if (isSsr && import.meta.server) {
109
107
  return `http://${name}${portString}`
110
108
  } else {
111
109
  return `https://${nameSubdomainString}${getRootHost(host)}`
@@ -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
- })