UI improvements

This commit is contained in:
Michael Fatemi 2021-07-15 18:40:26 -04:00
parent cb7a6c8d05
commit 6529d1c39f
16 changed files with 172 additions and 196 deletions

View File

@ -1,6 +1,7 @@
import { CSSProperties, lazy, Suspense } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import NotificationsProvider from '../state/Notifications/NotificationsProvider';
import Header from './Header/Header';
import { useMe } from './hooks';
import WheelShare from './WheelShare';
import WheelShareLoggedOut from './WheelShareLoggedOut';
@ -31,11 +32,8 @@ export default function App() {
<Switch>
{user ? (
<NotificationsProvider>
<Header />
<Route path="/" exact component={WheelShare} />
<Route
component={Authenticator}
path="/auth/:provider/callback"
/>
<Route path="/carpools/:id" component={CarpoolPage} />
<Route path="/events/:id" component={EventPage} />
<Route path="/groups/:id" component={Group} />

View File

@ -1,14 +1,8 @@
import { useParams } from 'react-router-dom';
import Header from '../Header/Header';
import Carpool from './Carpool';
export default function CarpoolPage() {
const id = +useParams<{ id: string }>().id;
return (
<>
<Header />
<Carpool id={id} />
</>
);
return <Carpool id={id} />;
}

View File

@ -1,19 +1,14 @@
import { useCallback, useEffect, useState } from 'react';
import { green, lightgrey } from '../../lib/colors';
import getPlaceDetails from '../../lib/getPlaceDetails';
import { addOrUpdateEventSignup, getEvent, removeEventSignup } from '../api';
import { useMe } from '../hooks';
import { getEvent } from '../api';
import { IEvent } from '../types';
import UIButton from '../UI/UIButton';
import UILink from '../UI/UILink';
import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UISecondaryBox from '../UI/UISecondaryBox';
import UISecondaryHeader from '../UI/UISecondaryHeader';
import useImmutable from '../useImmutable';
import EventCarpools from './EventCarpools';
import EventContext from './EventContext';
import EventDetails from './EventDetails';
import EventSignups from './EventSignups';
import EventInterestForm from './EventInterestForm';
import EventPlaceholder from './EventPlaceholder';
function GroupName({ group }: { group: IEvent['group'] }) {
return <UILink href={`/groups/${group.id}`}>{group.name}</UILink>;
@ -26,89 +21,28 @@ export default function Event({
id: number;
initial?: IEvent;
}) {
const [event, setEvent] = useImmutable<IEvent>({
id,
name: '',
group: {
id: 0,
name: '',
},
signups: {},
carpools: [],
startTime: '',
endTime: '',
daysOfWeek: 0,
placeId: '',
formattedAddress: '',
latitude: 0,
longitude: 0,
duration: 0,
...(initial || {}),
});
const [found, setFound] = useState(false);
const me = useMe() || { id: 0, name: '' };
const [event, setEvent] = useImmutable<IEvent | null>(initial ?? null);
const [loading, setLoading] = useState(true);
const [tentativeInvites] = useImmutable<Record<number, boolean>>({});
const refresh = useCallback(() => {
getEvent(id).then((e) => {
if (e) {
setFound(true);
setEvent(e);
} else {
setFound(false);
}
});
setLoading(true);
getEvent(id)
.then((e) => e && setEvent(e))
.finally(() => setLoading(false));
}, [id, setEvent]);
useEffect(refresh, [refresh]);
const updateSignup = useCallback(
async (placeId: string | null) => {
await addOrUpdateEventSignup(id, placeId);
if (placeId) {
const details = await getPlaceDetails(placeId);
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId,
...details,
};
} else {
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId: null,
latitude: null,
longitude: null,
formattedAddress: null,
};
}
},
[event.signups, id, me.id, me.name]
);
const removeSignup = useCallback(async () => {
await removeEventSignup(id);
if (event.signups[me.id]) {
delete event.signups[me.id];
}
}, [id, me.id, event.signups]);
const interested = !!event.signups[me.id];
if (!found) {
return (
<>
<h1>Event Not Found</h1>
</>
);
if (loading) {
return <EventPlaceholder />;
}
const { name, group, formattedAddress, startTime, endTime } = event;
if (!event) {
return <h1>Event Not Found</h1>;
}
const { name, group } = event;
return (
<EventContext.Provider
@ -124,36 +58,8 @@ export default function Event({
<UISecondaryHeader>{name}</UISecondaryHeader>
{group && <GroupName group={group} />}
</div>
<EventDetails {...{ startTime, endTime, formattedAddress }} />
<UIButton
onClick={interested ? () => removeSignup() : () => updateSignup(null)}
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) => {
updateSignup(placeId);
}}
style={
event.signups[me.id]?.placeId != null
? { border: '2px solid ' + green }
: {}
}
placeId={event.signups[me.id]?.placeId}
/>
<br />
<EventCarpools />
{event.signups !== null && <EventSignups />}
</>
)}
<EventDetails />
<EventInterestForm />
</UISecondaryBox>
</EventContext.Provider>
);

View File

@ -1,16 +1,13 @@
import formatStartAndEndTime from '../../lib/dates';
import EventIcon from '@material-ui/icons/Event';
import LocationOnIcon from '@material-ui/icons/LocationOn';
import { useContext } from 'react';
import EventContext from './EventContext';
export default function EventDetails() {
const { startTime, endTime, formattedAddress } =
useContext(EventContext).event;
export default function Details({
startTime,
endTime,
formattedAddress,
}: {
startTime: string;
endTime: string | null;
formattedAddress: string;
}) {
return (
<div
style={{

View File

@ -37,3 +37,7 @@ export function useMyCarpool() {
return carpool;
}
export function useMutableEvent() {
return useContext(EventContext).event;
}

View File

@ -0,0 +1,84 @@
import { useCallback } from 'react';
import { green, lightgrey } from '../../lib/colors';
import getPlaceDetails from '../../lib/getPlaceDetails';
import { addOrUpdateEventSignup, removeEventSignup } from '../api';
import { useMe } from '../hooks';
import UIButton from '../UI/UIButton';
import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import EventCarpools from './EventCarpools';
import { useMutableEvent } from './EventHooks';
import EventSignups from './EventSignups';
export default function EventInterestForm() {
const event = useMutableEvent();
const me = useMe() || { id: 0, name: '' };
const updateSignup = useCallback(
async (placeId: string | null) => {
await addOrUpdateEventSignup(event.id, placeId);
if (placeId) {
const details = await getPlaceDetails(placeId);
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId,
...details,
};
} else {
event.signups[me.id] = {
user: { id: me.id, name: me.name },
placeId: null,
latitude: null,
longitude: null,
formattedAddress: null,
};
}
},
[event.id, event.signups, me.id, me.name]
);
const removeSignup = useCallback(async () => {
await removeEventSignup(event.id);
if (event.signups[me.id]) {
delete event.signups[me.id];
}
}, [event.id, event.signups, me.id]);
const interested = !!event.signups[me.id];
return (
<>
<UIButton
onClick={interested ? () => removeSignup() : () => updateSignup(null)}
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) => {
updateSignup(placeId);
}}
style={
event.signups[me.id]?.placeId != null
? { border: '2px solid ' + green }
: {}
}
placeId={event.signups[me.id]?.placeId}
/>
<br />
<EventCarpools />
{event.signups !== null && <EventSignups />}
</>
)}
</>
);
}

View File

@ -1,14 +1,8 @@
import { useParams } from 'react-router-dom';
import Header from '../Header/Header';
import Event from './Event';
export default function EventPage() {
const id = +useParams<{ id: string }>().id;
return (
<>
<Header />
<Event id={id} />
</>
);
return <Event id={id} />;
}

View File

@ -0,0 +1,7 @@
import UISecondaryBox from '../UI/UISecondaryBox';
export default function EventPlaceholder() {
return (
<UISecondaryBox style={{ height: '10rem' }}>Loading...</UISecondaryBox>
);
}

View File

@ -84,7 +84,7 @@ export default function EventCreator({ group }: { group: IGroup }) {
]);
return (
<UISecondaryBox style={{ width: '100%', boxSizing: 'border-box' }}>
<UISecondaryBox style={{ width: '100%', textAlign: 'center' }}>
<h1 style={{ textAlign: 'center', marginBottom: '0.5rem' }}>
Create Event
</h1>

View File

@ -1,5 +1,6 @@
import { useContext } from 'react';
import { GroupContext } from '../Group/Group';
import UIPressable from '../UI/UIPressable';
import useToggle from '../useToggle';
import EventCreator from './EventCreator';
@ -8,22 +9,14 @@ export default function EventCreatorLink() {
const { group } = useContext(GroupContext);
return (
<div>
<div
style={{
cursor: 'pointer',
userSelect: 'none',
}}
onClick={toggle}
>
Create Event
</div>
<>
<UIPressable onClick={toggle}>Create Event</UIPressable>
{open && (
<>
<br />
<EventCreator group={group} />
</>
)}
</div>
</>
);
}

View File

@ -19,48 +19,50 @@ const DEFAULT_GROUP = (): IGroup => ({
export const GroupContext = createContext({ group: DEFAULT_GROUP() });
export default function Group({ id }: { id: number }) {
const [group, setGroup] = useImmutable<IGroup>(DEFAULT_GROUP());
const [found, setFound] = useState(false);
const [group, setGroup] = useImmutable<IGroup | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
getGroup(id)
.then(setGroup)
.catch(() => setFound(false));
.finally(() => setLoading(false));
}, [id, setGroup]);
return found ? (
if (loading) {
return <h1>Loading...</h1>;
}
return group ? (
<GroupContext.Provider value={{ group }}>
<h1>{group.name}</h1>
<div
style={{
textAlign: 'center',
maxWidth: '30rem',
marginLeft: 'auto',
marginRight: 'auto',
display: 'flex',
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
<h1>{group.name}</h1>
<UILink href="/">Home</UILink>
<br />
<br />
<GroupMembersLink />
<br />
<GroupSettingsLink />
<br />
<EventCreatorLink />
<br />
{group.events.length > 0 ? (
<EventStream events={group.events} />
) : (
<span>
There are no events yet. Click 'create event' above to add one!
</span>
)}
</div>
<br />
{group.events.length > 0 ? (
<EventStream events={group.events} />
) : (
<span>
There are no events yet. Click 'create event' above to add one!
</span>
)}
</GroupContext.Provider>
) : (
<>
<h1>Group not found</h1>
</>
<h1>Group not found</h1>
);
}

View File

@ -1,4 +1,4 @@
import { useCallback, useContext } from 'react';
import { useCallback, useContext, useState } from 'react';
import { lightgrey } from '../../lib/colors';
import { generateCode, resetCode } from '../api';
import UIButton from '../UI/UIButton';
@ -7,6 +7,8 @@ import { GroupContext } from './Group';
export default function GroupInviteCodeGenerator() {
const { group } = useContext(GroupContext);
const [shown, setShown] = useState(false);
const generateJoinCode = useCallback(() => {
generateCode(group.id).then((code) => {
group.joinCode = code;
@ -22,11 +24,15 @@ export default function GroupInviteCodeGenerator() {
if (group.joinCode) {
return (
<>
<span>
<span style={{ userSelect: 'none' }}>
Join this group with the code{' '}
<b>
<code>{group.joinCode}</code>
</b>
<code
style={{ userSelect: 'text' }}
onClick={() => setShown((shown) => !shown)}
>
{shown ? group.joinCode : 'XXXXXX'}
</code>{' '}
(click to show/hide)
</span>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<UIButton

View File

@ -17,14 +17,15 @@ export default function GroupMembersLink() {
{open && (
<>
<br />
<UISecondaryBox>
<UISecondaryBox style={{ width: '100%', textAlign: 'center' }}>
<h1>Members</h1>
<GroupInviteCodeGenerator />
{group.users.map(({ name }) => (
<span key={name}>{name}</span>
))}
<br />
<GroupInviteCodeGenerator />
</UISecondaryBox>
</>
)}

View File

@ -20,7 +20,7 @@ export default function GroupSettings({ group }: { group: IGroup }) {
}, [group.id]);
return (
<UISecondaryBox>
<UISecondaryBox style={{ width: '100%', textAlign: 'center' }}>
<h1>Settings</h1>
{deletionSuccessful !== true && (
<UIPressable onClick={onClickedDelete}>Delete Group</UIPressable>

View File

@ -1,4 +1,5 @@
import { useContext } from 'react';
import UIPressable from '../UI/UIPressable';
import useToggle from '../useToggle';
import { GroupContext } from './Group';
import GroupSettings from './GroupSettings';
@ -8,22 +9,14 @@ export default function GroupSettingsLink() {
const { group } = useContext(GroupContext);
return (
<div>
<div
style={{
cursor: 'pointer',
userSelect: 'none',
}}
onClick={toggle}
>
Settings
</div>
<>
<UIPressable onClick={toggle}>Settings</UIPressable>
{open && (
<>
<br />
<GroupSettings group={group} />
</>
)}
</div>
</>
);
}

View File

@ -1,13 +1,10 @@
import ActiveCarpools from './ActiveCarpools/ActiveCarpools';
import ActiveEvents from './ActiveEvents/Events';
import Groups from './Groups/Groups';
import Header from './Header/Header';
export default function WheelShare() {
return (
<>
<Header />
<Groups />
<ActiveCarpools />
<ActiveEvents />