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

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