Skip to content

Commit

Permalink
1st working version
Browse files Browse the repository at this point in the history
  • Loading branch information
fhennig committed Sep 30, 2024
1 parent 3abcfaf commit bbc2bf1
Show file tree
Hide file tree
Showing 9 changed files with 513 additions and 254 deletions.
146 changes: 146 additions & 0 deletions website/src/components/Group/GroupForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useState, type FC, type FormEvent } from "react";

Check failure on line 1 in website/src/components/Group/GroupForm.tsx

View workflow job for this annotation

GitHub Actions / Check format and types

There should be at least one empty line between import groups
import type { NewGroup } from "../../types/backend";
import { ErrorFeedback } from '../ErrorFeedback.tsx';
import { AddressLineOneInput, AddressLineTwoInput, CityInput, CountryInput, EmailContactInput, GroupNameInput, InstitutionNameInput, PostalCodeInput, StateInput } from "./Inputs";

Check failure on line 4 in website/src/components/Group/GroupForm.tsx

View workflow job for this annotation

GitHub Actions / Check format and types

`./Inputs` import should occur before type import of `../../types/backend`
import useClientFlag from "../../hooks/isClient";

Check failure on line 5 in website/src/components/Group/GroupForm.tsx

View workflow job for this annotation

GitHub Actions / Check format and types

`../../hooks/isClient` import should occur before type import of `../../types/backend`

interface GroupFormProps {
title: string;
buttonText: string;
defaultGroupData?: NewGroup;
onSubmit: (group: NewGroup) => Promise<GroupSubmitResult>;

}

export type GroupSubmitSuccess = {
succeeded: true;
nextPageHref: string;
};
export type GroupSubmitError = {
succeeded: false;
errorMessage: string;
};
export type GroupSubmitResult = GroupSubmitSuccess | GroupSubmitError;

const chooseCountry = 'Choose a country...';

export const GroupForm: FC<GroupFormProps> = ({title, buttonText, defaultGroupData, onSubmit}) => {
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);

const internalOnSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

const formData = new FormData(e.currentTarget);

const groupName = formData.get(fieldMapping.groupName.id) as string;
const institution = formData.get(fieldMapping.institution.id) as string;
const contactEmail = formData.get(fieldMapping.contactEmail.id) as string;
const country = formData.get(fieldMapping.country.id) as string;
const line1 = formData.get(fieldMapping.line1.id) as string;
const line2 = formData.get(fieldMapping.line2.id) as string;
const city = formData.get(fieldMapping.city.id) as string;
const state = formData.get(fieldMapping.state.id) as string;
const postalCode = formData.get(fieldMapping.postalCode.id) as string;

if (country === chooseCountry) {
setErrorMessage('Please choose a country');
return false;
}

const group: NewGroup = {
groupName,
institution,
contactEmail,
address: { line1, line2, city, postalCode, state, country },
};

const result = await onSubmit(group);

if (result.succeeded) {
window.location.href = result.nextPageHref;
} else {
setErrorMessage(result.errorMessage);
}
};

const isClient = useClientFlag();

return (
<div className='p-4 max-w-6xl mx-auto'>
<h2 className='title'>{title}</h2>

{errorMessage !== undefined && (
<ErrorFeedback message={errorMessage} onClose={() => setErrorMessage(undefined)} />
)}

<form onSubmit={internalOnSubmit}>
<div className='border-b border-gray-900/10 pb-12 '>
<p className='mt-1 text-sm leading-6 text-gray-600'>
The information you enter on this form will be publicly available on your group page.
</p>

<div className='mt-5 grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-6'>
<GroupNameInput defaultValue={defaultGroupData?.groupName}/>
<EmailContactInput defaultValue={defaultGroupData?.contactEmail} />
<InstitutionNameInput defaultValue={defaultGroupData?.institution} />
<AddressLineOneInput defaultValue={defaultGroupData?.address.line1} />
<AddressLineTwoInput defaultValue={defaultGroupData?.address.line2} />
<CityInput defaultValue={defaultGroupData?.address.city} />
<StateInput defaultValue={defaultGroupData?.address.state} />
<PostalCodeInput defaultValue={defaultGroupData?.address.postalCode} />
<CountryInput defaultValue={defaultGroupData?.address.country} />
</div>

<div className='flex justify-end py-8 gap-4 '>
<button
type='submit'
className='btn btn-primary px-4 py-2 loculusColor text-white rounded'
disabled={!isClient}
>
{buttonText}
</button>
</div>
</div>
</form>
</div>
);
}

const fieldMapping = {
groupName: {
id: 'group-name',
required: true,
},
institution: {
id: 'institution-name',
required: true,
},
contactEmail: {
id: 'email',
required: true,
},
country: {
id: 'country',
required: true,
},
line1: {
id: 'address-line-1',
required: true,
},
line2: {
id: 'address-line-2',
required: false,
},
city: {
id: 'city',
required: true,
},
state: {
id: 'state',
required: false,
},
postalCode: {
id: 'postal-code',
required: true,
},
} as const;
237 changes: 237 additions & 0 deletions website/src/components/Group/Inputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { type ComponentProps, type FC, type FormEvent, type PropsWithChildren, useState } from 'react';

Check failure on line 1 in website/src/components/Group/Inputs.tsx

View workflow job for this annotation

GitHub Actions / Check format and types

'FormEvent' is defined but never used

Check failure on line 1 in website/src/components/Group/Inputs.tsx

View workflow job for this annotation

GitHub Actions / Check format and types

'useState' is defined but never used

import { listOfCountries } from './listOfCountries.ts';

const chooseCountry = 'Choose a country...';

const fieldMapping = {
groupName: {
id: 'group-name',
required: true,
},
institution: {
id: 'institution-name',
required: true,
},
contactEmail: {
id: 'email',
required: true,
},
country: {
id: 'country',
required: true,
},
line1: {
id: 'address-line-1',
required: true,
},
line2: {
id: 'address-line-2',
required: false,
},
city: {
id: 'city',
required: true,
},
state: {
id: 'state',
required: false,
},
postalCode: {
id: 'postal-code',
required: true,
},
} as const;

const groupCreationCssClass =
'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6';

type LabelledInputContainerProps = PropsWithChildren<{
label: string;
htmlFor: string;
className: string;
required?: boolean;
}>;

const LabelledInputContainer: FC<LabelledInputContainerProps> = ({ children, label, htmlFor, className, required }) => (
<div className={className}>
<label htmlFor={htmlFor} className='block text-sm font-medium leading-6 text-gray-900'>
{label}
{required === true && <span className='ml-1 text-red-600'>*</span>}
</label>
<div className='mt-1'>{children}</div>
</div>
);

type TextInputProps = {
className: string;
label: string;
name: string;
fieldMappingKey: keyof typeof fieldMapping;
type: ComponentProps<'input'>['type'];
defaultValue?: string;
};

const TextInput: FC<TextInputProps> = ({ className, label, name, fieldMappingKey, type, defaultValue }) => (
<LabelledInputContainer
className={className}
label={label}
htmlFor={name}
required={fieldMapping[fieldMappingKey].required}
>
<input
type={type}
name={name}
required={fieldMapping[fieldMappingKey].required}
id={fieldMapping[fieldMappingKey].id}
className={groupCreationCssClass}
autoComplete={type === 'email' ? 'email' : undefined}
defaultValue={defaultValue}
/>
</LabelledInputContainer>
);

type GroupNameInputProps = {
defaultValue?: string;
};

export const GroupNameInput: FC<GroupNameInputProps> = ({ defaultValue }) => (
<TextInput
className='sm:col-span-4'
type='text'
label='Group name'
name='group-name'
fieldMappingKey='groupName'
defaultValue={defaultValue}
/>
);

type InstitutionNameInputProps = {
defaultValue?: string;
};

export const InstitutionNameInput: FC<InstitutionNameInputProps> = ({ defaultValue }) => (
<TextInput
className='sm:col-span-4'
type='text'
label='Institution'
name='institution-name'
fieldMappingKey='institution'
defaultValue={defaultValue}
/>
);

type EmailContactInputProps = {
defaultValue?: string;
};

export const EmailContactInput: FC<EmailContactInputProps> = ({ defaultValue }) => (
<TextInput
className='sm:col-span-4'
type='email'
label='Contact email address'
name='email'
fieldMappingKey='contactEmail'
defaultValue={defaultValue}
/>
);

type CountryInputProps = {
defaultValue?: string;
};

export const CountryInput: FC<CountryInputProps> = ({ defaultValue }) => (
<LabelledInputContainer label='Country' htmlFor='country' className='sm:col-span-3' required>
<select
id={fieldMapping.country.id}
name='country'
required={fieldMapping.country.required}
autoComplete='country-name'
className='block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:max-w-xs sm:text-sm sm:leading-6'
defaultValue={defaultValue}
>
<option>{chooseCountry}</option>
{listOfCountries.map((country) => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
</LabelledInputContainer>
);

type AddressLineOneInputProps = {
defaultValue?: string;
};

export const AddressLineOneInput: FC<AddressLineOneInputProps> = ({ defaultValue }) => (
<TextInput
className='col-span-full'
type='text'
label='Address Line 1'
name='address-line-1'
fieldMappingKey='line1'
defaultValue={defaultValue}
/>
);

type AddressLineTwoInputProps = {
defaultValue?: string;
};

export const AddressLineTwoInput: FC<AddressLineTwoInputProps> = ({ defaultValue }) => (
<TextInput
className='col-span-full'
type='text'
label='Address Line 2'
name='address-line-2'
fieldMappingKey='line2'
defaultValue={defaultValue}
/>
);

type CityInputProps = {
defaultValue?: string;
};

export const CityInput: FC<CityInputProps> = ({ defaultValue }) => (
<TextInput
className='sm:col-span-2 sm:col-start-1'
type='text'
label='City'
name='city'
fieldMappingKey='city'
defaultValue={defaultValue}
/>
);

type StateInputProps = {
defaultValue?: string;
};

export const StateInput: FC<StateInputProps> = ({ defaultValue }) => (
<TextInput
className='sm:col-span-2'
type='text'
label='State / Province'
name='state'
fieldMappingKey='state'
defaultValue={defaultValue}
/>
);


type PostalCodeInputProps = {
defaultValue?: string;
};

export const PostalCodeInput: FC<PostalCodeInputProps> = ({ defaultValue }) => (
<TextInput
className='sm:col-span-2'
type='text'
label='ZIP / Postal code'
name='postal-code'
fieldMappingKey='postalCode'
defaultValue={defaultValue}
/>
);
Loading

0 comments on commit bbc2bf1

Please sign in to comment.