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

Sign up to get free protection for your applications and to get access to all the features.
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>