make invitation list responsive, add carpool context

This commit is contained in:
Michael Fatemi 2021-07-13 14:10:11 -04:00
parent 16a0ae696f
commit efd596f80e
7 changed files with 205 additions and 179 deletions

6
.env
View File

@ -1,3 +1,5 @@
REACT_APP_API_DOMAIN_=http://localhost:5000/
REACT_APP_API_DOMAIN__=https://wheelshare-altbackend-2efyw.ondigitalocean.app/
REACT_APP_API_DOMAIN_LOCAL=http://localhost:5000/
REACT_APP_API_DOMAIN_DOCN=https://wheelshare-altbackend-2efyw.ondigitalocean.app/
REACT_APP_API_DOMAIN_WWW=https://api.wheelshare.app/
REACT_APP_API_DOMAIN=https://api.wheelshare.app/

View File

@ -1,20 +1,27 @@
import EventIcon from '@material-ui/icons/Event';
import LocationOnIcon from '@material-ui/icons/LocationOn';
import MailOutlineIcon from '@material-ui/icons/MailOutline';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useEffect, useState } from 'react';
import { createContext } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ICarpool } from '../types';
import UISecondaryBox from '../UI/UISecondaryBox';
import MemberList from './MemberList';
import InvitationList from './InvitationList';
import UIButton from '../UI/UIButton';
import { cancelCarpoolInvite, getCarpool, sendCarpoolInvite } from '../api';
import { lightgrey } from '../colors';
import { getCarpool } from '../api';
import { ICarpool } from '../types';
import UIButton from '../UI/UIButton';
import UISecondaryBox from '../UI/UISecondaryBox';
import useToggle from '../useToggle';
import CarpoolDetails from './CarpoolDetails';
import InvitationList from './InvitationList';
import MemberList from './MemberList';
export const CarpoolContext = createContext({
carpool: null! as ICarpool,
sendInvite: (user: { id: number; name: string }) => {
console.error('not implemented: sendInvite');
},
cancelInvite: (user: { id: number; name: string }) => {
console.error('not implemented: cancelInvite');
},
});
export default function Carpool() {
const id = +useParams<{ id: string }>().id;
@ -26,72 +33,102 @@ export default function Carpool() {
const [invitationsOpen, toggleInvitationsOpen] = useToggle(false);
const sendInvite = useCallback(
(user: { id: number; name: string }) => {
if (carpool) {
sendCarpoolInvite(id, user.id)
.then(() => {
setCarpool(
(carpool) =>
carpool && {
...carpool,
invitations: [
...carpool.invitations,
{ isRequest: false, user },
],
}
);
})
.catch(console.error);
} else {
console.error(
'Trying to send invite when carpool has not been loaded.'
);
}
},
[carpool, id]
);
const cancelInvite = useCallback(
(user: { id: number; name: string }) => {
cancelCarpoolInvite(id, user.id)
.then(() => {
setCarpool(
(carpool) =>
carpool && {
...carpool,
invitations: carpool.invitations.filter(
(invite) => invite.user.id !== user.id
),
}
);
})
.catch(console.error);
},
[id]
);
if (!carpool) {
return <>Loading...</>;
}
return (
<UISecondaryBox style={{ width: '100%', alignItems: 'center' }}>
{carpool ? (
<>
<h1 style={{ marginBottom: '0rem' }}>{carpool.name}</h1>
<h2 style={{ marginBottom: '0rem' }}>{carpool.event.name}</h2>
<div
style={{
display: 'flex',
flexDirection: 'row',
margin: '0.5rem 0',
}}
>
{/* Requests */}
<UIButton
style={{
marginRight: '0.25rem',
backgroundColor: lightgrey,
display: 'flex',
alignItems: 'center',
}}
onClick={console.log}
>
<MailOutlineIcon style={{ marginRight: '0.5rem' }} /> 1 request
</UIButton>
{/* Invitations */}
<UIButton
style={{
marginLeft: '0.25rem',
backgroundColor: lightgrey,
display: 'flex',
alignItems: 'center',
}}
onClick={toggleInvitationsOpen}
>
<PersonAddIcon style={{ marginRight: '0.5rem' }} /> Invite
</UIButton>
</div>
{invitationsOpen && <InvitationList carpool={carpool} />}
<div style={{ fontSize: '1.5rem', fontWeight: 400 }}>
<CarpoolContext.Provider value={{ carpool, sendInvite, cancelInvite }}>
<UISecondaryBox style={{ width: '100%', alignItems: 'center' }}>
{carpool ? (
<>
<h1 style={{ marginBottom: '0rem' }}>{carpool.name}</h1>
<h2 style={{ marginBottom: '0rem' }}>{carpool.event.name}</h2>
<div
style={{
color: '#303030',
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
margin: '0.5rem 0',
}}
>
<LocationOnIcon style={{ marginRight: '1rem' }} />
{carpool.event.formattedAddress}
{/* Requests */}
<UIButton
style={{
marginRight: '0.25rem',
backgroundColor: lightgrey,
display: 'flex',
alignItems: 'center',
}}
onClick={console.log}
>
<MailOutlineIcon style={{ marginRight: '0.5rem' }} /> 1 request
</UIButton>
{/* Invitations */}
<UIButton
style={{
marginLeft: '0.25rem',
backgroundColor: lightgrey,
display: 'flex',
alignItems: 'center',
}}
onClick={toggleInvitationsOpen}
>
<PersonAddIcon style={{ marginRight: '0.5rem' }} /> Invite
</UIButton>
</div>
<div
style={{
color: '#303030',
display: 'flex',
alignItems: 'center',
}}
>
<EventIcon style={{ marginRight: '1rem' }} />
DAWN - DUSK
</div>
</div>
<MemberList members={carpool.members} />
</>
) : (
<h2>Loading</h2>
)}
</UISecondaryBox>
{invitationsOpen && <InvitationList />}
<CarpoolDetails carpool={carpool} />
<MemberList members={carpool.members} />
</>
) : (
<h2>Loading</h2>
)}
</UISecondaryBox>
</CarpoolContext.Provider>
);
}

View File

@ -0,0 +1,31 @@
import EventIcon from '@material-ui/icons/Event';
import LocationOnIcon from '@material-ui/icons/LocationOn';
import { ICarpool } from '../types';
export default function CarpoolDetails({ carpool }: { carpool: ICarpool }) {
return (
<div style={{ fontSize: '1.5rem', fontWeight: 400 }}>
<div
style={{
color: '#303030',
display: 'flex',
alignItems: 'center',
}}
>
<LocationOnIcon style={{ marginRight: '1rem' }} />
{carpool.event.formattedAddress}
</div>
<div
style={{
color: '#303030',
display: 'flex',
alignItems: 'center',
}}
>
<EventIcon style={{ marginRight: '1rem' }} />
DAWN - DUSK
</div>
</div>
);
}

View File

@ -1,72 +0,0 @@
import { createContext, ReactNode, useMemo, useState } from 'react';
import * as immutable from 'immutable';
import { useEffect } from 'react';
import { getCarpool } from '../api';
class Member extends immutable.Record({
id: 0,
name: '',
}) {}
class Invitation extends immutable.Record({
user: new Member(),
isRequest: false,
}) {}
class CarpoolState extends immutable.Record({
id: 0,
name: '',
members: immutable.List<Member>(),
invitations: immutable.List<Invitation>(),
}) {}
type Subscriber = (state: CarpoolState) => void;
class CarpoolSDK {
private _state = new CarpoolState();
get state() {
return this._state;
}
set state(state: CarpoolState) {
this._state = state;
}
private subscribers: Subscriber[] = [];
subscribe(subscriber: Subscriber) {
this.subscribers.push(subscriber);
return () => {
this.subscribers = this.subscribers.filter((s) => s !== subscriber);
};
}
}
export const CarpoolContext = createContext({
sdk: new CarpoolSDK(),
carpool: new CarpoolState(),
});
export default function CarpoolProvider({
id,
children,
}: {
id: number;
children: ReactNode;
}) {
const [carpool, setCarpool] = useState(new CarpoolState());
const sdk = useMemo(() => new CarpoolSDK(), []);
useEffect(() => {
getCarpool(id).then((carpool) => {});
}, [id]);
useEffect(() => {
const remove = sdk.subscribe(setCarpool);
return () => remove();
}, [sdk]);
return (
<CarpoolContext.Provider value={{ sdk, carpool }}>
{children}
</CarpoolContext.Provider>
);
}

View File

@ -1,19 +1,20 @@
import CancelIcon from '@material-ui/icons/Cancel';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import { useMemo } from 'react';
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { getEventSignups } from '../api';
import { ICarpool, IEventSignup } from '../types';
import { useMe } from '../hooks';
import { IEventSignup } from '../types';
import { CarpoolContext } from './Carpool';
function InvitationRow({
carpoolId,
userId,
userName,
user,
isInvited,
}: {
carpoolId: number;
userId: number;
userName: string;
user: { id: number; name: string };
isInvited: boolean;
}) {
const { sendInvite, cancelInvite } = useContext(CarpoolContext);
return (
<div
style={{
@ -24,49 +25,65 @@ function InvitationRow({
padding: '0.25rem',
}}
>
<span>{userName}</span>
<span>{user.name}</span>
{isInvited ? (
<CancelIcon
onClick={() => cancelInvite(user)}
style={{ cursor: 'pointer' }}
/>
) : (
<PersonAddIcon
onClick={() => sendInvite(user)}
style={{ cursor: 'pointer' }}
/>
)}
</div>
);
}
export default function InvitationList({ carpool }: { carpool: ICarpool }) {
export default function InvitationList() {
const { carpool } = useContext(CarpoolContext);
const me = useMe()!;
const eventId = carpool.event.id;
const [availableSignups, setAvailableSignups] =
useState<IEventSignup[] | null>(null);
useEffect(() => {
getEventSignups(eventId).then(setAvailableSignups);
}, [eventId]);
getEventSignups(eventId).then((signups) =>
setAvailableSignups(signups.filter((signup) => signup.user.id !== me.id))
);
}, [eventId, me.id]);
const existingSignups = useMemo(
const invitedUserIDs = useMemo(
() =>
new Set(
carpool.invitations
.filter((invitation) => !invitation.isRequest)
.map((invitation) => invitation.user.id)
),
[carpool]
[carpool.invitations]
);
const availableSignupsAlreadyInvited = useMemo(
() =>
availableSignups
? availableSignups.filter((signup) =>
existingSignups.has(signup.userId)
invitedUserIDs.has(signup.user.id)
)
: null,
[availableSignups, existingSignups]
[availableSignups, invitedUserIDs]
);
const availableSignupsNotInvited = useMemo(
() =>
availableSignups
? availableSignups.filter(
(signup) => !existingSignups.has(signup.userId)
(signup) => !invitedUserIDs.has(signup.user.id)
)
: null,
[availableSignups, existingSignups]
[availableSignups, invitedUserIDs]
);
return (
@ -80,24 +97,20 @@ export default function InvitationList({ carpool }: { carpool: ICarpool }) {
>
<h1 style={{ marginBottom: '0.25rem' }}>Invite Somebody</h1>
{availableSignups === null && 'Loading'}
{availableSignupsAlreadyInvited?.map((signup) => (
<InvitationRow
key={signup.user.id}
user={signup.user}
isInvited={true}
/>
))}
{availableSignupsNotInvited?.map((signup) => (
<InvitationRow
key={signup.user.id}
userId={signup.user.id}
userName={signup.user.name}
carpoolId={carpool.id}
user={signup.user}
isInvited={false}
/>
))}
{availableSignupsAlreadyInvited?.map((signup) => (
<InvitationRow
key={signup.userId}
userId={signup.user.id}
userName={signup.user.name}
carpoolId={carpool.id}
isInvited
/>
))}
</div>
);
}

View File

@ -15,13 +15,14 @@ async function post(path: string, data: any) {
return await res.json();
}
async function delete$(path: string) {
async function delete$(path: string, body?: any) {
const res = await fetch(base + path, {
method: 'delete',
headers: {
Authorization: 'Bearer ' + localStorage.getItem('session_token'),
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
return await res.json();
}
@ -176,3 +177,11 @@ export async function createCarpool({
}) {
return await post('/carpools/', { eventId, name });
}
export async function sendCarpoolInvite(carpoolId: number, userId: number) {
return await post('/carpools/' + carpoolId + '/invite', { userId });
}
export async function cancelCarpoolInvite(carpoolId: number, userId: number) {
return await delete$('/carpools/' + carpoolId + '/invite', { userId });
}

View File

@ -33,7 +33,13 @@ export type ICarpool = {
id: number;
name: string;
}[];
invitations: IInvitation[];
invitations: {
user: {
id: number;
name: string;
};
isRequest: boolean;
}[];
};
/**
@ -81,7 +87,7 @@ export type IEvent = {
export type IEventSignup = {
eventId: number;
userId: number;
// userId: number;
user: {
id: number;
name: string;