@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.
- package/components/vio/button/VioButton.vue +1 -3
- package/components/vio/button/VioButtonColored.vue +1 -1
- package/components/vio/button/VioButtonHome.vue +28 -0
- package/components/vio/button/VioButtonIcon.vue +31 -0
- package/components/vio/button/VioButtonList.vue +5 -0
- package/components/vio/button/VioButtonShare.vue +45 -0
- package/components/vio/card/state/VioCardStateInfo.vue +14 -0
- package/components/vio/icon/{IconArrowRight.vue → VioIconArrowRight.vue} +1 -1
- package/components/vio/icon/{IconCalendar.vue → VioIconCalendar.vue} +1 -1
- package/components/vio/icon/VioIconChartBar.vue +31 -0
- package/components/vio/icon/{IconChatOutline.vue → VioIconChatOutline.vue} +1 -1
- package/components/vio/icon/{IconDownload.vue → VioIconDownload.vue} +1 -1
- package/components/vio/icon/VioIconHeart.vue +31 -0
- package/components/vio/icon/{IconHome.vue → VioIconHome.vue} +1 -1
- package/components/vio/icon/{IconLightbulb.vue → VioIconLightbulb.vue} +1 -1
- package/components/vio/icon/{IconMusic.vue → VioIconMusic.vue} +1 -1
- package/components/vio/icon/VioIconShare.vue +31 -0
- package/components/vio/icon/VioIconSignIn.vue +31 -0
- package/components/vio/icon/VioIconTv.vue +31 -0
- package/components/vio/loader/Loader.vue +43 -0
- package/components/vio/loader/LoaderImage.vue +81 -0
- package/components/vio/loader/indicator/LoaderIndicatorText.vue +9 -0
- package/locales/de.json +1 -0
- package/locales/en.json +1 -0
- package/nuxt.config.ts +1 -0
- package/package.json +5 -1
- package/plugins/dayjs.ts +2 -0
- package/utils/auth.ts +118 -0
- package/utils/constants.ts +7 -0
- package/utils/networking.ts +31 -1
- package/utils/{routing.ts → utils.ts} +3 -0
- package/components/vio/icon/IconShare.vue +0 -27
- /package/components/vio/icon/{IconChatSolid.vue → VioIconChatSolid.vue} +0 -0
- /package/components/vio/icon/{IconCheckCircle.vue → VioIconCheckCircle.vue} +0 -0
- /package/components/vio/icon/{IconContainer.vue → VioIconContainer.vue} +0 -0
- /package/components/vio/icon/{IconExclamationCircle.vue → VioIconExclamationCircle.vue} +0 -0
- /package/components/vio/icon/{IconHourglass.vue → VioIconHourglass.vue} +0 -0
- /package/components/vio/icon/{IconMixcloud.vue → VioIconMixcloud.vue} +0 -0
- /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
|
})
|
@@ -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,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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
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
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@dargmuesli/nuxt-vio",
|
3
|
-
"version": "3.
|
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
|
+
}
|
package/utils/constants.ts
CHANGED
@@ -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,
|
package/utils/networking.ts
CHANGED
@@ -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>
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|