Skip to content

Commit

Permalink
Make availability look pretty
Browse files Browse the repository at this point in the history
  • Loading branch information
domdomegg committed Apr 26, 2024
1 parent 4b695c9 commit a4bc8da
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 96 deletions.
3 changes: 0 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"tailwindCSS.experimental.classRegex": [
["clsx\\(([^)]*)\\)", "'([^']*)'"]
],
"tailwindCSS.experimental.configFile": {
"apps/availability/tailwind.config.ts": "libraries/ui/**"
},
"cSpell.words": [
"BlueDot",
"bluedotimpact",
Expand Down
8 changes: 0 additions & 8 deletions apps/availability/src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@

/* TODO: move these styles out of here */

.setup input {
@apply px-3 py-2 text-gray-700 rounded border focus:outline focus:outline-gray-400;
}

.setup button {
@apply px-3 py-1 rounded text-white bg-blue-500 hover:bg-blue-600 disabled:bg-blue-300;
}

.timezone-select.solid-select-container {
@apply w-32;
}
Expand Down
137 changes: 64 additions & 73 deletions apps/availability/src/pages/form/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import {
Controller, FormProvider, useForm, useFormContext,
} from 'react-hook-form';
import Select from 'react-select';
import {
Box, Button, H1, Input,
Textarea,
} from '@bluedot/ui';
import clsx from 'clsx';
import { SpinnerIcon } from '../../components/SpinnerIcon';
import { parseOffsetFromStringToMinutes, offsets } from '../../lib/date';
import { pad, snapToRect } from '../../lib/util';
Expand All @@ -21,7 +26,7 @@ const serializeCoord = ({ day, time }: Coord) => `${day},${time}`;

// consts
const MINUTES_IN_UNIT = 30;
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
const browserTimezoneName = new Intl.DateTimeFormat().resolvedOptions().timeZone;

// utils
Expand Down Expand Up @@ -228,25 +233,25 @@ const TimeAvWidget: React.FC<{ show24: boolean }> = ({ show24 }) => {
}, [cellRefs, dragState, mainGrid, setValue]);

return (
<div className="w-full touch-none">
<div className="w-full touch-none text-xs text-stone-500">
<div className="flex">
<div className="w-12" />
<div className="grid grid-cols-7 w-full text-sm sm:text-base">
{days.map((day) => <div key={day} className="h-8 mx-auto">{day}</div>)}
<div className="grid grid-cols-7 w-full text-center">
{days.map((day) => <div key={day}>{day.slice(0, 1)}</div>)}
</div>
</div>
<div className="flex">
<div className="w-12">
{times.map((time, i) => i % 2 === 0 && (
<div key={time} className="h-8 text-xs text-gray-700 flex justify-end px-1 py-px">
<div key={time} className="h-8 flex justify-end px-1 py-px">
<div className="-translate-y-2">{timeToLabel(time)}</div>
</div>
))}
</div>
<div className="w-full">
<div
ref={mainGrid}
className="grid grid-cols-7 bg-gray-400 border-t border-l border-gray-800 w-full"
className="grid grid-cols-7 bg-white border-t border-l border-gray-800 w-full"
>
{cellCoords.map((coord, i) => {
const isBlocked = timeAv[serializeCoord(coord)];
Expand Down Expand Up @@ -276,9 +281,7 @@ const TimeAvWidget: React.FC<{ show24: boolean }> = ({ show24 }) => {
// eslint-disable-next-line react/no-array-index-key
key={i}
ref={(ref) => { cellRefs[i] = { ref, coord }; }}
className={`relative h-4 ${
isBlocked ? 'bg-green-400' : 'bg-red-50'
} border-gray-800 border-r border-b ${borderStyle}`}
className={clsx(`relative h-4 border-gray-800 border-r border-b ${borderStyle}`, isBlocked && 'bg-green-400')}
onMouseDown={(e) => {
e.preventDefault();
dragStart(coord);
Expand All @@ -305,55 +308,53 @@ const TimeAv: React.FC = () => {
const { setValue } = useFormContext<FormData>();

return (
<div className="mt-4 flex justify-between space-x-2">
<div className="sm:flex gap-4">
<TimeAvWidget show24={show24} />
<div className="w-20 sm:w-32 flex flex-col justify-start items-end space-y-2">
<div className="h-6" />
<button
type="button"
className="w-full px-1 py-1 text-xs text-gray-500 rounded border border-gray-300 hover:shadow"
onClick={() => setShow24(!show24)}
>
{show24 ? (
<>
<span className="hidden sm:inline">Show less hours</span>
<span className="sm:hidden">Less hours</span>
</>
) : (
<>
<span className="hidden sm:inline">Show 24 hours</span>
<span className="sm:hidden">24 hours</span>
</>
)}
</button>
<button type="button" className="w-full px-1 py-1 text-xs text-gray-500 rounded border border-gray-300 hover:shadow" onClick={() => setValue('timeAv', {})}>
<div className="sm:w-40 sm:mt-4 flex sm:flex-col gap-2">
<Button className="w-full text-sm" onPress={() => setShow24(!show24)}>
Show {show24 ? 'less' : 'more'}
</Button>
<Button className="w-full text-sm" onPress={() => setValue('timeAv', {})}>
Clear
</button>
</Button>
</div>
</div>
);
};

const TimeOffsetSelector: React.FC = () => {
const TimeOffsetSelector: React.FC<{ className?: string }> = ({ className }) => {
const { control } = useFormContext<FormData>();
const options = offsets.map((s) => ({ value: s, label: s }));

const [detected, setDetected] = useState(true);

return (
<div>
<label className="text-xs text-gray-500 mb-1">Time offset ({detected ? `Automatically set for ${browserTimezoneName}` : 'Overwritten'})</label>
<div className={className}>
<label className="text-xs text-stone-500 block">Time offset {detected ? `(Automatically set to ${browserTimezoneName})` : ''}</label>
<Controller
render={({ field: { onChange, value, ref } }) => (
<Select
ref={ref}
options={options}
className="w-60"
className="w-full"
value={{ value, label: value }}
onChange={(val) => {
setDetected(false);
onChange(val?.value);
}}
theme={(theme) => ({
...theme,
borderRadius: 2,
colors: {
...theme.colors,
primary: '#0037FF',
},
})}
classNames={{
control: (state) => clsx('!border-2 !border-stone-200 !rounded-sm !min-h-0 !shadow-none', state.isFocused && '!border-bluedot-normal'),
valueContainer: () => '!py-0',
dropdownIndicator: () => '!py-0',
}}
/>
)}
name="timezone"
Expand Down Expand Up @@ -434,60 +435,50 @@ const Form: React.FC<{
}

return (
<div className="text-gray-700">
<div className="max-w-md mx-auto my-4 px-4 sm:p-0">
<div className="text-2xl">{title}</div>
<div className="text-lg">Time availability form</div>
<div className="space-y-2 mt-4 text-sm">
<p>Submit your time availability so that we can schedule the weekly discussion sessions at times that suit you. Your discussion sessions will be at the same time each week.</p>
<p><b>Please indicate all the times that you will be regularly free during the weeks of the course.</b></p>
<p>It’s okay if you can’t make the odd week here and there - there will be an option to switch cohort for a week if you can’t make your usual time.</p>
</div>
</div>
<div className="bg-gray-50 min-h-screen py-4">
<div className="max-w-md mx-auto px-4 sm:p-0 text-sm space-y-2">
<div>
<label className="text-xs text-gray-500 mb-1">Email (Don't change if pre-filled)<br />
<input
<div className="bg-cream-normal py-16 px-4">
<Box className="max-w-2xl mx-auto">
<div className="m-12">
<H1 className="!text-5xl">{title}</H1>
<div className="space-y-2 mt-4">
<p>Submit your availability so we can schedule your discussion sessions at times that suit you.</p>
</div>
<div className="grid sm:grid-cols-2 gap-4 sm:gap-2 mt-6">
<label className="text-xs text-stone-500 block">Email<br />
<Input
type="text"
placeholder="Email"
className="px-3 py-2 text-gray-700 rounded border focus:outline focus:outline-gray-400 w-60"
placeholder="[email protected]"
className="w-full"
{...register('email')}
/>
</label>
<TimeOffsetSelector />
</div>
<TimeOffsetSelector />
<div className="h-2" />
<div className="text-xs text-gray-500 space-y">
<p>Click and drag to select your availability. Times are in your selected time offset - note that daylight savings may change your offset during the course, but your cohort will usually stay at the same UTC time.</p>
<p />
<div className="text-xs text-stone-500 mt-6 mb-4 space-y-2">
<p>Click and drag to indicate the times you will be regularly free during the course. It’s okay if you can’t make the odd week here and there - you can switch cohort for weeks where you can’t make your usual time.</p>
</div>
<TimeAv />
<div>
<textarea
cols={40}
rows={3}
placeholder="Additional comments"
className="px-2 py-1 w-full text-xs text-gray-700 rounded border focus:outline focus:outline-gray-400"
<label className="text-xs text-stone-500 block mt-4">(Optional) Additional comments<br />
<Textarea
className="w-full mt-1"
{...register('comment')}
/>
</div>
<div className="flex items-center pt-6 pb-16 setup space-x-3">
</label>
<div className="mt-6">
{submitting && <div className="flex w-full justify-center"><SpinnerIcon /></div>}
{!submitting && (
<>
<button type="button" onClick={submit} disabled={!isValidEmail() || !longEnoughInterval()}>
Submit
</button>
<p className="text-[10px] text-gray-500 leading-3">
{!isValidEmail() && 'Please input a valid email. '}
{!longEnoughInterval() && `Please fill out at least one interval of length at least ${minLength} minutes.`}
<p className="text-xs text-stone-500 mb-1">
{!isValidEmail() && <>Input a valid email.<br /></>}
{!longEnoughInterval() && `Fill out at least one interval of length at least ${minLength} minutes.`}
</p>
<Button onPress={submit} disabled={!isValidEmail() || !longEnoughInterval()}>
Submit
</Button>
</>
)}
</div>
</div>
</div>
</Box>
</div>
);
};
Expand Down Expand Up @@ -532,7 +523,7 @@ const FormWrapper: React.FC = () => {

if (info.type === 'error') {
return (
<div className="w-full h-screen flex justify-center items-center text-gray-700">
<div className="w-full h-screen flex justify-center items-center">
Error loading form: {info.error}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/meet/src/components/CustomNameView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Button } from '@bluedot/ui';
import { Button, Input } from '@bluedot/ui';
import { PageState } from '../lib/client/pageState';
import useJoinAs from '../lib/client/useJoinAs';
import { Page } from './Page';
Expand All @@ -23,7 +23,7 @@ const CustomNameView: React.FC<CustomNameViewProps> = ({ page: { cohortClassId }
<>
<p>If you're sure this is the meeting for you, enter your name below</p>
<form className="flex gap-2 mt-2" onSubmit={(event) => { event.preventDefault(); return joinAs({ name }); }}>
<input type="text" autoComplete="name" placeholder="Your name" className="px-2 py-1 rounded border-2 focus-visible:outline-1 focus-visible:outline-bluedot-normal" value={name} onChange={(value) => setName(value.target.value)} />
<Input type="text" autoComplete="name" placeholder="Your name" value={name} onChange={(value) => setName(value.target.value)} />
<Button onPress={() => joinAs({ name })}>Join now</Button>
</form>
</>
Expand Down
8 changes: 5 additions & 3 deletions apps/meet/src/components/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Box } from '@bluedot/ui';

export const Page: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<div className="tw-preflight">
<div className="min-h-screen bg-cream-normal py-16">
<main className="max-w-3xl border-2 border-stone-300 mx-4 md:mx-auto">
<div className="min-h-screen bg-cream-normal py-16 px-4">
<Box className="max-w-3xl mx-auto">
<div className="m-12">
{children}
</div>
</main>
</Box>
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/meet/src/components/SelectPersonView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import useAxios from 'axios-hooks';
import { useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import { Button, Link } from '@bluedot/ui';
import { Button, Input, Link } from '@bluedot/ui';
import { PageState } from '../lib/client/pageState';
import { MeetingParticipantsRequest, MeetingParticipantsResponse } from '../pages/api/public/meeting-participants';
import useJoinAs from '../lib/client/useJoinAs';
Expand Down Expand Up @@ -121,7 +121,7 @@ const SelectPersonView: React.FC<SelectPersonViewProps> = ({ page: { cohortId },
</Link>
</div>
<div className="mt-4">
<label><input type="checkbox" checked={joinWithApp} onChange={(event) => setJoinWithApp(event.target.checked)} /> Join with the Zoom app</label>
<label><Input type="checkbox" checked={joinWithApp} onChange={(event) => setJoinWithApp(event.target.checked)} /> Join with the Zoom app</label>
</div>
</>
)}
Expand Down
8 changes: 4 additions & 4 deletions apps/meet/src/pages/record-attendance.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSearchParams } from 'next/navigation';
import useAxios from 'axios-hooks';
import { useState } from 'react';
import { Button } from '@bluedot/ui';
import { Button, Input } from '@bluedot/ui';
import { Page } from '../components/Page';
import { H1 } from '../components/Text';
import { RecordAttendanceRequest, RecordAttendanceResponse } from './api/public/record-attendance';
Expand Down Expand Up @@ -60,11 +60,11 @@ const RecordAttendancePage: React.FC<{ cohortClassId: string, participantId: str
{['Used native Zoom app', 'Used direct Zoom link in browser', 'Joined with wrong name', 'Joined with a custom name', 'Not sure, but I attended'].map((reason) => <Button onPress={() => recordAttendance({ reason })}>{reason}</Button>)}
</div>

<div className="mt-4 flex md:w-1/2">
<div className="mt-4 flex gap-2 md:w-1/2">
<label className="flex items-center flex-1">Other:
<input type="text" value={otherReason} onChange={(event) => setOtherReason(event.currentTarget.value)} className="ml-2 w-full px-2 py-1 rounded border-2 focus-visible:outline-1 focus-visible:outline-bluedot-normal" />
<Input type="text" value={otherReason} onChange={(value) => setOtherReason(value.target.value)} className="ml-2 w-full" />
</label>
<Button onPress={() => recordAttendance({ reason: otherReason })} disabled={!otherReason.length} className="ml-2">Submit</Button>
<Button onPress={() => recordAttendance({ reason: otherReason })} disabled={!otherReason.length}>Submit</Button>
</div>
</Page>
);
Expand Down
5 changes: 4 additions & 1 deletion libraries/eslint-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ const rules = {
],
optionalDependencies: false,
}],
'jsx-a11y/label-has-associated-control': ['error', {
controlComponents: ['Input', 'Textarea'],
}],
// Almost always a false positive on the <Link> component
'jsx-a11y/anchor-is-valid': ['off']
'jsx-a11y/anchor-is-valid': ['off'],
};

/** @type {import("eslint").Linter.RulesRecord} */
Expand Down
13 changes: 13 additions & 0 deletions libraries/ui/src/Box.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import clsx from 'clsx';

export type BoxProps = React.PropsWithChildren<{
className?: string
}>;

export const Box: React.FC<BoxProps> = ({ children, className }) => {
return (
<div className={clsx('border-2 border-stone-300', className)}>
{children}
</div>
);
};
20 changes: 20 additions & 0 deletions libraries/ui/src/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import clsx from 'clsx';
import { forwardRef } from 'react';

export type InputProps = {
className?: string
}
& React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
& React.RefAttributes<HTMLInputElement>;

export const Input: React.ForwardRefExoticComponent<InputProps> = forwardRef(({ className, ...props }, ref) => {
if (props.type === 'checkbox') {
return (
<input className={className} {...props} ref={ref} />
);
}

return (
<input className={clsx('px-2 py-1 border-2 border-stone-200 rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-bluedot-normal text-base text-black bg-white', props.disabled && 'opacity-40 pointer-events-none', className)} {...props} ref={ref} />
);
});
Loading

0 comments on commit a4bc8da

Please sign in to comment.