mirror of
https://github.com/fergalmoran/onearmy-community-platform.git
synced 2025-12-25 11:08:23 +00:00
feat: add wrapper to settings form
This commit is contained in:
@@ -7,14 +7,16 @@ import { Flex } from 'theme-ui'
|
|||||||
import { SettingsFormTab } from './SettingsFormTab'
|
import { SettingsFormTab } from './SettingsFormTab'
|
||||||
import { SettingsFormTabList } from './SettingsFormTabList'
|
import { SettingsFormTabList } from './SettingsFormTabList'
|
||||||
|
|
||||||
|
import type { ThemeUIStyleObject } from 'theme-ui'
|
||||||
import type { ITab } from './SettingsFormTab'
|
import type { ITab } from './SettingsFormTab'
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
tabs: ITab[]
|
tabs: ITab[]
|
||||||
|
sx?: ThemeUIStyleObject | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsFormWrapper = (props: IProps) => {
|
export const SettingsFormWrapper = (props: IProps) => {
|
||||||
const { tabs } = props
|
const { sx, tabs } = props
|
||||||
const [value, setValue] = useState<number>(0)
|
const [value, setValue] = useState<number>(0)
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
@@ -25,6 +27,7 @@ export const SettingsFormWrapper = (props: IProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Flex sx={sx}>
|
||||||
<Tabs value={value} onChange={handleChange}>
|
<Tabs value={value} onChange={handleChange}>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@@ -47,5 +50,6 @@ export const SettingsFormWrapper = (props: IProps) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,377 +1,32 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { Loader, SettingsFormWrapper } from 'oa-components'
|
||||||
import { Form } from 'react-final-form'
|
|
||||||
import { useParams } from 'react-router'
|
|
||||||
import { ARRAY_ERROR, FORM_ERROR } from 'final-form'
|
|
||||||
import arrayMutators from 'final-form-arrays'
|
|
||||||
import { toJS } from 'mobx'
|
|
||||||
import { Button, ExternalLink, Loader, TextNotification } from 'oa-components'
|
|
||||||
import { IModerationStatus } from 'oa-shared'
|
|
||||||
import { UnsavedChangesDialog } from 'src/common/Form/UnsavedChangesDialog'
|
|
||||||
import { useCommonStores } from 'src/common/hooks/useCommonStores'
|
import { useCommonStores } from 'src/common/hooks/useCommonStores'
|
||||||
import { isPreciousPlastic } from 'src/config/config'
|
import { Flex, Text } from 'theme-ui'
|
||||||
import { logger } from 'src/logger'
|
|
||||||
import { isModuleSupported, MODULE } from 'src/modules'
|
|
||||||
import { ProfileType } from 'src/modules/profile/types'
|
|
||||||
import { Alert, Box, Card, Flex, Heading, Text } from 'theme-ui'
|
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
|
|
||||||
import { AccountSettingsSection } from './content/formSections/AccountSettings.section'
|
import { UserProfile } from './content/formSections/UserProfile.section'
|
||||||
import { CollectionSection } from './content/formSections/Collection.section'
|
|
||||||
import { EmailNotificationsSection } from './content/formSections/EmailNotifications.section'
|
|
||||||
import { ExpertiseSection } from './content/formSections/Expertise.section'
|
|
||||||
import { FocusSection } from './content/formSections/Focus.section'
|
|
||||||
import { ImpactSection } from './content/formSections/Impact/Impact.section'
|
|
||||||
import { PatreonIntegration } from './content/formSections/PatreonIntegration'
|
|
||||||
import { PublicContactSection } from './content/formSections/PublicContact.section'
|
|
||||||
import { SettingsErrors } from './content/formSections/SettingsErrors'
|
|
||||||
import { SettingsMapPinSection } from './content/formSections/SettingsMapPinSection'
|
|
||||||
import { UserInfosSection } from './content/formSections/UserInfos.section'
|
|
||||||
import { WorkspaceSection } from './content/formSections/Workspace.section'
|
|
||||||
import { ProfileGuidelines } from './content/PostingGuidelines'
|
|
||||||
import { buttons, headings } from './labels'
|
|
||||||
import INITIAL_VALUES from './Template'
|
|
||||||
|
|
||||||
import type { IMapPin } from 'src/models'
|
|
||||||
import type { IUserPP } from 'src/models/userPreciousPlastic.models'
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
formValues: IUserPP
|
|
||||||
showDeleteDialog?: boolean
|
|
||||||
showLocationDropdown: boolean
|
|
||||||
user?: IUserPP
|
|
||||||
userMapPin: IMapPin | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type INotification = { message: string; icon: string; show: boolean }
|
|
||||||
|
|
||||||
const MapPinModerationComments = (props: { mapPin: IMapPin | null }) => {
|
|
||||||
const { mapPin } = props
|
|
||||||
return mapPin?.comments &&
|
|
||||||
mapPin.moderation == IModerationStatus.IMPROVEMENTS_NEEDED ? (
|
|
||||||
<Alert variant="info" sx={{ mt: 3, fontSize: 2, textAlign: 'left' }}>
|
|
||||||
<Box>
|
|
||||||
This map pin has been marked as requiring further changes. Specifically
|
|
||||||
the moderator comments are:
|
|
||||||
<br />
|
|
||||||
<em>{mapPin?.comments}</em>
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkspaceMapPinRequiredStars = () => {
|
|
||||||
const { description } = headings.workspace
|
|
||||||
const { themeStore } = useCommonStores().stores
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Alert sx={{ fontSize: 2, textAlign: 'left', my: 2 }} variant="failure">
|
|
||||||
<Box>
|
|
||||||
<ExternalLink
|
|
||||||
href={themeStore?.currentTheme.styles.communityProgramURL}
|
|
||||||
sx={{ textDecoration: 'underline', color: 'currentcolor' }}
|
|
||||||
>
|
|
||||||
{description}
|
|
||||||
</ExternalLink>
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { mapsStore, userStore } = useCommonStores().stores
|
const { userStore } = useCommonStores().stores
|
||||||
const [state, setState] = useState<IState>({} as any)
|
const user = userStore.activeUser
|
||||||
const [notification, setNotification] = useState<INotification>({
|
|
||||||
message: '',
|
|
||||||
icon: '',
|
|
||||||
show: false,
|
|
||||||
})
|
|
||||||
const [shouldUpdate, setShouldUpdate] = useState<boolean>(true)
|
|
||||||
const { id } = useParams()
|
|
||||||
|
|
||||||
const toggleLocationDropdown = () => {
|
|
||||||
setState((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
showLocationDropdown: !prevState.showLocationDropdown,
|
|
||||||
formValues: {
|
|
||||||
...prevState.formValues,
|
|
||||||
mapPinDescription: '',
|
|
||||||
location: null,
|
|
||||||
country: null,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let user = userStore.user as IUserPP
|
|
||||||
let userMapPin: IMapPin | null = null
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
if (!shouldUpdate) return
|
|
||||||
if (id) {
|
|
||||||
user = await userStore.getUserProfile(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isModuleSupported(MODULE.MAP)) {
|
|
||||||
userMapPin = (await mapsStore.getPin(user.userName)) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure user form includes all user fields (merge any legacy user with correct format)
|
|
||||||
const baseValues: IUserPP = {
|
|
||||||
...INITIAL_VALUES,
|
|
||||||
// use toJS to avoid mobx monitoring of modified fields (e.g. out of bound arrays on link push)
|
|
||||||
...toJS(user),
|
|
||||||
}
|
|
||||||
const { coverImages, openingHours, links } = baseValues
|
|
||||||
// replace empty arrays with placeholders for filling forms
|
|
||||||
const formValues: IUserPP = {
|
|
||||||
...baseValues,
|
|
||||||
coverImages: new Array(4)
|
|
||||||
.fill(null)
|
|
||||||
.map((v, i) => (coverImages[i] ? coverImages[i] : v)),
|
|
||||||
links: (links.length > 0 ? links : [{} as any]).map((i) => ({
|
|
||||||
...i,
|
|
||||||
key: uuid(),
|
|
||||||
})),
|
|
||||||
openingHours: openingHours!.length > 0 ? openingHours : [{} as any],
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove as updated by sub-form
|
|
||||||
if (formValues.impact) {
|
|
||||||
delete formValues.impact
|
|
||||||
}
|
|
||||||
|
|
||||||
setState({
|
|
||||||
formValues,
|
|
||||||
user,
|
|
||||||
showLocationDropdown: !user?.location?.latlng,
|
|
||||||
userMapPin,
|
|
||||||
})
|
|
||||||
setShouldUpdate(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
init()
|
|
||||||
}, [shouldUpdate])
|
|
||||||
|
|
||||||
const saveProfile = async (values: IUserPP) => {
|
|
||||||
const vals = { ...values }
|
|
||||||
vals.coverImages = (vals.coverImages as any[]).filter((cover) =>
|
|
||||||
cover ? true : false,
|
|
||||||
)
|
|
||||||
// Remove undefined vals from obj before sending to firebase
|
|
||||||
Object.keys(vals).forEach((key) => {
|
|
||||||
if (vals[key] === undefined) {
|
|
||||||
delete vals[key]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.debug({ profile: vals }, 'SettingsPage.saveProfile')
|
|
||||||
await userStore.updateUserProfile(vals, 'settings-save-profile', id)
|
|
||||||
|
|
||||||
setShouldUpdate(true)
|
|
||||||
return setNotification({
|
|
||||||
message: 'Profile Saved',
|
|
||||||
icon: 'check',
|
|
||||||
show: true,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn({ error, profile: vals }, 'SettingsPage.saveProfile.error')
|
|
||||||
setNotification({ message: 'Save Failed', icon: 'close', show: true })
|
|
||||||
return { [FORM_ERROR]: 'Save Failed' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateForm = (v: IUserPP) => {
|
|
||||||
const errors: any = {}
|
|
||||||
// must have at least 1 cover (awkard react final form array format)
|
|
||||||
if (!v.coverImages[0]) {
|
|
||||||
errors.coverImages = []
|
|
||||||
errors.coverImages[ARRAY_ERROR] = 'Must have at least one cover image'
|
|
||||||
}
|
|
||||||
if (!v.links[0]) {
|
|
||||||
errors.links = []
|
|
||||||
errors.links[ARRAY_ERROR] = 'Must have at least one link'
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const { formValues, user, userMapPin } = state
|
|
||||||
const formId = 'userProfileForm'
|
|
||||||
|
|
||||||
return user ? (
|
return user ? (
|
||||||
<Form
|
<SettingsFormWrapper
|
||||||
id={formId}
|
sx={{ maxWidth: '850px', alignSelf: 'center', paddingTop: [1, 5] }}
|
||||||
onSubmit={saveProfile}
|
tabs={[
|
||||||
initialValues={formValues}
|
{
|
||||||
validate={validateForm}
|
title: 'Profile',
|
||||||
mutators={{ ...arrayMutators }}
|
header: (
|
||||||
validateOnBlur
|
<Flex sx={{ gap: 2, flexDirection: 'column' }}>
|
||||||
render={({
|
<Text as="h3">✏️ Complete your profile</Text>
|
||||||
form,
|
<Text>
|
||||||
submitFailed,
|
In order to post comments or create content, we'd like you to
|
||||||
submitting,
|
share something about yourself.
|
||||||
values,
|
</Text>
|
||||||
handleSubmit,
|
|
||||||
hasValidationErrors,
|
|
||||||
valid,
|
|
||||||
invalid,
|
|
||||||
errors,
|
|
||||||
}) => {
|
|
||||||
const { createProfile, editProfile } = headings
|
|
||||||
const heading = user.profileType ? editProfile : createProfile
|
|
||||||
const isMember = values.profileType === ProfileType.MEMBER
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex mx={-2} bg={'inherit'} sx={{ flexWrap: 'wrap' }}>
|
|
||||||
<UnsavedChangesDialog
|
|
||||||
uploadComplete={userStore.updateStatus.Complete}
|
|
||||||
message={
|
|
||||||
'You are leaving this page without saving. Do you want to continue ?'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
width: ['100%', '100%', `${(2 / 3) * 100}%`],
|
|
||||||
my: 4,
|
|
||||||
bg: 'inherit',
|
|
||||||
px: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ width: '100%' }}>
|
|
||||||
<form id="userProfileForm" onSubmit={handleSubmit}>
|
|
||||||
<Flex sx={{ flexDirection: 'column' }}>
|
|
||||||
<Card
|
|
||||||
sx={{
|
|
||||||
background: 'softblue',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex px={3} py={2}>
|
|
||||||
<Heading as="h1">{heading}</Heading>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
),
|
||||||
<Box
|
body: <UserProfile />,
|
||||||
sx={{
|
glyph: 'thunderbolt',
|
||||||
display: ['block', 'block', 'none'],
|
},
|
||||||
mt: 3,
|
]}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ProfileGuidelines />
|
|
||||||
</Box>
|
|
||||||
{isModuleSupported(MODULE.MAP) && <FocusSection />}
|
|
||||||
|
|
||||||
{values.profileType === ProfileType.WORKSPACE && (
|
|
||||||
<WorkspaceSection />
|
|
||||||
)}
|
|
||||||
{values.profileType === ProfileType.COLLECTION_POINT && (
|
|
||||||
<CollectionSection
|
|
||||||
required={
|
|
||||||
values.collectedPlasticTypes
|
|
||||||
? values.collectedPlasticTypes.length === 0
|
|
||||||
: true
|
|
||||||
}
|
|
||||||
formValues={values}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{values.profileType === ProfileType.MACHINE_BUILDER && (
|
|
||||||
<ExpertiseSection
|
|
||||||
required={
|
|
||||||
values.machineBuilderXp
|
|
||||||
? values.machineBuilderXp.length === 0
|
|
||||||
: true
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<UserInfosSection
|
|
||||||
formValues={values}
|
|
||||||
mutators={form.mutators}
|
|
||||||
showLocationDropdown={state.showLocationDropdown}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isModuleSupported(MODULE.MAP) && (
|
|
||||||
<SettingsMapPinSection
|
|
||||||
toggleLocationDropdown={toggleLocationDropdown}
|
|
||||||
>
|
|
||||||
<MapPinModerationComments mapPin={userMapPin} />
|
|
||||||
{!isMember && <WorkspaceMapPinRequiredStars />}
|
|
||||||
</SettingsMapPinSection>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{!isMember && isPreciousPlastic() && <ImpactSection />}
|
|
||||||
|
|
||||||
<EmailNotificationsSection
|
|
||||||
notificationSettings={values.notification_settings}
|
|
||||||
/>
|
|
||||||
{!isMember && (
|
|
||||||
<PublicContactSection
|
|
||||||
isContactableByPublic={values.isContactableByPublic}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PatreonIntegration user={user} />
|
|
||||||
</form>
|
|
||||||
<AccountSettingsSection />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
{/* desktop guidelines container */}
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
width: ['100%', '100%', `${100 / 3}%`],
|
|
||||||
flexDirection: 'column',
|
|
||||||
bg: 'inherit',
|
|
||||||
px: 2,
|
|
||||||
height: 'auto',
|
|
||||||
mt: [0, 0, 4],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: ['relative', 'relative', 'sticky'],
|
|
||||||
top: 3,
|
|
||||||
maxWidth: ['100%', '100%', '400px'],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isModuleSupported(MODULE.MAP) && (
|
|
||||||
<Box mb={3} sx={{ display: ['none', 'none', 'block'] }}>
|
|
||||||
<ProfileGuidelines />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
large
|
|
||||||
form={formId}
|
|
||||||
data-cy="save"
|
|
||||||
title={
|
|
||||||
invalid ? `Errors: ${Object.keys(errors || {})}` : 'Submit'
|
|
||||||
}
|
|
||||||
mb={3}
|
|
||||||
sx={{ width: '100%', justifyContent: 'center' }}
|
|
||||||
variant={'primary'}
|
|
||||||
type="submit"
|
|
||||||
// disable button when form invalid or during submit.
|
|
||||||
// ensure enabled after submit error
|
|
||||||
disabled={submitting || (submitFailed && hasValidationErrors)}
|
|
||||||
>
|
|
||||||
{buttons.save}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SettingsErrors
|
|
||||||
errors={errors}
|
|
||||||
isVisible={submitFailed && hasValidationErrors}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{valid && notification.message !== '' && (
|
|
||||||
<TextNotification
|
|
||||||
isVisible={notification.show}
|
|
||||||
variant={valid ? 'success' : 'failure'}
|
|
||||||
>
|
|
||||||
<Text>{buttons.success}</Text>
|
|
||||||
</TextNotification>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const AccountSettingsSection = observer(() => {
|
|||||||
const { description, title } = fields.deleteAccount
|
const { description, title } = fields.deleteAccount
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ padding: 4, marginTop: 4 }}>
|
<Card sx={{ padding: 4 }}>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { SelectField } from 'src/common/Form/Select.field'
|
|||||||
import { buttons, fields } from 'src/pages/UserSettings/labels'
|
import { buttons, fields } from 'src/pages/UserSettings/labels'
|
||||||
import { formatLink } from 'src/utils/formatters'
|
import { formatLink } from 'src/utils/formatters'
|
||||||
import { required, validateEmail, validateUrl } from 'src/utils/validators'
|
import { required, validateEmail, validateUrl } from 'src/utils/validators'
|
||||||
import { Box, Flex, Grid } from 'theme-ui'
|
import { Flex } from 'theme-ui'
|
||||||
|
|
||||||
const COM_TYPE_MOCKS = [
|
const COM_TYPE_MOCKS = [
|
||||||
{
|
{
|
||||||
@@ -84,7 +84,6 @@ export const ProfileLinkField = (props: IProps) => {
|
|||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
showIconOnly={true}
|
showIconOnly={true}
|
||||||
onClick={() => toggleDeleteModal()}
|
onClick={() => toggleDeleteModal()}
|
||||||
ml={2}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
@@ -92,11 +91,12 @@ export const ProfileLinkField = (props: IProps) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex my={[2]} sx={{ flexDirection: ['column', 'column', 'row'] }}>
|
<Flex
|
||||||
<Grid mb={[1, 1, 0]} gap={0} sx={{ width: ['100%', '100%', '210px'] }}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
sx={{
|
||||||
mr: 2,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: [1, 2],
|
||||||
|
marginBottom: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Field
|
<Field
|
||||||
@@ -110,24 +110,8 @@ export const ProfileLinkField = (props: IProps) => {
|
|||||||
placeholder={buttons.link.type}
|
placeholder={buttons.link.type}
|
||||||
validate={required}
|
validate={required}
|
||||||
validateFields={[]}
|
validateFields={[]}
|
||||||
style={{ width: '100%', height: '40px' }}
|
style={{ flex: 3 }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
{isDeleteEnabled ? (
|
|
||||||
<DeleteButton
|
|
||||||
sx={{
|
|
||||||
display: ['block', 'block', 'none'],
|
|
||||||
}}
|
|
||||||
ml={'2px'}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
mb={[1, 1, 0]}
|
|
||||||
gap={0}
|
|
||||||
columns={['auto', 'auto', 'auto']}
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<Field
|
<Field
|
||||||
data-cy={`input-link-${index}`}
|
data-cy={`input-link-${index}`}
|
||||||
name={`${name}.url`}
|
name={`${name}.url`}
|
||||||
@@ -137,16 +121,9 @@ export const ProfileLinkField = (props: IProps) => {
|
|||||||
placeholder={fields.links.placeholder}
|
placeholder={fields.links.placeholder}
|
||||||
format={(v) => formatLink(v, state.linkType)}
|
format={(v) => formatLink(v, state.linkType)}
|
||||||
formatOnBlur={true}
|
formatOnBlur={true}
|
||||||
style={{ width: '100%', height: '40px', marginBottom: '0px' }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
{isDeleteEnabled ? <DeleteButton /> : null}
|
||||||
{isDeleteEnabled ? (
|
|
||||||
<DeleteButton
|
|
||||||
sx={{
|
|
||||||
display: ['none', 'none', 'block'],
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{
|
{
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={!!state.showDeleteModal}
|
isOpen={!!state.showDeleteModal}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field } from 'react-final-form'
|
import { Field } from 'react-final-form'
|
||||||
import { useTheme } from '@emotion/react'
|
import { useTheme } from '@emotion/react'
|
||||||
import { Button, ExternalLink } from 'oa-components'
|
import { ExternalLink } from 'oa-components'
|
||||||
import { getSupportedProfileTypes } from 'src/modules/profile'
|
import { getSupportedProfileTypes } from 'src/modules/profile'
|
||||||
import { buttons, fields, headings } from 'src/pages/UserSettings/labels'
|
import { buttons, fields, headings } from 'src/pages/UserSettings/labels'
|
||||||
import { Box, Flex, Grid, Heading, Paragraph, Text } from 'theme-ui'
|
import { Box, Flex, Grid, Heading, Paragraph, Text } from 'theme-ui'
|
||||||
@@ -11,7 +11,7 @@ import { FlexSectionContainer } from './elements'
|
|||||||
import type { ProfileTypeLabel } from 'src/modules/profile/types'
|
import type { ProfileTypeLabel } from 'src/modules/profile/types'
|
||||||
|
|
||||||
const ProfileTypes = () => {
|
const ProfileTypes = () => {
|
||||||
const { description, error, title } = fields.activities
|
const { description, error } = fields.activities
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const profileTypes = getSupportedProfileTypes().filter(({ label }) =>
|
const profileTypes = getSupportedProfileTypes().filter(({ label }) =>
|
||||||
Object.keys(theme.badges).includes(label),
|
Object.keys(theme.badges).includes(label),
|
||||||
@@ -31,8 +31,21 @@ const ProfileTypes = () => {
|
|||||||
{headings.focus}
|
{headings.focus}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<Flex sx={{ alignItems: 'baseline', marginY: 2 }}>
|
||||||
|
<Paragraph my={4}>
|
||||||
|
{description}{' '}
|
||||||
|
<ExternalLink
|
||||||
|
href={theme.profileGuidelinesURL}
|
||||||
|
sx={{ textDecoration: 'underline', color: 'grey' }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{buttons.guidelines}
|
||||||
|
</ExternalLink>
|
||||||
|
</Paragraph>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Paragraph my={4}>{title}</Paragraph>
|
|
||||||
<Grid columns={['repeat(auto-fill, minmax(125px, 1fr))']} gap={2}>
|
<Grid columns={['repeat(auto-fill, minmax(125px, 1fr))']} gap={2}>
|
||||||
{profileTypes.map((profile, index: number) => (
|
{profileTypes.map((profile, index: number) => (
|
||||||
<Box key={index}>
|
<Box key={index}>
|
||||||
@@ -52,14 +65,6 @@ const ProfileTypes = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Flex sx={{ flexWrap: 'wrap', alignItems: 'center' }} mt={4}>
|
|
||||||
<Text my={2}>{description}</Text>
|
|
||||||
<ExternalLink href={theme.profileGuidelinesURL}>
|
|
||||||
<Button ml={[1, 2, 2]} variant="outline" type="button">
|
|
||||||
{buttons.guidelines}
|
|
||||||
</Button>
|
|
||||||
</ExternalLink>
|
|
||||||
</Flex>
|
|
||||||
{props.meta.error && <Text color={theme.colors.red}>{error}</Text>}
|
{props.meta.error && <Text color={theme.colors.red}>{error}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
</FlexSectionContainer>
|
</FlexSectionContainer>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export const UserInfosSection = (props: IProps) => {
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box data-cy="UserInfos: links">
|
<Box data-cy="UserInfos: links">
|
||||||
<Flex sx={{ alignItems: 'center', width: '100%', wrap: 'nowrap' }}>
|
<Flex>
|
||||||
<Text mb={2} mt={7} sx={{ fontSize: 2 }}>
|
<Text mb={2} mt={7} sx={{ fontSize: 2 }}>
|
||||||
{`${fields.links.title} *`}
|
{`${fields.links.title} *`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { Form } from 'react-final-form'
|
||||||
|
import { useParams } from 'react-router'
|
||||||
|
import { ARRAY_ERROR, FORM_ERROR } from 'final-form'
|
||||||
|
import arrayMutators from 'final-form-arrays'
|
||||||
|
import { toJS } from 'mobx'
|
||||||
|
import { Button, ExternalLink, TextNotification } from 'oa-components'
|
||||||
|
import { IModerationStatus } from 'oa-shared'
|
||||||
|
import { UnsavedChangesDialog } from 'src/common/Form/UnsavedChangesDialog'
|
||||||
|
import { useCommonStores } from 'src/common/hooks/useCommonStores'
|
||||||
|
import { isPreciousPlastic } from 'src/config/config'
|
||||||
|
import { logger } from 'src/logger'
|
||||||
|
import { isModuleSupported, MODULE } from 'src/modules'
|
||||||
|
import { ProfileType } from 'src/modules/profile/types'
|
||||||
|
import { Alert, Box, Flex, Text } from 'theme-ui'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
import { buttons, headings } from '../../labels'
|
||||||
|
import INITIAL_VALUES from '../../Template'
|
||||||
|
import { ImpactSection } from './Impact/Impact.section'
|
||||||
|
import { AccountSettingsSection } from './AccountSettings.section'
|
||||||
|
import { CollectionSection } from './Collection.section'
|
||||||
|
import { FlexSectionContainer } from './elements'
|
||||||
|
import { EmailNotificationsSection } from './EmailNotifications.section'
|
||||||
|
import { ExpertiseSection } from './Expertise.section'
|
||||||
|
import { FocusSection } from './Focus.section'
|
||||||
|
import { PatreonIntegration } from './PatreonIntegration'
|
||||||
|
import { PublicContactSection } from './PublicContact.section'
|
||||||
|
import { SettingsErrors } from './SettingsErrors'
|
||||||
|
import { SettingsMapPinSection } from './SettingsMapPinSection'
|
||||||
|
import { UserInfosSection } from './UserInfos.section'
|
||||||
|
import { WorkspaceSection } from './Workspace.section'
|
||||||
|
|
||||||
|
import type { IMapPin } from 'src/models'
|
||||||
|
import type { IUserPP } from 'src/models/userPreciousPlastic.models'
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
formValues: IUserPP
|
||||||
|
showDeleteDialog?: boolean
|
||||||
|
showLocationDropdown: boolean
|
||||||
|
user?: IUserPP
|
||||||
|
userMapPin: IMapPin | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type INotification = {
|
||||||
|
message: string
|
||||||
|
icon: string
|
||||||
|
show: boolean
|
||||||
|
variant: 'success' | 'failure'
|
||||||
|
}
|
||||||
|
|
||||||
|
const MapPinModerationComments = (props: { mapPin: IMapPin | null }) => {
|
||||||
|
const { mapPin } = props
|
||||||
|
return mapPin?.comments &&
|
||||||
|
mapPin.moderation == IModerationStatus.IMPROVEMENTS_NEEDED ? (
|
||||||
|
<Alert variant="info" sx={{ mt: 3, fontSize: 2, textAlign: 'left' }}>
|
||||||
|
<Box>
|
||||||
|
This map pin has been marked as requiring further changes. Specifically
|
||||||
|
the moderator comments are:
|
||||||
|
<br />
|
||||||
|
<em>{mapPin?.comments}</em>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorkspaceMapPinRequiredStars = () => {
|
||||||
|
const { description } = headings.workspace
|
||||||
|
const { themeStore } = useCommonStores().stores
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert sx={{ fontSize: 2, textAlign: 'left', my: 2 }} variant="failure">
|
||||||
|
<Box>
|
||||||
|
<ExternalLink
|
||||||
|
href={themeStore?.currentTheme.styles.communityProgramURL}
|
||||||
|
sx={{ textDecoration: 'underline', color: 'currentcolor' }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</ExternalLink>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserProfile = () => {
|
||||||
|
const { mapsStore, userStore } = useCommonStores().stores
|
||||||
|
const [state, setState] = useState<IState>({} as any)
|
||||||
|
const [notification, setNotification] = useState<INotification>({
|
||||||
|
message: '',
|
||||||
|
icon: '',
|
||||||
|
show: false,
|
||||||
|
variant: 'success',
|
||||||
|
})
|
||||||
|
const [shouldUpdate, setShouldUpdate] = useState<boolean>(true)
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const toggleLocationDropdown = () => {
|
||||||
|
setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
showLocationDropdown: !prevState.showLocationDropdown,
|
||||||
|
formValues: {
|
||||||
|
...prevState.formValues,
|
||||||
|
mapPinDescription: '',
|
||||||
|
location: null,
|
||||||
|
country: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let user = userStore.user as IUserPP
|
||||||
|
let userMapPin: IMapPin | null = null
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
if (!shouldUpdate) return
|
||||||
|
if (id) {
|
||||||
|
user = await userStore.getUserProfile(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isModuleSupported(MODULE.MAP)) {
|
||||||
|
userMapPin = (await mapsStore.getPin(user.userName)) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure user form includes all user fields (merge any legacy user with correct format)
|
||||||
|
const baseValues: IUserPP = {
|
||||||
|
...INITIAL_VALUES,
|
||||||
|
// use toJS to avoid mobx monitoring of modified fields (e.g. out of bound arrays on link push)
|
||||||
|
...toJS(user),
|
||||||
|
}
|
||||||
|
const { coverImages, openingHours, links } = baseValues
|
||||||
|
// replace empty arrays with placeholders for filling forms
|
||||||
|
const formValues: IUserPP = {
|
||||||
|
...baseValues,
|
||||||
|
coverImages: new Array(4)
|
||||||
|
.fill(null)
|
||||||
|
.map((v, i) => (coverImages[i] ? coverImages[i] : v)),
|
||||||
|
links: (links.length > 0 ? links : [{} as any]).map((i) => ({
|
||||||
|
...i,
|
||||||
|
key: uuid(),
|
||||||
|
})),
|
||||||
|
openingHours: openingHours!.length > 0 ? openingHours : [{} as any],
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove as updated by sub-form
|
||||||
|
if (formValues.impact) {
|
||||||
|
delete formValues.impact
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({
|
||||||
|
formValues,
|
||||||
|
user,
|
||||||
|
showLocationDropdown: !user?.location?.latlng,
|
||||||
|
userMapPin,
|
||||||
|
})
|
||||||
|
setShouldUpdate(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
}, [shouldUpdate])
|
||||||
|
|
||||||
|
const saveProfile = async (values: IUserPP) => {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
const vals = { ...values }
|
||||||
|
vals.coverImages = (vals.coverImages as any[]).filter((cover) =>
|
||||||
|
cover ? true : false,
|
||||||
|
)
|
||||||
|
// Remove undefined vals from obj before sending to firebase
|
||||||
|
Object.keys(vals).forEach((key) => {
|
||||||
|
if (vals[key] === undefined) {
|
||||||
|
delete vals[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug({ profile: vals }, 'SettingsPage.saveProfile')
|
||||||
|
await userStore.updateUserProfile(vals, 'settings-save-profile', id)
|
||||||
|
|
||||||
|
setShouldUpdate(true)
|
||||||
|
return setNotification({
|
||||||
|
message: 'Profile Saved',
|
||||||
|
icon: 'check',
|
||||||
|
show: true,
|
||||||
|
variant: 'success',
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn({ error, profile: vals }, 'SettingsPage.saveProfile.error')
|
||||||
|
setNotification({
|
||||||
|
message: 'Save Failed',
|
||||||
|
icon: 'close',
|
||||||
|
show: true,
|
||||||
|
variant: 'failure',
|
||||||
|
})
|
||||||
|
return { [FORM_ERROR]: 'Save Failed' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateForm = (v: IUserPP) => {
|
||||||
|
const errors: any = {}
|
||||||
|
// must have at least 1 cover (awkard react final form array format)
|
||||||
|
if (!v.coverImages[0]) {
|
||||||
|
errors.coverImages = []
|
||||||
|
errors.coverImages[ARRAY_ERROR] = 'Must have at least one cover image'
|
||||||
|
}
|
||||||
|
if (!v.links[0]) {
|
||||||
|
errors.links = []
|
||||||
|
errors.links[ARRAY_ERROR] = 'Must have at least one link'
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formValues, user, userMapPin } = state
|
||||||
|
const formId = 'userProfileForm'
|
||||||
|
|
||||||
|
return (
|
||||||
|
user && (
|
||||||
|
<Form
|
||||||
|
id={formId}
|
||||||
|
onSubmit={saveProfile}
|
||||||
|
initialValues={formValues}
|
||||||
|
validate={validateForm}
|
||||||
|
mutators={{ ...arrayMutators }}
|
||||||
|
validateOnBlur
|
||||||
|
render={({
|
||||||
|
form,
|
||||||
|
submitFailed,
|
||||||
|
submitting,
|
||||||
|
touched,
|
||||||
|
values,
|
||||||
|
handleSubmit,
|
||||||
|
hasValidationErrors,
|
||||||
|
invalid,
|
||||||
|
errors,
|
||||||
|
pristine,
|
||||||
|
}) => {
|
||||||
|
const isMember = values.profileType === ProfileType.MEMBER
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
bg={'inherit'}
|
||||||
|
sx={{ flexDirection: 'column', padding: 4, gap: 4 }}
|
||||||
|
>
|
||||||
|
<UnsavedChangesDialog
|
||||||
|
uploadComplete={userStore.updateStatus.Complete}
|
||||||
|
message={
|
||||||
|
'You are leaving this page without saving. Do you want to continue ?'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<>
|
||||||
|
{!!notification.show && pristine && !touched && (
|
||||||
|
<TextNotification
|
||||||
|
isVisible={notification.show}
|
||||||
|
variant={notification.variant}
|
||||||
|
>
|
||||||
|
<Text>{buttons.success}</Text>
|
||||||
|
</TextNotification>
|
||||||
|
)}
|
||||||
|
{(errors || submitFailed) && touched && !pristine && (
|
||||||
|
<SettingsErrors
|
||||||
|
errors={errors}
|
||||||
|
isVisible={!!(errors && Object.keys(errors).length > 0)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
|
||||||
|
<form id="userProfileForm" onSubmit={handleSubmit}>
|
||||||
|
<Flex sx={{ flexDirection: 'column', gap: 4 }}>
|
||||||
|
{isModuleSupported(MODULE.MAP) && <FocusSection />}
|
||||||
|
|
||||||
|
{values.profileType === ProfileType.WORKSPACE && (
|
||||||
|
<WorkspaceSection />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{values.profileType === ProfileType.COLLECTION_POINT && (
|
||||||
|
<CollectionSection
|
||||||
|
required={
|
||||||
|
values.collectedPlasticTypes
|
||||||
|
? values.collectedPlasticTypes.length === 0
|
||||||
|
: true
|
||||||
|
}
|
||||||
|
formValues={values}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{values.profileType === ProfileType.MACHINE_BUILDER && (
|
||||||
|
<ExpertiseSection
|
||||||
|
required={
|
||||||
|
values.machineBuilderXp
|
||||||
|
? values.machineBuilderXp.length === 0
|
||||||
|
: true
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UserInfosSection
|
||||||
|
formValues={values}
|
||||||
|
mutators={form.mutators}
|
||||||
|
showLocationDropdown={state.showLocationDropdown}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isModuleSupported(MODULE.MAP) && (
|
||||||
|
<SettingsMapPinSection
|
||||||
|
toggleLocationDropdown={toggleLocationDropdown}
|
||||||
|
>
|
||||||
|
<MapPinModerationComments mapPin={userMapPin} />
|
||||||
|
{!isMember && <WorkspaceMapPinRequiredStars />}
|
||||||
|
</SettingsMapPinSection>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{!isMember && isPreciousPlastic() && (
|
||||||
|
<FlexSectionContainer>
|
||||||
|
<ImpactSection />
|
||||||
|
</FlexSectionContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EmailNotificationsSection
|
||||||
|
notificationSettings={values.notification_settings}
|
||||||
|
/>
|
||||||
|
{!isMember && (
|
||||||
|
<FlexSectionContainer>
|
||||||
|
<PublicContactSection
|
||||||
|
isContactableByPublic={values.isContactableByPublic}
|
||||||
|
/>
|
||||||
|
</FlexSectionContainer>
|
||||||
|
)}
|
||||||
|
<PatreonIntegration user={user} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<AccountSettingsSection />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
large
|
||||||
|
form={formId}
|
||||||
|
data-cy="save"
|
||||||
|
title={
|
||||||
|
invalid ? `Errors: ${Object.keys(errors || {})}` : 'Submit'
|
||||||
|
}
|
||||||
|
variant={'primary'}
|
||||||
|
type="submit"
|
||||||
|
// disable button when form invalid or during submit.
|
||||||
|
// ensure enabled after submit error
|
||||||
|
disabled={submitting || (submitFailed && hasValidationErrors)}
|
||||||
|
sx={{ alignSelf: 'flex-start' }}
|
||||||
|
>
|
||||||
|
{buttons.save}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import { FactoryUser } from 'src/test/factories/User'
|
|||||||
import { testingThemeStyles } from 'src/test/utils/themeUtils'
|
import { testingThemeStyles } from 'src/test/utils/themeUtils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { SettingsPage } from './SettingsPage'
|
import { UserProfile } from './UserProfile.section'
|
||||||
|
|
||||||
import type { IUserPPDB } from 'src/models'
|
import type { IUserPPDB } from 'src/models'
|
||||||
|
|
||||||
@@ -69,21 +69,6 @@ describe('UserSettings', () => {
|
|||||||
vi.resetAllMocks()
|
vi.resetAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('displays user settings', async () => {
|
|
||||||
mockUser = FactoryUser()
|
|
||||||
|
|
||||||
// Act
|
|
||||||
let wrapper
|
|
||||||
act(() => {
|
|
||||||
wrapper = Wrapper(mockUser)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(wrapper.getByText('Edit profile'))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays one photo for member', async () => {
|
it('displays one photo for member', async () => {
|
||||||
mockUser = FactoryUser({ profileType: 'member' })
|
mockUser = FactoryUser({ profileType: 'member' })
|
||||||
// Act
|
// Act
|
||||||
@@ -269,7 +254,7 @@ const Wrapper = (user: IUserPPDB, routerInitialEntry?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const router = createMemoryRouter(
|
const router = createMemoryRouter(
|
||||||
createRoutesFromElements(<Route index element={<SettingsPage />} />),
|
createRoutesFromElements(<Route index element={<UserProfile />} />),
|
||||||
{
|
{
|
||||||
initialEntries: [routerInitialEntry ? routerInitialEntry : ''],
|
initialEntries: [routerInitialEntry ? routerInitialEntry : ''],
|
||||||
},
|
},
|
||||||
@@ -7,7 +7,7 @@ import Sheetpress from 'src/assets/images/workspace-focus/sheetpress.png'
|
|||||||
import Shredder from 'src/assets/images/workspace-focus/shredder.png'
|
import Shredder from 'src/assets/images/workspace-focus/shredder.png'
|
||||||
import { fields } from 'src/pages/UserSettings/labels'
|
import { fields } from 'src/pages/UserSettings/labels'
|
||||||
import { required } from 'src/utils/validators'
|
import { required } from 'src/utils/validators'
|
||||||
import { Box, Flex, Heading, Text } from 'theme-ui'
|
import { Box, Flex, Grid, Heading, Text } from 'theme-ui'
|
||||||
|
|
||||||
import { CustomRadioField } from './Fields/CustomRadio.field'
|
import { CustomRadioField } from './Fields/CustomRadio.field'
|
||||||
import { FlexSectionContainer } from './elements'
|
import { FlexSectionContainer } from './elements'
|
||||||
@@ -66,7 +66,7 @@ export const WorkspaceSection = () => {
|
|||||||
<Text mt={4} mb={4}>
|
<Text mt={4} mb={4}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex sx={{ flexWrap: ['wrap', 'wrap', 'nowrap'] }}>
|
<Grid columns={['repeat(auto-fill, minmax(125px, 1fr))']} gap={2}>
|
||||||
{WORKSPACE_TYPES.map((workspace, index: number) => (
|
{WORKSPACE_TYPES.map((workspace, index: number) => (
|
||||||
<CustomRadioField
|
<CustomRadioField
|
||||||
data-cy={workspace.label}
|
data-cy={workspace.label}
|
||||||
@@ -83,7 +83,7 @@ export const WorkspaceSection = () => {
|
|||||||
subText={workspace.subText}
|
subText={workspace.subText}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Grid>
|
||||||
{meta.touched && meta.error && (
|
{meta.touched && meta.error && (
|
||||||
<Text
|
<Text
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field } from 'react-final-form'
|
import { Field } from 'react-final-form'
|
||||||
import styled from '@emotion/styled'
|
import styled from '@emotion/styled'
|
||||||
import { Card, Flex } from 'theme-ui'
|
import { Flex } from 'theme-ui'
|
||||||
|
|
||||||
export const Label = (props) => (
|
export const Label = (props) => (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -36,9 +36,5 @@ export const HiddenInput = styled(Field)`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const FlexSectionContainer = (props) => (
|
export const FlexSectionContainer = (props) => (
|
||||||
<Card mt={4} style={{ overflow: 'visible' }}>
|
<Flex sx={{ flexDirection: 'column', paddingY: 2 }}>{props.children}</Flex>
|
||||||
<Flex p={4} sx={{ flexWrap: 'nowrap', flexDirection: 'column' }}>
|
|
||||||
{props.children}
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const buttons = {
|
|||||||
|
|
||||||
export const fields: ILabels = {
|
export const fields: ILabels = {
|
||||||
activities: {
|
activities: {
|
||||||
description: 'Not sure about your focus?',
|
description: 'Choose your main activity. Not sure?',
|
||||||
error: 'Please select a focus',
|
error: 'Please select a focus',
|
||||||
title: 'What is your main activity?',
|
title: 'What is your main activity?',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user