Skip to content
This repository has been archived by the owner on Jun 9, 2023. It is now read-only.

Add Redux/Thunk with bare minimum setup #166

Merged
merged 6 commits into from
Dec 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
.next/
next.config.js
client/@types/
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"prettier"
],
"env": {
"browser": true,
"node": true,
"es6": true
},
Expand Down
10 changes: 10 additions & 0 deletions client/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* For special typings in NextJS/ReactJS using TS...
* there are some pretty good patterns followed here - https://github.com/deptno/next.js-typescript-starter-kit
*/

interface Window {
__REDUX_DEVTOOLS_EXTENSION__: any;
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any;
__REACT_DEVTOOLS_GLOBAL_HOOK__: any;
}
24 changes: 7 additions & 17 deletions client/components/AddSponsor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from 'react';
import fetch from 'isomorphic-fetch';
import useForm from 'react-hook-form';

import {
Expand All @@ -8,29 +7,20 @@ import {
ResponseDiv,
SubmitBtn,
} from 'client/styles/components/AddSponsor';
import {
IAddSponsorProps,
ISponsorData,
} from 'client/interfaces/components/AddSponsor';

const AddSponsor: React.FC<IAddSponsorProps> = ({ eventId, chapterId }) => {
const AddSponsor: React.FC = () => {
const [responseMsg, setResponseMsg] = React.useState('');

const { register, handleSubmit, errors } = useForm();

// TODO: Get data from store
// const eventId = useSelector(state => state.selectedChapter.eventId);
// const chapterId = useSelector(state => state.selectedChapter.id);

const onSubmit = async data => {
const { name, website, type }: ISponsorData = data;
try {
await fetch(`/${chapterId}/events/${eventId}/sponsors`, {
// TODO: create route
method: 'post',
body: {
name,
website,
type,
},
});
setResponseMsg(`${name} has been added as a ${type} sponsor.`);
// await dispatch(sponsorActions.submit(eventId, chapterId));
setResponseMsg(`${data.name} has been added as a ${data.type} sponsor.`);
} catch (e) {
setResponseMsg('Uh oh, something went wrong.');
// TODO: more descriptive error messages
Expand Down
14 changes: 14 additions & 0 deletions client/components/ProgressCardContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { CardContent, CircularProgress } from '@material-ui/core';

interface IProps {
loading: boolean;
}

const ProgressCardContent: React.FC<IProps> = props => (
<CardContent>
{props.loading ? <CircularProgress /> : props.children}
</CardContent>
);

export default ProgressCardContent;
3 changes: 2 additions & 1 deletion client/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SomeComponent from './SomeComponent';
import AddSponsor from './AddSponsor';
import ProgressCardContent from './ProgressCardContent';

export { SomeComponent, AddSponsor };
export { SomeComponent, AddSponsor, ProgressCardContent };
15 changes: 0 additions & 15 deletions client/interfaces/components/AddSponsor.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions client/interfaces/models.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Here types are inherited from actual DB Modal Types
// created [here](types/models.d.ts)
// This is re

import { ISponsor } from 'types/models';

export type ISponsorResponse = Omit<ISponsor, 'createdAt' | 'updatedAt'>;
55 changes: 55 additions & 0 deletions client/modules/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useEffect } from 'react';
import { Card, Typography, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/styles';
import { useSelector, useDispatch } from 'react-redux';
import { SomeComponent, ProgressCardContent } from 'client/components';
import { AppStoreState } from 'client/store/reducers';
import { chapterActions } from 'client/store/actions';

const useStyles = makeStyles(() => ({
root: {
padding: 15,
},
}));

const Home: React.FC = () => {
const classes = useStyles();

const { error, loading, name, description } = useSelector(
Zeko369 marked this conversation as resolved.
Show resolved Hide resolved
(state: AppStoreState) => ({
error: state.chapter.error,
loading: state.chapter.loading,
name: state.chapter.name,
description: state.chapter.description,
}),
);
const dispatch = useDispatch();

useEffect(() => {
dispatch(chapterActions.fetchChapter('1'));
}, []);

return (
<>
<SomeComponent />
<Grid container className={classes.root} spacing={2}>
<Grid item xs={10}>
{!error && (
<Card>
<ProgressCardContent loading={loading}>
<Typography gutterBottom variant="h5" component="h2">
{name}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{description}
</Typography>
</ProgressCardContent>
</Card>
)}
</Grid>
</Grid>
</>
);
};

export default Home;
32 changes: 32 additions & 0 deletions client/services/http-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* This Class is a very minimal wrapper around fetch
* to make API calls easy.
*/

import fetch from 'cross-fetch';

export class HttpService<V = any> {
public static baseUrl = '/api/v1'; // TODO: need to create some ENV for this
public static baseHeaders = {
'Content-Type': 'application/json',
};

public stringifyParams(params: Record<string, string>) {
return Object.keys(params).reduce((acc, key, i) => {
return Object.prototype.hasOwnProperty.call(params, key)
? `${acc}${i !== 0 ? '&' : '?'}${key}=${params[key]}`
: acc;
}, '');
}

public async get(
url: string,
params: Record<string, string>,
headers: Record<string, string>,
) {
return fetch(HttpService.baseUrl + url + this.stringifyParams(params), {
headers: { ...HttpService.baseHeaders, ...headers },
method: 'GET',
}).then<V>(res => res.json());
}
}
48 changes: 48 additions & 0 deletions client/store/actions/chapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ActionCreator } from 'redux';
import { HttpService } from 'client/services/http-service';
import { chapterTypes } from '../types';

/****************
* Actions
****************/
export const fetchStart = (): chapterTypes.IChapterActionTypes => {
return {
type: chapterTypes.FETCH_START,
};
};

export const fetchSuccess = (
chapter: chapterTypes.IChapterModal,
): chapterTypes.IChapterActionTypes => {
return {
type: chapterTypes.FETCH_SUCCESS,
payload: chapter,
};
};

export const fetchFail = (error: string): chapterTypes.IChapterActionTypes => {
return {
payload: error,
type: chapterTypes.FETCH_FAIL,
};
};

/****************
* Side-Effects
****************/
export const fetchChapter: ActionCreator<
chapterTypes.ThunkResult<Promise<void>>
> = (id: string) => async dispatch => {
dispatch(fetchStart());

// TODO: for the PR to be simple, haven't added any specific HTTP Service,
// But we can make HTTPService some kind of builder, to return us back with specific
// modal service, like ChapterHttpService.
const http = new HttpService<chapterTypes.IChapterModal>();
try {
const resData = await http.get(`/chapters/${id}`, {}, {});
dispatch(fetchSuccess(resData));
} catch (err) {
dispatch(fetchFail(err));
}
};
3 changes: 3 additions & 0 deletions client/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as chapterActions from './chapter';

export { chapterActions };
39 changes: 39 additions & 0 deletions client/store/reducers/chapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import produce from 'immer';
import { chapterTypes } from '../types';

const initialState: chapterTypes.IChapterStoreState = {
name: '',
description: '',
category: '',
loading: false,
error: '',
};

const reducer = (
state = initialState,
action: chapterTypes.IChapterActionTypes,
) =>
produce(state, draft => {
Zeko369 marked this conversation as resolved.
Show resolved Hide resolved
switch (action.type) {
case chapterTypes.FETCH_START:
draft.error = '';
draft.loading = true;
break;
case chapterTypes.FETCH_SUCCESS:
draft.id = action.payload.id;
draft.name = action.payload.name;
draft.description = action.payload.description;
draft.category = action.payload.category;
draft.error = '';
draft.loading = false;
break;
case chapterTypes.FETCH_FAIL:
draft.error = action.payload;
draft.loading = false;
break;
default:
return state;
}
});

export default reducer;
8 changes: 8 additions & 0 deletions client/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import chapterReducer from './chapter';

export const rootReducer = combineReducers({
chapter: chapterReducer,
});

export type AppStoreState = ReturnType<typeof rootReducer>;
45 changes: 45 additions & 0 deletions client/store/types/chapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ThunkAction } from 'redux-thunk';
import { AppStoreState } from '../reducers';

export const FETCH_START = 'fcc/chapter/CHAPTER_START';
export const FETCH_SUCCESS = 'fcc/chapter/CHAPTER_SUCCESS';
export const FETCH_FAIL = 'fcc/chapter/CHAPTER_FAIL';

export interface IChapterModal {
id?: string;
phoenisx marked this conversation as resolved.
Show resolved Hide resolved
name: string;
description: string;
category: string;
}

export interface IChapterStoreState extends IChapterModal {
loading: boolean;
error: string; // Should reflect a generic Error Type here
}

interface IChapterFetchStartAction {
type: typeof FETCH_START;
}

interface IChapterFetchSuccessAction {
type: typeof FETCH_SUCCESS;
payload: IChapterModal;
}

interface IChapterFetchFailureAction {
type: typeof FETCH_FAIL;
payload: string;
}

export type IChapterActionTypes =
| IChapterFetchStartAction
| IChapterFetchSuccessAction
| IChapterFetchFailureAction;

// https://github.com/reduxjs/redux-thunk/issues/103#issuecomment-298526567
export type ThunkResult<R> = ThunkAction<
R,
AppStoreState,
null,
IChapterActionTypes
>;
3 changes: 3 additions & 0 deletions client/store/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as chapterTypes from './chapter';

export { chapterTypes };