diff --git a/src/components/App.tsx b/src/components/App.tsx index 40b03b2..f7457be 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,5 +1,6 @@ import { CSSProperties, lazy, Suspense } from 'react'; import { BrowserRouter, Route, Switch } from 'react-router-dom'; +import GroupsProvider from '../state/GroupsProvider'; import NotificationsProvider from '../state/Notifications/NotificationsProvider'; import { useMe } from './hooks'; import WheelShare from './WheelShare'; @@ -24,28 +25,30 @@ export default function App() { const user = useMe(); return ( -
-
- - - - - + +
+
+ + - - - - - + + + + + + + + +
-
+ ); } diff --git a/src/components/EventCreator/EventCreator.tsx b/src/components/EventCreator/EventCreator.tsx index fe9fc91..8da8878 100644 --- a/src/components/EventCreator/EventCreator.tsx +++ b/src/components/EventCreator/EventCreator.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; -import { createEvent } from '../api'; import { green, lightgrey } from '../../lib/colors'; -import { IGroup } from '../Group'; +import { createEvent } from '../api'; +import { IGroup } from '../types'; import UIButton from '../UI/UIButton'; import UIDateInput from '../UI/UIDateInput'; import UIDatetimeInput from '../UI/UIDatetimeInput'; diff --git a/src/components/EventCreator/EventCreatorLink.tsx b/src/components/EventCreator/EventCreatorLink.tsx index 6e47db0..9c73eda 100644 --- a/src/components/EventCreator/EventCreatorLink.tsx +++ b/src/components/EventCreator/EventCreatorLink.tsx @@ -1,5 +1,5 @@ import EventCreator from './EventCreator'; -import { IGroup } from '../Group'; +import { IGroup } from '../types'; import useToggle from '../useToggle'; export default function EventCreatorLink({ group }: { group: IGroup }) { diff --git a/src/components/Group.tsx b/src/components/Group.tsx index e120fe8..eaccfb9 100644 --- a/src/components/Group.tsx +++ b/src/components/Group.tsx @@ -5,15 +5,9 @@ import { getGroup, getGroupEvents } from './api'; import EventCreatorLink from './EventCreator/EventCreatorLink'; import EventStream from './EventStream'; import GroupSettingsLink from './GroupSettings/GroupSettingsLink'; -import { IEvent } from './types'; +import { IEvent, IGroup } from './types'; import UILink from './UI/UILink'; -export type IGroup = { - id: number; - events: IEvent[]; - name: string; -}; - export default function Group() { const { id } = useParams<{ id: string }>(); const [loading, setLoading] = useState(true); diff --git a/src/components/GroupSettings/GroupSettingsLink.tsx b/src/components/GroupSettings/GroupSettingsLink.tsx index 0c992ea..7b97911 100644 --- a/src/components/GroupSettings/GroupSettingsLink.tsx +++ b/src/components/GroupSettings/GroupSettingsLink.tsx @@ -1,4 +1,4 @@ -import { IGroup } from '../Group'; +import { IGroup } from '../types'; import useToggle from '../useToggle'; import GroupSettings from './GroupSettings'; diff --git a/src/components/Groups/GroupList.tsx b/src/components/Groups/GroupList.tsx index ac031a9..541e76c 100644 --- a/src/components/Groups/GroupList.tsx +++ b/src/components/Groups/GroupList.tsx @@ -1,4 +1,4 @@ -import { IGroup } from '../Group'; +import { IGroup } from '../types'; import UISecondaryBox from '../UI/UISecondaryBox'; function GroupListItem({ group }: { group: IGroup }) { diff --git a/src/components/Groups/Groups.tsx b/src/components/Groups/Groups.tsx index 13531c6..ad72e18 100644 --- a/src/components/Groups/Groups.tsx +++ b/src/components/Groups/Groups.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { getGroups } from '../api'; -import { IGroup } from '../Group'; +import { IGroup } from '../types'; import GroupCreatorLink from '../GroupCreator/GroupCreatorLink'; import GroupJoinerLink from '../GroupJoinerLink'; import GroupList from './GroupList'; diff --git a/src/components/api.ts b/src/components/api.ts index 7e01c8c..99303bd 100644 --- a/src/components/api.ts +++ b/src/components/api.ts @@ -1,5 +1,5 @@ import { GroupPreview } from './GroupJoinerLink'; -import { IInvitation, IEventSignup, ICarpool, IEvent } from './types'; +import { IInvitation, IEventSignup, ICarpool, IEvent, IGroup } from './types'; const base = process.env.REACT_APP_API_DOMAIN + 'api'; @@ -94,7 +94,7 @@ export async function getEvent(id: number): Promise { return await get('/events/' + id); } -export async function getGroup(id: number) { +export async function getGroup(id: number): Promise { return await get('/groups/' + id); } diff --git a/src/components/types.ts b/src/components/types.ts index 0fa7130..ad940cf 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -49,6 +49,7 @@ export type ICarpool = { export type IGroup = { id: number; name: string; + events: IEvent[]; }; /** diff --git a/src/state/EventsProvider.tsx b/src/state/EventsProvider.tsx new file mode 100644 index 0000000..d02c132 --- /dev/null +++ b/src/state/EventsProvider.tsx @@ -0,0 +1,74 @@ +import { createContext, ReactNode, useCallback, useState } from 'react'; +import * as immutable from 'immutable'; +import { useMemo } from 'react'; + +export class EventSignup extends immutable.Record({ + userId: 0, + placeId: '', + formattedAddress: '', + latitute: 0, + longitude: 0, +}) {} + +export class EventState extends immutable.Record({ + id: 0, + name: '', + signups: immutable.Map(), +}) { + addSignup( + userId: number, + placeId: string, + formattedAddress: string, + latitute: number, + longitude: number + ) { + return this.set( + 'signups', + this.signups.set( + userId.toString(), + new EventSignup({ + userId, + placeId, + formattedAddress, + latitute, + longitude, + }) + ) + ); + } + setName(name: string) { + return this.set('name', name); + } +} + +type EventsProps = { + events: immutable.Map; + upsertEvent: (event: EventState) => void; +}; + +export const EventsContext = createContext({ + events: immutable.Map(), + upsertEvent: () => {}, +}); + +export default function EventsProvider({ children }: { children: ReactNode }) { + const [events, setEvents] = useState(immutable.Map()); + + const upsertEvent = useCallback( + (event: EventState) => { + setEvents(events.set(event.id, event)); + }, + [events] + ); + + const value: EventsProps = useMemo(() => { + return { + events, + upsertEvent, + }; + }, [events, upsertEvent]); + + return ( + {children} + ); +} diff --git a/src/state/GroupHooks.tsx b/src/state/GroupHooks.tsx new file mode 100644 index 0000000..5a438d8 --- /dev/null +++ b/src/state/GroupHooks.tsx @@ -0,0 +1,7 @@ +import { useContext } from 'react'; +import { GroupsContext, GroupState } from './GroupsProvider'; + +export function useGroup(id: number): GroupState | null { + const { groups } = useContext(GroupsContext); + return groups.get(id, null); +} diff --git a/src/state/GroupsProvider.tsx b/src/state/GroupsProvider.tsx new file mode 100644 index 0000000..74cde72 --- /dev/null +++ b/src/state/GroupsProvider.tsx @@ -0,0 +1,105 @@ +import * as immutable from 'immutable'; +import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { createContext } from 'react'; +import { getGroup as fetchGroup } from '../components/api'; + +export class GroupState extends immutable.Record({ + id: 0, + name: '', + memberIds: immutable.Set(), + joinCode: null as string | null, +}) { + setJoinCode(joinCode: string) { + return this.set('joinCode', joinCode); + } + setName(name: string) { + return this.set('name', name); + } + addMember(memberId: number) { + return this.set('memberIds', this.memberIds.add(memberId)); + } + removeMember(memberId: number) { + return this.set('memberIds', this.memberIds.remove(memberId)); + } + setMemberIds(memberIds: immutable.Set) { + return this.set('memberIds', memberIds); + } +} + +function u(name: string) { + return () => { + throw new Error(`${name} is not implemented`); + }; +} + +type GroupContextProps = { + groups: immutable.Map; + addGroup: (group: GroupState) => void; + getGroup: (id: number) => Promise; + renameGroup: (groupId: number, name: string) => void; + generateJoinCode: (groupId: number) => void; +}; + +// A React context that provides access to the current user's groups. +export const GroupsContext = createContext({ + groups: immutable.Map(), + addGroup: u('addGroup'), + getGroup: u('getGroup'), + renameGroup: u('renameGroup'), + generateJoinCode: u('generateJoinCode'), +}); + +export default function GroupsProvider({ children }: { children: ReactNode }) { + // usestates for all the properties in GroupsContext + const [groups, setGroups] = useState(immutable.Map()); + + const addGroup = useCallback((group: GroupState) => { + setGroups((groups) => groups.set(group.id, group)); + }, []); + + const renameGroup = useCallback((groupId: number, name: string) => { + setGroups((groups) => groups.setIn([groupId, 'name'], name)); + }, []); + + const generateJoinCode = useCallback((groupId: number) => { + // TODO actually use the API here, this was generated by copilot + setGroups((groups) => + groups.setIn( + [groupId, 'joinCode'], + Math.random().toString(36).substr(2, 5) + ) + ); + }, []); + + const getGroup = useCallback( + async (id: number) => { + const group = groups.get(id); + if (group) { + return group; + } else { + const group = await fetchGroup(id); + const state = new GroupState({ + id: group.id, + name: group.name, + }); + addGroup(state); + return state; + } + }, + [addGroup, groups] + ); + + const value: GroupContextProps = useMemo(() => { + return { + groups, + addGroup, + getGroup, + renameGroup, + generateJoinCode, + }; + }, [addGroup, generateJoinCode, getGroup, groups, renameGroup]); + + return ( + {children} + ); +}