@dargmuesli/nuxt-vio 3.1.1 → 3.2.0-beta.1

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.
Files changed (39) hide show
  1. package/components/vio/button/VioButton.vue +1 -3
  2. package/components/vio/button/VioButtonColored.vue +1 -1
  3. package/components/vio/button/VioButtonHome.vue +28 -0
  4. package/components/vio/button/VioButtonIcon.vue +31 -0
  5. package/components/vio/button/VioButtonList.vue +5 -0
  6. package/components/vio/button/VioButtonShare.vue +45 -0
  7. package/components/vio/card/state/VioCardStateInfo.vue +14 -0
  8. package/components/vio/icon/{IconArrowRight.vue → VioIconArrowRight.vue} +1 -1
  9. package/components/vio/icon/{IconCalendar.vue → VioIconCalendar.vue} +1 -1
  10. package/components/vio/icon/VioIconChartBar.vue +31 -0
  11. package/components/vio/icon/{IconChatOutline.vue → VioIconChatOutline.vue} +1 -1
  12. package/components/vio/icon/{IconDownload.vue → VioIconDownload.vue} +1 -1
  13. package/components/vio/icon/VioIconHeart.vue +31 -0
  14. package/components/vio/icon/{IconHome.vue → VioIconHome.vue} +1 -1
  15. package/components/vio/icon/{IconLightbulb.vue → VioIconLightbulb.vue} +1 -1
  16. package/components/vio/icon/{IconMusic.vue → VioIconMusic.vue} +1 -1
  17. package/components/vio/icon/VioIconShare.vue +31 -0
  18. package/components/vio/icon/VioIconSignIn.vue +31 -0
  19. package/components/vio/icon/VioIconTv.vue +31 -0
  20. package/components/vio/loader/Loader.vue +43 -0
  21. package/components/vio/loader/LoaderImage.vue +81 -0
  22. package/components/vio/loader/indicator/LoaderIndicatorText.vue +9 -0
  23. package/locales/de.json +1 -0
  24. package/locales/en.json +1 -0
  25. package/nuxt.config.ts +1 -0
  26. package/package.json +5 -1
  27. package/plugins/dayjs.ts +2 -0
  28. package/utils/auth.ts +118 -0
  29. package/utils/constants.ts +7 -0
  30. package/utils/networking.ts +31 -1
  31. package/utils/{routing.ts → utils.ts} +3 -0
  32. package/components/vio/icon/IconShare.vue +0 -27
  33. /package/components/vio/icon/{IconChatSolid.vue → VioIconChatSolid.vue} +0 -0
  34. /package/components/vio/icon/{IconCheckCircle.vue → VioIconCheckCircle.vue} +0 -0
  35. /package/components/vio/icon/{IconContainer.vue → VioIconContainer.vue} +0 -0
  36. /package/components/vio/icon/{IconExclamationCircle.vue → VioIconExclamationCircle.vue} +0 -0
  37. /package/components/vio/icon/{IconHourglass.vue → VioIconHourglass.vue} +0 -0
  38. /package/components/vio/icon/{IconMixcloud.vue → VioIconMixcloud.vue} +0 -0
  39. /package/components/vio/icon/{IconPlay.vue → VioIconPlay.vue} +0 -0
@@ -56,9 +56,7 @@ const emit = defineEmits<{
56
56
  const classesComputed = computed(() => {
57
57
  return [
58
58
  props.classes,
59
- ...(props.isBlock
60
- ? ['block']
61
- : ['inline-flex items-center justify-center gap-2']),
59
+ ...(props.isBlock ? ['block'] : ['inline-flex items-center gap-2']),
62
60
  ...(props.isLinkColored ? ['text-link-dark dark:text-link-bright'] : []),
63
61
  ].join(' ')
64
62
  })
@@ -2,7 +2,7 @@
2
2
  <VioButton
3
3
  :is-to-relative="isToRelative"
4
4
  :aria-label="ariaLabel"
5
- class="rounded-md border px-4 py-2 font-medium"
5
+ class="justify-center rounded-md border px-4 py-2 font-medium"
6
6
  :class="
7
7
  [
8
8
  ...(isPrimary
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <VioButtonColored
3
+ :aria-label="t('home')"
4
+ :to="localePath('/')"
5
+ @click="emit('click')"
6
+ >
7
+ {{ t('home') }}
8
+ <template #prefix>
9
+ <VioIconHome />
10
+ </template>
11
+ </VioButtonColored>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ const localePath = useLocalePath()
16
+ const { t } = useI18n()
17
+
18
+ const emit = defineEmits<{
19
+ click: []
20
+ }>()
21
+ </script>
22
+
23
+ <i18n lang="yaml">
24
+ de:
25
+ home: Nach Hause
26
+ en:
27
+ home: Head home
28
+ </i18n>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <VioButton
3
+ :aria-label="ariaLabel"
4
+ class="flex justify-center"
5
+ :disabled="disabled"
6
+ :title="ariaLabel"
7
+ :to="to"
8
+ :type="type"
9
+ @click="emit('click')"
10
+ >
11
+ <slot />
12
+ </VioButton>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ export interface Props {
17
+ ariaLabel: string
18
+ disabled?: boolean
19
+ to?: string
20
+ type?: 'button' | 'submit' | 'reset'
21
+ }
22
+ withDefaults(defineProps<Props>(), {
23
+ disabled: false,
24
+ to: undefined,
25
+ type: 'button',
26
+ })
27
+
28
+ const emit = defineEmits<{
29
+ click: []
30
+ }>()
31
+ </script>
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="flex flex-wrap gap-2 lg:gap-4">
3
+ <slot />
4
+ </div>
5
+ </template>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <span v-if="url" class="flex items-center gap-2">
3
+ <slot />
4
+ <VioButtonColored :aria-label="t('share')" @click="copy(url)">
5
+ <template #prefix>
6
+ <VioIconShare />
7
+ </template>
8
+ </VioButtonColored>
9
+ </span>
10
+ <span v-else class="unready inline-block">
11
+ <slot name="unready" />
12
+ </span>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ export interface Props {
17
+ url: string
18
+ }
19
+ withDefaults(defineProps<Props>(), {})
20
+
21
+ const { t } = useI18n()
22
+
23
+ // methods
24
+ const copy = async (string: string) => {
25
+ if (typeof window === 'undefined') return
26
+
27
+ try {
28
+ await navigator.clipboard.writeText(string)
29
+ showToast({ title: t('donationUrlCopySuccess') })
30
+ } catch (error: any) {
31
+ alert(t('donationUrlCopyError'))
32
+ }
33
+ }
34
+ </script>
35
+
36
+ <i18n lang="yaml">
37
+ de:
38
+ donationUrlCopyError: 'Fehler: Der Spendenlink konnte leider nicht in die Zwischenablage kopiert werden!'
39
+ donationUrlCopySuccess: Der Spendenlink wurde in die Zwischenablage kopiert.
40
+ share: Teilen
41
+ en:
42
+ donationUrlCopyError: 'Error: Unfortunately, the donation link could not be copied to the clipboard!'
43
+ donationUrlCopySuccess: The donation link has been copied to the clipboard.
44
+ share: Share
45
+ </i18n>
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <VioCardState background-color="bg-blue-600" :is-edgy="isEdgy">
3
+ <slot />
4
+ </VioCardState>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ export interface Props {
9
+ isEdgy?: boolean
10
+ }
11
+ withDefaults(defineProps<Props>(), {
12
+ isEdgy: false,
13
+ })
14
+ </script>
@@ -3,7 +3,7 @@
3
3
  fill="none"
4
4
  viewBox="0 0 24 24"
5
5
  stroke="currentColor"
6
- stroke-width="2"
6
+ stroke-width="1.5"
7
7
  :title="title || t('title')"
8
8
  >
9
9
  <path
@@ -8,7 +8,7 @@
8
8
  <path
9
9
  stroke-linecap="round"
10
10
  stroke-linejoin="round"
11
- stroke-width="2"
11
+ stroke-width="1.5"
12
12
  d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
13
13
  />
14
14
  </VioIconContainer>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <VioIconContainer
3
+ fill="none"
4
+ viewBox="0 0 24 24"
5
+ stroke="currentColor"
6
+ stroke-width="1.5"
7
+ :title="title || t('title')"
8
+ >
9
+ <path
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
13
+ />
14
+ </VioIconContainer>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ export interface Props {
19
+ title?: string // eslint-disable-line vue/require-default-prop
20
+ }
21
+ withDefaults(defineProps<Props>(), {})
22
+
23
+ const { t } = useI18n()
24
+ </script>
25
+
26
+ <i18n lang="yaml">
27
+ de:
28
+ title: Balkendiagramm
29
+ en:
30
+ title: Bar chart
31
+ </i18n>
@@ -5,7 +5,7 @@
5
5
  fill="none"
6
6
  viewBox="0 0 24 24"
7
7
  stroke="currentColor"
8
- stroke-width="2"
8
+ stroke-width="1.5"
9
9
  >
10
10
  <path
11
11
  stroke-linecap="round"
@@ -3,12 +3,12 @@
3
3
  fill="none"
4
4
  viewBox="0 0 24 24"
5
5
  stroke="currentColor"
6
+ stroke-width="1.5"
6
7
  :title="title || t('title')"
7
8
  >
8
9
  <path
9
10
  stroke-linecap="round"
10
11
  stroke-linejoin="round"
11
- stroke-width="2"
12
12
  d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
13
13
  />
14
14
  </VioIconContainer>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <VioIconContainer
3
+ fill="none"
4
+ viewBox="0 0 24 24"
5
+ stroke="currentColor"
6
+ stroke-width="1.5"
7
+ :title="title || t('title')"
8
+ >
9
+ <path
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"
13
+ />
14
+ </VioIconContainer>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ export interface Props {
19
+ title?: string // eslint-disable-line vue/require-default-prop
20
+ }
21
+ withDefaults(defineProps<Props>(), {})
22
+
23
+ const { t } = useI18n()
24
+ </script>
25
+
26
+ <i18n lang="yaml">
27
+ de:
28
+ title: Herz
29
+ en:
30
+ title: Heart
31
+ </i18n>
@@ -3,12 +3,12 @@
3
3
  fill="none"
4
4
  viewBox="0 0 24 24"
5
5
  stroke="currentColor"
6
+ stroke-width="1.5"
6
7
  :title="title || t('title')"
7
8
  >
8
9
  <path
9
10
  stroke-linecap="round"
10
11
  stroke-linejoin="round"
11
- stroke-width="2"
12
12
  d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
13
13
  />
14
14
  </VioIconContainer>
@@ -5,7 +5,7 @@
5
5
  fill="none"
6
6
  viewBox="0 0 24 24"
7
7
  stroke="currentColor"
8
- stroke-width="2"
8
+ stroke-width="1.5"
9
9
  >
10
10
  <path
11
11
  stroke-linecap="round"
@@ -5,7 +5,7 @@
5
5
  fill="none"
6
6
  viewBox="0 0 24 24"
7
7
  stroke="currentColor"
8
- stroke-width="2"
8
+ stroke-width="1.5"
9
9
  >
10
10
  <path
11
11
  stroke-linecap="round"
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <VioIconContainer
3
+ fill="none"
4
+ viewBox="0 0 24 24"
5
+ stroke="currentColor"
6
+ stroke-width="1.5"
7
+ :title="title || t('title')"
8
+ >
9
+ <path
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
13
+ />
14
+ </VioIconContainer>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ export interface Props {
19
+ title?: string // eslint-disable-line vue/require-default-prop
20
+ }
21
+ withDefaults(defineProps<Props>(), {})
22
+
23
+ const { t } = useI18n()
24
+ </script>
25
+
26
+ <i18n lang="yaml">
27
+ de:
28
+ title: Einloggen
29
+ en:
30
+ title: Login
31
+ </i18n>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <VioIconContainer
3
+ fill="none"
4
+ viewBox="0 0 24 24"
5
+ stroke="currentColor"
6
+ stroke-width="1.5"
7
+ :title="title || t('title')"
8
+ >
9
+ <path
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
13
+ />
14
+ </VioIconContainer>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ export interface Props {
19
+ title?: string // eslint-disable-line vue/require-default-prop
20
+ }
21
+ withDefaults(defineProps<Props>(), {})
22
+
23
+ const { t } = useI18n()
24
+ </script>
25
+
26
+ <i18n lang="yaml">
27
+ de:
28
+ title: Einloggen
29
+ en:
30
+ title: Login
31
+ </i18n>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <VioIconContainer
3
+ fill="none"
4
+ viewBox="0 0 24 24"
5
+ stroke="currentColor"
6
+ stroke-width="1.5"
7
+ :title="title || t('title')"
8
+ >
9
+ <path
10
+ stroke-linecap="round"
11
+ stroke-linejoin="round"
12
+ d="M6 20.25h12m-7.5-3v3m3-3v3m-10.125-3h17.25c.621 0 1.125-.504 1.125-1.125V4.875c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125z"
13
+ />
14
+ </VioIconContainer>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ export interface Props {
19
+ title?: string // eslint-disable-line vue/require-default-prop
20
+ }
21
+ withDefaults(defineProps<Props>(), {})
22
+
23
+ const { t } = useI18n()
24
+ </script>
25
+
26
+ <i18n lang="yaml">
27
+ de:
28
+ title: TV
29
+ en:
30
+ title: TV
31
+ </i18n>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="api.isFetching" class="aspect-square" :class="classes">
4
+ <VioLoaderIndicatorPing v-if="indicator === 'ping'" />
5
+ <VioLoaderIndicatorSpinner v-else-if="indicator === 'spinner'" />
6
+ <VioLoaderIndicatorText v-else-if="indicator === 'text'" />
7
+ <VioLoaderIndicatorText v-else />
8
+ </div>
9
+ <VioCardStateAlert v-if="errorMessages.length">
10
+ <VioLayoutSpanList :span="errorMessages" />
11
+ </VioCardStateAlert>
12
+ <slot v-if="api.data && Object.keys(api.data).length" />
13
+ </div>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { UnwrapRef } from 'vue'
18
+
19
+ import { ApiData } from '@dargmuesli/nuxt-vio/types/api'
20
+
21
+ export interface Props {
22
+ api: UnwrapRef<ApiData>
23
+ errorPgIds?: Record<string, string>
24
+ classes?: string
25
+ indicator?: string
26
+ }
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ errorPgIds: undefined,
29
+ classes: undefined,
30
+ indicator: undefined,
31
+ })
32
+
33
+ // computations
34
+ const errorMessages = computed(() =>
35
+ getCombinedErrorMessages(props.api.errors, props.errorPgIds),
36
+ )
37
+ </script>
38
+
39
+ <script lang="ts">
40
+ export default {
41
+ name: 'MaevsiLoader',
42
+ }
43
+ </script>
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <div :class="[aspect, classes]">
3
+ <VioLoaderIndicatorPing v-if="isLoading" />
4
+ <VioCardStateAlert v-if="isError">
5
+ {{ t('error') }}
6
+ </VioCardStateAlert>
7
+ <img
8
+ v-if="srcWhenLoaded"
9
+ :alt="alt"
10
+ :class="[aspect, classes]"
11
+ :height="height"
12
+ :src="srcWhenLoaded"
13
+ :width="width"
14
+ />
15
+ <img :alt="alt" class="hidden" :height="height" :src="src" :width="width" />
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { debounce } from 'lodash-es'
21
+
22
+ export interface Props {
23
+ alt: string
24
+ aspect: string
25
+ classes?: string
26
+ height: string
27
+ src: string
28
+ width: string
29
+ }
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ classes: undefined,
32
+ })
33
+ const srcProp = toRef(() => props.src)
34
+
35
+ const { t } = useI18n()
36
+
37
+ // data
38
+ const img = ref<HTMLImageElement>()
39
+ const isError = ref(false)
40
+ const isLoading = ref(false)
41
+ const loadingId = Math.random()
42
+ const loadingIds = useState('loadingIds', () => [] as number[])
43
+ const srcWhenLoaded = ref<string | undefined>(srcProp.value)
44
+
45
+ // methods
46
+ const loadOnClient = () => {
47
+ loadingStartDebounced()
48
+
49
+ img.value = new Image()
50
+ img.value.onload = () => {
51
+ loadingStop()
52
+ srcWhenLoaded.value = img.value?.src
53
+ }
54
+ img.value.onerror = () => {
55
+ loadingStop()
56
+ isError.value = true
57
+ }
58
+ img.value.src = props.src
59
+ }
60
+ const loadingStart = () => {
61
+ srcWhenLoaded.value = undefined
62
+ isLoading.value = true
63
+ loadingIds.value.push(loadingId)
64
+ }
65
+ const loadingStartDebounced = debounce(loadingStart, 100)
66
+ const loadingStop = () => {
67
+ loadingStartDebounced.cancel()
68
+ isLoading.value = false
69
+ loadingIds.value.splice(loadingIds.value.indexOf(loadingId), 1)
70
+ }
71
+
72
+ // lifecycle
73
+ onMounted(loadOnClient)
74
+ </script>
75
+
76
+ <i18n lang="yaml">
77
+ de:
78
+ error: Bild konnte nicht geladen werden!
79
+ en:
80
+ error: Image could not be loaded!
81
+ </i18n>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <VioLoaderIndicator class="animate-pulse">
3
+ {{ t('globalStatusLoading') }}
4
+ </VioLoaderIndicator>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ const { t } = useI18n()
9
+ </script>
package/locales/de.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "globalStatusError": "Fehler",
4
4
  "globalStatusLoading": "Lade...",
5
5
  "globalValidationFailed": "Bitte überprüfe deine Eingaben 🙈",
6
+ "globalValidationFormatIncorrect": "Falsches Format",
6
7
  "globalValidationFormatUrlHttps": "Muss mit \"https://\" beginnen",
7
8
  "globalValidationLength": "Zu lang",
8
9
  "globalValidationRequired": "Pflichtfeld"
package/locales/en.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "globalStatusError": "Error",
4
4
  "globalStatusLoading": "Loading...",
5
5
  "globalValidationFailed": "Please check your input 🙈",
6
+ "globalValidationFormatIncorrect": "Incorrect format",
6
7
  "globalValidationFormatUrlHttps": "Must start with \"https://\"",
7
8
  "globalValidationLength": "Too long",
8
9
  "globalValidationRequired": "Required"
package/nuxt.config.ts CHANGED
@@ -63,6 +63,7 @@ export default defineNuxtConfig(
63
63
  },
64
64
  typescript: {
65
65
  shim: false,
66
+ strict: true,
66
67
  tsConfig: {
67
68
  compilerOptions: {
68
69
  esModuleInterop: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dargmuesli/nuxt-vio",
3
- "version": "3.1.1",
3
+ "version": "3.2.0-beta.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -72,7 +72,10 @@
72
72
  "devDependencies": {
73
73
  "@intlify/eslint-plugin-vue-i18n": "3.0.0-next.3",
74
74
  "@nuxtjs/eslint-config-typescript": "12.0.0",
75
+ "@types/cookie": "0.5.1",
76
+ "@types/lodash-es": "4.17.8",
75
77
  "@types/marked": "5.0.1",
78
+ "cookie": "0.5.0",
76
79
  "eslint": "8.47.0",
77
80
  "eslint-config-prettier": "9.0.0",
78
81
  "eslint-plugin-compat": "4.1.4",
@@ -80,6 +83,7 @@
80
83
  "eslint-plugin-prettier": "5.0.0",
81
84
  "eslint-plugin-yml": "1.8.0",
82
85
  "lint-staged": "14.0.1",
86
+ "lodash-es": "4.17.21",
83
87
  "nuxt": "3.6.5",
84
88
  "prettier": "3.0.2",
85
89
  "prettier-plugin-tailwindcss": "0.5.3",
package/plugins/dayjs.ts CHANGED
@@ -6,12 +6,14 @@ import de from 'dayjs/locale/de'
6
6
 
7
7
  import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
8
8
  import localizedFormat from 'dayjs/plugin/localizedFormat'
9
+ import relativeTime from 'dayjs/plugin/relativeTime'
9
10
  import timezone from 'dayjs/plugin/timezone'
10
11
  import utc from 'dayjs/plugin/utc'
11
12
 
12
13
  export default defineNuxtPlugin((_nuxtApp) => {
13
14
  dayjs.extend(isSameOrBefore)
14
15
  dayjs.extend(localizedFormat)
16
+ dayjs.extend(relativeTime)
15
17
  dayjs.extend(timezone)
16
18
  dayjs.extend(utc)
17
19
 
package/utils/auth.ts ADDED
@@ -0,0 +1,118 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http'
2
+
3
+ import { consola } from 'consola'
4
+ import { parse, serialize } from 'cookie'
5
+ import { decodeJwt } from 'jose'
6
+ import { Store } from 'pinia'
7
+
8
+ import { useVioAuthStore } from '../store/auth'
9
+ import { xhrPromise } from '../utils/networking'
10
+ import { JWT_NAME } from './constants'
11
+
12
+ export const getJwtFromCookie = ({ req }: { req: IncomingMessage }) => {
13
+ if (req.headers.cookie) {
14
+ const cookies = parse(req.headers.cookie)
15
+
16
+ if (cookies[JWT_NAME()]) {
17
+ const cookie = decodeJwt(cookies[JWT_NAME()])
18
+
19
+ if (cookie.exp !== undefined && cookie.exp > Date.now() / 1000) {
20
+ return {
21
+ jwt: cookies[JWT_NAME()],
22
+ jwtDecoded: cookie,
23
+ }
24
+ } else {
25
+ consola.info('Token expired.')
26
+ }
27
+ } else {
28
+ consola.debug('No token cookie.')
29
+ }
30
+ } else {
31
+ consola.debug('No cookie header.')
32
+ }
33
+ }
34
+
35
+ export const jwtStore = async ({
36
+ $urqlReset,
37
+ store,
38
+ res,
39
+ jwt,
40
+ }: {
41
+ $urqlReset: Function
42
+ store: Store
43
+ res?: ServerResponse
44
+ jwt?: string
45
+ }) => {
46
+ $urqlReset()
47
+
48
+ consola.trace('Storing the following JWT: ' + jwt)
49
+ ;(store as unknown as { jwtSet: (jwtNew?: string) => void }).jwtSet(jwt)
50
+
51
+ if (process.server) {
52
+ res?.setHeader(
53
+ 'Set-Cookie',
54
+ serialize(JWT_NAME(), jwt || '', {
55
+ expires: jwt ? new Date(Date.now() + 86400 * 1000 * 31) : new Date(0),
56
+ httpOnly: true,
57
+ path: '/',
58
+ sameSite: 'lax', // Cannot be 'strict' to allow authentications after clicking on links within webmailers.
59
+ secure: true,
60
+ }),
61
+ )
62
+ } else {
63
+ try {
64
+ await xhrPromise('POST', '/api/auth', jwt || '')
65
+ } catch (error: any) {
66
+ return Promise.reject(Error('Authentication api call failed.'))
67
+ }
68
+ }
69
+ }
70
+
71
+ export const useJwtStore = () => {
72
+ const { $urqlReset } = useNuxtApp()
73
+ const store = useVioAuthStore()
74
+ const event = useRequestEvent()
75
+
76
+ if (typeof $urqlReset !== 'function')
77
+ throw new Error('`$urqlReset` is not a function!')
78
+
79
+ return {
80
+ async jwtStore(jwt?: string) {
81
+ await jwtStore({
82
+ $urqlReset,
83
+ store,
84
+ res: process.server ? event.node.res : undefined,
85
+ jwt,
86
+ })
87
+ },
88
+ }
89
+ }
90
+
91
+ export const signOut = async ({
92
+ $urqlReset,
93
+ store,
94
+ res,
95
+ }: {
96
+ $urqlReset: Function
97
+ store: Store
98
+ res?: ServerResponse
99
+ }) => await jwtStore({ $urqlReset, store, res })
100
+
101
+ export const useSignOut = () => {
102
+ const { $urqlReset } = useNuxtApp()
103
+ const store = useVioAuthStore()
104
+ const event = useRequestEvent()
105
+
106
+ if (typeof $urqlReset !== 'function')
107
+ throw new Error('`$urqlReset` is not a function!')
108
+
109
+ return {
110
+ async signOut() {
111
+ await signOut({
112
+ $urqlReset,
113
+ store,
114
+ res: process.server ? event.node.res : undefined,
115
+ })
116
+ },
117
+ }
118
+ }
@@ -1,3 +1,5 @@
1
+ import { helpers } from '@vuelidate/validators'
2
+
1
3
  export const SITE_NAME = 'Vio'
2
4
 
3
5
  export const BASE_URL =
@@ -36,9 +38,14 @@ export const I18N_VUE_CONFIG = {
36
38
  fallbackWarn: false, // covered by linting
37
39
  missingWarn: false, // covered by linting
38
40
  }
41
+ export const JWT_NAME = () =>
42
+ `${process.env.NODE_ENV === 'production' ? '__Secure-' : ''}jwt`
43
+ export const REGEX_UUID =
44
+ /^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/
39
45
  export const TIMEZONE_COOKIE_NAME = [COOKIE_PREFIX, 'tz'].join(COOKIE_SEPARATOR)
40
46
  export const TIMEZONE_HEADER_KEY = `X-${SITE_NAME}-Timezone`
41
47
  export const VALIDATION_SUGGESTION_TITLE_LENGTH_MAXIMUM = 300
48
+ export const VERIFICATION_FORMAT_UUID = helpers.regex(REGEX_UUID)
42
49
  export const VIO_NUXT_BASE_CONFIG = ({
43
50
  baseUrl,
44
51
  siteName,
@@ -6,9 +6,17 @@ import { H3Event, getCookie } from 'h3'
6
6
  import { ofetch } from 'ofetch'
7
7
  import { Ref } from 'vue'
8
8
 
9
- import type { BackendError } from '../types/api'
9
+ import type { ApiData, BackendError } from '../types/api'
10
10
  import { TIMEZONE_COOKIE_NAME } from './constants'
11
11
 
12
+ export const getApiDataDefault = (): ApiData =>
13
+ computed(() =>
14
+ reactive({
15
+ data: undefined,
16
+ ...getApiMeta(),
17
+ }),
18
+ )
19
+
12
20
  export const getApiMeta = (
13
21
  queries?: {
14
22
  error: Ref<CombinedError | undefined>
@@ -122,3 +130,25 @@ export const getTimezone = async (event: H3Event) => {
122
130
 
123
131
  return undefined
124
132
  }
133
+
134
+ // TODO: use fetch
135
+ export const xhrPromise = (method: string, url: string, jwt: string) =>
136
+ new Promise((resolve, reject) => {
137
+ const xhr = new XMLHttpRequest()
138
+ xhr.open(method, url)
139
+
140
+ if (jwt) {
141
+ xhr.setRequestHeader('Authorization', 'Bearer ' + jwt)
142
+ }
143
+
144
+ xhr.onload = () => {
145
+ if (xhr.status >= 200 && xhr.status < 300) {
146
+ resolve(xhr.response)
147
+ } else {
148
+ reject(new Error(`${xhr.status}\n${xhr.statusText}`))
149
+ }
150
+ }
151
+ xhr.onerror = () => reject(new Error(`${xhr.status}\n${xhr.statusText}`))
152
+
153
+ xhr.send()
154
+ })
@@ -2,3 +2,6 @@ import { RouteLocationRaw } from '#vue-router'
2
2
 
3
3
  export const append = (path: string, pathToAppend?: RouteLocationRaw) =>
4
4
  path + (path.endsWith('/') ? '' : '/') + (pathToAppend ?? '')
5
+
6
+ export const arrayRemoveNulls = <T>(array?: Array<T>) =>
7
+ array?.flatMap((x: T) => (x ? [x] : [])) || []
@@ -1,27 +0,0 @@
1
- <template>
2
- <svg
3
- xmlns="http://www.w3.org/2000/svg"
4
- :class="classes"
5
- fill="none"
6
- viewBox="0 0 24 24"
7
- stroke="currentColor"
8
- stroke-width="2"
9
- >
10
- <path
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
14
- />
15
- </svg>
16
- </template>
17
-
18
- <script lang="ts">
19
- export default defineComponent({
20
- props: {
21
- classes: {
22
- default: 'h-5 md:h-6 w-5 md:w-6',
23
- type: String,
24
- },
25
- },
26
- })
27
- </script>