Ver código fonte

progress on forms

Tomi Cvetic 4 anos atrás
pai
commit
0759e7f760

+ 1 - 1
frontend/src/app/components/AdminPage.tsx

@@ -23,7 +23,7 @@ const AdminPage: FunctionComponent = ({ children }) => {
           .admin-page {
             min-height: 100%;
             display: grid;
-            grid-template-columns: 300px 1fr;
+            grid-template-columns: 200px 1fr;
           }
           .admin-content {
             padding: 2em 3em;

+ 10 - 4
frontend/src/form/components/Checkbox.tsx

@@ -3,11 +3,12 @@ import { DetailedHTMLProps, InputHTMLAttributes } from 'react'
 type ICheckbox = {
   value?: boolean
   label?: string
+  className?: string
 } & Omit<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, 'value'>
 
-const Checkbox = ({ name, label, id, value = false, type, ...props }: ICheckbox) => {
+const Checkbox = ({ name, label, id, value = false, className, type, ...props }: ICheckbox) => {
   return (
-    <div className='checkbox'>
+    <div className={className}>
       <label htmlFor={id || name}>
         {label || name}
         <span className={value ? 'checked' : 'unchecked'} />
@@ -17,6 +18,10 @@ const Checkbox = ({ name, label, id, value = false, type, ...props }: ICheckbox)
         input {
           display: none;
         }
+        label {
+          display: flex;
+          align-items: center;
+        }
         label span {
           position: relative;
           display: block;
@@ -24,9 +29,10 @@ const Checkbox = ({ name, label, id, value = false, type, ...props }: ICheckbox)
           height: 1.2em;
           background: transparent;
           border: 2px solid black;
+          margin-left: 0.4em;
         }
         label span.checked {
-          border: 2px solid green;
+          border: 2px solid #005500;
         }
 
         label span::after,
@@ -44,7 +50,7 @@ const Checkbox = ({ name, label, id, value = false, type, ...props }: ICheckbox)
         }
         label span.checked::before,
         label span.checked::after {
-          background-color: green;
+          background-color: #005500;
         }
 
         label span::before {

+ 1 - 0
frontend/src/form/components/DateTimeInput.tsx

@@ -79,6 +79,7 @@ const DateTimeInput = ({
         input {
           display: block;
           flex-grow: 1;
+          width: 45%;
         }
       `}</style>
     </div>

+ 44 - 48
frontend/src/training/components/BlockInputs.tsx

@@ -1,40 +1,36 @@
 import FormatSelector from './FormatSelector'
 import { TextInput } from '../../form'
 import BlockInstanceInputs from './BlockInstanceInputs'
-import {
-  emptyBlockInstance,
-  emptyExercise,
-  emptyExerciseInstance
-} from '../utils'
+import { emptyBlockInstance, emptyExercise, emptyExerciseInstance } from '../utils'
 import ExerciseInstanceInputs from './ExerciseInstanceInputs'
 import { TBlock } from '../types'
 import { useState, ChangeEvent } from 'react'
 import BlockSelector from './BlockSelector'
+import BlockList from './BlockList'
 
 interface IBlockInputs {
   onChange: GenericEventHandler
   value: TBlock
   name: string
+  className?: string
 }
 
-const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
+const BlockInputs = ({ onChange, value, name, className }: IBlockInputs) => {
+  const [state, setState] = useState({ new: {}, old: {} })
   return (
-    <>
-      <p>
-        {value.id} {value.title}
-      </p>
-      <BlockSelector
-        name={name}
-        value={value}
-        label='Existing block'
-        onChange={onChange}
-      />
-      <TextInput
-        name={`${name}.title`}
-        label='Title'
-        value={value.title}
-        onChange={onChange}
-      />
+    <div className={className}>
+      {!value.id.startsWith('++') && (
+        <div className='block-info'>
+          <div>{value.id}</div>
+          <div>{/*value.createdAt*/}</div>
+        </div>
+      )}
+      <label>New block</label>
+      <input type='radio' name='source' value='new' />
+      <label>Use existing block</label>
+      <input type='radio' name='source' value='existing' />
+      <BlockSelector name={name} value={value} label='Existing block' onChange={onChange} />
+      <TextInput name={`${name}.title`} label='Title' value={value.title} onChange={onChange} />
       <TextInput
         name={`${name}.description`}
         label='Description'
@@ -48,11 +44,7 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
         type='number'
         onChange={onChange}
       />
-      <FormatSelector
-        name={`${name}.format`}
-        value={value.format}
-        onChange={onChange}
-      />
+      <FormatSelector name={`${name}.format`} value={value.format} onChange={onChange} />
       <TextInput
         name={`${name}.rest`}
         label='Rest'
@@ -64,36 +56,35 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
         name={`${name}.videos`}
         label='Video'
         value={value.videos && value.videos.length > 0 ? value.videos[0] : ''}
-        onChange={event =>
+        onChange={(event) =>
           onChange({
             target: {
               type: 'custom',
               name: `${name}.videos`,
-              value: [event.target.value]
-            }
+              value: [event.target.value],
+            },
           })
         }
       />
       <label>Blocks</label>
-      {value.blocks && value.blocks.length > 0 && (
-        <BlockInstanceInputs
-          name={`${name}.blocks`}
-          value={value.blocks}
-          onChange={onChange}
-        />
-      )}
+      <BlockList
+        name={`${name}.blocks`}
+        value={value.blocks}
+        onChange={onChange}
+        className='training-blocks'
+      />
       <button
-        onClick={event => {
+        onClick={(event) => {
           event.preventDefault()
           const newBlock = emptyBlockInstance({
-            order: value.blocks ? value.blocks.length : 0
+            order: value.blocks ? value.blocks.length : 0,
           })
           onChange({
             target: {
               type: 'custom',
               name: `${name}.blocks`,
-              value: value.blocks ? [...value.blocks, newBlock] : [newBlock]
-            }
+              value: value.blocks ? [...value.blocks, newBlock] : [newBlock],
+            },
           })
         }}
         type='button'
@@ -109,26 +100,31 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
         />
       )}
       <button
-        onClick={event => {
+        onClick={(event) => {
           event.preventDefault()
           const newExercise = emptyExerciseInstance({
-            order: value.exercises ? value.exercises.length : 0
+            order: value.exercises ? value.exercises.length : 0,
           })
           onChange({
             target: {
               type: 'custom',
               name: `${name}.exercises`,
-              value: value.exercises
-                ? [...value.exercises, newExercise]
-                : [newExercise]
-            }
+              value: value.exercises ? [...value.exercises, newExercise] : [newExercise],
+            },
           })
         }}
         type='button'
       >
         Add exercise
       </button>
-    </>
+
+      <style jsx>{`
+        .block-info {
+          display: flex;
+          justify-content: space-between;
+        }
+      `}</style>
+    </div>
   )
 }
 

+ 70 - 124
frontend/src/training/components/BlockInstanceInputs.tsx

@@ -1,140 +1,86 @@
-import { useState, useEffect } from 'react'
-import arrayMove from 'array-move'
 import BlockInputs from './BlockInputs'
-import { SortableList } from '../../sortable'
 import { TextInput } from '../../form'
 import { TBlockInstance } from '../types'
 
 const BlockInstanceInputs = ({
-  value = [],
+  value,
   name,
   onChange,
+  className,
 }: {
-  value?: TBlockInstance[]
+  value: TBlockInstance
   name: string
   onChange: GenericEventHandler
+  className?: string
 }) => {
-  const [state, setState] = useState(value.map((item) => item.id))
-
-  function updateOrderProperty<T extends { id: U }, U>(values: T[], orderList: U[]) {
-    const orderedValues = values.map((value) => {
-      const order = orderList.findIndex((orderedId) => orderedId === value.id)
-      return { ...value, order }
-    })
-    onChange({ target: { type: 'custom', name, value: orderedValues } })
-  }
-
-  function onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
-    const newOrder = arrayMove(state, oldIndex, newIndex)
-    setState(newOrder)
-    updateOrderProperty(value, newOrder)
-  }
-
-  useEffect(() => {
-    const missingIds = value
-      .filter((item) => !state.includes(item.id))
-      .filter((item) => !item.id.startsWith('--'))
-      .map((item) => item.id)
-    const stateWithoutRemovedItems = state.filter((stateId) =>
-      value.find((item) => stateId === item.id)
-    )
-    setState([...stateWithoutRemovedItems, ...missingIds])
-  }, [value])
-
-  const items = state
-    .map((stateId) => {
-      const itemIndex = value.findIndex((item) => item.id === stateId)
-      if (itemIndex < 0) return null
-      const item = value[itemIndex]
-      return (
-        <div key={item.id}>
-          <p>{item.id}</p>
-          <TextInput
-            name={`${name}.${itemIndex}.order`}
-            label='Order'
-            value={item.order}
-            type='number'
-            onChange={onChange}
-            className='bi-order'
-          />
-          <TextInput
-            name={`${name}.${itemIndex}.rounds`}
-            label='Rounds'
-            value={item.rounds}
-            type='number'
-            onChange={onChange}
-            className='bi-rounds'
-          />
-          <TextInput
-            name={`${name}.${itemIndex}.variation`}
-            label='Variation'
-            value={item.variation}
-            onChange={onChange}
-            className='bi-variation'
-          />
-          <div className='bi-block'>
-            {item.block && (
-              <BlockInputs
-                name={`${name}.${itemIndex}.block`}
-                value={item.block}
-                onChange={onChange}
-              />
-            )}
-          </div>
-          <button
-            type='button'
-            onClick={(ev) => {
-              const newOrder = state.filter((orderedId) => item.id !== orderedId)
-              setState(newOrder)
-              const newValues = item.id?.startsWith('++')
-                ? [...value.slice(0, itemIndex), ...value.slice(itemIndex + 1)]
-                : [
-                    ...value.slice(0, itemIndex),
-                    { ...item, id: `--${item.id}` },
-                    ...value.slice(itemIndex + 1),
-                  ]
-              updateOrderProperty(newValues, newOrder)
-            }}
-            className='bi-button'
-          >
-            Delete block
-          </button>
-
-          <style jsx>{`
-            @media (min-width: 768px) {
-              div {
-                display: grid;
-                grid-template-areas:
-                  'order  variation'
-                  'rounds variation'
-                  'block  block'
-                  'button button';
-                grid-template-columns: 1fr 2fr;
-              }
-
-              div :global(.bi-order) {
-                grid-area: order;
-              }
-              div :global(.bi-rounds) {
-                grid-area: rounds;
-              }
-              div :global(.bi-variation) {
-                grid-area: variation;
-              }
-              div :global(.bi-block) {
-                grid-area: block;
-              }
-              div :global(.bi-button) {
-                grid-area: button;
-              }
-            }
-          `}</style>
+  return (
+    <div className={className}>
+      {!value.id.startsWith('++') && (
+        <div className='bi-info'>
+          <div>Block Instance ID{value.id}</div>
+          <div>Created at: {value}</div>
         </div>
-      )
-    })
-    .filter((block) => block !== null)
+      )}
+      <TextInput
+        name={`${name}.order`}
+        label='Order'
+        value={value.order}
+        type='number'
+        onChange={onChange}
+        className='bi-order'
+      />
+      <TextInput
+        name={`${name}.rounds`}
+        label='Rounds'
+        value={value.rounds}
+        type='number'
+        onChange={onChange}
+        className='bi-rounds'
+      />
+      <TextInput
+        name={`${name}.variation`}
+        label='Variation'
+        value={value.variation}
+        onChange={onChange}
+        className='bi-variation'
+      />
+      <BlockInputs
+        name={`${name}.block`}
+        value={value.block}
+        onChange={onChange}
+        className='bi-block'
+      />
+
+      <style jsx>{`
+        @media (min-width: 768px) {
+          div {
+            display: grid;
+            grid-template-areas:
+              'order  rounds variation'
+              'block  block  block'
+              'button button button';
+            grid-template-columns: 1fr 1fr 2fr;
+          }
 
-  return <SortableList items={items} onSortEnd={onSortEnd} useDragHandle lockAxis={'y'} />
+          div :global(.bi-order) {
+            grid-area: order;
+          }
+          div :global(.bi-rounds) {
+            grid-area: rounds;
+          }
+          div :global(.bi-variation) {
+            grid-area: variation;
+          }
+          div :global(.bi-block) {
+            grid-area: block;
+          }
+          div :global(.bi-button) {
+            grid-area: button;
+          }
+        }
+      `}</style>
+    </div>
+  )
 }
 
 export default BlockInstanceInputs

+ 104 - 0
frontend/src/training/components/BlockList.tsx

@@ -0,0 +1,104 @@
+import { useState, useEffect } from 'react'
+import arrayMove from 'array-move'
+import { SortableList } from '../../sortable'
+import { TBlockInstance } from '../types'
+import { emptyBlockInstance } from '../utils'
+import BlockInstanceInputs from './BlockInstanceInputs'
+
+const BlockList = ({
+  value = [],
+  name,
+  onChange,
+  className,
+}: {
+  value?: TBlockInstance[]
+  name: string
+  onChange: GenericEventHandler
+  className?: string
+}) => {
+  const [state, setState] = useState(value.map((item) => item.id))
+
+  function updateOrderProperty<T extends { id: U }, U>(values: T[], orderList: U[]) {
+    const orderedValues = values.map((value) => {
+      const order = orderList.findIndex((orderedId) => orderedId === value.id)
+      return { ...value, order }
+    })
+    onChange({ target: { type: 'custom', name, value: orderedValues } })
+  }
+
+  function onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
+    const newOrder = arrayMove(state, oldIndex, newIndex)
+    setState(newOrder)
+    updateOrderProperty(value, newOrder)
+  }
+
+  useEffect(() => {
+    const missingIds = value
+      .filter((item) => !state.includes(item.id))
+      .filter((item) => !item.id.startsWith('--'))
+      .map((item) => item.id)
+    const stateWithoutRemovedItems = state.filter((stateId) =>
+      value.find((item) => stateId === item.id)
+    )
+    setState([...stateWithoutRemovedItems, ...missingIds])
+  }, [value])
+
+  const items = state
+    .map((stateId) => {
+      const itemIndex = value.findIndex((item) => item.id === stateId)
+      if (itemIndex < 0) return null
+      const item = value[itemIndex]
+      return (
+        <BlockInstanceInputs
+          key={item.id}
+          name={`${name}.${itemIndex}`}
+          value={item}
+          onChange={onChange}
+        />
+      )
+    })
+    .filter((block) => block !== null)
+
+  return (
+    <fieldset className={className}>
+      <h2>Blocks</h2>
+      <SortableList items={items} onSortEnd={onSortEnd} useDragHandle lockAxis={'y'} />
+
+      <button
+        onClick={(event) => {
+          event.preventDefault()
+          onChange(addBlock(name, value))
+        }}
+        type='button'
+      >
+        Add block
+      </button>
+      <style jsx>
+        {`
+          h2 {
+            margin: 0.4em 0.3em;
+            padding: 0;
+          }
+          fieldset {
+            padding: 0;
+          }
+        `}
+      </style>
+    </fieldset>
+  )
+}
+
+export default BlockList
+
+export function addBlock(name: string, blockList: TBlockInstance[]) {
+  const newBlock = emptyBlockInstance({
+    order: blockList ? blockList.filter((block) => !block.id.startsWith('--')).length : 0,
+  })
+  return {
+    target: {
+      type: 'custom',
+      name,
+      value: blockList ? [...blockList, newBlock] : [newBlock],
+    },
+  }
+}

+ 23 - 29
frontend/src/training/components/EditTraining.tsx

@@ -6,6 +6,7 @@ import BlockInstanceInputs from './BlockInstanceInputs'
 import { TTraining } from '../types'
 import Registrations from './Registrations'
 import Ratings from './Ratings'
+import BlockList from './BlockList'
 
 const EditTraining = ({ training }: { training?: TTraining }) => {
   const { values, touched, onChange, loadData } = useForm(training || emptyTraining())
@@ -35,6 +36,12 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
       }}
     >
       <fieldset className='fields-training'>
+        {!values.id.startsWith('++') && (
+          <div className='training-info'>
+            <div>Training ID: {values.id}, </div>
+            <div>Created at: {values.createdAt}</div>
+          </div>
+        )}
         <TextInput
           name='title'
           label='Title'
@@ -77,34 +84,14 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
           label='Published'
           value={values.published}
           onChange={onChange}
-          className='published'
+          className='training-published'
+        />
+        <BlockList
+          name='blocks'
+          value={values.blocks}
+          onChange={onChange}
+          className='training-blocks'
         />
-        <fieldset className='training-blocks'>
-          <label>Blocks</label>
-          {values.blocks && (
-            <BlockInstanceInputs name='blocks' value={values.blocks} onChange={onChange} />
-          )}
-          <button
-            onClick={(event) => {
-              event.preventDefault()
-              const newBlock = emptyBlockInstance({
-                order: values.blocks
-                  ? values.blocks.filter((block) => !block.id.startsWith('--')).length
-                  : 0,
-              })
-              onChange({
-                target: {
-                  type: 'custom',
-                  name: 'blocks',
-                  value: values.blocks ? [...values.blocks, newBlock] : [newBlock],
-                },
-              })
-            }}
-            type='button'
-          >
-            Add block
-          </button>
-        </fieldset>
         <button type='submit' disabled={createData.loading} className='training-button'>
           Save Training
         </button>
@@ -122,13 +109,20 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
             display: grid;
             grid-gap: 0.2em 2em;
             grid-template-areas:
+              'info info info'
               'title title type'
               'time time location'
-              'published attendance empty'
-              'registrations ratings empty'
+              'published attendance empty2'
+              'registrations ratings empty3'
               'blocks blocks blocks'
               'button button button';
             grid-template-columns: 1fr 1fr 1fr;
+            align-items: center;
+          }
+          .fields-training :global(.training-info) {
+            grid-area: info;
+            display: flex;
+            justify-content: space-between;
           }
           .fields-training :global(.training-title) {
             grid-area: title;

+ 24 - 1
frontend/src/training/components/TrainingTypeSelector.tsx

@@ -38,7 +38,21 @@ const TrainingTypeSelector = ({
   return (
     <div className={className}>
       <label>{label}</label>
-      <select id={name} name={name} value={id} onChange={onChange}>
+      <select
+        id={name}
+        name={name}
+        value={id}
+        onChange={(event) => {
+          const changeEvent: CustomChangeEvent = {
+            target: {
+              type: 'custom',
+              value: { id: event.target.value },
+              name,
+            },
+          }
+          onChange(changeEvent)
+        }}
+      >
         {trainingTypes.loading && 'loading training types...'}
         {trainingTypes.error && 'error loading training types'}
         {trainingTypes.data &&
@@ -74,12 +88,21 @@ const TrainingTypeSelector = ({
       </Modal>
 
       <style jsx>{`
+        div {
+          display: flex;
+          align-items: center;
+        }
+
         label,
         select,
         button {
           display: inline-block;
           width: auto;
         }
+        select {
+          flex-grow: 1;
+          margin: 0 0.6em;
+        }
       `}</style>
     </div>
   )