@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.
Files changed (68) hide show
  1. package/app.config.ts +78 -34
  2. package/components/{VioApp.vue → vio/_/VioApp.vue} +31 -8
  3. package/components/{VioError.vue → vio/_/VioError.vue} +1 -1
  4. package/components/vio/button/VioButtonColored.vue +52 -0
  5. package/components/vio/card/VioCard.vue +19 -0
  6. package/components/vio/card/state/VioCardState.vue +20 -0
  7. package/components/vio/card/state/VioCardStateAlert.vue +14 -0
  8. package/components/vio/form/VioForm.vue +84 -0
  9. package/components/vio/form/VioFormCheckbox.vue +27 -0
  10. package/components/vio/form/input/VioFormInput.vue +192 -0
  11. package/components/vio/form/input/VioFormInputIconWrapper.vue +7 -0
  12. package/components/vio/form/input/VioFormInputUrl.vue +54 -0
  13. package/components/vio/form/input/state/VioFormInputState.vue +5 -0
  14. package/components/vio/form/input/state/VioFormInputStateError.vue +32 -0
  15. package/components/vio/form/input/state/VioFormInputStateInfo.vue +32 -0
  16. package/components/vio/icon/IconArrowRight.vue +31 -0
  17. package/components/vio/icon/IconCalendar.vue +31 -0
  18. package/components/vio/icon/IconChatOutline.vue +27 -0
  19. package/components/vio/icon/IconChatSolid.vue +26 -0
  20. package/components/vio/icon/IconCheckCircle.vue +29 -0
  21. package/components/vio/icon/IconContainer.vue +15 -0
  22. package/components/vio/icon/IconDownload.vue +31 -0
  23. package/components/vio/icon/IconExclamationCircle.vue +29 -0
  24. package/components/vio/icon/IconHome.vue +31 -0
  25. package/components/vio/icon/IconHourglass.vue +32 -0
  26. package/components/vio/icon/IconLightbulb.vue +27 -0
  27. package/components/vio/icon/IconLogo.vue +17 -0
  28. package/components/vio/icon/IconMixcloud.vue +23 -0
  29. package/components/vio/icon/IconMusic.vue +27 -0
  30. package/components/vio/icon/IconPlay.vue +25 -0
  31. package/components/vio/icon/IconShare.vue +27 -0
  32. package/components/{VioLayout.vue → vio/layout/VioLayout.vue} +1 -1
  33. package/components/vio/layout/VioLayoutBreadcrumbs.vue +83 -0
  34. package/components/vio/layout/VioLayoutFooter.vue +40 -0
  35. package/components/vio/layout/VioLayoutFooterCategory.vue +17 -0
  36. package/components/vio/layout/VioLayoutHeader.vue +98 -0
  37. package/components/vio/layout/VioLayoutSpanList.vue +20 -0
  38. package/components/vio/loader/indicator/VioLoaderIndicator.vue +14 -0
  39. package/components/vio/loader/indicator/VioLoaderIndicatorPing.vue +12 -0
  40. package/components/vio/loader/indicator/VioLoaderIndicatorSpinner.vue +24 -0
  41. package/components/{VioLegalNotice.vue → vio/page/VioPageLegalNotice.vue} +1 -1
  42. package/components/{VioPrivacyPolicy.vue → vio/page/VioPagePrivacyPolicy.vue} +1 -1
  43. package/composables/useAppLayout.ts +2 -2
  44. package/composables/useDateTime.ts +17 -0
  45. package/composables/useFavicons.ts +2 -2
  46. package/composables/useFireError.ts +17 -0
  47. package/composables/useGetServiceHref.ts +47 -0
  48. package/composables/useHeadDefault.ts +34 -0
  49. package/composables/useStrapiFetch.ts +10 -0
  50. package/error.vue +5 -2
  51. package/locales/de.json +7 -1
  52. package/locales/en.json +7 -1
  53. package/nuxt.config.ts +38 -2
  54. package/package.json +23 -10
  55. package/pages/legal-notice.vue +1 -1
  56. package/pages/privacy-policy.vue +1 -1
  57. package/plugins/dayjs.ts +34 -0
  58. package/plugins/i18n.ts +5 -0
  59. package/plugins/marked.ts +9 -0
  60. package/server/middleware/headers.ts +41 -10
  61. package/tailwind.config.ts +129 -8
  62. package/utils/constants.ts +9 -1
  63. package/utils/form.ts +19 -0
  64. package/utils/networking.ts +82 -0
  65. package/utils/text.ts +20 -0
  66. /package/components/{VioLink.vue → vio/_/VioLink.vue} +0 -0
  67. /package/components/{VioButton.vue → vio/button/VioButton.vue} +0 -0
  68. /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
- 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,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
- fontWeight: theme('fontWeight.bold'),
8
- overflow: 'hidden',
9
- textOverflow: 'ellipsis',
10
- }) as Record<string, string>
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
@@ -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