@graphcommerce/ecommerce-ui 8.1.0-canary.8 → 9.0.0-canary.100
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/CHANGELOG.md +232 -37
- package/Config.graphqls +23 -0
- package/components/FormComponents/ActionCardListForm.tsx +88 -0
- package/components/FormComponents/AutoCompleteElement.tsx +101 -90
- package/components/FormComponents/CheckboxButtonGroup.tsx +27 -31
- package/components/FormComponents/CheckboxElement.tsx +56 -60
- package/components/FormComponents/EmailElement.tsx +27 -0
- package/components/FormComponents/MultiSelectElement.tsx +103 -110
- package/components/FormComponents/NumberFieldElement.tsx +9 -1
- package/components/FormComponents/RadioButtonGroup.tsx +27 -29
- package/components/FormComponents/SliderElement.tsx +28 -29
- package/components/FormComponents/SwitchElement.tsx +15 -13
- package/components/FormComponents/TelephoneElement.tsx +23 -0
- package/components/FormComponents/TextFieldElement.tsx +10 -5
- package/components/FormComponents/ToggleButtonGroup.tsx +49 -48
- package/components/FormComponents/index.ts +6 -3
- package/components/PreviewMode/LightTooltip.tsx +11 -0
- package/components/PreviewMode/PreviewMode.tsx +148 -0
- package/components/PreviewMode/PreviewModeActions.tsx +6 -0
- package/components/PreviewMode/PreviewModeToolbar.tsx +6 -0
- package/components/PreviewMode/index.ts +5 -0
- package/components/PreviewMode/previewModeDefaults.ts +5 -0
- package/components/PreviewMode/usePreviewModeForm.ts +6 -0
- package/components/index.ts +1 -0
- package/package.json +7 -7
- package/plugins/PreviewModeFramerNextPages.tsx +38 -0
- package/route/preview.ts +60 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Controller,
|
|
3
2
|
ControllerProps,
|
|
4
3
|
FieldError,
|
|
5
4
|
FieldValues,
|
|
5
|
+
useController,
|
|
6
6
|
} from '@graphcommerce/react-hook-form'
|
|
7
7
|
import { i18n } from '@lingui/core'
|
|
8
8
|
import {
|
|
@@ -16,8 +16,6 @@ import {
|
|
|
16
16
|
|
|
17
17
|
export type SliderElementProps<T extends FieldValues> = Omit<SliderProps, 'control'> & {
|
|
18
18
|
label?: string
|
|
19
|
-
/** @deprecated Form value parsing should happen in the handleSubmit function of the form */
|
|
20
|
-
parseError?: (error: FieldError) => string
|
|
21
19
|
required?: boolean
|
|
22
20
|
formControlProps?: FormControlProps
|
|
23
21
|
} & Omit<ControllerProps<T>, 'render'>
|
|
@@ -27,39 +25,40 @@ export function SliderElement<TFieldValues extends FieldValues>({
|
|
|
27
25
|
control,
|
|
28
26
|
label,
|
|
29
27
|
rules = {},
|
|
30
|
-
parseError,
|
|
31
28
|
required,
|
|
32
29
|
formControlProps,
|
|
30
|
+
defaultValue,
|
|
31
|
+
disabled,
|
|
32
|
+
shouldUnregister,
|
|
33
33
|
...other
|
|
34
34
|
}: SliderElementProps<TFieldValues>) {
|
|
35
35
|
if (required && !rules.required) {
|
|
36
36
|
rules.required = i18n._(/* i18n */ 'This field is required')
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
field,
|
|
41
|
+
fieldState: { invalid, error },
|
|
42
|
+
} = useController({
|
|
43
|
+
name,
|
|
44
|
+
control,
|
|
45
|
+
rules,
|
|
46
|
+
defaultValue,
|
|
47
|
+
disabled,
|
|
48
|
+
shouldUnregister,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const parsedHelperText = error ? error.message : null
|
|
52
|
+
|
|
38
53
|
return (
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: null
|
|
49
|
-
return (
|
|
50
|
-
<FormControl error={invalid} required={required} fullWidth {...formControlProps}>
|
|
51
|
-
{label && (
|
|
52
|
-
<FormLabel component='legend' error={invalid}>
|
|
53
|
-
{label}
|
|
54
|
-
</FormLabel>
|
|
55
|
-
)}
|
|
56
|
-
<Slider {...other} {...field} valueLabelDisplay={other.valueLabelDisplay || 'auto'} />
|
|
57
|
-
{parsedHelperText && (
|
|
58
|
-
<FormHelperText error={invalid}>{parsedHelperText}</FormHelperText>
|
|
59
|
-
)}
|
|
60
|
-
</FormControl>
|
|
61
|
-
)
|
|
62
|
-
}}
|
|
63
|
-
/>
|
|
54
|
+
<FormControl error={invalid} required={required} fullWidth {...formControlProps}>
|
|
55
|
+
{label && (
|
|
56
|
+
<FormLabel component='legend' error={invalid}>
|
|
57
|
+
{label}
|
|
58
|
+
</FormLabel>
|
|
59
|
+
)}
|
|
60
|
+
<Slider {...other} {...field} valueLabelDisplay={other.valueLabelDisplay || 'auto'} />
|
|
61
|
+
{parsedHelperText && <FormHelperText error={invalid}>{parsedHelperText}</FormHelperText>}
|
|
62
|
+
</FormControl>
|
|
64
63
|
)
|
|
65
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FieldValues, ControllerProps, useController } from '@graphcommerce/react-hook-form'
|
|
2
2
|
import { FormControlLabel, FormControlLabelProps, Switch } from '@mui/material'
|
|
3
3
|
|
|
4
4
|
type IProps = Omit<FormControlLabelProps, 'control'>
|
|
@@ -8,18 +8,20 @@ export type SwitchElementProps<T extends FieldValues> = IProps & Omit<Controller
|
|
|
8
8
|
export function SwitchElement<TFieldValues extends FieldValues>({
|
|
9
9
|
name,
|
|
10
10
|
control,
|
|
11
|
+
defaultValue,
|
|
12
|
+
disabled,
|
|
13
|
+
shouldUnregister,
|
|
14
|
+
rules,
|
|
11
15
|
...other
|
|
12
16
|
}: SwitchElementProps<TFieldValues>) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/>
|
|
24
|
-
)
|
|
17
|
+
const { field } = useController({
|
|
18
|
+
name,
|
|
19
|
+
control,
|
|
20
|
+
defaultValue,
|
|
21
|
+
disabled,
|
|
22
|
+
shouldUnregister,
|
|
23
|
+
rules,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return <FormControlLabel control={<Switch {...field} checked={!!field.value} />} {...other} />
|
|
25
27
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FieldValues, phonePattern } from '@graphcommerce/react-hook-form'
|
|
2
|
+
import { Trans, t } from '@lingui/macro'
|
|
3
|
+
import { TextFieldElement, TextFieldElementProps } from './TextFieldElement'
|
|
4
|
+
|
|
5
|
+
export type TelephoneElementProps<T extends FieldValues> = TextFieldElementProps<T>
|
|
6
|
+
|
|
7
|
+
export function TelephoneElement<TFieldValues extends FieldValues>(
|
|
8
|
+
props: TelephoneElementProps<TFieldValues>,
|
|
9
|
+
): JSX.Element {
|
|
10
|
+
const { rules, ...rest } = props
|
|
11
|
+
return (
|
|
12
|
+
<TextFieldElement
|
|
13
|
+
type='text'
|
|
14
|
+
label={<Trans>Telephone</Trans>}
|
|
15
|
+
autoComplete='tel'
|
|
16
|
+
rules={{
|
|
17
|
+
pattern: { value: phonePattern, message: t`Invalid phone number` },
|
|
18
|
+
...rules,
|
|
19
|
+
}}
|
|
20
|
+
{...rest}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import { InputCheckmark } from '@graphcommerce/next-ui'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
FieldValues,
|
|
5
|
+
UseControllerProps,
|
|
6
|
+
emailPattern,
|
|
7
|
+
useController,
|
|
8
|
+
} from '@graphcommerce/react-hook-form'
|
|
4
9
|
import { i18n } from '@lingui/core'
|
|
5
10
|
import { TextField, TextFieldProps } from '@mui/material'
|
|
6
11
|
|
|
@@ -22,7 +27,9 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
|
|
|
22
27
|
control,
|
|
23
28
|
defaultValue,
|
|
24
29
|
rules = validation,
|
|
30
|
+
shouldUnregister,
|
|
25
31
|
showValid,
|
|
32
|
+
disabled,
|
|
26
33
|
...rest
|
|
27
34
|
}: TextFieldElementProps<TFieldValues>): JSX.Element {
|
|
28
35
|
if (required && !rules.required) {
|
|
@@ -31,9 +38,7 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
|
|
|
31
38
|
|
|
32
39
|
if (type === 'email' && !rules.pattern) {
|
|
33
40
|
rules.pattern = {
|
|
34
|
-
|
|
35
|
-
value:
|
|
36
|
-
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
|
41
|
+
value: emailPattern,
|
|
37
42
|
message: i18n._(/* i18n */ 'Please enter a valid email address'),
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -41,7 +46,7 @@ export function TextFieldElement<TFieldValues extends FieldValues>({
|
|
|
41
46
|
const {
|
|
42
47
|
field: { onChange, ref, value = '', ...field },
|
|
43
48
|
fieldState: { error },
|
|
44
|
-
} = useController({ name, control, rules, defaultValue })
|
|
49
|
+
} = useController({ name, control, rules, defaultValue, shouldUnregister, disabled })
|
|
45
50
|
|
|
46
51
|
return (
|
|
47
52
|
<TextField
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Controller,
|
|
3
2
|
ControllerProps,
|
|
4
3
|
FieldError,
|
|
5
4
|
FieldValues,
|
|
5
|
+
useController,
|
|
6
6
|
} from '@graphcommerce/react-hook-form'
|
|
7
7
|
import { i18n } from '@lingui/core'
|
|
8
8
|
import {
|
|
@@ -25,8 +25,6 @@ type SingleToggleButtonProps = Omit<ToggleButtonProps, 'value' | 'children'> & {
|
|
|
25
25
|
export type ToggleButtonGroupElementProps<T extends FieldValues> = ToggleButtonGroupProps & {
|
|
26
26
|
required?: boolean
|
|
27
27
|
label?: string
|
|
28
|
-
/** @deprecated Form value parsing should happen in the handleSubmit function of the form */
|
|
29
|
-
parseError?: (error: FieldError) => string
|
|
30
28
|
options: SingleToggleButtonProps[]
|
|
31
29
|
formLabelProps?: FormLabelProps
|
|
32
30
|
helperText?: string
|
|
@@ -39,9 +37,11 @@ export function ToggleButtonGroupElement<TFieldValues extends FieldValues = Fiel
|
|
|
39
37
|
rules = {},
|
|
40
38
|
required,
|
|
41
39
|
options = [],
|
|
42
|
-
parseError,
|
|
43
40
|
helperText,
|
|
44
41
|
formLabelProps,
|
|
42
|
+
defaultValue,
|
|
43
|
+
disabled,
|
|
44
|
+
shouldUnregister,
|
|
45
45
|
...toggleButtonGroupProps
|
|
46
46
|
}: ToggleButtonGroupElementProps<TFieldValues>) {
|
|
47
47
|
if (required && !rules.required) {
|
|
@@ -49,50 +49,51 @@ export function ToggleButtonGroupElement<TFieldValues extends FieldValues = Fiel
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const isRequired = required || !!rules?.required
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
field: { value, onChange, onBlur },
|
|
55
|
+
fieldState: { invalid, error },
|
|
56
|
+
} = useController({
|
|
57
|
+
name,
|
|
58
|
+
control,
|
|
59
|
+
rules,
|
|
60
|
+
defaultValue,
|
|
61
|
+
disabled,
|
|
62
|
+
shouldUnregister,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const renderHelperText = error ? error.message : helperText
|
|
66
|
+
|
|
52
67
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
86
|
-
{options.map(({ label, id, ...toggleProps }) => (
|
|
87
|
-
<ToggleButton value={id} {...toggleProps} key={id}>
|
|
88
|
-
{label}
|
|
89
|
-
</ToggleButton>
|
|
90
|
-
))}
|
|
91
|
-
</ToggleButtonGroup>
|
|
92
|
-
{renderHelperText && <FormHelperText>{renderHelperText}</FormHelperText>}
|
|
93
|
-
</FormControl>
|
|
94
|
-
)
|
|
95
|
-
}}
|
|
96
|
-
/>
|
|
68
|
+
<FormControl error={invalid} required={isRequired}>
|
|
69
|
+
{label && (
|
|
70
|
+
<FormLabel
|
|
71
|
+
{...formLabelProps}
|
|
72
|
+
error={invalid}
|
|
73
|
+
required={isRequired}
|
|
74
|
+
sx={{ mb: 1, ...formLabelProps?.sx }}
|
|
75
|
+
>
|
|
76
|
+
{label}
|
|
77
|
+
</FormLabel>
|
|
78
|
+
)}
|
|
79
|
+
<ToggleButtonGroup
|
|
80
|
+
{...toggleButtonGroupProps}
|
|
81
|
+
value={value}
|
|
82
|
+
onBlur={onBlur}
|
|
83
|
+
onChange={(event, val) => {
|
|
84
|
+
onChange(val)
|
|
85
|
+
if (typeof toggleButtonGroupProps.onChange === 'function') {
|
|
86
|
+
toggleButtonGroupProps.onChange(event, val)
|
|
87
|
+
}
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{options.map(({ label, id, ...toggleProps }) => (
|
|
91
|
+
<ToggleButton value={id} {...toggleProps} key={id}>
|
|
92
|
+
{label}
|
|
93
|
+
</ToggleButton>
|
|
94
|
+
))}
|
|
95
|
+
</ToggleButtonGroup>
|
|
96
|
+
{renderHelperText && <FormHelperText>{renderHelperText}</FormHelperText>}
|
|
97
|
+
</FormControl>
|
|
97
98
|
)
|
|
98
99
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
export * from './ActionCardListForm'
|
|
1
2
|
export * from './AutoCompleteElement'
|
|
2
3
|
export * from './CheckboxButtonGroup'
|
|
3
4
|
export * from './CheckboxElement'
|
|
5
|
+
export * from './EmailElement'
|
|
4
6
|
export * from './MultiSelectElement'
|
|
7
|
+
export * from './NumberFieldElement'
|
|
5
8
|
export * from './PasswordElement'
|
|
6
9
|
export * from './PasswordRepeatElement'
|
|
7
|
-
export * from './NumberFieldElement'
|
|
8
|
-
export * from './SliderElement'
|
|
9
|
-
export * from './SwitchElement'
|
|
10
10
|
export * from './RadioButtonGroup'
|
|
11
11
|
export * from './SelectElement'
|
|
12
|
+
export * from './SliderElement'
|
|
13
|
+
export * from './SwitchElement'
|
|
14
|
+
export * from './TelephoneElement'
|
|
12
15
|
export * from './TextFieldElement'
|
|
13
16
|
export * from './ToggleButtonGroup'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { styled, Tooltip, tooltipClasses } from '@mui/material'
|
|
2
|
+
|
|
3
|
+
export const LightTooltip = styled<typeof Tooltip>(({ className, ...props }) => (
|
|
4
|
+
<Tooltip {...props} classes={{ popper: className }} />
|
|
5
|
+
))(({ theme }) => ({
|
|
6
|
+
[`& .${tooltipClasses.tooltip}`]: {
|
|
7
|
+
backgroundColor: theme.palette.common.white,
|
|
8
|
+
color: theme.palette.text.primary,
|
|
9
|
+
boxShadow: theme.shadows[1],
|
|
10
|
+
},
|
|
11
|
+
}))
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { PreviewData } from '@graphcommerce/graphql'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
IconSvg,
|
|
5
|
+
MessageSnackbar,
|
|
6
|
+
iconChevronRight,
|
|
7
|
+
iconClose,
|
|
8
|
+
iconContrast,
|
|
9
|
+
iconInfo,
|
|
10
|
+
iconRefresh,
|
|
11
|
+
} from '@graphcommerce/next-ui'
|
|
12
|
+
import { FormAutoSubmit, FormPersist, FormProvider, useForm } from '@graphcommerce/react-hook-form'
|
|
13
|
+
import { Box, IconButton } from '@mui/material'
|
|
14
|
+
import { useRouter } from 'next/router'
|
|
15
|
+
import { TextFieldElement } from '../FormComponents'
|
|
16
|
+
import { LightTooltip } from './LightTooltip'
|
|
17
|
+
import { PreviewModeActions } from './PreviewModeActions'
|
|
18
|
+
import { PreviewModeToolbar } from './PreviewModeToolbar'
|
|
19
|
+
import { previewModeDefaults } from './previewModeDefaults'
|
|
20
|
+
|
|
21
|
+
export function getPreviewUrl() {
|
|
22
|
+
const url = new URL(window.location.href)
|
|
23
|
+
url.pathname = '/api/preview'
|
|
24
|
+
;[...url.searchParams.entries()].forEach(([key]) => url.searchParams.delete(key))
|
|
25
|
+
return url
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function PreviewModeEnabled() {
|
|
29
|
+
const form = useForm<{ secret: string; previewData: PreviewData }>({
|
|
30
|
+
defaultValues: { previewData: previewModeDefaults() },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const submit = form.handleSubmit((formValues) => {
|
|
34
|
+
const url = getPreviewUrl()
|
|
35
|
+
url.searchParams.append('action', 'update')
|
|
36
|
+
|
|
37
|
+
Object.entries(formValues).forEach(([key, value]) => {
|
|
38
|
+
url.searchParams.append(key, JSON.stringify(value))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
window.location.href = url.toString()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const exitHandler = form.handleSubmit(() => {
|
|
45
|
+
const url = getPreviewUrl()
|
|
46
|
+
url.searchParams.append('action', 'exit')
|
|
47
|
+
|
|
48
|
+
window.location.href = url.toString()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const revalidateHandler = form.handleSubmit((formValues) => {
|
|
52
|
+
const url = getPreviewUrl()
|
|
53
|
+
Object.entries(formValues).forEach(([key, value]) => {
|
|
54
|
+
url.searchParams.append(key, `${value}`)
|
|
55
|
+
})
|
|
56
|
+
url.searchParams.append('action', 'revalidate')
|
|
57
|
+
window.location.href = url.toString()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<FormProvider {...form}>
|
|
62
|
+
<MessageSnackbar
|
|
63
|
+
variant='pill'
|
|
64
|
+
severity='warning'
|
|
65
|
+
disableBackdropClick
|
|
66
|
+
disableClose
|
|
67
|
+
open
|
|
68
|
+
icon={iconContrast}
|
|
69
|
+
onClose={() => {}}
|
|
70
|
+
action={
|
|
71
|
+
<>
|
|
72
|
+
<PreviewModeActions />
|
|
73
|
+
<LightTooltip title='Revalidate / Regenerate Page' placement='top'>
|
|
74
|
+
<IconButton color='secondary' type='submit' onClick={revalidateHandler}>
|
|
75
|
+
<IconSvg src={iconRefresh} />
|
|
76
|
+
</IconButton>
|
|
77
|
+
</LightTooltip>
|
|
78
|
+
<LightTooltip title='Stop preview mode' placement='top'>
|
|
79
|
+
<IconButton color='secondary' type='submit' onClick={exitHandler}>
|
|
80
|
+
<IconSvg src={iconClose} />
|
|
81
|
+
</IconButton>
|
|
82
|
+
</LightTooltip>
|
|
83
|
+
</>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<Box sx={{ display: 'grid', gridAutoFlow: 'column', placeItems: 'center', gap: 4 }}>
|
|
87
|
+
<Box sx={{ display: 'flex', placeItems: 'center' }}>
|
|
88
|
+
Preview Mode{' '}
|
|
89
|
+
<LightTooltip title='You are currently viewing the website in Preview Mode (caches are disabled).'>
|
|
90
|
+
<IconButton size='small'>
|
|
91
|
+
<IconSvg src={iconInfo} />
|
|
92
|
+
</IconButton>
|
|
93
|
+
</LightTooltip>
|
|
94
|
+
</Box>
|
|
95
|
+
<PreviewModeToolbar />
|
|
96
|
+
</Box>
|
|
97
|
+
</MessageSnackbar>
|
|
98
|
+
<FormPersist form={form} name='PreviewModePreviewData' />
|
|
99
|
+
<FormAutoSubmit control={form.control} submit={submit} />
|
|
100
|
+
</FormProvider>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function PreviewModeDisabled() {
|
|
105
|
+
const form = useForm<{ secret: string }>({})
|
|
106
|
+
|
|
107
|
+
const submit = form.handleSubmit((formValues) => {
|
|
108
|
+
const url = getPreviewUrl()
|
|
109
|
+
url.searchParams.append('action', 'enable')
|
|
110
|
+
|
|
111
|
+
Object.entries(formValues).forEach(([key, value]) => {
|
|
112
|
+
url.searchParams.append(key, typeof value === 'string' ? value : JSON.stringify(value))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
window.location.href = url.toString()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<FormProvider {...form}>
|
|
120
|
+
<MessageSnackbar
|
|
121
|
+
variant='pill'
|
|
122
|
+
severity='warning'
|
|
123
|
+
disableBackdropClick
|
|
124
|
+
disableClose
|
|
125
|
+
open
|
|
126
|
+
icon={iconContrast}
|
|
127
|
+
onClose={() => {}}
|
|
128
|
+
action={
|
|
129
|
+
<IconButton color='secondary' type='submit' onClick={submit}>
|
|
130
|
+
<IconSvg src={iconChevronRight} />
|
|
131
|
+
</IconButton>
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
<Box sx={{ display: 'grid', gridAutoFlow: 'column', placeItems: 'center', gap: 4 }}>
|
|
135
|
+
<Box sx={{ display: 'flex', placeItems: 'center' }}>Preview Mode</Box>
|
|
136
|
+
<TextFieldElement control={form.control} name='secret' label='Secret' />
|
|
137
|
+
</Box>
|
|
138
|
+
</MessageSnackbar>
|
|
139
|
+
<FormPersist form={form} name='PreviewModePreviewData' />
|
|
140
|
+
</FormProvider>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function PreviewMode() {
|
|
145
|
+
const router = useRouter()
|
|
146
|
+
|
|
147
|
+
return router.isPreview ? <PreviewModeEnabled /> : <PreviewModeDisabled />
|
|
148
|
+
}
|
package/components/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@graphcommerce/ecommerce-ui",
|
|
3
3
|
"homepage": "https://www.graphcommerce.org/",
|
|
4
4
|
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "9.0.0-canary.100",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
8
|
"eslintConfig": {
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@graphcommerce/eslint-config-pwa": "^
|
|
16
|
-
"@graphcommerce/graphql": "^
|
|
17
|
-
"@graphcommerce/next-ui": "^
|
|
18
|
-
"@graphcommerce/prettier-config-pwa": "^
|
|
19
|
-
"@graphcommerce/react-hook-form": "^
|
|
20
|
-
"@graphcommerce/typescript-config-pwa": "^
|
|
15
|
+
"@graphcommerce/eslint-config-pwa": "^9.0.0-canary.100",
|
|
16
|
+
"@graphcommerce/graphql": "^9.0.0-canary.100",
|
|
17
|
+
"@graphcommerce/next-ui": "^9.0.0-canary.100",
|
|
18
|
+
"@graphcommerce/prettier-config-pwa": "^9.0.0-canary.100",
|
|
19
|
+
"@graphcommerce/react-hook-form": "^9.0.0-canary.100",
|
|
20
|
+
"@graphcommerce/typescript-config-pwa": "^9.0.0-canary.100",
|
|
21
21
|
"@lingui/core": "^4.2.1",
|
|
22
22
|
"@lingui/macro": "^4.2.1",
|
|
23
23
|
"@lingui/react": "^4.2.1",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PagesProps } from '@graphcommerce/framer-next-pages'
|
|
2
|
+
import type { PluginConfig, PluginProps } from '@graphcommerce/next-config'
|
|
3
|
+
import dynamic from 'next/dynamic'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
const PreviewMode = dynamic(
|
|
7
|
+
async () => (await import('../components/PreviewMode/PreviewMode')).PreviewMode,
|
|
8
|
+
{},
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export const config: PluginConfig = {
|
|
12
|
+
type: 'component',
|
|
13
|
+
module: '@graphcommerce/framer-next-pages',
|
|
14
|
+
ifConfig: 'previewSecret',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function FramerNextPages(props: PluginProps<PagesProps>) {
|
|
18
|
+
const { Prev, router, ...rest } = props
|
|
19
|
+
const [enabled, setEnabled] = useState(router.isPreview)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const handler = (e: KeyboardEvent) => {
|
|
23
|
+
if (e.altKey && e.code === 'Backquote') {
|
|
24
|
+
setEnabled((prev) => !prev)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
window.addEventListener('keydown', handler, false)
|
|
29
|
+
return () => window.removeEventListener('keydown', handler)
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<Prev {...rest} router={router} />
|
|
35
|
+
{enabled && <PreviewMode />}
|
|
36
|
+
</>
|
|
37
|
+
)
|
|
38
|
+
}
|
package/route/preview.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PreviewData } from '@graphcommerce/graphql'
|
|
2
|
+
import { NextApiRequest, NextApiResponse } from 'next'
|
|
3
|
+
import { previewModeDefaults } from '../components/PreviewMode/previewModeDefaults'
|
|
4
|
+
|
|
5
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
6
|
+
const { action } = req.query
|
|
7
|
+
|
|
8
|
+
// const domain = req.url ? new URL(req.url) : undefined
|
|
9
|
+
const referer = req.headers.referer ? new URL(req.headers.referer) : undefined
|
|
10
|
+
const redirectTo =
|
|
11
|
+
req.headers.redirectTo ??
|
|
12
|
+
(referer && req.headers.host === referer.host ? referer.pathname : '/')
|
|
13
|
+
|
|
14
|
+
if (!action) {
|
|
15
|
+
res.status(400).json({ message: 'No action provided' })
|
|
16
|
+
res.end()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (action === 'enable' && req.query.secret) {
|
|
21
|
+
if (req.query.secret !== import.meta.graphCommerce.previewSecret) {
|
|
22
|
+
// This secret should only be known to this API route and the CMS
|
|
23
|
+
res.status(401).json({ message: 'Invalid token' })
|
|
24
|
+
res.end()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
res.setDraftMode({ enable: true })
|
|
29
|
+
const previewData = req.query.previewDat
|
|
30
|
+
? (JSON.parse(`${req.query.previewData}`) as PreviewData)
|
|
31
|
+
: previewModeDefaults()
|
|
32
|
+
|
|
33
|
+
res.setPreviewData(previewData)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (action === 'exit') {
|
|
37
|
+
res.setDraftMode({ enable: false })
|
|
38
|
+
res.clearPreviewData()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (action === 'revalidate') {
|
|
42
|
+
const url = referer ? new URL(referer) : undefined
|
|
43
|
+
|
|
44
|
+
if (!url) {
|
|
45
|
+
res.status(401).json({ message: 'No referer header found' })
|
|
46
|
+
res.end()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await res.revalidate(url.pathname)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (action === 'update' && req.preview && req.query.previewData) {
|
|
54
|
+
// todo we should probabaly validate this.
|
|
55
|
+
res.setPreviewData(JSON.parse(`${req.query.previewData}`) as PreviewData)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
res.writeHead(307, { Location: redirectTo })
|
|
59
|
+
res.end()
|
|
60
|
+
}
|