@dargmuesli/nuxt-vio 2.0.1 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.config.ts +78 -34
- package/components/{VioApp.vue → vio/_/VioApp.vue} +31 -8
- package/components/{VioError.vue → vio/_/VioError.vue} +1 -1
- package/components/vio/button/VioButtonColored.vue +52 -0
- package/components/vio/card/VioCard.vue +19 -0
- package/components/vio/card/state/VioCardState.vue +20 -0
- package/components/vio/card/state/VioCardStateAlert.vue +14 -0
- package/components/vio/form/VioForm.vue +84 -0
- package/components/vio/form/VioFormCheckbox.vue +27 -0
- package/components/vio/form/input/VioFormInput.vue +192 -0
- package/components/vio/form/input/VioFormInputIconWrapper.vue +7 -0
- package/components/vio/form/input/VioFormInputUrl.vue +54 -0
- package/components/vio/form/input/state/VioFormInputState.vue +5 -0
- package/components/vio/form/input/state/VioFormInputStateError.vue +32 -0
- package/components/vio/form/input/state/VioFormInputStateInfo.vue +32 -0
- package/components/vio/icon/IconArrowRight.vue +31 -0
- package/components/vio/icon/IconCalendar.vue +31 -0
- package/components/vio/icon/IconChatOutline.vue +27 -0
- package/components/vio/icon/IconChatSolid.vue +26 -0
- package/components/vio/icon/IconCheckCircle.vue +29 -0
- package/components/vio/icon/IconContainer.vue +15 -0
- package/components/vio/icon/IconDownload.vue +31 -0
- package/components/vio/icon/IconExclamationCircle.vue +29 -0
- package/components/vio/icon/IconHome.vue +31 -0
- package/components/vio/icon/IconHourglass.vue +32 -0
- package/components/vio/icon/IconLightbulb.vue +27 -0
- package/components/vio/icon/IconLogo.vue +17 -0
- package/components/vio/icon/IconMixcloud.vue +23 -0
- package/components/vio/icon/IconMusic.vue +27 -0
- package/components/vio/icon/IconPlay.vue +25 -0
- package/components/vio/icon/IconShare.vue +27 -0
- package/components/{VioLayout.vue → vio/layout/VioLayout.vue} +1 -1
- package/components/vio/layout/VioLayoutBreadcrumbs.vue +83 -0
- package/components/vio/layout/VioLayoutFooter.vue +40 -0
- package/components/vio/layout/VioLayoutFooterCategory.vue +17 -0
- package/components/vio/layout/VioLayoutHeader.vue +98 -0
- package/components/vio/layout/VioLayoutSpanList.vue +20 -0
- package/components/vio/loader/indicator/VioLoaderIndicator.vue +14 -0
- package/components/vio/loader/indicator/VioLoaderIndicatorPing.vue +12 -0
- package/components/vio/loader/indicator/VioLoaderIndicatorSpinner.vue +24 -0
- package/components/{VioLegalNotice.vue → vio/page/VioPageLegalNotice.vue} +1 -1
- package/components/{VioPrivacyPolicy.vue → vio/page/VioPagePrivacyPolicy.vue} +1 -1
- package/composables/useAppLayout.ts +2 -2
- package/composables/useDateTime.ts +17 -0
- package/composables/useFavicons.ts +2 -2
- package/composables/useFireError.ts +17 -0
- package/composables/useGetServiceHref.ts +47 -0
- package/composables/useHeadDefault.ts +34 -0
- package/composables/useStrapiFetch.ts +10 -0
- package/error.vue +5 -2
- package/locales/de.json +7 -1
- package/locales/en.json +7 -1
- package/nuxt.config.ts +38 -2
- package/package.json +23 -10
- package/pages/legal-notice.vue +1 -1
- package/pages/privacy-policy.vue +1 -1
- package/plugins/dayjs.ts +34 -0
- package/plugins/i18n.ts +5 -0
- package/plugins/marked.ts +9 -0
- package/server/middleware/headers.ts +41 -10
- package/tailwind.config.ts +129 -8
- package/utils/constants.ts +9 -1
- package/utils/form.ts +19 -0
- package/utils/networking.ts +82 -0
- package/utils/text.ts +20 -0
- /package/components/{VioLink.vue → vio/_/VioLink.vue} +0 -0
- /package/components/{VioButton.vue → vio/button/VioButton.vue} +0 -0
- /package/components/{VioHr.vue → vio/layout/VioLayoutHr.vue} +0 -0
@@ -1,13 +1,44 @@
|
|
1
|
-
import { defineEventHandler } from 'h3'
|
1
|
+
import { appendHeader, defineEventHandler } from 'h3'
|
2
|
+
import type { H3Event } from 'h3'
|
3
|
+
import { AppConfig } from 'nuxt/schema'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
+
import { TIMEZONE_HEADER_KEY } from '../../utils/constants'
|
6
|
+
import { getTimezone } from '../../utils/networking'
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
//
|
9
|
-
|
10
|
-
// 'Report-To',
|
11
|
-
// '{"group":"default","max_age":31536000,"endpoints":[{"url":"https://dargmuesli.report-uri.com/a/d/g"}],"include_subdomains":true}'
|
12
|
-
// )
|
8
|
+
export default defineEventHandler(async (event) => {
|
9
|
+
setRequestHeader(event, TIMEZONE_HEADER_KEY, await getTimezone(event))
|
10
|
+
// setContentSecurityPolicy(event);
|
11
|
+
setResponseHeaders(event)
|
13
12
|
})
|
13
|
+
|
14
|
+
// const setContentSecurityPolicy = (event: H3Event) => {
|
15
|
+
// const config = useAppConfig();
|
16
|
+
|
17
|
+
// appendHeader(
|
18
|
+
// event,
|
19
|
+
// "Content-Security-Policy",
|
20
|
+
// getCspAsString(config.public.vio.server.middleware.headers.csp)
|
21
|
+
// );
|
22
|
+
// };
|
23
|
+
|
24
|
+
const setRequestHeader = (event: H3Event, name: string, value: string) => {
|
25
|
+
event.node.req.headers[name] = value
|
26
|
+
}
|
27
|
+
|
28
|
+
const setResponseHeaders = (event: H3Event) => {
|
29
|
+
const config = useAppConfig() as AppConfig
|
30
|
+
|
31
|
+
for (const entry of Object.entries(
|
32
|
+
config.vio.server.middleware.headers.csp.default,
|
33
|
+
)) {
|
34
|
+
appendHeader(event, entry[0], entry[1])
|
35
|
+
}
|
36
|
+
|
37
|
+
if (process.env.NODE_ENV === 'production') {
|
38
|
+
for (const entry of Object.entries(
|
39
|
+
config.vio.server.middleware.headers.csp.production,
|
40
|
+
)) {
|
41
|
+
appendHeader(event, entry[0], entry[1])
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
package/tailwind.config.ts
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
+
import { Config } from 'tailwindcss'
|
1
2
|
import colors from 'tailwindcss/colors'
|
2
3
|
import { PluginAPI } from 'tailwindcss/types/config'
|
4
|
+
import formsPlugin from '@tailwindcss/forms'
|
3
5
|
import typographyPlugin from '@tailwindcss/typography'
|
4
6
|
|
5
|
-
const heading = (theme: PluginAPI['theme']) =>
|
6
|
-
(
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
const heading = (theme: PluginAPI['theme']): Record<string, string> => ({
|
8
|
+
fontWeight: theme('fontWeight.bold'),
|
9
|
+
// marginBottom: theme('margin.1'),
|
10
|
+
// marginTop: theme('margin.4'),
|
11
|
+
// set overflow truncate/ellipsis in the surrounding container, or larger fonts will be cut off due to their line-heights
|
12
|
+
})
|
11
13
|
|
12
14
|
const gray = colors.gray // or slate, zinc, neutral, stone
|
13
15
|
|
@@ -39,14 +41,42 @@ const prose = (theme: PluginAPI['theme']) => ({
|
|
39
41
|
})
|
40
42
|
|
41
43
|
export default {
|
44
|
+
content: [
|
45
|
+
'./components/**/*.{js,vue,ts}',
|
46
|
+
'./composables/**/*.{js,vue,ts}',
|
47
|
+
'./layouts/**/*.vue',
|
48
|
+
'./pages/**/*.vue',
|
49
|
+
'./plugins/**/*.{js,ts}',
|
50
|
+
// './nuxt.config.{js,ts}', // Does not work with i18n as of 2022-12-01
|
51
|
+
'./app.vue',
|
52
|
+
],
|
42
53
|
darkMode: 'class',
|
43
54
|
plugins: [
|
55
|
+
formsPlugin,
|
44
56
|
typographyPlugin,
|
45
|
-
({ addBase, addComponents, theme }: PluginAPI) => {
|
57
|
+
({ addBase, addComponents, addUtilities, theme }: PluginAPI) => {
|
46
58
|
addBase({
|
59
|
+
':disabled': {
|
60
|
+
cursor: theme('cursor.not-allowed'),
|
61
|
+
opacity: theme('opacity.50'),
|
62
|
+
},
|
63
|
+
'a[target="_blank"]:after': {
|
64
|
+
backgroundColor: 'currentColor',
|
65
|
+
content: '""',
|
66
|
+
display: 'inline-table', // inline-table centers the element vertically in the tiptap text area, instead of inline-block
|
67
|
+
mask: 'url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJhcnJvdy11cC1yaWdodC1mcm9tLXNxdWFyZSIgY2xhc3M9InN2Zy1pbmxpbmUtLWZhIGZhLWFycm93LXVwLXJpZ2h0LWZyb20tc3F1YXJlIiByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIgZD0iTTM4NCAzMjBjLTE3LjY3IDAtMzIgMTQuMzMtMzIgMzJ2OTZINjRWMTYwaDk2YzE3LjY3IDAgMzItMTQuMzIgMzItMzJzLTE0LjMzLTMyLTMyLTMyTDY0IDk2Yy0zNS4zNSAwLTY0IDI4LjY1LTY0IDY0VjQ0OGMwIDM1LjM0IDI4LjY1IDY0IDY0IDY0aDI4OGMzNS4zNSAwIDY0LTI4LjY2IDY0LTY0di05NkM0MTYgMzM0LjMgNDAxLjcgMzIwIDM4NCAzMjB6TTUwMi42IDkuMzY3QzQ5Ni44IDMuNTc4IDQ4OC44IDAgNDgwIDBoLTE2MGMtMTcuNjcgMC0zMS4xIDE0LjMyLTMxLjEgMzEuMWMwIDE3LjY3IDE0LjMyIDMxLjEgMzEuOTkgMzEuMWg4Mi43NUwxNzguNyAyOTAuN2MtMTIuNSAxMi41LTEyLjUgMzIuNzYgMCA0NS4yNkMxOTEuMiAzNDguNSAyMTEuNSAzNDguNSAyMjQgMzM2bDIyNC0yMjYuOFYxOTJjMCAxNy42NyAxNC4zMyAzMS4xIDMxLjEgMzEuMVM1MTIgMjA5LjcgNTEyIDE5MlYzMS4xQzUxMiAyMy4xNiA1MDguNCAxNS4xNiA1MDIuNiA5LjM2N3oiPjwvcGF0aD48L3N2Zz4K) no-repeat 50% 50%',
|
68
|
+
maskSize: 'cover',
|
69
|
+
height: theme('fontSize.xs'),
|
70
|
+
marginLeft: '5px',
|
71
|
+
width: theme('fontSize.xs'),
|
72
|
+
},
|
73
|
+
address: {
|
74
|
+
margin: theme('margin.4'),
|
75
|
+
},
|
47
76
|
h1: {
|
48
77
|
...heading(theme),
|
49
78
|
fontSize: theme('fontSize.4xl'),
|
79
|
+
// marginBottom: theme('margin.4'),
|
50
80
|
textAlign: 'center',
|
51
81
|
},
|
52
82
|
h2: {
|
@@ -70,14 +100,77 @@ export default {
|
|
70
100
|
},
|
71
101
|
})
|
72
102
|
addComponents({
|
103
|
+
'::placeholder': {
|
104
|
+
fontStyle: 'italic',
|
105
|
+
'.form-input&,.form-textarea&': {
|
106
|
+
opacity: '0.5',
|
107
|
+
},
|
108
|
+
},
|
109
|
+
'.form-input': {
|
110
|
+
appearance: 'none',
|
111
|
+
backgroundColor: theme('colors.gray.50'),
|
112
|
+
borderColor: theme('colors.gray.300'),
|
113
|
+
borderRadius: theme('borderRadius.DEFAULT'),
|
114
|
+
borderWidth: theme('borderWidth.DEFAULT'),
|
115
|
+
boxShadow: theme('boxShadow.sm'),
|
116
|
+
color: theme('colors.text.dark'),
|
117
|
+
lineHeight: theme('lineHeight.tight'),
|
118
|
+
padding: theme('padding.2') + ' ' + theme('padding.4'),
|
119
|
+
width: theme('width.full'),
|
120
|
+
'&:focus': {
|
121
|
+
backgroundColor: theme('colors.white'),
|
122
|
+
},
|
123
|
+
},
|
124
|
+
'.form-input-error': {
|
125
|
+
input: {
|
126
|
+
borderColor: theme('colors.red.500'),
|
127
|
+
},
|
128
|
+
},
|
129
|
+
'.form-input-success': {
|
130
|
+
input: {
|
131
|
+
borderColor: theme('colors.green.600'),
|
132
|
+
},
|
133
|
+
},
|
134
|
+
'.form-input-warning': {
|
135
|
+
input: {
|
136
|
+
borderColor: theme('colors.yellow.600'),
|
137
|
+
},
|
138
|
+
},
|
139
|
+
'.fullscreen': {
|
140
|
+
bottom: '0',
|
141
|
+
height: theme('height.full'),
|
142
|
+
left: '0',
|
143
|
+
position: 'absolute',
|
144
|
+
right: '0',
|
145
|
+
top: '0',
|
146
|
+
width: theme('width.full'),
|
147
|
+
},
|
73
148
|
'.object-position-custom': {
|
74
149
|
objectPosition: '50% 30%',
|
75
150
|
},
|
76
151
|
})
|
152
|
+
addUtilities({
|
153
|
+
'.disabled': {
|
154
|
+
cursor: theme('cursor.not-allowed'),
|
155
|
+
opacity: theme('opacity.50'),
|
156
|
+
},
|
157
|
+
'.max-w-xxs': {
|
158
|
+
maxWidth: '15rem',
|
159
|
+
},
|
160
|
+
'.min-w-xxs': {
|
161
|
+
minWidth: '15rem',
|
162
|
+
},
|
163
|
+
'.mb-20vh': {
|
164
|
+
marginBottom: '20vh',
|
165
|
+
},
|
166
|
+
})
|
77
167
|
},
|
78
168
|
],
|
79
169
|
theme: {
|
80
170
|
extend: {
|
171
|
+
animation: {
|
172
|
+
shake: 'shake 0.6s ease-in-out 0s 1 normal forwards running',
|
173
|
+
},
|
81
174
|
colors: {
|
82
175
|
background: {
|
83
176
|
bright: colors.white,
|
@@ -94,6 +187,34 @@ export default {
|
|
94
187
|
dark: gray['900'],
|
95
188
|
},
|
96
189
|
},
|
190
|
+
keyframes: {
|
191
|
+
shake: {
|
192
|
+
'0%': {
|
193
|
+
transform: 'translateX(0)',
|
194
|
+
},
|
195
|
+
'15%': {
|
196
|
+
transform: 'translateX(0.375rem)',
|
197
|
+
},
|
198
|
+
'30%': {
|
199
|
+
transform: 'translateX(-0.375rem)',
|
200
|
+
},
|
201
|
+
'45%': {
|
202
|
+
transform: 'translateX(0.375rem)',
|
203
|
+
},
|
204
|
+
'60%': {
|
205
|
+
transform: 'translateX(-0.375rem)',
|
206
|
+
},
|
207
|
+
'75%': {
|
208
|
+
transform: 'translateX(0.375rem)',
|
209
|
+
},
|
210
|
+
'90%': {
|
211
|
+
transform: 'translateX(-0.375rem)',
|
212
|
+
},
|
213
|
+
'100%': {
|
214
|
+
transform: 'translateX(0)',
|
215
|
+
},
|
216
|
+
},
|
217
|
+
},
|
97
218
|
screens: {
|
98
219
|
12: { raw: '(min-aspect-ratio: 2/1)' },
|
99
220
|
},
|
@@ -107,4 +228,4 @@ export default {
|
|
107
228
|
}),
|
108
229
|
},
|
109
230
|
},
|
110
|
-
}
|
231
|
+
} as Config
|
package/utils/constants.ts
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
export const SITE_NAME = 'Vio'
|
2
|
+
|
3
|
+
export const COOKIE_PREFIX = SITE_NAME.toLocaleLowerCase()
|
4
|
+
export const COOKIE_SEPARATOR = '_'
|
5
|
+
export const FETCH_RETRY_AMOUNT = 3
|
6
|
+
export const I18N_COOKIE_NAME = 'i18n_r'
|
1
7
|
export const I18N_MODULE_CONFIG = {
|
2
8
|
langDir: 'locales',
|
3
9
|
lazy: true,
|
@@ -20,4 +26,6 @@ export const I18N_VUE_CONFIG = {
|
|
20
26
|
fallbackWarn: false, // covered by linting
|
21
27
|
missingWarn: false, // covered by linting
|
22
28
|
}
|
23
|
-
export const
|
29
|
+
export const TIMEZONE_COOKIE_NAME = [COOKIE_PREFIX, 'tz'].join(COOKIE_SEPARATOR)
|
30
|
+
export const TIMEZONE_HEADER_KEY = `X-${SITE_NAME}-Timezone`
|
31
|
+
export const VALIDATION_SUGGESTION_TITLE_LENGTH_MAXIMUM = 300
|
package/utils/form.ts
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
import type { ApiData } from '../types/api'
|
2
|
+
|
3
|
+
export const formPreSubmit = async (
|
4
|
+
api: ApiData,
|
5
|
+
v$: any,
|
6
|
+
isFormSent: Ref<boolean>,
|
7
|
+
): Promise<boolean> => {
|
8
|
+
api.value.errors = []
|
9
|
+
v$.value.$touch()
|
10
|
+
|
11
|
+
const isFormValid = await v$.value.$validate()
|
12
|
+
isFormSent.value = isFormValid
|
13
|
+
|
14
|
+
if (!isFormValid) {
|
15
|
+
throw new Error('Form is invalid!')
|
16
|
+
}
|
17
|
+
|
18
|
+
return isFormValid
|
19
|
+
}
|
package/utils/networking.ts
CHANGED
@@ -1,7 +1,89 @@
|
|
1
1
|
import { IncomingMessage } from 'node:http'
|
2
2
|
|
3
|
+
import { CombinedError } from '@urql/core'
|
4
|
+
import { H3Event, getCookie } from 'h3'
|
5
|
+
|
6
|
+
import { ofetch } from 'ofetch'
|
7
|
+
import { Ref } from 'vue'
|
8
|
+
|
9
|
+
import type { BackendError } from '../types/api'
|
10
|
+
import { TIMEZONE_COOKIE_NAME } from './constants'
|
11
|
+
|
12
|
+
export const getApiMeta = (
|
13
|
+
queries?: {
|
14
|
+
error: Ref<CombinedError | undefined>
|
15
|
+
fetching: Ref<boolean>
|
16
|
+
}[],
|
17
|
+
) => ({
|
18
|
+
errors: queries
|
19
|
+
? queries.reduce((p, c) => {
|
20
|
+
if (c.error.value) {
|
21
|
+
return [...p, c.error.value]
|
22
|
+
} else {
|
23
|
+
return p
|
24
|
+
}
|
25
|
+
}, [] as BackendError[])
|
26
|
+
: [],
|
27
|
+
isFetching: queries
|
28
|
+
? queries.reduce((p, c) => p || c.fetching.value, false)
|
29
|
+
: false,
|
30
|
+
})
|
31
|
+
|
32
|
+
export const getCombinedErrorMessages = (
|
33
|
+
errors: BackendError[],
|
34
|
+
pgIds?: Record<string, string>,
|
35
|
+
) => {
|
36
|
+
const errorMessages: string[] = []
|
37
|
+
|
38
|
+
for (const error of errors) {
|
39
|
+
if ('errcode' in error) {
|
40
|
+
const translation = pgIds && pgIds[`postgres${error.errcode}`]
|
41
|
+
|
42
|
+
if (translation) {
|
43
|
+
errorMessages.push(translation)
|
44
|
+
} else {
|
45
|
+
errorMessages.push(error.message)
|
46
|
+
}
|
47
|
+
} else {
|
48
|
+
const combinedError = error
|
49
|
+
|
50
|
+
if (combinedError.networkError) {
|
51
|
+
errorMessages.push(combinedError.message)
|
52
|
+
}
|
53
|
+
|
54
|
+
for (const graphqlError of combinedError.graphQLErrors) {
|
55
|
+
errorMessages.push(graphqlError.message)
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
return errorMessages
|
61
|
+
}
|
62
|
+
|
63
|
+
export const getCspAsString = (csp = {} as Record<string, Array<string>>) =>
|
64
|
+
Object.keys(csp).reduce((p, c) => (p += `${c} ${csp[c].join(' ')};`), '')
|
65
|
+
|
66
|
+
export const getDomainTldPort = (host: string) => {
|
67
|
+
const hostParts = host.split('.')
|
68
|
+
|
69
|
+
if (/^localhost(:[0-9]+)?$/.test(hostParts[hostParts.length - 1]))
|
70
|
+
return hostParts[hostParts.length - 1]
|
71
|
+
|
72
|
+
if (hostParts.length === 1) throw new Error('Host is too short!')
|
73
|
+
|
74
|
+
return `${hostParts[hostParts.length - 2]}.${hostParts[hostParts.length - 1]}`
|
75
|
+
}
|
76
|
+
|
3
77
|
export const getHost = (req: IncomingMessage) => {
|
4
78
|
if (!req.headers.host) throw new Error('Host header is not given!')
|
5
79
|
|
6
80
|
return req.headers.host
|
7
81
|
}
|
82
|
+
|
83
|
+
export const getTimezone = async (event: H3Event) =>
|
84
|
+
getCookie(event, TIMEZONE_COOKIE_NAME) ||
|
85
|
+
(
|
86
|
+
await ofetch(
|
87
|
+
`http://ip-api.com/json/${event.node.req.headers['x-real-ip']}`,
|
88
|
+
)
|
89
|
+
).timezone
|
package/utils/text.ts
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
import Clipboard from 'clipboard'
|
2
|
+
|
3
|
+
export const copyText = (text: string) =>
|
4
|
+
new Promise((resolve, reject) => {
|
5
|
+
const fakeElement = document.createElement('button')
|
6
|
+
const clipboard = new Clipboard(fakeElement, {
|
7
|
+
text: () => text,
|
8
|
+
action: () => 'copy',
|
9
|
+
container: document.body,
|
10
|
+
})
|
11
|
+
clipboard.on('success', (e) => {
|
12
|
+
clipboard.destroy()
|
13
|
+
resolve(e)
|
14
|
+
})
|
15
|
+
clipboard.on('error', (e) => {
|
16
|
+
clipboard.destroy()
|
17
|
+
reject(e)
|
18
|
+
})
|
19
|
+
fakeElement.click()
|
20
|
+
})
|
File without changes
|
File without changes
|
File without changes
|