From 179287bf06a5693b55eb6fe8935feff0166c81ed Mon Sep 17 00:00:00 2001 From: Vass Bence <49574140+vassbence@users.noreply.github.com> Date: Wed, 9 Oct 2024 17:56:24 +0200 Subject: [PATCH] work --- src/components/Form/Form.stories.tsx | 149 +++++++- src/components/Form/Form.tsx | 406 ++++++++++++---------- src/components/Form/FormField.stories.tsx | 23 ++ src/components/Form/FormField.tsx | 5 + src/components/Form/FormFieldGroup.tsx | 27 +- src/components/Form/FormSidebar.tsx | 42 +++ src/components/Form/index.tsx | 1 + src/components/Note/Note.stories.tsx | 52 +++ src/components/Note/index.tsx | 112 ++++++ src/components/Patterns.stories.tsx | 3 - src/components/Sidebar/index.tsx | 19 +- src/index.ts | 1 + 12 files changed, 632 insertions(+), 208 deletions(-) create mode 100644 src/components/Form/FormSidebar.tsx create mode 100644 src/components/Note/Note.stories.tsx create mode 100644 src/components/Note/index.tsx diff --git a/src/components/Form/Form.stories.tsx b/src/components/Form/Form.stories.tsx index 337cc04..90af798 100644 --- a/src/components/Form/Form.stories.tsx +++ b/src/components/Form/Form.stories.tsx @@ -1,19 +1,19 @@ import React, { useEffect, useState } from 'react' -import { FormErrors, FormField, FormFieldGroup, useForm } from './index.js' -import { TextInput } from '../TextInput/index.js' -import { TextAreaInput } from '../TextAreaInput/index.js' -import { NumberInput } from '../NumberInput/index.js' -import { CheckboxInput } from '../CheckboxInput/index.js' -import { SwitchInput } from '../SwitchInput/index.js' -import { Calendar } from '../Calendar/index.js' +import { FormErrors } from './index.js' import { Button } from '../Button/index.js' -import { format } from 'date-fns' import { Form } from './Form.js' import { Badge } from '../Badge/index.js' +import { Sidebar } from '../Sidebar/index.js' +import { AppHeader } from '../AppHeader/index.js' +import { colors } from '../../utils/colors.js' +import { ScrollArea } from '../ScrollArea/index.js' export default { title: 'Form', component: () => {}, + parameters: { + layout: 'fullscreen', + }, } export const Component = () => { @@ -126,6 +126,139 @@ export const Component = () => { ) } +export const FullScreenGroups = () => { + return ( +
({ + title: { + type: 'text', + label: 'Title', + description: 'Used internally and publicly', + }, + type: { + type: 'select', + label: 'Type', + options: [ + { label: 'Cashcall', value: 'cashcall' }, + { label: 'SMS', value: 'sms' }, + ], + note: + values.type === 'sms' + ? { + title: 'What are SMS games?', + description: 'Lorem ipsum', + } + : { + title: 'What are cashcall games?', + description: + 'In cashcall games the participant calls or texts a phone number. Based on a set of rules they have the chance to win awards.', + }, + }, + platform: { + type: 'select', + label: 'Platform', + options: [ + { label: 'Bild', value: 'bild' }, + { label: 'Bild+', value: 'bildplus' }, + ], + }, + price: { + type: 'number', + label: 'Price (EUR)', + }, + terms: { + type: 'textarea', + label: 'Terms & Conditions', + }, + privacy: { + type: 'textarea', + label: 'Privacy policy', + }, + })} + onSubmit={console.log} + > + {({ submitForm }) => ( +
+
+ + + + + + + + + + + + Games + + + + +
+
+ + Create Game + + + + +
+ +
+ +
+
+ +
+
+
+ )} +
+ ) +} + export const Async = () => { const [data, setData] = useState() diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 96f61d0..e5601a2 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -1,29 +1,25 @@ import { createContext, ReactNode, useContext, useMemo } from 'react' import { useForm, UseFormProps } from './useForm.js' -import { FormField } from './FormField.js' +import { FormField, FormFieldProps } from './FormField.js' import { FormFieldGroup } from './FormFieldGroup.js' import { TextInput } from '../TextInput/index.js' import { TextAreaInput } from '../TextAreaInput/index.js' import { SwitchInput } from '../SwitchInput/index.js' import { CheckboxInput } from '../CheckboxInput/index.js' import { NumberInput } from '../NumberInput/index.js' -import { Calendar } from '../Calendar/index.js' -import { Button } from '../Button/index.js' -import { format } from 'date-fns' import { SelectInput, SelectInputProps } from '../SelectInput/index.js' import { DateInput, DateInputProps } from '../DateInput/index.js' import { RichTextEditor } from '../RichTextEditor/index.js' +import { IconProps } from '../Icon/index.js' +import { FormValues } from './types.js' +import { FormSidebar } from './FormSidebar.js' const FormContext = createContext<{ - fields: FormProps['fields'] + fields: FormFields + groups: FormProps['groups'] form: ReturnType } | null>(null) -type CommonFormFieldProps = { - label?: string - description?: string -} - type TextFormField = { type: 'text' } type TextAreaFormField = { type: 'textarea' } type NumberFormField = { type: 'number' } @@ -34,29 +30,42 @@ type SelectFormField = { type: 'select' } & Pick type RichTextFormField = { type: 'richtext' } +type FormFields = { + [key: string]: ( + | TextFormField + | TextAreaFormField + | NumberFormField + | SwitchFormField + | CheckboxFormField + | DateTimeFormField + | SelectFormField + | RichTextFormField + ) & + Pick +} type FormProps = { children: ReactNode | ((form: ReturnType) => React.ReactNode) - fields: { - [key: string]: ( - | TextFormField - | TextAreaFormField - | NumberFormField - | SwitchFormField - | CheckboxFormField - | DateTimeFormField - | SelectFormField - | RichTextFormField - ) & - CommonFormFieldProps - } + groups?: { + label: string + description?: string + icon?: IconProps['variant'] + fields: string[] + }[] + fields: FormFields | ((values: FormValues) => FormFields) } & UseFormProps -function Form({ fields, children, ...useFormProps }: FormProps) { +function Form({ fields, groups, children, ...useFormProps }: FormProps) { const form = useForm(useFormProps) return ( - + {typeof children === 'function' ? children(form) : children} ) @@ -64,176 +73,209 @@ function Form({ fields, children, ...useFormProps }: FormProps) { type FormFieldsProps = { horizontal?: boolean + fullScreen?: boolean } -function FormFields({ horizontal }: FormFieldsProps) { +// TODO move into it's own component +function FormFields({ horizontal, fullScreen }: FormFieldsProps) { const ctx = useContext(FormContext) if (!ctx) { throw new Error('missing form context') } - const { fields, form } = ctx + const { fields, groups, form } = ctx // TODO do not repeat the FormFields so much, make them only once - const children = useMemo( - () => - Object.entries(fields).map(([key, field]) => { - switch (field.type) { - case 'text': - return ( - - form.setValue(key, value)} - error={!!form.errors[key]} - /> - - ) - case 'textarea': - return ( - - form.setValue(key, value)} - error={!!form.errors[key]} - /> - - ) - case 'switch': - return ( - - form.setValue(key, value)} - error={!!form.errors[key]} - /> - - ) - case 'checkbox': - return ( - - form.setValue(key, value)} - error={!!form.errors[key]} - /> - - ) - case 'number': - return ( - - form.setValue(key, value)} - error={!!form.errors[key]} - /> - - ) - case 'datetime': - return ( - - { - form.setValue(key, value) - }} - /> - - ) - case 'select': - return ( - - { - form.setValue(key, value) - }} - filterable={field.filterable} - options={field.options} - /> - - ) - case 'richtext': - return ( - - {/* TODO missing disabled and error */} - { - form.setValue(key, value) - }} - /> - - ) - default: - throw new Error('Form: unknown field type') - } - }), - [fields, form], - ) + function renderFields(fields: FormProps['fields']) { + return Object.entries(fields).map(([key, field]) => { + switch (field.type) { + case 'text': + return ( + + form.setValue(key, value)} + error={!!form.errors[key]} + /> + + ) + case 'textarea': + return ( + + form.setValue(key, value)} + error={!!form.errors[key]} + /> + + ) + case 'switch': + return ( + + form.setValue(key, value)} + error={!!form.errors[key]} + /> + + ) + case 'checkbox': + return ( + + form.setValue(key, value)} + error={!!form.errors[key]} + /> + + ) + case 'number': + return ( + + form.setValue(key, value)} + error={!!form.errors[key]} + /> + + ) + case 'datetime': + return ( + + { + form.setValue(key, value) + }} + /> + + ) + case 'select': + return ( + + { + form.setValue(key, value) + }} + filterable={field.filterable} + options={field.options} + /> + + ) + case 'richtext': + return ( + + {/* TODO missing disabled and error */} + { + form.setValue(key, value) + }} + /> + + ) + default: + throw new Error('Form: unknown field type') + } + }) + } - return {children} + if (groups) { + return groups.map((group) => ( + + {renderFields( + Object.keys(fields).reduce((acc, curr) => { + if (group.fields.includes(curr)) { + acc[curr] = fields[curr] + } + + return acc + }, {} as FormFields), + )} + + )) + } + + return ( + + {renderFields(fields)} + + ) } Form.Fields = FormFields +Form.Sidebar = FormSidebar -export { Form } +export { Form, FormContext } export type { FormProps, FormFieldsProps } diff --git a/src/components/Form/FormField.stories.tsx b/src/components/Form/FormField.stories.tsx index e4ada27..2d3091f 100644 --- a/src/components/Form/FormField.stories.tsx +++ b/src/components/Form/FormField.stories.tsx @@ -15,6 +15,17 @@ export const Vertical = () => { ) } +export const VerticalNote = () => { + return ( + + {}} /> + + ) +} + export const Horizontal = () => { return ( { ) } + +export const HorizontalNote = () => { + return ( + + {}} /> + + ) +} diff --git a/src/components/Form/FormField.tsx b/src/components/Form/FormField.tsx index 311ea6e..6571ad6 100644 --- a/src/components/Form/FormField.tsx +++ b/src/components/Form/FormField.tsx @@ -2,6 +2,7 @@ import { ReactNode } from 'react' import { Text } from '../Text/index.js' import { colors } from '../../utils/colors.js' import { Icon } from '../Icon/index.js' +import { Note, NoteProps } from '../Note/index.js' type FormFieldProps = { label?: string @@ -9,6 +10,7 @@ type FormFieldProps = { error?: string children: ReactNode horizontal?: boolean + note?: Pick } function FormField({ @@ -17,6 +19,7 @@ function FormField({ description, error, horizontal = false, + note, }: FormFieldProps) { if (horizontal) { return ( @@ -65,6 +68,7 @@ function FormField({ {description} )} + {note && } ) @@ -107,6 +111,7 @@ function FormField({ {description} )} + {note && } ) } diff --git a/src/components/Form/FormFieldGroup.tsx b/src/components/Form/FormFieldGroup.tsx index 6e4cb04..8cc2780 100644 --- a/src/components/Form/FormFieldGroup.tsx +++ b/src/components/Form/FormFieldGroup.tsx @@ -1,23 +1,40 @@ import { ReactNode } from 'react' import { Text } from '../Text/index.js' import { colors } from '../../utils/colors.js' +import { radius } from '../../utils/radius.js' type FormFieldGroupProps = { label?: string children: ReactNode + fullScreen?: boolean } -function FormFieldGroup({ label, children }: FormFieldGroupProps) { +function FormFieldGroup({ label, children, fullScreen }: FormFieldGroupProps) { return ( -
+
{label && (
{label} -
+ {!fullScreen && ( +
+ )}
)}
diff --git a/src/components/Form/FormSidebar.tsx b/src/components/Form/FormSidebar.tsx new file mode 100644 index 0000000..229f048 --- /dev/null +++ b/src/components/Form/FormSidebar.tsx @@ -0,0 +1,42 @@ +import { useContext, useEffect, useState } from 'react' +import { FormContext } from './Form.js' +import { Sidebar } from '../Sidebar/index.js' + +type FormSidebarProps = {} + +// TODO add customizable description and icon +// TODO scroll into view when clicking on an item +// TODO auto updating active based on scroll position +function FormSidebar(props: FormSidebarProps) { + const [active, setActive] = useState() + const ctx = useContext(FormContext) + + if (!ctx) { + throw new Error('missing form context') + } + + const { groups } = ctx + + useEffect(() => { + if (!active) { + setActive(groups[0].label) + } + }, [groups]) + + return ( + + + + {groups.map((group) => ( + + {group.label} + + ))} + + + + ) +} + +export { FormSidebar } +export type { FormSidebarProps } diff --git a/src/components/Form/index.tsx b/src/components/Form/index.tsx index 2fa5420..f7cb9be 100644 --- a/src/components/Form/index.tsx +++ b/src/components/Form/index.tsx @@ -2,4 +2,5 @@ export * from './useForm.js' export * from './types.js' export * from './FormField.js' export * from './FormFieldGroup.js' +export * from './FormSidebar.js' export * from './Form.js' diff --git a/src/components/Note/Note.stories.tsx b/src/components/Note/Note.stories.tsx new file mode 100644 index 0000000..c22273d --- /dev/null +++ b/src/components/Note/Note.stories.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { Note } from './index.js' +import { Button } from '../Button/index.js' + +export default { + title: 'Note', + component: Note, +} + +export const Default = { + args: { + title: 'Title', + description: 'Description', + children: , + }, +} + +export const Red = { + args: { + title: 'Title', + description: 'Description', + children: , + color: 'red', + }, +} + +export const Orange = { + args: { + title: 'Title', + description: 'Description', + children: , + color: 'orange', + }, +} + +export const Green = { + args: { + title: 'Title', + description: 'Description', + children: , + color: 'green', + }, +} + +export const Blue = { + args: { + title: 'Title', + description: 'Description', + children: , + color: 'blue', + }, +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx new file mode 100644 index 0000000..8d223fa --- /dev/null +++ b/src/components/Note/index.tsx @@ -0,0 +1,112 @@ +import { ReactNode } from 'react' +import { Icon, IconProps } from '../Icon/index.js' +import { radius } from '../../utils/radius.js' +import { Text } from '../Text/index.js' +import { Color, colors } from '../../utils/colors.js' + +type NoteProps = { + title: string + description?: string + icon?: IconProps['variant'] + children?: ReactNode + color?: 'neutral' | 'orange' | 'red' | 'green' | 'blue' +} + +function Note({ + title, + description, + color = 'neutral', + icon = 'error', + children, +}: NoteProps) { + return ( +
+ {icon && } +
+ + {title} + + {description && ( + + {description} + + )} +
+ {children && ( +
+ {children} +
+ )} +
+ ) +} + +export { Note } +export type { NoteProps } diff --git a/src/components/Patterns.stories.tsx b/src/components/Patterns.stories.tsx index abdb172..8c8746c 100644 --- a/src/components/Patterns.stories.tsx +++ b/src/components/Patterns.stories.tsx @@ -487,15 +487,12 @@ export function App() { }, { key: 'name', - type: 'title', }, { key: 'size', - type: 'description', }, { key: 'statusText', - type: 'description', }, ]} /> diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index f0e8828..40bfb04 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -15,31 +15,30 @@ type SidebarProps = { value?: string onChange: (value: string) => void children: ReactNode + side?: 'left' | 'right' } -function Sidebar({ children, value, onChange }: SidebarProps) { +function Sidebar({ children, value, onChange, side = 'left' }: SidebarProps) { return (
-