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

Sign up to get free protection for your applications and to get access to all the features.
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