mirror of
https://github.com/myfatemi04/wheelshare-frontend.git
synced 2025-04-21 11:20:17 -04:00
Organize
This commit is contained in:
parent
7ce134377f
commit
98a8e51b7b
|
@ -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';
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
162
src/components/Event/Event.tsx
Normal file
162
src/components/Event/Event.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
89
src/components/Event/EventCarpools.tsx
Normal file
89
src/components/Event/EventCarpools.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
39
src/components/Event/EventDetails.tsx
Normal file
39
src/components/Event/EventDetails.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
91
src/components/Event/EventSignups.tsx
Normal file
91
src/components/Event/EventSignups.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
86
src/components/EventCreator/DaysOfWeekSelector.tsx
Normal file
86
src/components/EventCreator/DaysOfWeekSelector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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 (
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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('');
|
|
@ -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);
|
|
@ -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 = {
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
27
src/components/GroupSettings/GroupSettingsLink.tsx
Normal file
27
src/components/GroupSettings/GroupSettingsLink.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
|
@ -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() {
|
|
@ -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,
|
|
@ -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,
|
|
@ -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()!;
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
27
src/components/dates.ts
Normal 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()}`;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user