@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.
- 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
|