Skip to content

Commit

Permalink
Update data in realtime
Browse files Browse the repository at this point in the history
  • Loading branch information
abraham committed Jan 3, 2022
1 parent 70c157c commit c76d790
Show file tree
Hide file tree
Showing 20 changed files with 332 additions and 100 deletions.
25 changes: 24 additions & 1 deletion firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
{
"indexes": [],
"fieldOverrides": []
"fieldOverrides": [
{
"collectionGroup": "members",
"fieldPath": "name",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
}
]
}
12 changes: 12 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ service cloud.firestore {
// Disallow writes
allow write: if false;

// TODO: remove
match /members/{memberId} {
// Applies to single document read requests
allow get;
Expand All @@ -198,6 +199,17 @@ service cloud.firestore {
}
}

match /{path=**}/members/{memberId} {
// Applies to single document read requests
allow get;

// Applies to queries and collection read requests
allow list;

// Disallow writes
allow write: if false;
}

match /tickets/{ticket} {
// Applies to single document read requests
allow get;
Expand Down
4 changes: 2 additions & 2 deletions src/models/member.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Social } from './social';
import { Id } from './types';
import { ParentId } from './types';

export interface MemberData {
name: string;
Expand All @@ -10,4 +10,4 @@ export interface MemberData {
title: string;
}

export type Member = Id & MemberData;
export type Member = ParentId & MemberData;
4 changes: 4 additions & 0 deletions src/models/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export interface Id {
id: string;
}

export type ParentId = Id & {
parentId: string;
};
43 changes: 18 additions & 25 deletions src/pages/team-page.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Failure, Initialized, Pending } from '@abraham/remotedata';
import { Failure, Pending } from '@abraham/remotedata';
import { computed, customElement, property } from '@polymer/decorators';
import '@polymer/iron-icon';
import '@polymer/marked-element';
Expand All @@ -7,25 +7,12 @@ import { html, PolymerElement } from '@polymer/polymer';
import 'plastic-image';
import '../elements/shared-styles';
import { ReduxMixin } from '../mixins/redux-mixin';
import { RootState, store } from '../store';
import { fetchTeams } from '../store/teams/actions';
import { initialTeamsState } from '../store/teams/state';
import { RootState } from '../store';
import { selectTeamsAndMembers } from '../store/teams-members/selectors';
import { initialTeamsMembersState } from '../store/teams-members/state';

@customElement('team-page')
export class TeamPage extends ReduxMixin(PolymerElement) {
@property({ type: Object })
teams = initialTeamsState;

@computed('teams')
get pending() {
return this.teams instanceof Pending;
}

@computed('teams')
get failure() {
return this.teams instanceof Failure;
}

static get template() {
return html`
<style include="shared-styles flex flex-alignment">
Expand Down Expand Up @@ -158,7 +145,7 @@ export class TeamPage extends ReduxMixin(PolymerElement) {
<p>Error loading teams.</p>
</template>
<template is="dom-repeat" items="[[teams.data]]" as="team">
<template is="dom-repeat" items="[[teamsMembers.data]]" as="team">
<div class="team-title">[[team.title]]</div>
<div class="team-block">
Expand Down Expand Up @@ -198,14 +185,20 @@ export class TeamPage extends ReduxMixin(PolymerElement) {
`;
}

override stateChanged(state: RootState) {
this.teams = state.teams;
@property({ type: Object })
teamsMembers = initialTeamsMembersState;

@computed('teamsMembers')
get pending() {
return this.teamsMembers instanceof Pending;
}

override connectedCallback() {
super.connectedCallback();
if (this.teams instanceof Initialized) {
store.dispatch(fetchTeams);
}
@computed('teamsMembers')
get failure() {
return this.teamsMembers instanceof Failure;
}

override stateChanged(state: RootState) {
this.teamsMembers = selectTeamsAndMembers(state);
}
}
4 changes: 3 additions & 1 deletion src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { featuredSessionsReducer } from './featured-sessions/reducers';
import { feedbackReducer } from './feedback/reducers';
import { filtersReducer } from './filters/reducers';
import { galleryReducer } from './gallery/reducers';
import { membersReducer } from './members/reducers';
import { notificationsReducer } from './notifications/reducers';
import { partnersReducer } from './partners/reducers';
import { potentialPartnersReducer } from './potential-partners/reducers';
Expand All @@ -32,8 +33,9 @@ export const store = configureStore({
dialogs: dialogsReducer,
featuredSessions: featuredSessionsReducer,
feedback: feedbackReducer,
gallery: galleryReducer,
filters: filtersReducer,
gallery: galleryReducer,
members: membersReducer,
notifications: notificationsReducer,
partners: partnersReducer,
potentialPartners: potentialPartnersReducer,
Expand Down
46 changes: 46 additions & 0 deletions src/store/members/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Failure, Initialized, Pending, RemoteData, Success } from '@abraham/remotedata';
import { collectionGroup, onSnapshot, orderBy, query, Unsubscribe } from 'firebase/firestore';
import { Dispatch } from 'redux';
import { db } from '../../firebase';
import { Member } from '../../models/member';
import { dataWithParentId } from '../../utils/firestore';
import {
FETCH_MEMBERS,
FETCH_MEMBERS_FAILURE,
FETCH_MEMBERS_SUCCESS,
MembersActions,
} from './types';

type Subscription = RemoteData<Error, Unsubscribe>;
let subscription: Subscription = new Initialized();

export const unsubscribe = () => {
if (subscription instanceof Success) {
subscription.data();
}
};

const subscribe = (path: string, dispatch: Dispatch<MembersActions>) => {
const unsubscribe = onSnapshot(
query(collectionGroup(db, path), orderBy('name', 'asc')),
(snapshot) => {
const payload = snapshot.docs.map<Member>(dataWithParentId);
dispatch({ type: FETCH_MEMBERS_SUCCESS, payload });
},
(payload) => {
subscription = new Failure(payload);
dispatch({ type: FETCH_MEMBERS_FAILURE, payload });
}
);

return new Success(unsubscribe);
};

export const fetchMembers = () => async (dispatch: Dispatch<MembersActions>) => {
if (subscription instanceof Initialized) {
subscription = new Pending();
dispatch({ type: FETCH_MEMBERS });

subscription = subscribe('members', dispatch);
}
};
27 changes: 27 additions & 0 deletions src/store/members/reducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Failure, Pending, Success } from '@abraham/remotedata';
import { initialMembersState, MembersState } from './state';
import {
FETCH_MEMBERS,
FETCH_MEMBERS_FAILURE,
FETCH_MEMBERS_SUCCESS,
MembersActions,
} from './types';

export const membersReducer = (
state = initialMembersState,
action: MembersActions
): MembersState => {
switch (action.type) {
case FETCH_MEMBERS:
return new Pending();

case FETCH_MEMBERS_FAILURE:
return new Failure(action.payload);

case FETCH_MEMBERS_SUCCESS:
return new Success(action.payload);

default:
return state;
}
};
13 changes: 13 additions & 0 deletions src/store/members/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Initialized, Pending } from '@abraham/remotedata';
import { RootState, store } from '..';
import { fetchMembers } from './actions';
import { MembersState } from './state';

export const selectMembers = (state: RootState): MembersState => {
if (state.members instanceof Initialized) {
store.dispatch(fetchMembers());
return new Pending();
} else {
return state.members;
}
};
5 changes: 5 additions & 0 deletions src/store/members/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Initialized, RemoteData } from '@abraham/remotedata';
import { Member } from '../../models/member';

export type MembersState = RemoteData<Error, Member[]>;
export const initialMembersState: MembersState = new Initialized();
24 changes: 24 additions & 0 deletions src/store/members/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Member } from '../../models/member';

export const FETCH_MEMBERS = 'FETCH_MEMBERS';
export const FETCH_MEMBERS_FAILURE = 'FETCH_MEMBERS_FAILURE';
export const FETCH_MEMBERS_SUCCESS = 'FETCH_MEMBERS_SUCCESS';

interface FetchMembersAction {
type: typeof FETCH_MEMBERS;
}

interface FetchMembersFailureAction {
type: typeof FETCH_MEMBERS_FAILURE;
payload: Error;
}

interface FetchMembersSuccessAction {
type: typeof FETCH_MEMBERS_SUCCESS;
payload: Member[];
}

export type MembersActions =
| FetchMembersAction
| FetchMembersFailureAction
| FetchMembersSuccessAction;
45 changes: 27 additions & 18 deletions src/store/speakers/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { collection, getDocs, orderBy, query } from 'firebase/firestore';
import { Failure, Initialized, Pending, RemoteData, Success } from '@abraham/remotedata';
import { collection, onSnapshot, orderBy, query, Unsubscribe } from 'firebase/firestore';
import { Dispatch } from 'redux';
import { db } from '../../firebase';
import { SpeakerWithTags } from '../../models/speaker';
Expand All @@ -10,28 +11,36 @@ import {
SpeakerActions,
} from './types';

const getSpeakers = async (): Promise<SpeakerWithTags[]> => {
const { docs } = await getDocs(
query(collection(db, 'generatedSpeakers'), orderBy('order', 'asc'))
type Subscription = RemoteData<Error, Unsubscribe>;
let subscription: Subscription = new Initialized();

export const unsubscribe = () => {
if (subscription instanceof Success) {
subscription.data();
}
};

const subscribe = (path: string, dispatch: Dispatch<SpeakerActions>) => {
const unsubscribe = onSnapshot(
query(collection(db, path), orderBy('name', 'asc')),
(snapshot) => {
const payload = snapshot.docs.map<SpeakerWithTags>(mergeDataAndId);
dispatch({ type: FETCH_SPEAKERS_SUCCESS, payload });
},
(payload) => {
subscription = new Failure(payload);
dispatch({ type: FETCH_SPEAKERS_FAILURE, payload });
}
);

return docs.map<SpeakerWithTags>(mergeDataAndId);
return new Success(unsubscribe);
};

export const fetchSpeakers = async (dispatch: Dispatch<SpeakerActions>) => {
dispatch({
type: FETCH_SPEAKERS,
});
if (subscription instanceof Initialized) {
subscription = new Pending();
dispatch({ type: FETCH_SPEAKERS });

try {
dispatch({
type: FETCH_SPEAKERS_SUCCESS,
payload: await getSpeakers(),
});
} catch (error) {
dispatch({
type: FETCH_SPEAKERS_FAILURE,
payload: error,
});
subscription = subscribe('generatedSpeakers', dispatch);
}
};
35 changes: 35 additions & 0 deletions src/store/teams-members/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Failure, Initialized, Pending, Success } from '@abraham/remotedata';
import { createSelector } from '@reduxjs/toolkit';
import { Member } from '../../models/member';
import { Team, TeamWithoutMembers } from '../../models/team';
import { selectMembers } from '../members/selectors';
import { MembersState } from '../members/state';
import { selectTeams } from '../teams/selectors';
import { TeamsState } from '../teams/state';
import { TeamsMembersState } from './state';

const mergeMembers = (team: TeamWithoutMembers, possibleMembers: Member[]): Team => {
return {
...team,
members: possibleMembers.filter((member) => member.parentId === team.id),
};
};

export const selectTeamsAndMembers = createSelector(
selectTeams,
selectMembers,
(teams: TeamsState, members: MembersState): TeamsMembersState => {
if (teams instanceof Success && members instanceof Success) {
const merged = teams.data.map((team) => mergeMembers(team, members.data));
return new Success(merged);
} else if (teams instanceof Pending || members instanceof Pending) {
return new Pending();
} else if (teams instanceof Failure) {
return new Failure(teams.error);
} else if (members instanceof Failure) {
return new Failure(members.error);
} else {
return new Initialized();
}
}
);
5 changes: 5 additions & 0 deletions src/store/teams-members/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Initialized, RemoteData } from '@abraham/remotedata';
import { Team } from '../../models/team';

export type TeamsMembersState = RemoteData<Error, Team[]>;
export const initialTeamsMembersState: TeamsMembersState = new Initialized();
Loading

0 comments on commit c76d790

Please sign in to comment.