This commit is contained in:
Michael Fatemi 2021-07-08 13:51:24 -04:00
parent 7ce134377f
commit 98a8e51b7b
38 changed files with 576 additions and 723 deletions

View File

@ -2,7 +2,7 @@ import { CSSProperties, lazy, Suspense, useEffect, useState } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { getReceivedInvitationsAndRequests } from './api'; import { getReceivedInvitationsAndRequests } from './api';
import { useMe } from './hooks'; import { useMe } from './hooks';
import Notifications from './Notifications'; import Notifications from './Notifications/Notifications';
import { IInvitation } from './types'; import { IInvitation } from './types';
import WheelShare from './WheelShare'; import WheelShare from './WheelShare';
import WheelShareLoggedOut from './WheelShareLoggedOut'; import WheelShareLoggedOut from './WheelShareLoggedOut';

View File

@ -1,151 +0,0 @@
import { CSSProperties } from 'react';
import {
MouseEventHandler,
ReactEventHandler,
useCallback,
useState,
} from 'react';
export type AvailabilityKind = 'going' | 'interested' | 'not-interested';
const availabilityNames: Record<AvailabilityKind, string> = {
going: 'Going',
interested: 'Interested',
'not-interested': 'Not interested',
};
const optionStyle: CSSProperties = {
height: '3rem',
backgroundColor: '#e0e0e0',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
transition: 'background-color 100ms cubic-bezier',
userSelect: 'none',
position: 'relative',
fontWeight: 'normal',
textTransform: 'uppercase',
};
const selectedOptionStyle = {
...optionStyle,
fontWeight: 600,
};
function Option({
bind,
current,
onSelected,
}: {
bind: AvailabilityKind;
current: AvailabilityKind;
onSelected: (kind: AvailabilityKind) => void;
}) {
const selected = current === bind;
const select: MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
onSelected(bind);
},
[onSelected, bind]
);
return (
<div style={selected ? selectedOptionStyle : optionStyle} onClick={select}>
{availabilityNames[bind]}
</div>
);
}
// eslint-disable-next-line
function Availability__old({
selected,
onSelected: onSelectedInner,
}: {
selected: AvailabilityKind;
onSelected: (kind: AvailabilityKind) => void;
}) {
const [focused, setFocused] = useState(false);
const onSelected = useCallback(
(kind: AvailabilityKind) => {
setFocused(false);
onSelectedInner(kind);
},
[onSelectedInner]
);
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
borderRadius: '0.5rem',
overflow: 'hidden',
marginTop: '1rem',
marginBottom: '1rem',
}}
tabIndex={0}
onBlur={() => setFocused(false)}
>
{focused ? (
<>
<Option bind="going" current={selected} onSelected={onSelected} />
<Option
bind="interested"
current={selected}
onSelected={onSelected}
/>
<Option
bind="not-interested"
current={selected}
onSelected={onSelected}
/>
</>
) : (
<Option
bind={selected}
current={selected}
onSelected={() => setFocused(true)}
/>
)}
</div>
);
}
export default function Availability({
selected,
onSelected: onSelectedInner,
}: {
selected: AvailabilityKind;
onSelected: (kind: AvailabilityKind) => void;
}) {
const onSelected: ReactEventHandler<HTMLSelectElement> = useCallback(
(event) => {
onSelectedInner(
// @ts-ignore
event.target.value
);
event.preventDefault();
},
[onSelectedInner]
);
return (
<select
value={selected}
onChange={onSelected}
style={{
fontFamily: 'Inter',
fontSize: '1rem',
border: '0px solid black',
borderRadius: '0.5rem',
padding: '0.5rem',
marginTop: '1rem',
}}
>
<option value="going">Going</option>
<option value="interested">Interested</option>
<option value="not-interested">Not interested</option>
</select>
);
}

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { getCarpool } from './api'; import { getCarpool } from './api';
import { ICarpool } from './types'; import { ICarpool } from './types';
import UISecondaryBox from './UISecondaryBox'; import UISecondaryBox from './UI/UISecondaryBox';
function MemberList({ members }: { members: ICarpool['members'] }) { function MemberList({ members }: { members: ICarpool['members'] }) {
return ( return (

View File

@ -1,409 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
addOrUpdateEventSignup,
getEventSignups,
removeEventSignup,
} from './api';
import { green, lightgrey } from './colors';
import { useMe } from './hooks';
import latlongdist, { R_miles } from './latlongdist';
import UIButton from './UIButton';
import UIPlacesAutocomplete from './UIPlacesAutocomplete';
import UISecondaryBox from './UISecondaryBox';
import UISecondaryHeader from './UISecondaryHeader';
import usePlace from './usePlace';
import useThrottle from './useThrottle';
import useToggle from './useToggle';
export type IEvent = {
id: number;
name: string;
group: string;
formattedAddress: string;
startTime: string;
endTime: string;
latitude: number;
longitude: number;
};
function formatStartAndEndTime(
startDatetimeString: string,
endDatetimeString: string
) {
const startDatetime = new Date(startDatetimeString);
const endDatetime = new Date(endDatetimeString);
if (isNaN(startDatetime.valueOf())) {
console.error('Invalid datetime:', startDatetimeString);
return '(invalid)';
}
if (isNaN(endDatetime.valueOf())) {
console.error('Invalid datetime:', startDatetimeString);
return '(invalid)';
}
const startDateString = startDatetime.toLocaleDateString();
const endDateString = endDatetime.toLocaleDateString();
if (startDateString === endDateString) {
const startTimeString = startDatetime.toLocaleTimeString();
const endTimeString = endDatetime.toLocaleTimeString();
return `${startDateString}, ${startTimeString} - ${endTimeString}`;
} else {
return `${startDatetime.toLocaleString()} - ${endDatetime.toLocaleString()}`;
}
}
function GroupName({ name }: { name: string }) {
return (
<span
style={{
color: '#303030',
textAlign: 'center',
}}
>
{name}
</span>
);
}
function Details({
startTime,
endTime,
formattedAddress,
}: {
startTime: string;
endTime: string;
formattedAddress: string;
}) {
return (
<div
style={{
marginTop: '0.5rem',
textAlign: 'left',
}}
>
<span
style={{
color: '#303030',
}}
>
<b>When: </b>
{formatStartAndEndTime(startTime, endTime)}
</span>
<br />
<br />
<span
style={{
color: '#303030',
}}
>
<b>Where: </b>
{formattedAddress}
</span>
</div>
);
}
export type ICarpool = {
driver: {
id: number;
name: string;
};
startTime: string;
endTime: string;
extraDistance: number;
};
function CarpoolRow({ carpool }: { carpool: ICarpool }) {
const PADDING = '1rem';
return (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
padding: PADDING,
borderRadius: '0.5rem',
border: '1px solid #e0e0e0',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}
>
<div>
<span style={{ fontWeight: 500 }}>{carpool.driver.name}</span>
<br />
Time:{' '}
<b>
{carpool.startTime} - {carpool.endTime}
</b>
<br />
Offset from route: <b>{carpool.extraDistance} miles</b>
</div>
<div
style={{
borderRadius: '0.5em',
cursor: 'pointer',
padding: '0.5em',
position: 'absolute',
right: PADDING,
userSelect: 'none',
backgroundColor: '#e0e0e0',
}}
>
Request to join
</div>
</div>
);
}
const dummyCarpoolData: ICarpool[] = [
{
driver: {
id: 0,
name: 'Michael Fatemi',
},
startTime: '10:00',
endTime: '10:10',
extraDistance: 6.9,
},
{
driver: {
id: 1,
name: 'Joshua Hsueh',
},
startTime: '10:05',
endTime: '10:10',
extraDistance: 420,
},
];
function Carpools({ event }: { event: IEvent }) {
// eslint-disable-next-line
const [carpools, _setCarpools] = useState(dummyCarpoolData);
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>Carpools</h3>
{carpools.map((carpool) => (
<CarpoolRow carpool={carpool} key={carpool.driver.id} />
))}
</div>
);
}
export type IEventSignup = {
user: {
id: number;
name: number;
};
placeId: string;
formattedAddress: string;
latitude: number;
longitude: number;
};
function Signups({
event,
signups,
myPlaceId,
}: {
event: IEvent;
signups: IEventSignup[];
myPlaceId: string | null;
}) {
const PADDING = '1rem';
const placeDetails = usePlace(myPlaceId);
const locationLongitude = event.latitude;
const locationLatitude = event.longitude;
const me = useMe();
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>People</h3>
{signups.map(({ latitude, longitude, user }) => {
if (user.id === me?.id) {
return null;
}
let extraDistance = null;
if (placeDetails != null) {
const myLatitude = placeDetails.latitude;
const myLongitude = placeDetails.longitude;
const meToThem = latlongdist(
latitude,
longitude,
locationLongitude,
locationLatitude,
R_miles
);
const themToLocation = latlongdist(
latitude,
longitude,
myLatitude,
myLongitude,
R_miles
);
const totalWithThem = meToThem + themToLocation;
const totalWithoutThem = latlongdist(
locationLongitude,
locationLatitude,
myLatitude,
myLongitude,
R_miles
);
extraDistance = totalWithThem - totalWithoutThem;
}
return (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
padding: '1rem',
borderRadius: '0.5rem',
border: '1px solid #e0e0e0',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}
key={user.id}
>
<b>{user.name}</b>
{extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''}
<div
style={{
borderRadius: '0.5em',
cursor: 'pointer',
padding: '0.5em',
position: 'absolute',
right: PADDING,
userSelect: 'none',
backgroundColor: '#e0e0e0',
}}
>
Invite to carpool
</div>
</div>
);
})}
</div>
);
}
export default function Event({ event }: { event: IEvent }) {
const { name, group, formattedAddress, startTime, endTime } = event;
const [haveRide, toggleHaveRide] = useToggle(false);
const [placeId, setPlaceId] = useState<string | null>(null);
const [interested, setInterested] = useState(false);
const [updating, setUpdating] = useState(false);
const [signups, setSignups] = useState<IEventSignup[]>([]);
const toggleInterested = useCallback(() => setInterested((i) => !i), []);
const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
const existingSignup = useRef({
interested: false,
placeId: null as string | null,
eventId: null as number | null,
});
const me = useMe();
useEffect(() => {
const removeSignup = () => {
if (prev.interested) {
removeEventSignup(event.id)
.then(() => {
prev.interested = false;
})
.finally(() => setUpdating(false));
}
};
const addOrUpdateSignup = () => {
if (!prev.interested) {
addOrUpdateEventSignup(event.id, placeId)
.then(() => {
prev.placeId = placeId;
prev.eventId = event.id;
prev.interested = true;
})
.finally(() => setUpdating(false));
}
};
const prev = existingSignup.current;
if (!interested) {
removeSignup();
} else {
addOrUpdateSignup();
}
}, [event.id, interested, placeId, updating]);
useEffect(() => {
getEventSignups(event.id)
.then((signups) => {
setSignups(signups);
for (let signup of signups) {
if (signup.user.id === me?.id) {
setInterested(true);
existingSignup.current.eventId = event.id;
existingSignup.current.placeId = signup.placeId;
existingSignup.current.interested = true;
}
}
})
.catch(console.error);
}, [event.id, me?.id]);
return (
<UISecondaryBox>
<UISecondaryHeader>{name}</UISecondaryHeader>
<GroupName name={group} />
<Details {...{ startTime, endTime, formattedAddress }} />
<UIButton
onClick={toggleInterestedThrottled}
style={{
backgroundColor: interested ? green : lightgrey,
color: interested ? 'white' : 'black',
transition: 'color 0.2s, background-color 0.2s',
}}
>
{interested ? 'Interested' : 'Not interested'}
</UIButton>
{interested && (
<>
<UIPlacesAutocomplete
placeholder="Pickup and dropoff location"
onSelected={(_address, placeID) => {
setPlaceId(placeID);
}}
style={placeId != null ? { border: '2px solid ' + green } : {}}
/>
{false && (
<div
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none',
}}
onClick={toggleHaveRide}
>
<input
type="checkbox"
style={{
borderRadius: '0.5em',
width: '2em',
height: '2em',
margin: '1em',
}}
checked={haveRide}
/>
I don't have any way to get there yet
</div>
)}
<Carpools event={event} />
<Signups event={event} myPlaceId={placeId} signups={signups} />
</>
)}
</UISecondaryBox>
);
}

View File

@ -0,0 +1,162 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
addOrUpdateEventSignup,
getEventSignups,
removeEventSignup,
} from '../api';
import { green, lightgrey } from '../colors';
import { useMe } from '../hooks';
import UIButton from '../UI/UIButton';
import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UISecondaryBox from '../UI/UISecondaryBox';
import UISecondaryHeader from '../UI/UISecondaryHeader';
import useThrottle from '../useThrottle';
import useToggle from '../useToggle';
import EventCarpools from './EventCarpools';
import EventDetails from './EventDetails';
import EventSignups from './EventSignups';
export type IEvent = {
id: number;
name: string;
group: string;
formattedAddress: string;
startTime: string;
endTime: string;
latitude: number;
longitude: number;
};
function GroupName({ name }: { name: string }) {
return <span style={{ color: '#303030', textAlign: 'center' }}>{name}</span>;
}
export type IEventSignup = {
user: {
id: number;
name: number;
};
placeId: string;
formattedAddress: string;
latitude: number;
longitude: number;
};
export default function Event({ event }: { event: IEvent }) {
const { name, group, formattedAddress, startTime, endTime } = event;
const [haveRide, toggleHaveRide] = useToggle(false);
const [placeId, setPlaceId] = useState<string | null>(null);
const [interested, setInterested] = useState(false);
const [updating, setUpdating] = useState(false);
const [signups, setSignups] = useState<IEventSignup[]>([]);
const toggleInterested = useCallback(() => setInterested((i) => !i), []);
const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
const existingSignup = useRef({
interested: false,
placeId: null as string | null,
eventId: null as number | null,
});
const me = useMe();
useEffect(() => {
const removeSignup = () => {
if (prev.interested) {
removeEventSignup(event.id)
.then(() => {
prev.interested = false;
})
.finally(() => setUpdating(false));
}
};
const addOrUpdateSignup = () => {
if (!prev.interested) {
addOrUpdateEventSignup(event.id, placeId)
.then(() => {
prev.placeId = placeId;
prev.eventId = event.id;
prev.interested = true;
})
.finally(() => setUpdating(false));
}
};
const prev = existingSignup.current;
if (!interested) {
removeSignup();
} else {
addOrUpdateSignup();
}
}, [event.id, interested, placeId, updating]);
useEffect(() => {
getEventSignups(event.id)
.then((signups) => {
setSignups(signups);
for (let signup of signups) {
if (signup.user.id === me?.id) {
setInterested(true);
existingSignup.current.eventId = event.id;
existingSignup.current.placeId = signup.placeId;
existingSignup.current.interested = true;
}
}
})
.catch(console.error);
}, [event.id, me?.id]);
return (
<UISecondaryBox>
<UISecondaryHeader>{name}</UISecondaryHeader>
<GroupName name={group} />
<EventDetails {...{ startTime, endTime, formattedAddress }} />
<UIButton
onClick={toggleInterestedThrottled}
style={{
backgroundColor: interested ? green : lightgrey,
color: interested ? 'white' : 'black',
transition: 'color 0.2s, background-color 0.2s',
}}
>
{interested ? 'Interested' : 'Not interested'}
</UIButton>
{interested && (
<>
<UIPlacesAutocomplete
placeholder="Pickup and dropoff location"
onSelected={(_address, placeID) => {
setPlaceId(placeID);
}}
style={placeId != null ? { border: '2px solid ' + green } : {}}
/>
{false && (
<div
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none',
}}
onClick={toggleHaveRide}
>
<input
type="checkbox"
style={{
borderRadius: '0.5em',
width: '2em',
height: '2em',
margin: '1em',
}}
checked={haveRide}
/>
I don't have any way to get there yet
</div>
)}
<EventCarpools event={event} />
<EventSignups event={event} myPlaceId={placeId} signups={signups} />
</>
)}
</UISecondaryBox>
);
}

View File

@ -0,0 +1,89 @@
import { useState } from 'react';
import { IEvent } from './Event';
export type ICarpool = {
driver: {
id: number;
name: string;
};
startTime: string;
endTime: string;
extraDistance: number;
};
function CarpoolRow({ carpool }: { carpool: ICarpool }) {
const PADDING = '1rem';
return (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
padding: PADDING,
borderRadius: '0.5rem',
border: '1px solid #e0e0e0',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}
>
<div>
<span style={{ fontWeight: 500 }}>{carpool.driver.name}</span>
<br />
Time:{' '}
<b>
{carpool.startTime} - {carpool.endTime}
</b>
<br />
Offset from route: <b>{carpool.extraDistance} miles</b>
</div>
<div
style={{
borderRadius: '0.5em',
cursor: 'pointer',
padding: '0.5em',
position: 'absolute',
right: PADDING,
userSelect: 'none',
backgroundColor: '#e0e0e0',
}}
>
Request to join
</div>
</div>
);
}
const dummyCarpoolData: ICarpool[] = [
{
driver: {
id: 0,
name: 'Michael Fatemi',
},
startTime: '10:00',
endTime: '10:10',
extraDistance: 6.9,
},
{
driver: {
id: 1,
name: 'Joshua Hsueh',
},
startTime: '10:05',
endTime: '10:10',
extraDistance: 420,
},
];
export default function Carpools({ event }: { event: IEvent }) {
// eslint-disable-next-line
const [carpools, _setCarpools] = useState(dummyCarpoolData);
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>Carpools</h3>
{carpools.map((carpool) => (
<CarpoolRow carpool={carpool} key={carpool.driver.id} />
))}
</div>
);
}

View File

@ -0,0 +1,39 @@
import formatStartAndEndTime from '../dates';
export default function Details({
startTime,
endTime,
formattedAddress,
}: {
startTime: string;
endTime: string;
formattedAddress: string;
}) {
return (
<div
style={{
marginTop: '0.5rem',
textAlign: 'left',
}}
>
<span
style={{
color: '#303030',
}}
>
<b>When: </b>
{formatStartAndEndTime(startTime, endTime)}
</span>
<br />
<br />
<span
style={{
color: '#303030',
}}
>
<b>Where: </b>
{formattedAddress}
</span>
</div>
);
}

View File

@ -0,0 +1,91 @@
import { useMe } from '../hooks';
import latlongdist, { R_miles } from '../latlongdist';
import usePlace from '../usePlace';
import { IEvent, IEventSignup } from './Event';
export default function Signups({
event,
signups,
myPlaceId,
}: {
event: IEvent;
signups: IEventSignup[];
myPlaceId: string | null;
}) {
const PADDING = '1rem';
const placeDetails = usePlace(myPlaceId);
const locationLongitude = event.latitude;
const locationLatitude = event.longitude;
const me = useMe();
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>People</h3>
{signups.map(({ latitude, longitude, user }) => {
if (user.id === me?.id) {
return null;
}
let extraDistance = null;
if (placeDetails != null) {
const myLatitude = placeDetails.latitude;
const myLongitude = placeDetails.longitude;
const meToThem = latlongdist(
latitude,
longitude,
locationLongitude,
locationLatitude,
R_miles
);
const themToLocation = latlongdist(
latitude,
longitude,
myLatitude,
myLongitude,
R_miles
);
const totalWithThem = meToThem + themToLocation;
const totalWithoutThem = latlongdist(
locationLongitude,
locationLatitude,
myLatitude,
myLongitude,
R_miles
);
extraDistance = totalWithThem - totalWithoutThem;
}
return (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
padding: '1rem',
borderRadius: '0.5rem',
border: '1px solid #e0e0e0',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}
key={user.id}
>
<b>{user.name}</b>
{extraDistance ? `: +${extraDistance.toFixed(1)} miles` : ''}
<div
style={{
borderRadius: '0.5em',
cursor: 'pointer',
padding: '0.5em',
position: 'absolute',
right: PADDING,
userSelect: 'none',
backgroundColor: '#e0e0e0',
}}
>
Invite to carpool
</div>
</div>
);
})}
</div>
);
}

View File

@ -0,0 +1,86 @@
import { Dispatch, SetStateAction, useCallback } from 'react';
import { toggleBit } from '../bits';
import { green, lightgrey } from '../colors';
const DAY_NAMES = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
export default function DaysOfWeekSelector({
daysOfWeek,
update,
disabled = false,
}: {
daysOfWeek: number;
update: Dispatch<SetStateAction<number>>;
disabled?: boolean;
}) {
const toggleDayOfWeek = useCallback(
function (idx: 1 | 2 | 3 | 4 | 5 | 6 | 7) {
update((daysOfWeek) => toggleBit(daysOfWeek, idx));
},
[update]
);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
margin: '1rem auto',
}}
>
{DAY_NAMES.map((name, idx) => {
const mask = 0b1000_0000 >> (idx + 1);
const active = (daysOfWeek & mask) !== 0;
return (
<div
style={{
borderRadius: '100%',
cursor: 'pointer',
backgroundColor: active
? disabled
? // lighter version of green
'rgba(96, 247, 96, 0.5)'
: green
: disabled
? // lighter version of lightgrey
'rgba(224, 224, 224, 0.5)'
: lightgrey,
color: active
? 'white'
: disabled
? 'rgba(0, 0, 0, 0.5)'
: 'black',
userSelect: 'none',
width: '2em',
height: '2em',
margin: '0.5rem',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={() => {
if (!disabled) {
toggleDayOfWeek(
// @ts-ignore
idx + 1
);
}
}}
key={name}
>
{name.charAt(0)}
</div>
);
})}
</div>
);
}

View File

@ -1,101 +1,18 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { createEvent } from './api'; import { createEvent } from '../api';
import { toggleBit } from './bits'; import { green, lightgrey } from '../colors';
import { green, lightgrey } from './colors'; import { IGroup } from '../Group';
import { IGroup } from './Group'; import UIButton from '../UI/UIButton';
import UIButton from './UIButton'; import UIDateInput from '../UI/UIDateInput';
import UIDateInput from './UIDateInput'; import UIDatetimeInput from '../UI/UIDatetimeInput';
import UIDatetimeInput from './UIDatetimeInput'; import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UIPlacesAutocomplete from './UIPlacesAutocomplete'; import UISecondaryBox from '../UI/UISecondaryBox';
import UISecondaryBox from './UISecondaryBox'; import UITextInput from '../UI/UITextInput';
import UITextInput from './UITextInput'; import useToggle from '../useToggle';
import useToggle from './useToggle'; import DaysOfWeekSelector from './DaysOfWeekSelector';
const noop = () => {}; const noop = () => {};
const DAY_NAMES = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
function DaysOfWeekSelector({
daysOfWeek,
update,
disabled = false,
}: {
daysOfWeek: number;
update: Dispatch<SetStateAction<number>>;
disabled?: boolean;
}) {
const toggleDayOfWeek = useCallback(
function (idx: 1 | 2 | 3 | 4 | 5 | 6 | 7) {
update((daysOfWeek) => toggleBit(daysOfWeek, idx));
},
[update]
);
return (
<div
style={{
display: 'flex',
flexDirection: 'row',
margin: '1rem auto',
}}
>
{DAY_NAMES.map((name, idx) => {
const mask = 0b1000_0000 >> (idx + 1);
const active = (daysOfWeek & mask) !== 0;
return (
<div
style={{
borderRadius: '100%',
cursor: 'pointer',
backgroundColor: active
? disabled
? // lighter version of green
'rgba(96, 247, 96, 0.5)'
: green
: disabled
? // lighter version of lightgrey
'rgba(224, 224, 224, 0.5)'
: lightgrey,
color: active
? 'white'
: disabled
? 'rgba(0, 0, 0, 0.5)'
: 'black',
userSelect: 'none',
width: '2em',
height: '2em',
margin: '0.5rem',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={() => {
if (!disabled) {
toggleDayOfWeek(
// @ts-ignore
idx + 1
);
}
}}
key={name}
>
{name.charAt(0)}
</div>
);
})}
</div>
);
}
export default function EventCreator({ group }: { group: IGroup }) { export default function EventCreator({ group }: { group: IGroup }) {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [startTime, setStartTime] = useState<Date | null>(null); const [startTime, setStartTime] = useState<Date | null>(null);

View File

@ -1,6 +1,6 @@
import EventCreator from './EventCreator'; import EventCreator from './EventCreator';
import { IGroup } from './Group'; import { IGroup } from '../Group';
import useToggle from './useToggle'; import useToggle from '../useToggle';
export default function EventCreatorLink({ group }: { group: IGroup }) { export default function EventCreatorLink({ group }: { group: IGroup }) {
const [open, toggle] = useToggle(false); const [open, toggle] = useToggle(false);

View File

@ -1,4 +1,4 @@
import Event, { IEvent } from './Event'; import Event, { IEvent } from './Event/Event';
export default function EventStream({ events }: { events: IEvent[] }) { export default function EventStream({ events }: { events: IEvent[] }) {
return ( return (

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getEvents } from './api'; import { getEvents } from './api';
import { IEvent } from './Event'; import { IEvent } from './Event/Event';
import EventStream from './EventStream'; import EventStream from './EventStream';
export default function Events() { export default function Events() {

View File

@ -2,11 +2,11 @@ import { useEffect, useState } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getGroup, getGroupEvents } from './api'; import { getGroup, getGroupEvents } from './api';
import { IEvent } from './Event'; import { IEvent } from './Event/Event';
import EventCreatorLink from './EventCreatorLink'; import EventCreatorLink from './EventCreator/EventCreatorLink';
import EventStream from './EventStream'; import EventStream from './EventStream';
import GroupSettingsLink from './GroupSettingsLink'; import GroupSettingsLink from './GroupSettings/GroupSettingsLink';
import UILink from './UILink'; import UILink from './UI/UILink';
export type IGroup = { export type IGroup = {
id: number; id: number;

View File

@ -1,9 +1,9 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { createGroup } from './api'; import { createGroup } from '../api';
import UIButton from './UIButton'; import UIButton from '../UI/UIButton';
import UILink from './UILink'; import UILink from '../UI/UILink';
import UISecondaryBox from './UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
import UITextInput from './UITextInput'; import UITextInput from '../UI/UITextInput';
export default function GroupCreator() { export default function GroupCreator() {
const [name, setName] = useState(''); const [name, setName] = useState('');

View File

@ -1,5 +1,5 @@
import GroupCreator from './GroupCreator'; import GroupCreator from './GroupCreator';
import useToggle from './useToggle'; import useToggle from '../useToggle';
export default function GroupCreatorLink() { export default function GroupCreatorLink() {
const [open, toggle] = useToggle(false); const [open, toggle] = useToggle(false);

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { joinGroup, resolveCode } from './api'; import { joinGroup, resolveCode } from './api';
import UIButton from './UIButton'; import UIButton from './UI/UIButton';
import UIPressable from './UIPressable'; import UIPressable from './UI/UIPressable';
import UISecondaryBox from './UISecondaryBox'; import UISecondaryBox from './UI/UISecondaryBox';
import UITextInput from './UITextInput'; import UITextInput from './UI/UITextInput';
import useToggle from './useToggle'; import useToggle from './useToggle';
export type GroupPreview = { export type GroupPreview = {

View File

@ -1,12 +1,11 @@
import { useCallback, useState } from 'react'; import { useState, useCallback } from 'react';
import { deleteGroup } from './api'; import { deleteGroup } from '../api';
import { IGroup } from './Group'; import { IGroup } from '../types';
import UILink from './UILink'; import UILink from '../UI/UILink';
import UIPressable from './UIPressable'; import UIPressable from '../UI/UIPressable';
import UISecondaryBox from './UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
import useToggle from './useToggle';
function GroupSettings({ group }: { group: IGroup }) { export default function GroupSettings({ group }: { group: IGroup }) {
const [deletionSuccessful, setDeletionSuccessful] = const [deletionSuccessful, setDeletionSuccessful] =
useState<boolean | null>(null); useState<boolean | null>(null);
@ -41,27 +40,3 @@ function GroupSettings({ group }: { group: IGroup }) {
</UISecondaryBox> </UISecondaryBox>
); );
} }
export default function GroupSettingsLink({ group }: { group: IGroup }) {
const [open, toggle] = useToggle(false);
return (
<div>
<div
style={{
cursor: 'pointer',
userSelect: 'none',
}}
onClick={toggle}
>
Settings
</div>
{open && (
<>
<br />
<GroupSettings group={group} />
</>
)}
</div>
);
}

View File

@ -0,0 +1,27 @@
import { IGroup } from '../Group';
import useToggle from '../useToggle';
import GroupSettings from './GroupSettings';
export default function GroupSettingsLink({ group }: { group: IGroup }) {
const [open, toggle] = useToggle(false);
return (
<div>
<div
style={{
cursor: 'pointer',
userSelect: 'none',
}}
onClick={toggle}
>
Settings
</div>
{open && (
<>
<br />
<GroupSettings group={group} />
</>
)}
</div>
);
}

View File

@ -1,5 +1,5 @@
import { IGroup } from './Group'; import { IGroup } from '../Group';
import UISecondaryBox from './UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
function GroupListItem({ group }: { group: IGroup }) { function GroupListItem({ group }: { group: IGroup }) {
return ( return (

View File

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getGroups } from './api'; import { getGroups } from '../api';
import { IGroup } from './Group'; import { IGroup } from '../Group';
import GroupCreatorLink from './GroupCreatorLink'; import GroupCreatorLink from '../GroupCreator/GroupCreatorLink';
import GroupJoinerLink from './GroupJoinerLink'; import GroupJoinerLink from '../GroupJoinerLink';
import GroupList from './GroupList'; import GroupList from './GroupList';
export default function Groups() { export default function Groups() {

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { acceptInvite, acceptRequest, denyInvite, denyRequest } from './api'; import { acceptInvite, acceptRequest, denyInvite, denyRequest } from '../api';
import { IInvitation } from './types'; import { IInvitation } from '../types';
import UIButton from './UIButton'; import UIButton from '../UI/UIButton';
export default function Notification({ export default function Notification({
notification, notification,

View File

@ -1,5 +1,5 @@
import Notification from './Notification'; import Notification from './Notification';
import { IInvitation } from './types'; import { IInvitation } from '../types';
export default function Notifications({ export default function Notifications({
notifications, notifications,

View File

@ -1,9 +1,9 @@
import logout from './Authentication/logout'; import logout from './Authentication/logout';
import Events from './Events'; import Events from './Events';
import Groups from './Groups'; import Groups from './Groups/Groups';
import { useMe } from './hooks'; import { useMe } from './hooks';
import UIPressable from './UIPressable'; import UIPressable from './UI/UIPressable';
import UIPrimaryTitle from './UIPrimaryTitle'; import UIPrimaryTitle from './UI/UIPrimaryTitle';
export default function WheelShare() { export default function WheelShare() {
const { name } = useMe()!; const { name } = useMe()!;

View File

@ -1,6 +1,6 @@
import authorizationEndpoint from './Authentication/authorizationEndpoint'; import authorizationEndpoint from './Authentication/authorizationEndpoint';
import UILink from './UILink'; import UILink from './UI/UILink';
import UIPrimaryTitle from './UIPrimaryTitle'; import UIPrimaryTitle from './UI/UIPrimaryTitle';
export default function WheelShareLoggedOut() { export default function WheelShareLoggedOut() {
return ( return (

View File

@ -1,4 +1,4 @@
import { IEventSignup } from './Event'; import { IEventSignup } from './Event/Event';
import { GroupPreview } from './GroupJoinerLink'; import { GroupPreview } from './GroupJoinerLink';
import { IInvitation } from './types'; import { IInvitation } from './types';

27
src/components/dates.ts Normal file
View File

@ -0,0 +1,27 @@
export default function formatStartAndEndTime(
startDatetimeString: string,
endDatetimeString: string
) {
const startDatetime = new Date(startDatetimeString);
const endDatetime = new Date(endDatetimeString);
if (isNaN(startDatetime.valueOf())) {
console.error('Invalid datetime:', startDatetimeString);
return '(invalid)';
}
if (isNaN(endDatetime.valueOf())) {
console.error('Invalid datetime:', startDatetimeString);
return '(invalid)';
}
const startDateString = startDatetime.toLocaleDateString();
const endDateString = endDatetime.toLocaleDateString();
if (startDateString === endDateString) {
const startTimeString = startDatetime.toLocaleTimeString();
const endTimeString = endDatetime.toLocaleTimeString();
return `${startDateString}, ${startTimeString} - ${endTimeString}`;
} else {
return `${startDatetime.toLocaleString()} - ${endDatetime.toLocaleString()}`;
}
}