@dargmuesli/nuxt-vio 2.0.1 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/app.config.ts +78 -34
  2. package/components/{VioApp.vue → _/VioApp.vue} +23 -5
  3. package/components/button/VioButtonColored.vue +52 -0
  4. package/components/card/VioCard.vue +19 -0
  5. package/components/card/state/VioCardState.vue +20 -0
  6. package/components/card/state/VioCardStateAlert.vue +14 -0
  7. package/components/form/VioForm.vue +84 -0
  8. package/components/form/VioFormCheckbox.vue +27 -0
  9. package/components/form/input/VioFormInput.vue +192 -0
  10. package/components/form/input/VioFormInputIconWrapper.vue +7 -0
  11. package/components/form/input/VioFormInputUrl.vue +54 -0
  12. package/components/form/input/state/VioFormInputState.vue +5 -0
  13. package/components/form/input/state/VioFormInputStateError.vue +32 -0
  14. package/components/form/input/state/VioFormInputStateInfo.vue +32 -0
  15. package/components/icon/IconArrowRight.vue +31 -0
  16. package/components/icon/IconCalendar.vue +31 -0
  17. package/components/icon/IconChatOutline.vue +27 -0
  18. package/components/icon/IconChatSolid.vue +26 -0
  19. package/components/icon/IconCheckCircle.vue +29 -0
  20. package/components/icon/IconContainer.vue +15 -0
  21. package/components/icon/IconDownload.vue +31 -0
  22. package/components/icon/IconExclamationCircle.vue +29 -0
  23. package/components/icon/IconHome.vue +31 -0
  24. package/components/icon/IconHourglass.vue +32 -0
  25. package/components/icon/IconLightbulb.vue +27 -0
  26. package/components/icon/IconLogo.vue +17 -0
  27. package/components/icon/IconMixcloud.vue +23 -0
  28. package/components/icon/IconMusic.vue +27 -0
  29. package/components/icon/IconPlay.vue +25 -0
  30. package/components/icon/IconShare.vue +27 -0
  31. package/components/layout/VioLayoutBreadcrumbs.vue +83 -0
  32. package/components/layout/VioLayoutFooter.vue +40 -0
  33. package/components/layout/VioLayoutFooterCategory.vue +17 -0
  34. package/components/layout/VioLayoutHeader.vue +98 -0
  35. package/components/layout/VioLayoutSpanList.vue +20 -0
  36. package/components/loader/indicator/VioLoaderIndicator.vue +14 -0
  37. package/components/loader/indicator/VioLoaderIndicatorPing.vue +12 -0
  38. package/components/loader/indicator/VioLoaderIndicatorSpinner.vue +24 -0
  39. package/components/{VioLegalNotice.vue → page/VioPageLegalNotice.vue} +1 -1
  40. package/components/{VioPrivacyPolicy.vue → page/VioPagePrivacyPolicy.vue} +1 -1
  41. package/composables/useAppLayout.ts +2 -2
  42. package/composables/useDateTime.ts +17 -0
  43. package/composables/useFavicons.ts +2 -2
  44. package/composables/useFireError.ts +17 -0
  45. package/composables/useGetServiceHref.ts +47 -0
  46. package/composables/useHeadDefault.ts +34 -0
  47. package/composables/useHeadLayout.ts +67 -0
  48. package/composables/useStrapiFetch.ts +10 -0
  49. package/error.vue +3 -2
  50. package/locales/de.json +7 -1
  51. package/locales/en.json +7 -1
  52. package/nuxt.config.ts +38 -2
  53. package/package.json +24 -10
  54. package/pages/legal-notice.vue +1 -1
  55. package/pages/privacy-policy.vue +1 -1
  56. package/plugins/dayjs.ts +46 -0
  57. package/plugins/i18n.ts +5 -0
  58. package/plugins/marked.ts +9 -0
  59. package/server/middleware/headers.ts +41 -10
  60. package/tailwind.config.ts +131 -8
  61. package/utils/constants.ts +9 -1
  62. package/utils/form.ts +19 -0
  63. package/utils/networking.ts +82 -0
  64. package/utils/text.ts +20 -0
  65. /package/components/{VioError.vue → _/VioError.vue} +0 -0
  66. /package/components/{VioLink.vue → _/VioLink.vue} +0 -0
  67. /package/components/{VioButton.vue → button/VioButton.vue} +0 -0
  68. /package/components/{VioLayout.vue → layout/VioLayout.vue} +0 -0
  69. /package/components/{VioHr.vue → layout/VioLayoutHr.vue} +0 -0
@@ -0,0 +1,46 @@
1
+ import dayjs, { extend, locale } from 'dayjs'
2
+
3
+ // workaround for [1]
4
+ import de from 'dayjs/locale/de'
5
+ // import 'dayjs/locale/de' does not make locale available
6
+
7
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
8
+ import localizedFormat from 'dayjs/plugin/localizedFormat'
9
+ import timezone from 'dayjs/plugin/timezone'
10
+ import utc from 'dayjs/plugin/utc'
11
+
12
+ export default defineNuxtPlugin((_nuxtApp) => {
13
+ extend(isSameOrBefore)
14
+ extend(localizedFormat)
15
+ extend(timezone)
16
+ extend(utc)
17
+
18
+ // workaround for [1]
19
+ locale(de)
20
+ // dayjs.locale(en) makes `format` error
21
+
22
+ return {
23
+ provide: {
24
+ dayjs,
25
+ },
26
+ }
27
+ })
28
+
29
+ // declare module '#app' {
30
+ // interface NuxtApp {
31
+ // $dayjs: DayjsFn
32
+ // }
33
+ // }
34
+
35
+ // declare module 'nuxt/dist/app/nuxt' {
36
+ // interface NuxtApp {
37
+ // $dayjs: DayjsFn
38
+ // }
39
+ // }
40
+
41
+ /*
42
+ [1]
43
+ https://github.com/nuxt/framework/issues/7534#issuecomment-1248596609
44
+ https://github.com/nuxt/framework/issues/7206
45
+ https://github.com/maevsi/maevsi/issues/956
46
+ */
@@ -0,0 +1,5 @@
1
+ export default defineNuxtPlugin((nuxtApp) => {
2
+ nuxtApp.hook('i18n:localeSwitched', ({ newLocale }) => {
3
+ nuxtApp.vueApp.$nuxt.$dayjs.locale(newLocale)
4
+ })
5
+ })
@@ -0,0 +1,9 @@
1
+ import { marked } from 'marked'
2
+
3
+ export default defineNuxtPlugin((_nuxtApp) => {
4
+ return {
5
+ provide: {
6
+ marked: marked.parse,
7
+ },
8
+ }
9
+ })
@@ -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
- export default defineEventHandler((event) => {
4
- const { res } = event.node
5
+ import { TIMEZONE_HEADER_KEY } from '../../utils/constants'
6
+ import { getTimezone } from '../../utils/networking'
5
7
 
6
- res.setHeader('Permissions-Policy', '')
7
-
8
- // // Disabled until there is better browser support (https://caniuse.com/?search=report-to)
9
- // res.setHeader(
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
+ }
@@ -1,13 +1,16 @@
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'
5
+ import lineClampPlugin from '@tailwindcss/line-clamp'
3
6
  import typographyPlugin from '@tailwindcss/typography'
4
7
 
5
- const heading = (theme: PluginAPI['theme']) =>
6
- ({
7
- fontWeight: theme('fontWeight.bold'),
8
- overflow: 'hidden',
9
- textOverflow: 'ellipsis',
10
- }) as Record<string, string>
8
+ const heading = (theme: PluginAPI['theme']): Record<string, string> => ({
9
+ fontWeight: theme('fontWeight.bold'),
10
+ // marginBottom: theme('margin.1'),
11
+ // marginTop: theme('margin.4'),
12
+ // set overflow truncate/ellipsis in the surrounding container, or larger fonts will be cut off due to their line-heights
13
+ })
11
14
 
12
15
  const gray = colors.gray // or slate, zinc, neutral, stone
13
16
 
@@ -39,14 +42,43 @@ const prose = (theme: PluginAPI['theme']) => ({
39
42
  })
40
43
 
41
44
  export default {
45
+ content: [
46
+ './components/**/*.{js,vue,ts}',
47
+ './composables/**/*.{js,vue,ts}',
48
+ './layouts/**/*.vue',
49
+ './pages/**/*.vue',
50
+ './plugins/**/*.{js,ts}',
51
+ // './nuxt.config.{js,ts}', // Does not work with i18n as of 2022-12-01
52
+ './app.vue',
53
+ ],
42
54
  darkMode: 'class',
43
55
  plugins: [
56
+ formsPlugin,
57
+ lineClampPlugin,
44
58
  typographyPlugin,
45
- ({ addBase, addComponents, theme }: PluginAPI) => {
59
+ ({ addBase, addComponents, addUtilities, theme }: PluginAPI) => {
46
60
  addBase({
61
+ ':disabled': {
62
+ cursor: theme('cursor.not-allowed'),
63
+ opacity: theme('opacity.50'),
64
+ },
65
+ 'a[target="_blank"]:after': {
66
+ backgroundColor: 'currentColor',
67
+ content: '""',
68
+ display: 'inline-table', // inline-table centers the element vertically in the tiptap text area, instead of inline-block
69
+ mask: 'url(data:image/svg+xml;base64,PHN2ZyBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhcyIgZGF0YS1pY29uPSJhcnJvdy11cC1yaWdodC1mcm9tLXNxdWFyZSIgY2xhc3M9InN2Zy1pbmxpbmUtLWZhIGZhLWFycm93LXVwLXJpZ2h0LWZyb20tc3F1YXJlIiByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIgZD0iTTM4NCAzMjBjLTE3LjY3IDAtMzIgMTQuMzMtMzIgMzJ2OTZINjRWMTYwaDk2YzE3LjY3IDAgMzItMTQuMzIgMzItMzJzLTE0LjMzLTMyLTMyLTMyTDY0IDk2Yy0zNS4zNSAwLTY0IDI4LjY1LTY0IDY0VjQ0OGMwIDM1LjM0IDI4LjY1IDY0IDY0IDY0aDI4OGMzNS4zNSAwIDY0LTI4LjY2IDY0LTY0di05NkM0MTYgMzM0LjMgNDAxLjcgMzIwIDM4NCAzMjB6TTUwMi42IDkuMzY3QzQ5Ni44IDMuNTc4IDQ4OC44IDAgNDgwIDBoLTE2MGMtMTcuNjcgMC0zMS4xIDE0LjMyLTMxLjEgMzEuMWMwIDE3LjY3IDE0LjMyIDMxLjEgMzEuOTkgMzEuMWg4Mi43NUwxNzguNyAyOTAuN2MtMTIuNSAxMi41LTEyLjUgMzIuNzYgMCA0NS4yNkMxOTEuMiAzNDguNSAyMTEuNSAzNDguNSAyMjQgMzM2bDIyNC0yMjYuOFYxOTJjMCAxNy42NyAxNC4zMyAzMS4xIDMxLjEgMzEuMVM1MTIgMjA5LjcgNTEyIDE5MlYzMS4xQzUxMiAyMy4xNiA1MDguNCAxNS4xNiA1MDIuNiA5LjM2N3oiPjwvcGF0aD48L3N2Zz4K) no-repeat 50% 50%',
70
+ maskSize: 'cover',
71
+ height: theme('fontSize.xs'),
72
+ marginLeft: '5px',
73
+ width: theme('fontSize.xs'),
74
+ },
75
+ address: {
76
+ margin: theme('margin.4'),
77
+ },
47
78
  h1: {
48
79
  ...heading(theme),
49
80
  fontSize: theme('fontSize.4xl'),
81
+ // marginBottom: theme('margin.4'),
50
82
  textAlign: 'center',
51
83
  },
52
84
  h2: {
@@ -70,14 +102,77 @@ export default {
70
102
  },
71
103
  })
72
104
  addComponents({
105
+ '::placeholder': {
106
+ fontStyle: 'italic',
107
+ '.form-input&,.form-textarea&': {
108
+ opacity: '0.5',
109
+ },
110
+ },
111
+ '.form-input': {
112
+ appearance: 'none',
113
+ backgroundColor: theme('colors.gray.50'),
114
+ borderColor: theme('colors.gray.300'),
115
+ borderRadius: theme('borderRadius.DEFAULT'),
116
+ borderWidth: theme('borderWidth.DEFAULT'),
117
+ boxShadow: theme('boxShadow.sm'),
118
+ color: theme('colors.text.dark'),
119
+ lineHeight: theme('lineHeight.tight'),
120
+ padding: theme('padding.2') + ' ' + theme('padding.4'),
121
+ width: theme('width.full'),
122
+ '&:focus': {
123
+ backgroundColor: theme('colors.white'),
124
+ },
125
+ },
126
+ '.form-input-error': {
127
+ input: {
128
+ borderColor: theme('colors.red.500'),
129
+ },
130
+ },
131
+ '.form-input-success': {
132
+ input: {
133
+ borderColor: theme('colors.green.600'),
134
+ },
135
+ },
136
+ '.form-input-warning': {
137
+ input: {
138
+ borderColor: theme('colors.yellow.600'),
139
+ },
140
+ },
141
+ '.fullscreen': {
142
+ bottom: '0',
143
+ height: theme('height.full'),
144
+ left: '0',
145
+ position: 'absolute',
146
+ right: '0',
147
+ top: '0',
148
+ width: theme('width.full'),
149
+ },
73
150
  '.object-position-custom': {
74
151
  objectPosition: '50% 30%',
75
152
  },
76
153
  })
154
+ addUtilities({
155
+ '.disabled': {
156
+ cursor: theme('cursor.not-allowed'),
157
+ opacity: theme('opacity.50'),
158
+ },
159
+ '.max-w-xxs': {
160
+ maxWidth: '15rem',
161
+ },
162
+ '.min-w-xxs': {
163
+ minWidth: '15rem',
164
+ },
165
+ '.mb-20vh': {
166
+ marginBottom: '20vh',
167
+ },
168
+ })
77
169
  },
78
170
  ],
79
171
  theme: {
80
172
  extend: {
173
+ animation: {
174
+ shake: 'shake 0.6s ease-in-out 0s 1 normal forwards running',
175
+ },
81
176
  colors: {
82
177
  background: {
83
178
  bright: colors.white,
@@ -94,6 +189,34 @@ export default {
94
189
  dark: gray['900'],
95
190
  },
96
191
  },
192
+ keyframes: {
193
+ shake: {
194
+ '0%': {
195
+ transform: 'translateX(0)',
196
+ },
197
+ '15%': {
198
+ transform: 'translateX(0.375rem)',
199
+ },
200
+ '30%': {
201
+ transform: 'translateX(-0.375rem)',
202
+ },
203
+ '45%': {
204
+ transform: 'translateX(0.375rem)',
205
+ },
206
+ '60%': {
207
+ transform: 'translateX(-0.375rem)',
208
+ },
209
+ '75%': {
210
+ transform: 'translateX(0.375rem)',
211
+ },
212
+ '90%': {
213
+ transform: 'translateX(-0.375rem)',
214
+ },
215
+ '100%': {
216
+ transform: 'translateX(0)',
217
+ },
218
+ },
219
+ },
97
220
  screens: {
98
221
  12: { raw: '(min-aspect-ratio: 2/1)' },
99
222
  },
@@ -107,4 +230,4 @@ export default {
107
230
  }),
108
231
  },
109
232
  },
110
- }
233
+ } as Config
@@ -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 SITE_NAME = 'Vio'
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
+ }
@@ -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