├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .sequelizerc ├── Changelog.md ├── LICENSE ├── Procfile ├── README.md ├── README_KO.md ├── app_mobx ├── Context.tsx ├── client.tsx ├── components │ ├── EntryBox.tsx │ ├── MainSection.tsx │ ├── Scoreboard.tsx │ ├── TopicItem.tsx │ └── TopicTextForm.tsx ├── containers │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── Message.tsx │ ├── Navigation.tsx │ └── Vote.tsx ├── css │ ├── common │ │ ├── button.ts │ │ ├── card.ts │ │ ├── input.ts │ │ ├── layout.ts │ │ └── typography.ts │ ├── components │ │ ├── about.ts │ │ ├── entrybox.ts │ │ ├── login.ts │ │ ├── mainSection.ts │ │ ├── message.ts │ │ ├── navigation.ts │ │ ├── scoreboard.ts │ │ ├── topicItem.ts │ │ └── vote.ts │ └── main.ts ├── images │ ├── apple-ninja152-precomposed.png │ ├── chrome-ninja192-precomposed.png │ ├── favicon.png │ ├── hourglass.svg │ └── ms-ninja144-precomposed.png ├── pages │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── NotFound.tsx │ ├── Page.tsx │ ├── Vote.tsx │ ├── assets.ts │ └── index.ts ├── routes.ts ├── services │ ├── authentication.ts │ ├── index.ts │ └── topics.ts ├── store │ ├── index.ts │ ├── message.ts │ ├── topic.ts │ └── user.ts └── useStore.ts ├── app_saga ├── actions │ ├── __tests__ │ │ ├── topics-test.js │ │ └── users-test.js │ ├── messages.ts │ ├── topics.ts │ └── users.ts ├── client.tsx ├── components │ ├── EntryBox.tsx │ ├── MainSection.tsx │ ├── Scoreboard.tsx │ ├── TopicItem.tsx │ └── TopicTextForm.tsx ├── containers │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── Message.tsx │ ├── Navigation.tsx │ └── Vote.tsx ├── css │ ├── common │ │ ├── button.ts │ │ ├── card.ts │ │ ├── input.ts │ │ ├── layout.ts │ │ └── typography.ts │ ├── components │ │ ├── about.ts │ │ ├── entrybox.ts │ │ ├── login.ts │ │ ├── mainSection.ts │ │ ├── message.ts │ │ ├── navigation.ts │ │ ├── scoreboard.ts │ │ ├── topicItem.ts │ │ └── vote.ts │ └── main.ts ├── images │ ├── apple-ninja152-precomposed.png │ ├── chrome-ninja192-precomposed.png │ ├── favicon.png │ ├── hourglass.svg │ └── ms-ninja144-precomposed.png ├── pages │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── NotFound.tsx │ ├── Page.tsx │ ├── Vote.tsx │ ├── assets.ts │ └── index.ts ├── reducers │ ├── index.ts │ ├── message.ts │ ├── topic.ts │ └── user.ts ├── routes.ts ├── sagas │ ├── index.ts │ ├── topics.ts │ └── users.ts ├── services │ ├── authentication.ts │ ├── index.ts │ └── topics.ts ├── store │ └── configureStore.ts └── types │ └── index.ts ├── app_thunk ├── actions │ ├── __tests__ │ │ ├── topics-test.js │ │ └── users-test.js │ ├── messages.ts │ ├── topics.ts │ └── users.ts ├── client.tsx ├── components │ ├── EntryBox.tsx │ ├── MainSection.tsx │ ├── Scoreboard.tsx │ ├── TopicItem.tsx │ └── TopicTextForm.tsx ├── containers │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── Message.tsx │ ├── Navigation.tsx │ └── Vote.tsx ├── css │ ├── common │ │ ├── button.ts │ │ ├── card.ts │ │ ├── input.ts │ │ ├── layout.ts │ │ └── typography.ts │ ├── components │ │ ├── about.ts │ │ ├── entrybox.ts │ │ ├── login.ts │ │ ├── mainSection.ts │ │ ├── message.ts │ │ ├── navigation.ts │ │ ├── scoreboard.ts │ │ ├── topicItem.ts │ │ └── vote.ts │ └── main.ts ├── images │ ├── apple-ninja152-precomposed.png │ ├── chrome-ninja192-precomposed.png │ ├── favicon.png │ ├── hourglass.svg │ └── ms-ninja144-precomposed.png ├── pages │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── NotFound.tsx │ ├── Page.tsx │ ├── Vote.tsx │ ├── assets.ts │ └── index.ts ├── reducers │ ├── index.ts │ ├── message.ts │ ├── topic.ts │ └── user.ts ├── routes.ts ├── services │ ├── authentication.ts │ ├── index.ts │ └── topics.ts ├── store │ └── configureStore.ts └── types │ └── index.ts ├── app_toolkit ├── actions │ ├── __tests__ │ │ ├── topics-test.js │ │ └── users-test.js │ ├── topics.ts │ └── users.ts ├── client.tsx ├── components │ ├── EntryBox.tsx │ ├── MainSection.tsx │ ├── Scoreboard.tsx │ ├── TopicItem.tsx │ └── TopicTextForm.tsx ├── containers │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── Message.tsx │ ├── Navigation.tsx │ └── Vote.tsx ├── css │ ├── common │ │ ├── button.ts │ │ ├── card.ts │ │ ├── input.ts │ │ ├── layout.ts │ │ └── typography.ts │ ├── components │ │ ├── about.ts │ │ ├── entrybox.ts │ │ ├── login.ts │ │ ├── mainSection.ts │ │ ├── message.ts │ │ ├── navigation.ts │ │ ├── scoreboard.ts │ │ ├── topicItem.ts │ │ └── vote.ts │ └── main.ts ├── images │ ├── apple-ninja152-precomposed.png │ ├── chrome-ninja192-precomposed.png │ ├── favicon.png │ ├── hourglass.svg │ └── ms-ninja144-precomposed.png ├── pages │ ├── About.tsx │ ├── App.tsx │ ├── Dashboard.tsx │ ├── LoginOrRegister.tsx │ ├── NotFound.tsx │ ├── Page.tsx │ ├── Vote.tsx │ ├── assets.ts │ └── index.ts ├── reducers │ ├── index.ts │ ├── message.ts │ ├── topic.ts │ └── user.ts ├── routes.ts ├── services │ ├── authentication.ts │ ├── index.ts │ └── topics.ts └── store │ └── configureStore.ts ├── cli.ts ├── config ├── app.ts ├── dbTypes.ts ├── env.ts ├── secrets.ts └── serverEnv.ts ├── docs ├── FAQ.md ├── FAQ_KO.md ├── apps.md ├── css.md ├── databases.md ├── deployment │ ├── DigitalOcean.md │ ├── Heroku.md │ ├── Heroku_KO.md │ ├── SaltStackonDigitalOcean.md │ ├── aws.md │ └── aws_KO.md └── development.md ├── gitignore ├── nodemon.json ├── package-lock.json ├── package.json ├── server_mongo ├── db │ ├── connect.ts │ ├── constants.ts │ ├── controllers │ │ ├── index.ts │ │ ├── topics.ts │ │ └── users.ts │ ├── index.ts │ ├── models │ │ ├── index.ts │ │ ├── topics.ts │ │ └── user.ts │ ├── passport │ │ ├── deserializeUser.ts │ │ ├── google.ts │ │ ├── index.ts │ │ └── local.ts │ ├── session.ts │ └── unsupportedMessage.ts ├── index.ts ├── init │ ├── express.ts │ ├── passport │ │ ├── google.ts │ │ ├── index.ts │ │ └── local.ts │ └── routes.ts └── render │ ├── middleware.ts │ ├── pageRenderer.tsx │ └── static-assets │ ├── dev.ts │ ├── index.ts │ └── prod.ts ├── server_mysql ├── db │ ├── index.ts │ ├── sequelize │ │ ├── connect.ts │ │ ├── constants.ts │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── topics.ts │ │ │ └── users.ts │ │ ├── index.ts │ │ ├── migrations │ │ │ ├── 20160416222221-add-topics.js │ │ │ ├── 20160416222345-add-users.js │ │ │ ├── 20160416222416-add-tokens.js │ │ │ ├── 20160416222449-add-google-id-to-users.js │ │ │ └── 20160416222520-add-sessions.js │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── tokens.ts │ │ │ ├── topics.ts │ │ │ └── users.ts │ │ ├── passport │ │ │ ├── deserializeUser.ts │ │ │ ├── google.ts │ │ │ ├── index.ts │ │ │ └── local.ts │ │ └── sequelize_config.ts │ ├── session.ts │ └── unsupportedMessage.ts ├── index.ts ├── init │ ├── express.ts │ ├── passport │ │ ├── google.ts │ │ ├── index.ts │ │ └── local.ts │ └── routes.ts └── render │ ├── middleware.ts │ ├── pageRenderer.tsx │ └── static-assets │ ├── dev.ts │ ├── index.ts │ └── prod.ts ├── server_none ├── db │ ├── controllers │ │ ├── index.ts │ │ ├── topics.ts │ │ └── users.ts │ ├── index.ts │ ├── passport │ │ ├── deserializeUser.ts │ │ ├── google.ts │ │ ├── index.ts │ │ └── local.ts │ └── unsupportedMessage.ts ├── index.ts ├── init │ ├── express.ts │ ├── passport │ │ ├── google.ts │ │ ├── index.ts │ │ └── local.ts │ └── routes.ts └── render │ ├── middleware.ts │ ├── pageRenderer.tsx │ └── static-assets │ ├── dev.ts │ ├── index.ts │ └── prod.ts ├── server_pg ├── db │ ├── index.ts │ ├── sequelize │ │ ├── connect.ts │ │ ├── constants.ts │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── topics.ts │ │ │ └── users.ts │ │ ├── index.ts │ │ ├── migrations │ │ │ ├── 20160416222221-add-topics.js │ │ │ ├── 20160416222345-add-users.js │ │ │ ├── 20160416222416-add-tokens.js │ │ │ ├── 20160416222449-add-google-id-to-users.js │ │ │ └── 20160416222520-add-sessions.js │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── tokens.ts │ │ │ ├── topics.ts │ │ │ └── users.ts │ │ ├── passport │ │ │ ├── deserializeUser.ts │ │ │ ├── google.ts │ │ │ ├── index.ts │ │ │ └── local.ts │ │ └── sequelize_config.ts │ ├── session.ts │ └── unsupportedMessage.ts ├── index.ts ├── init │ ├── express.ts │ ├── passport │ │ ├── google.ts │ │ ├── index.ts │ │ └── local.ts │ └── routes.ts └── render │ ├── middleware.ts │ ├── pageRenderer.tsx │ └── static-assets │ ├── dev.ts │ ├── index.ts │ └── prod.ts ├── tsconfig.json ├── types ├── custom.d.ts ├── express │ └── index.d.ts └── index.d.ts └── webpack ├── externals.ts ├── paths.ts ├── plugins.ts ├── resolve.ts ├── rules ├── css.ts ├── image.ts ├── index.ts └── typescript.ts └── webpack.config.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | ["@babel/preset-typescript", { 6 | "isTSX": true, 7 | "allExtensions": true 8 | }] 9 | ], 10 | "plugins": [ 11 | "@emotion", 12 | "@babel/plugin-transform-runtime", 13 | "@babel/plugin-proposal-class-properties" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /compiled/** 2 | /public/** 3 | /webpack/** 4 | /api/promiseMiddleware.js 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Note: This is just a template, so feel free to use/remove the unnecessary things 2 | 3 | ### Description 4 | - Type: Bug | Enhancement | Question 5 | - Related Issue: `#abc` 6 | 7 | --------------------------------------------------------------- 8 | ## Bug 9 | **Expected Behavior** 10 | 11 | **Actual Behavior** 12 | 13 | **Steps to Reproduce** 14 | 15 | ---------------------------------------------------------------- 16 | ## Enhancement 17 | 18 | **Reason to enhance/problem with existing solution** 19 | 20 | **Suggested enhancement** 21 | 22 | **Pros** 23 | 24 | **Cons** 25 | 26 | ----------------------------------------------------------------- 27 | 28 | ## Question 29 | 30 | **How to?** 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .tmp 4 | .idea 5 | public 6 | compiled 7 | *.swp 8 | .vscode 9 | .env 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .tmp 4 | .idea 5 | public 6 | compiled 7 | *.swp 8 | .vscode 9 | .env 10 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | config: './server/db/sequelize/sequelize_config.js', 3 | migrationsPath: './server/db/sequelize/migrations', 4 | modelsPath: './server/db/sequelize/models' 5 | }; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Choon Ken Ding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /app_mobx/Context.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import * as React from 'react'; 3 | import createStore, { Store } from './store'; 4 | 5 | // Grab the state from a global injected into 6 | // server-generated HTML 7 | export const store = createStore(typeof window !== 'undefined' ? window.__INITIAL_STATE__ : {}); 8 | export const storeContext = React.createContext(store); 9 | 10 | interface Props { 11 | children: React.ReactChildren; 12 | } 13 | 14 | export default (paramStore: Store): FC => ({ children }) => { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /app_mobx/client.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { syncHistoryWithStore } from 'mobx-react-router'; 5 | import { createBrowserHistory } from 'history'; 6 | 7 | import createStoreProvider, { store } from './Context'; 8 | import App from './pages/App'; 9 | import { routingStore } from './store'; 10 | 11 | const browserHistory = createBrowserHistory(); 12 | const history = syncHistoryWithStore(browserHistory, routingStore); 13 | const StoreProvider = createStoreProvider(store); 14 | render( 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('app'), 21 | ); 22 | -------------------------------------------------------------------------------- /app_mobx/components/EntryBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { EntryBoxWrapper, Header, Input } from '../css/components/entrybox'; 4 | 5 | interface Props { 6 | onEntryChange: (value: string) => void; 7 | onEntrySave: (value: string) => void; 8 | topic: string; 9 | } 10 | 11 | // Takes callback functions from props and passes it down to TopicTextInput 12 | // Essentially this is passing the callback function two levels down from parent 13 | // to grandchild. To make it cleaner, you could consider: 14 | // 1. moving `connect` down to this component so you could mapStateToProps and dispatch 15 | // 2. Move TopicTextInput up to EntryBox so it's less confusing 16 | const EntryBox: FC = ({ onEntryChange, onEntrySave, topic }) => { 17 | return ( 18 | 19 |
Vote for your top hack idea
20 | 25 |
26 | ); 27 | }; 28 | 29 | export default EntryBox; 30 | -------------------------------------------------------------------------------- /app_mobx/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Topic } from '../store/topic'; 3 | 4 | import TopicItem from './TopicItem'; 5 | import { Header, List, MainSectionWrapper } from '../css/components/mainSection'; 6 | 7 | interface Props { 8 | topics: Topic[], 9 | onIncrement: (id: string) => void, 10 | onDecrement: (id: string) => void, 11 | onDestroy: (id: string) => void, 12 | } 13 | const MainSection: FC = ({ 14 | topics, onIncrement, onDecrement, onDestroy 15 | }) => { 16 | const topicItems = topics.map((topic, key) => { 17 | return ( 18 | 25 | ); 26 | }); 27 | 28 | return ( 29 | 30 |
Vote for your favorite hack day idea
31 | {topicItems} 32 |
33 | ); 34 | }; 35 | 36 | export default MainSection; 37 | -------------------------------------------------------------------------------- /app_mobx/components/Scoreboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Count, Item, Topic, ScoreboardWrapper, Header, List } from '../css/components/scoreboard'; 3 | import { Topic as ITopic } from '../store/topic'; 4 | 5 | interface Props { 6 | topics: ITopic[]; 7 | } 8 | const Scoreboard: FC = ({topics}) => { 9 | const topicListItems = topics.map((topic, key) => { 10 | return ( 11 | 12 | {topic.text} 13 | {topic.count} 14 | 15 | ); 16 | }); 17 | return ( 18 | 19 |
Vote count
20 | 21 | {topicListItems} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Scoreboard; 28 | -------------------------------------------------------------------------------- /app_mobx/components/TopicItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { Decrement, Destroy, Increment, Topic, TopicItemWrapper } from '../css/components/topicItem'; 4 | 5 | interface Props { 6 | text: string, 7 | id: string, 8 | incrementCount: (id: string) => void, 9 | decrementCount: (id: string) => void, 10 | destroyTopic: (id: string) => void, 11 | } 12 | 13 | const TopicItem: FC = ({ 14 | text, id, incrementCount, decrementCount, destroyTopic, 15 | }) => { 16 | const onIncrement = () => { 17 | incrementCount(id); 18 | }; 19 | const onDecrement = () => { 20 | decrementCount(id); 21 | }; 22 | const onDestroy = () => { 23 | destroyTopic(id); 24 | }; 25 | 26 | return ( 27 | 28 | {text} 29 | 32 | + 33 | 34 | 37 | - 38 | 39 | 42 | {String.fromCharCode(215)} 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default TopicItem; 49 | -------------------------------------------------------------------------------- /app_mobx/components/TopicTextForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from 'react'; 2 | 3 | interface Props { 4 | onEntrySave: (value: string) => void, 5 | onEntryChange: (value: string) => void, 6 | value: string, 7 | className?: string, 8 | placeholder: string, 9 | } 10 | const TopicTextForm: FC = ({ 11 | onEntrySave, onEntryChange, value, className, placeholder, 12 | }) => { 13 | /* 14 | * Invokes the callback passed in as onSave, allowing this component to be 15 | * used in different ways. I personally think this makes it more reusable. 16 | */ 17 | const onSave = useCallback(() => { 18 | onEntrySave(value); 19 | }, [value]); 20 | 21 | /* 22 | * Invokes the callback passed in as onSave, allowing this component to be 23 | * used in different ways. I personally think this makes it more reusable. 24 | * @param {object} event 25 | */ 26 | const onChange = useCallback((event) => { 27 | onEntryChange(event.currentTarget.value); 28 | }, []); 29 | 30 | /* 31 | * Be careful that value is a dependency for onSave function! 32 | * @param {object} event 33 | */ 34 | const onSubmit = useCallback((event) => { 35 | event.preventDefault(); 36 | onSave(); 37 | }, [value]); 38 | 39 | return ( 40 |
41 | 48 |
49 | ); 50 | }; 51 | 52 | export default TopicTextForm; 53 | -------------------------------------------------------------------------------- /app_mobx/containers/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Contribute, Description, Header } from '../css/components/about'; 4 | 5 | /* 6 | * Note: This is kept as a container-level component, 7 | * i.e. We should keep this as the container that does the data-fetching 8 | * and dispatching of actions if you decide to have any sub-components. 9 | */ 10 | const About = () => { 11 | return ( 12 |
13 |
About Ninja Ocean
14 | 15 |

16 | Imagine an ocean of ninjas. Now think of it as a metaphor. 17 |
18 | Seriously, we love good tech. React, redux, scala, Haskell, machine learning, you name it! 19 |

20 |
21 | 22 |

23 | Want to contribute? Help us out! 24 | If you think the code on   25 | this repo 26 |  could be improved, please create an issue  27 | here 28 | ! 29 |

30 |
31 |
32 | ); 33 | }; 34 | 35 | export default About; 36 | -------------------------------------------------------------------------------- /app_mobx/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import { renderRoutes, RouteConfig } from 'react-router-config'; 4 | import { Global } from '@emotion/react'; 5 | 6 | import { AppWrapper, global } from '../css/main'; 7 | import Navigation from './Navigation'; 8 | import Message from './Message'; 9 | 10 | interface Props { 11 | route: RouteConfig; 12 | } 13 | const App: FC = ({ route }) => ( 14 | 15 | 18 | 19 | 20 | 21 | {renderRoutes(route.routes)} 22 | 23 | 24 | ); 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /app_mobx/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | * Note: This is kept as a container-level component, 5 | * i.e. We should keep this as the container that does the data-fetching 6 | * and dispatching of actions if you decide to have any sub-components. 7 | */ 8 | const Dashboard = () =>
Welcome to the Dasboard. Stay tuned...
; 9 | 10 | export default Dashboard; 11 | -------------------------------------------------------------------------------- /app_mobx/containers/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useObserver } from 'mobx-react'; 3 | 4 | import { MessageWrapper } from '../css/components/message'; 5 | import useStore from '../useStore'; 6 | 7 | const Message = () => { 8 | const { messageStore: { message, type, dismissMessage } } = useStore(); 9 | 10 | return useObserver(() => ( 11 | 0 : false} 14 | success={type === 'SUCCESS'} 15 | onClick={dismissMessage}> 16 | {message} 17 | 18 | )); 19 | }; 20 | 21 | export default Message; 22 | -------------------------------------------------------------------------------- /app_mobx/containers/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { useObserver } from 'mobx-react'; 4 | 5 | import { NavigationWrapper, Item, Logo } from '../css/components/navigation'; 6 | import useStore from '../useStore'; 7 | 8 | const LogOut = Item.withComponent<'button'>('button'); 9 | 10 | const Navigation = () => { 11 | const { userStore } = useStore(); 12 | const history = useHistory(); 13 | 14 | const dispatchLogOut = useCallback(() => { 15 | userStore.logOut(); 16 | history.push('/'); 17 | }, []); 18 | 19 | // activeClassName issues https://github.com/ReactTraining/react-router/issues/6201 20 | return useObserver(() => ( 21 | 22 | Ninja Ocean 23 | {userStore.authenticated ? ( 24 | Logout 25 | ) : ( 26 | Log in 27 | )} 28 | Dashboard 29 | About 30 | 31 | )); 32 | }; 33 | 34 | export default Navigation; 35 | -------------------------------------------------------------------------------- /app_mobx/containers/Vote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useObserver } from 'mobx-react'; 3 | 4 | import EntryBox from '../components/EntryBox'; 5 | import MainSection from '../components/MainSection'; 6 | import Scoreboard from '../components/Scoreboard'; 7 | import { VoteWrapper } from '../css/components/vote'; 8 | import useStore from '../useStore'; 9 | 10 | const Vote = () => { 11 | const { topicStore } = useStore(); 12 | 13 | return useObserver(() => ( 14 | 15 | 20 | 26 | 27 | 28 | )); 29 | }; 30 | 31 | export default Vote; 32 | -------------------------------------------------------------------------------- /app_mobx/css/common/button.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { small } from './typography'; 3 | 4 | export const button = css` 5 | ${small}; 6 | display: block; 7 | width: 100%; 8 | padding: 0.75rem 1.25rem; 9 | text-decoration: none; 10 | text-align: center; 11 | border-radius: 4px; 12 | border-style: solid; 13 | border-width: 2px; 14 | cursor: pointer; 15 | transition: 0.1s background-color ease; 16 | `; 17 | 18 | export const greenButton = css` 19 | ${button}; 20 | color: var(--colorWhite); 21 | background: var(--colorSalem); 22 | border-color: var(--colorSalem); 23 | 24 | &:hover { 25 | background: var(--colorDarkerSalem); 26 | border-color: var(--colorDarkerSalem); 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /app_mobx/css/common/card.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const card = css` 4 | border-radius: 4px; 5 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25); 6 | `; 7 | 8 | export const title = css` 9 | margin: 0; 10 | padding: 24px; 11 | border-radius: 4px 4px 0 0; 12 | `; 13 | -------------------------------------------------------------------------------- /app_mobx/css/common/input.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { normal } from './typography'; 3 | 4 | export const input = css` 5 | ${normal}; 6 | padding: 0.75rem 1rem; 7 | border: 2px solid var(--colorLoblolly); 8 | border-radius: 4px; 9 | width: 100%; 10 | 11 | background-color: var(--colorWhite); 12 | color: var(--colorLimedSpruce); 13 | margin-bottom: 0.75rem; 14 | `; 15 | -------------------------------------------------------------------------------- /app_mobx/css/common/layout.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const box = css` 4 | padding: 8px 24px; 5 | `; 6 | 7 | export const flexNotMoreThan200 = css` 8 | flex: 0 1 200px; 9 | `; 10 | -------------------------------------------------------------------------------- /app_mobx/css/common/typography.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const heading = css` 4 | font-size: 28px; 5 | font-weight: 700; 6 | line-height: 48px; 7 | `; 8 | 9 | export const subHeading = css` 10 | font-size: 21px; 11 | font-weight: 700; 12 | line-height: 24px; 13 | `; 14 | 15 | export const normal = css` 16 | font-size: 16px; 17 | font-weight: 400; 18 | line-height: 24px; 19 | `; 20 | 21 | export const small = css` 22 | font-size: 12px; 23 | font-weight: 400; 24 | line-height: 24px; 25 | `; 26 | -------------------------------------------------------------------------------- /app_mobx/css/components/about.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import { heading, normal, small } from '../common/typography'; 4 | 5 | export const Header = styled.h1` 6 | ${heading}; 7 | text-align: center; 8 | `; 9 | 10 | export const Description = styled.div` 11 | ${normal}; 12 | `; 13 | 14 | export const Contribute = styled.div` 15 | ${small}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_mobx/css/components/entrybox.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { heading } from '../common/typography'; 3 | import TopicTextForm from '../../components/TopicTextForm'; 4 | import { input } from '../common/input'; 5 | 6 | export const EntryBoxWrapper = styled.div` 7 | width: 100%; 8 | margin-bottom: 72px; 9 | `; 10 | 11 | export const Header = styled.h1` 12 | ${heading}; 13 | text-align: center; 14 | `; 15 | 16 | export const Input = styled(TopicTextForm)` 17 | ${heading}; 18 | ${input}; 19 | text-align: center; 20 | outline: none; 21 | `; 22 | -------------------------------------------------------------------------------- /app_mobx/css/components/mainSection.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | 5 | export const MainSectionWrapper = styled.div` 6 | ${card}; 7 | width: 50%; 8 | border-radius: var(--globalRadius); 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #ed193a; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | list-style: none; 20 | padding: 16px; 21 | `; 22 | -------------------------------------------------------------------------------- /app_mobx/css/components/message.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const MessageWrapper = styled.div<{ show: boolean; success: boolean; }>` 4 | display: none; 5 | padding: 12px 0; 6 | border-radius: var(--globalRadius); 7 | color: var(--colorBlack); 8 | font-size: var(--fontMedium); 9 | text-align: center; 10 | 11 | ${(props) => props.show && 'display: block;'}; 12 | ${(props) => props.success && ` 13 | background-color: var(--colorSalem); 14 | color: var(--colorWhite); 15 | `}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_mobx/css/components/navigation.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | export const NavigationWrapper = styled.nav` 5 | padding: 0 28px; 6 | `; 7 | 8 | export const Item = styled(NavLink)` 9 | display: inline-block; 10 | text-decoration: none; 11 | padding: 16px 32px; 12 | color: var(--colorBlack); 13 | background: transparent; 14 | border: none; 15 | font-family: var(--fontMontserrat); 16 | font-size: 16px; 17 | cursor: pointer; 18 | 19 | &.active { 20 | color: var(--colorDodgerBlue); 21 | } 22 | `; 23 | 24 | export const Logo = styled(Item)` 25 | font-size: var(--fontHuge); 26 | `; 27 | -------------------------------------------------------------------------------- /app_mobx/css/components/scoreboard.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | import { flexNotMoreThan200 } from '../common/layout'; 5 | 6 | export const ScoreboardWrapper = styled.div` 7 | ${card}; 8 | width: 40%; 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #0F9D58; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | padding: 16px; 20 | list-style: none; 21 | `; 22 | 23 | export const Item = styled.li` 24 | display: flex; 25 | justify-content: space-between; 26 | `; 27 | 28 | export const Topic = styled.span` 29 | ${flexNotMoreThan200}; 30 | font-size: var(--fontMedium); 31 | `; 32 | 33 | export const Count = styled.span``; 34 | -------------------------------------------------------------------------------- /app_mobx/css/components/topicItem.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { flexNotMoreThan200 } from '../common/layout'; 3 | 4 | export const TopicItemWrapper = styled.li` 5 | display: flex; 6 | justify-content: space-between; 7 | `; 8 | 9 | export const Topic = styled.span` 10 | ${flexNotMoreThan200}; 11 | font-size: var(--fontMedium); 12 | `; 13 | 14 | export const Button = styled.button` 15 | font-size: var(--fontMedium); 16 | width: 28px; 17 | height: 28px; 18 | border-radius: 50%; 19 | border: none; 20 | margin: 0 28px; 21 | color: #fff; 22 | `; 23 | 24 | export const Increment = styled(Button)` 25 | background-color: var(--colorSalem); 26 | `; 27 | 28 | export const Decrement = styled(Button)` 29 | background-color: var(--colorDodgerBlue); 30 | `; 31 | 32 | export const Destroy = styled(Button)` 33 | background-color: var(--colorPunch); 34 | `; 35 | -------------------------------------------------------------------------------- /app_mobx/css/components/vote.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const VoteWrapper = styled.div` 4 | display: flex; 5 | flex-flow: row wrap; 6 | justify-content: space-between; 7 | `; 8 | -------------------------------------------------------------------------------- /app_mobx/css/main.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { css } from '@emotion/react'; 3 | 4 | import { box } from './common/layout'; 5 | 6 | export const global = css` 7 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); 8 | :root { 9 | --globalRadius: 4px; 10 | --ViewTransitionIn: opacity .4s ease-in; 11 | --ViewTransitionOut: opacity .1s ease-out; 12 | --colorBlack: #000; 13 | --colorWhite: #fff; 14 | --colorDodgerBlue: #2196F3; 15 | --colorSalem: #0F9D58; 16 | --colorDarkerSalem: #0b6e3e; 17 | --colorPunch: #db4437; 18 | --colorLoblolly: #c3c8ce; 19 | --colorLimedSpruce: #333f48; 20 | --colorAthensGray: #f5f5f6; 21 | --colorMercury: #e3e3e3; 22 | --colorBombay: #b4bac1; 23 | --colorCrimson: #ed193a; 24 | --colorDarkerCrimson: color(var(--colorCrimson) blackness(+20%)); 25 | --fontMontserrat: 'Montserrat', Helvetica, Arial, sans-serif; 26 | --fontSmall: 12px; 27 | --fontMedium: 16px; 28 | --fontLarge: 21px; 29 | --fontHuge: 28px; 30 | } 31 | 32 | * { box-sizing: border-box; } 33 | 34 | body { 35 | margin: 0; 36 | font-family: var(--fontMontserrat); 37 | } 38 | `; 39 | 40 | export const AppWrapper = styled.div` 41 | ${box}; 42 | height: 100%; 43 | font-weight: normal; 44 | font-smoothing: antialiased; 45 | `; 46 | -------------------------------------------------------------------------------- /app_mobx/images/apple-ninja152-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_mobx/images/apple-ninja152-precomposed.png -------------------------------------------------------------------------------- /app_mobx/images/chrome-ninja192-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_mobx/images/chrome-ninja192-precomposed.png -------------------------------------------------------------------------------- /app_mobx/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_mobx/images/favicon.png -------------------------------------------------------------------------------- /app_mobx/images/ms-ninja144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_mobx/images/ms-ninja144-precomposed.png -------------------------------------------------------------------------------- /app_mobx/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import Page from './Page'; 3 | import AboutContainer from '../containers/About'; 4 | 5 | const About: FC = () => { 6 | const pageTitle = () => { 7 | return 'About | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of life' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default About; 34 | -------------------------------------------------------------------------------- /app_mobx/pages/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderRoutes } from 'react-router-config'; 3 | 4 | import Page from './Page'; 5 | import { link, meta, title } from './assets'; 6 | import routes from '../routes'; 7 | 8 | const App = () => ( 9 | 10 | {renderRoutes(routes)} 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /app_mobx/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useHistory } from 'react-router'; 3 | 4 | import Page from './Page'; 5 | import DashboardContainer from '../containers/Dashboard'; 6 | import useStore from '../useStore'; 7 | 8 | const Dashboard = () => { 9 | const { userStore: { authenticated } } = useStore(); 10 | const history = useHistory(); 11 | /* 12 | * Redirect to '/' if is not authenticated 13 | */ 14 | useEffect(() => { 15 | if (!authenticated) { 16 | history.replace('/'); 17 | } 18 | }, []); 19 | 20 | const pageTitle = () => { 21 | return 'Dashboard | reactGo'; 22 | }; 23 | 24 | const pageMeta = () => { 25 | return [ 26 | { name: 'description', content: 'A reactGo example of a dashboard page' }, 27 | ]; 28 | }; 29 | 30 | const pageLink = () => { 31 | return []; 32 | }; 33 | 34 | const getMetaData = () => ({ 35 | title: pageTitle(), 36 | meta: pageMeta(), 37 | link: pageLink(), 38 | }); 39 | 40 | return ( 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default Dashboard; 48 | -------------------------------------------------------------------------------- /app_mobx/pages/LoginOrRegister.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useHistory } from 'react-router'; 3 | 4 | import Page from './Page'; 5 | import LoginOrRegisterContainer from '../containers/LoginOrRegister'; 6 | import useStore from '../useStore'; 7 | 8 | const LoginOrRegister = () => { 9 | const { userStore: { authenticated }} = useStore(); 10 | const history = useHistory(); 11 | /* 12 | * Redirect to '/' if is already logged in. 13 | */ 14 | useEffect(() => { 15 | if (authenticated) { 16 | history.replace('/'); 17 | } 18 | }, []); 19 | 20 | const pageTitle = () => { 21 | return 'LoginOrRegister | reactGo'; 22 | }; 23 | 24 | const pageMeta = () => { 25 | return [ 26 | { name: 'description', content: 'A reactGo example of a login or register page' }, 27 | ]; 28 | }; 29 | 30 | const pageLink = () => { 31 | return []; 32 | }; 33 | 34 | const getMetaData = () => ({ 35 | title: pageTitle(), 36 | meta: pageMeta(), 37 | link: pageLink(), 38 | }); 39 | 40 | return ( 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default LoginOrRegister; 48 | -------------------------------------------------------------------------------- /app_mobx/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | 4 | const About = () => { 5 | const pageTitle = () => { 6 | return '404 Not Found | reactGo'; 7 | }; 8 | 9 | const pageMeta = () => { 10 | return [ 11 | { name: 'description', content: '404 Not Found' }, 12 | ]; 13 | }; 14 | 15 | const pageLink = () => { 16 | return []; 17 | }; 18 | 19 | const getMetaData = () => ({ 20 | title: pageTitle(), 21 | meta: pageMeta(), 22 | link: pageLink(), 23 | }); 24 | 25 | return ( 26 | 27 |
Oops, this page doesn't exist!
28 |
29 | ); 30 | }; 31 | 32 | export default About; 33 | -------------------------------------------------------------------------------- /app_mobx/pages/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | 4 | interface Props { 5 | title: string; 6 | link: Array<{}>; 7 | meta: Array<{}>; 8 | children: React.ReactNode; 9 | } 10 | const Page: FC = ({ 11 | title, link, meta, children, 12 | }) => { 13 | return ( 14 |
15 | 16 | {children} 17 |
18 | ); 19 | }; 20 | 21 | Page.defaultProps = { 22 | title: '', 23 | link: [], 24 | meta: [], 25 | }; 26 | 27 | export default Page; 28 | -------------------------------------------------------------------------------- /app_mobx/pages/Vote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import VoteContainer from '../containers/Vote'; 4 | 5 | const Vote = () => { 6 | const pageTitle = () => { 7 | return 'Vote | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of a voting page' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Vote; 34 | -------------------------------------------------------------------------------- /app_mobx/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as App } from './App'; 2 | export { default as Vote} from './Vote'; 3 | export { default as Dashboard } from './Dashboard'; 4 | export { default as LoginOrRegister } from './LoginOrRegister'; 5 | export { default as About } from './About'; 6 | export { default as NotFound } from './NotFound'; 7 | -------------------------------------------------------------------------------- /app_mobx/routes.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteConfig } from 'react-router-config'; 3 | import App from './containers/App'; 4 | import NotFound from './pages/NotFound'; 5 | import Vote from './pages/Vote'; 6 | import LoginOrRegister from './pages/LoginOrRegister'; 7 | import Dashboard from './pages/Dashboard'; 8 | import About from './pages/About'; 9 | import { Store } from './store'; 10 | 11 | const routes : RouteConfig[] = [{ 12 | component: App as React.ComponentType, 13 | routes: [{ 14 | path: '/', 15 | exact: true, 16 | component: Vote, 17 | fetchData: (store: Store) => { 18 | return store.topicStore.getTopics(); 19 | }, 20 | }, { 21 | path: '/login', 22 | component: LoginOrRegister, 23 | }, { 24 | path: '/dashboard', 25 | component: Dashboard, 26 | }, { 27 | path: '/about', 28 | component: About, 29 | }, { 30 | path: '*', 31 | component: NotFound, 32 | }], 33 | }]; 34 | 35 | export default routes; 36 | -------------------------------------------------------------------------------- /app_mobx/services/authentication.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default () => { 4 | return { 5 | login: ({ email, password }: { email: string, password: string }) => axios.post('/sessions', { email, password }), 6 | signUp: ({ email, password }: { email: string, password: string }) => axios.post('/users', { email, password }), 7 | logOut: () => axios.delete('/sessions') 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /app_mobx/services/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { apiEndpoint } from '../../config/app'; 4 | 5 | axios.defaults.baseURL = apiEndpoint; 6 | 7 | export { default as voteService } from './topics'; 8 | export { default as authService } from './authentication'; 9 | -------------------------------------------------------------------------------- /app_mobx/services/topics.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default () => { 4 | return { 5 | getTopics: () => axios.get('/topic'), 6 | deleteTopic: ({ id }: { id: string }) => axios.delete(`/topic/${id}`), 7 | updateTopic: ({ id, data }: { id: string, data: any }) => axios.put(`/topic/${id}`, data), 8 | createTopic: ({ id, data }: { id: string, data: any }) => axios.post(`/topic/${id}`, data), 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /app_mobx/store/index.ts: -------------------------------------------------------------------------------- 1 | import { RouterStore } from 'mobx-react-router'; 2 | import createUserStore, { UserStore } from './user'; 3 | import createTopicStore, { TopicStore } from './topic'; 4 | import createMessageStore, { MessageStore } from './message'; 5 | 6 | export const routingStore = new RouterStore(); 7 | 8 | export interface Store { 9 | userStore: UserStore, 10 | topicStore: TopicStore, 11 | messageStore: MessageStore, 12 | routingStore: RouterStore, 13 | } 14 | 15 | export default (initialState: Store) => { 16 | const userStore = createUserStore(initialState.userStore); 17 | const topicStore = createTopicStore(initialState.topicStore); 18 | const messageStore = createMessageStore(initialState.messageStore); 19 | return ({ 20 | userStore, 21 | topicStore, 22 | messageStore, 23 | routingStore, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /app_mobx/store/message.ts: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export interface MessageStore { 4 | message: string, 5 | type: string, 6 | dismissMessage(): void, 7 | successMessage(message: string): void, 8 | } 9 | 10 | export default (initialState: MessageStore): MessageStore => { 11 | const messageStore: MessageStore = observable({ 12 | ...initialState, 13 | message: '', 14 | type: 'SUCCESS', 15 | dismissMessage() { 16 | messageStore.message = ''; 17 | messageStore.type = 'SUCCESS'; 18 | }, 19 | successMessage(message: string) { 20 | messageStore.message = message; 21 | messageStore.type = 'SUCCESS'; 22 | }, 23 | }); 24 | return messageStore; 25 | }; 26 | -------------------------------------------------------------------------------- /app_mobx/useStore.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { storeContext } from './Context'; 3 | 4 | function useStore() { 5 | const store = React.useContext(storeContext); 6 | if (!store) { 7 | // this is especially useful in TypeScript so you don't need to be checking for null all the time 8 | throw new Error('useStore must be used within a StoreProvider.'); 9 | } 10 | return store; 11 | } 12 | 13 | export default useStore; 14 | -------------------------------------------------------------------------------- /app_saga/actions/messages.ts: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return: 0, no-else-return: 0 */ 2 | import * as types from '../types'; 3 | 4 | export function dismissMessage() { 5 | return { type: types.DISMISS_MESSAGE }; 6 | } 7 | 8 | export default { dismissMessage }; 9 | -------------------------------------------------------------------------------- /app_saga/actions/users.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../types'; 2 | 3 | export function beginLogin(data: { email: string; password: string }) { 4 | return { type: types.LOGIN_USER_REQUEST, data }; 5 | } 6 | 7 | export function loginSuccess(message: string) { 8 | return { 9 | type: types.LOGIN_USER_SUCCESS, 10 | message 11 | }; 12 | } 13 | 14 | export function loginError(message: string) { 15 | return { 16 | type: types.LOGIN_USER_FAILURE, 17 | message 18 | }; 19 | } 20 | 21 | export function signUpError(message: string) { 22 | return { 23 | type: types.SIGNUP_USER_FAILURE, 24 | message 25 | }; 26 | } 27 | 28 | export function beginSignUp(data: { email: string; password: string }) { 29 | return { type: types.SIGNUP_USER_REQUEST, data }; 30 | } 31 | 32 | export function signUpSuccess(message: string) { 33 | return { 34 | type: types.SIGNUP_USER_SUCCESS, 35 | message 36 | }; 37 | } 38 | 39 | export function beginLogout() { 40 | return { type: types.LOGOUT_USER_REQUEST}; 41 | } 42 | 43 | export function logoutSuccess() { 44 | return { type: types.LOGOUT_USER_SUCCESS }; 45 | } 46 | 47 | export function logoutError(error: string) { 48 | return { type: types.LOGOUT_USER_FAILURE, error }; 49 | } 50 | 51 | export function toggleLoginMode() { 52 | return { type: types.TOGGLE_LOGIN_MODE }; 53 | } 54 | -------------------------------------------------------------------------------- /app_saga/components/EntryBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { EntryBoxWrapper, Header, Input } from '../css/components/entrybox'; 4 | 5 | interface Props { 6 | onEntryChange: (value: string) => void; 7 | onEntrySave: (value: string) => void; 8 | topic: string; 9 | } 10 | 11 | // Takes callback functions from props and passes it down to TopicTextInput 12 | // Essentially this is passing the callback function two levels down from parent 13 | // to grandchild. To make it cleaner, you could consider: 14 | // 1. moving `connect` down to this component so you could mapStateToProps and dispatch 15 | // 2. Move TopicTextInput up to EntryBox so it's less confusing 16 | const EntryBox: FC = ({ onEntryChange, onEntrySave, topic }) => { 17 | return ( 18 | 19 |
Vote for your top hack idea
20 | 25 |
26 | ); 27 | }; 28 | 29 | export default EntryBox; 30 | -------------------------------------------------------------------------------- /app_saga/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Topic } from '../reducers/topic'; 3 | 4 | import TopicItem from './TopicItem'; 5 | import { Header, List, MainSectionWrapper } from '../css/components/mainSection'; 6 | 7 | interface Props { 8 | topics: Topic[], 9 | onIncrement: (id: string) => void, 10 | onDecrement: (id: string) => void, 11 | onDestroy: (id: string) => void, 12 | } 13 | const MainSection: FC = ({ 14 | topics, onIncrement, onDecrement, onDestroy 15 | }) => { 16 | const topicItems = topics.map((topic, key) => { 17 | return ( 18 | 25 | ); 26 | }); 27 | 28 | return ( 29 | 30 |
Vote for your favorite hack day idea
31 | {topicItems} 32 |
33 | ); 34 | }; 35 | 36 | export default MainSection; 37 | -------------------------------------------------------------------------------- /app_saga/components/Scoreboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Count, Item, Topic, ScoreboardWrapper, Header, List } from '../css/components/scoreboard'; 3 | import { Topic as ITopic } from '../reducers/topic'; 4 | 5 | interface Props { 6 | topics: ITopic[]; 7 | } 8 | const Scoreboard: FC = ({topics}) => { 9 | const topicListItems = topics.map((topic, key) => { 10 | return ( 11 | 12 | {topic.text} 13 | {topic.count} 14 | 15 | ); 16 | }); 17 | return ( 18 | 19 |
Vote count
20 | 21 | {topicListItems} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Scoreboard; 28 | -------------------------------------------------------------------------------- /app_saga/components/TopicItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { Decrement, Destroy, Increment, Topic, TopicItemWrapper } from '../css/components/topicItem'; 4 | 5 | interface Props { 6 | text: string, 7 | id: string, 8 | incrementCount: (id: string) => void, 9 | decrementCount: (id: string) => void, 10 | destroyTopic: (id: string) => void, 11 | } 12 | 13 | const TopicItem: FC = ({ 14 | text, id, incrementCount, decrementCount, destroyTopic, 15 | }) => { 16 | const onIncrement = () => { 17 | incrementCount(id); 18 | }; 19 | const onDecrement = () => { 20 | decrementCount(id); 21 | }; 22 | const onDestroy = () => { 23 | destroyTopic(id); 24 | }; 25 | 26 | return ( 27 | 28 | {text} 29 | 32 | + 33 | 34 | 37 | - 38 | 39 | 42 | {String.fromCharCode(215)} 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default TopicItem; 49 | -------------------------------------------------------------------------------- /app_saga/components/TopicTextForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from 'react'; 2 | 3 | interface Props { 4 | onEntrySave: (value: string) => void, 5 | onEntryChange: (value: string) => void, 6 | value: string, 7 | className?: string, 8 | placeholder: string, 9 | } 10 | 11 | const TopicTextForm: FC = ({ 12 | onEntrySave, onEntryChange, value, className, placeholder, 13 | }) => { 14 | /* 15 | * Invokes the callback passed in as onSave, allowing this component to be 16 | * used in different ways. I personally think this makes it more reusable. 17 | */ 18 | const onSave = useCallback(() => { 19 | onEntrySave(value); 20 | }, [value]); 21 | 22 | /* 23 | * Invokes the callback passed in as onSave, allowing this component to be 24 | * used in different ways. I personally think this makes it more reusable. 25 | * @param {object} event 26 | */ 27 | const onChange = useCallback((event) => { 28 | onEntryChange(event.currentTarget.value); 29 | }, []); 30 | 31 | /* 32 | * Be careful that value is a dependency for onSave function! 33 | * @param {object} event 34 | */ 35 | const onSubmit = useCallback((event) => { 36 | event.preventDefault(); 37 | onSave(); 38 | }, [value]); 39 | 40 | return ( 41 |
42 | 49 |
50 | ); 51 | }; 52 | 53 | export default TopicTextForm; 54 | -------------------------------------------------------------------------------- /app_saga/containers/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Contribute, Description, Header } from '../css/components/about'; 4 | 5 | /* 6 | * Note: This is kept as a container-level component, 7 | * i.e. We should keep this as the container that does the data-fetching 8 | * and dispatching of actions if you decide to have any sub-components. 9 | */ 10 | const About = () => { 11 | return ( 12 |
13 |
About Ninja Ocean
14 | 15 |

16 | Imagine an ocean of ninjas. Now think of it as a metaphor. 17 |
18 | Seriously, we love good tech. React, redux, scala, Haskell, machine learning, you name it! 19 |

20 |
21 | 22 |

23 | Want to contribute? Help us out! 24 | If you think the code on   25 | this repo 26 |  could be improved, please create an issue  27 | here 28 | ! 29 |

30 |
31 |
32 | ); 33 | }; 34 | 35 | export default About; 36 | -------------------------------------------------------------------------------- /app_saga/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import { renderRoutes, RouteConfig } from 'react-router-config'; 4 | import { Global } from '@emotion/react'; 5 | 6 | import { AppWrapper, global } from '../css/main'; 7 | import Navigation from './Navigation'; 8 | import Message from './Message'; 9 | 10 | interface Props { 11 | route: RouteConfig; 12 | } 13 | const App: FC = ({ route }) => ( 14 | 15 | 18 | 19 | 20 | 21 | {renderRoutes(route.routes)} 22 | 23 | 24 | ); 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /app_saga/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | * Note: This is kept as a container-level component, 5 | * i.e. We should keep this as the container that does the data-fetching 6 | * and dispatching of actions if you decide to have any sub-components. 7 | */ 8 | const Dashboard = () =>
Welcome to the Dasboard. Stay tuned...
; 9 | 10 | export default Dashboard; 11 | -------------------------------------------------------------------------------- /app_saga/containers/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { dismissMessage } from '../actions/messages'; 5 | import { MessageWrapper } from '../css/components/message'; 6 | import { RootState } from '../reducers'; 7 | 8 | const Message = () => { 9 | const { message, type } = useSelector((state: RootState) => state.message); 10 | const dispatch = useDispatch(); 11 | const dispatchDismissMessage = () => dispatch(dismissMessage()); 12 | 13 | return ( 14 | 0} 17 | success={type === 'SUCCESS'} 18 | onClick={dispatchDismissMessage}> 19 | {message} 20 | 21 | ); 22 | }; 23 | 24 | export default Message; 25 | -------------------------------------------------------------------------------- /app_saga/containers/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | import { NavigationWrapper, Item, Logo } from '../css/components/navigation'; 6 | import { beginLogout } from '../actions/users'; 7 | import { RootState } from '../reducers'; 8 | 9 | const LogOut = Item.withComponent('button'); 10 | 11 | const Navigation = () => { 12 | const user = useSelector((state: RootState) => state.user); 13 | const dispatch = useDispatch(); 14 | const history = useHistory(); 15 | 16 | const dispatchLogOut = useCallback(() => { 17 | dispatch(beginLogout()); 18 | history.push('/'); 19 | }, []); 20 | 21 | // activeClassName issues https://github.com/ReactTraining/react-router/issues/6201 22 | return ( 23 | 24 | Ninja Ocean 25 | {user.authenticated ? ( 26 | Logout 27 | ) : ( 28 | Log in 29 | )} 30 | Dashboard 31 | About 32 | 33 | ); 34 | }; 35 | 36 | export default Navigation; 37 | -------------------------------------------------------------------------------- /app_saga/css/common/button.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { small } from './typography'; 3 | 4 | export const button = css` 5 | ${small}; 6 | display: block; 7 | width: 100%; 8 | padding: 0.75rem 1.25rem; 9 | text-decoration: none; 10 | text-align: center; 11 | border-radius: 4px; 12 | border-style: solid; 13 | border-width: 2px; 14 | cursor: pointer; 15 | transition: 0.1s background-color ease; 16 | `; 17 | 18 | export const greenButton = css` 19 | ${button}; 20 | color: var(--colorWhite); 21 | background: var(--colorSalem); 22 | border-color: var(--colorSalem); 23 | 24 | &:hover { 25 | background: var(--colorDarkerSalem); 26 | border-color: var(--colorDarkerSalem); 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /app_saga/css/common/card.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const card = css` 4 | border-radius: 4px; 5 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25); 6 | `; 7 | 8 | export const title = css` 9 | margin: 0; 10 | padding: 24px; 11 | border-radius: 4px 4px 0 0; 12 | `; 13 | -------------------------------------------------------------------------------- /app_saga/css/common/input.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { normal } from './typography'; 3 | 4 | export const input = css` 5 | ${normal}; 6 | padding: 0.75rem 1rem; 7 | border: 2px solid var(--colorLoblolly); 8 | border-radius: 4px; 9 | width: 100%; 10 | 11 | background-color: var(--colorWhite); 12 | color: var(--colorLimedSpruce); 13 | margin-bottom: 0.75rem; 14 | `; 15 | -------------------------------------------------------------------------------- /app_saga/css/common/layout.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const box = css` 4 | padding: 8px 24px; 5 | `; 6 | 7 | export const flexNotMoreThan200 = css` 8 | flex: 0 1 200px; 9 | `; 10 | -------------------------------------------------------------------------------- /app_saga/css/common/typography.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const heading = css` 4 | font-size: 28px; 5 | font-weight: 700; 6 | line-height: 48px; 7 | `; 8 | 9 | export const subHeading = css` 10 | font-size: 21px; 11 | font-weight: 700; 12 | line-height: 24px; 13 | `; 14 | 15 | export const normal = css` 16 | font-size: 16px; 17 | font-weight: 400; 18 | line-height: 24px; 19 | `; 20 | 21 | export const small = css` 22 | font-size: 12px; 23 | font-weight: 400; 24 | line-height: 24px; 25 | `; 26 | -------------------------------------------------------------------------------- /app_saga/css/components/about.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import { heading, normal, small } from '../common/typography'; 4 | 5 | export const Header = styled.h1` 6 | ${heading}; 7 | text-align: center; 8 | `; 9 | 10 | export const Description = styled.div` 11 | ${normal}; 12 | `; 13 | 14 | export const Contribute = styled.div` 15 | ${small}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_saga/css/components/entrybox.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { heading } from '../common/typography'; 3 | import TopicTextForm from '../../components/TopicTextForm'; 4 | import { input } from '../common/input'; 5 | 6 | export const EntryBoxWrapper = styled.div` 7 | width: 100%; 8 | margin-bottom: 72px; 9 | `; 10 | 11 | export const Header = styled.h1` 12 | ${heading}; 13 | text-align: center; 14 | `; 15 | 16 | export const Input = styled(TopicTextForm)` 17 | ${heading}; 18 | ${input}; 19 | text-align: center; 20 | outline: none; 21 | `; 22 | -------------------------------------------------------------------------------- /app_saga/css/components/mainSection.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | 5 | export const MainSectionWrapper = styled.div` 6 | ${card}; 7 | width: 50%; 8 | border-radius: var(--globalRadius); 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #ed193a; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | list-style: none; 20 | padding: 16px; 21 | `; 22 | -------------------------------------------------------------------------------- /app_saga/css/components/message.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const MessageWrapper = styled.div<{ show: boolean; success: boolean; }>` 4 | display: none; 5 | padding: 12px 0; 6 | border-radius: var(--globalRadius); 7 | color: var(--colorBlack); 8 | font-size: var(--fontMedium); 9 | text-align: center; 10 | 11 | ${(props) => props.show && 'display: block;'}; 12 | ${(props) => props.success && ` 13 | background-color: var(--colorSalem); 14 | color: var(--colorWhite); 15 | `}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_saga/css/components/navigation.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | export const NavigationWrapper = styled.nav` 5 | padding: 0 28px; 6 | `; 7 | 8 | export const Item = styled(NavLink)` 9 | display: inline-block; 10 | text-decoration: none; 11 | padding: 16px 32px; 12 | color: var(--colorBlack); 13 | background: transparent; 14 | border: none; 15 | font-family: var(--fontMontserrat); 16 | font-size: 16px; 17 | cursor: pointer; 18 | 19 | &.active { 20 | color: var(--colorDodgerBlue); 21 | } 22 | `; 23 | 24 | export const Logo = styled(Item)` 25 | font-size: var(--fontHuge); 26 | `; 27 | -------------------------------------------------------------------------------- /app_saga/css/components/scoreboard.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | import { flexNotMoreThan200 } from '../common/layout'; 5 | 6 | export const ScoreboardWrapper = styled.div` 7 | ${card}; 8 | width: 40%; 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #0F9D58; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | padding: 16px; 20 | list-style: none; 21 | `; 22 | 23 | export const Item = styled.li` 24 | display: flex; 25 | justify-content: space-between; 26 | `; 27 | 28 | export const Topic = styled.span` 29 | ${flexNotMoreThan200}; 30 | font-size: var(--fontMedium); 31 | `; 32 | 33 | export const Count = styled.span``; 34 | -------------------------------------------------------------------------------- /app_saga/css/components/topicItem.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { flexNotMoreThan200 } from '../common/layout'; 3 | 4 | export const TopicItemWrapper = styled.li` 5 | display: flex; 6 | justify-content: space-between; 7 | `; 8 | 9 | export const Topic = styled.span` 10 | ${flexNotMoreThan200}; 11 | font-size: var(--fontMedium); 12 | `; 13 | 14 | export const Button = styled.button` 15 | font-size: var(--fontMedium); 16 | width: 28px; 17 | height: 28px; 18 | border-radius: 50%; 19 | border: none; 20 | margin: 0 28px; 21 | color: #fff; 22 | `; 23 | 24 | export const Increment = styled(Button)` 25 | background-color: var(--colorSalem); 26 | `; 27 | 28 | export const Decrement = styled(Button)` 29 | background-color: var(--colorDodgerBlue); 30 | `; 31 | 32 | export const Destroy = styled(Button)` 33 | background-color: var(--colorPunch); 34 | `; 35 | -------------------------------------------------------------------------------- /app_saga/css/components/vote.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const VoteWrapper = styled.div` 4 | display: flex; 5 | flex-flow: row wrap; 6 | justify-content: space-between; 7 | `; 8 | -------------------------------------------------------------------------------- /app_saga/css/main.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { css } from '@emotion/react'; 3 | 4 | import { box } from './common/layout'; 5 | 6 | export const global = css` 7 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); 8 | :root { 9 | --globalRadius: 4px; 10 | --ViewTransitionIn: opacity .4s ease-in; 11 | --ViewTransitionOut: opacity .1s ease-out; 12 | --colorBlack: #000; 13 | --colorWhite: #fff; 14 | --colorDodgerBlue: #2196F3; 15 | --colorSalem: #0F9D58; 16 | --colorDarkerSalem: #0b6e3e; 17 | --colorPunch: #db4437; 18 | --colorLoblolly: #c3c8ce; 19 | --colorLimedSpruce: #333f48; 20 | --colorAthensGray: #f5f5f6; 21 | --colorMercury: #e3e3e3; 22 | --colorBombay: #b4bac1; 23 | --colorCrimson: #ed193a; 24 | --colorDarkerCrimson: color(var(--colorCrimson) blackness(+20%)); 25 | --fontMontserrat: 'Montserrat', Helvetica, Arial, sans-serif; 26 | --fontSmall: 12px; 27 | --fontMedium: 16px; 28 | --fontLarge: 21px; 29 | --fontHuge: 28px; 30 | } 31 | 32 | * { box-sizing: border-box; } 33 | 34 | body { 35 | margin: 0; 36 | font-family: var(--fontMontserrat); 37 | } 38 | `; 39 | 40 | export const AppWrapper = styled.div` 41 | ${box}; 42 | height: 100%; 43 | font-weight: normal; 44 | font-smoothing: antialiased; 45 | `; 46 | -------------------------------------------------------------------------------- /app_saga/images/apple-ninja152-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_saga/images/apple-ninja152-precomposed.png -------------------------------------------------------------------------------- /app_saga/images/chrome-ninja192-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_saga/images/chrome-ninja192-precomposed.png -------------------------------------------------------------------------------- /app_saga/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_saga/images/favicon.png -------------------------------------------------------------------------------- /app_saga/images/ms-ninja144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_saga/images/ms-ninja144-precomposed.png -------------------------------------------------------------------------------- /app_saga/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import AboutContainer from '../containers/About'; 4 | 5 | const About = () => { 6 | const pageTitle = () => { 7 | return 'About | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of life' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default About; 34 | -------------------------------------------------------------------------------- /app_saga/pages/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderRoutes } from 'react-router-config'; 3 | 4 | import Page from './Page'; 5 | import { link, meta, title } from './assets'; 6 | import routes from '../routes'; 7 | 8 | const App = () => ( 9 | 10 | {renderRoutes(routes)} 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /app_saga/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { replace } from 'connected-react-router'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { RootState } from '../reducers'; 5 | 6 | import Page from './Page'; 7 | import DashboardContainer from '../containers/Dashboard'; 8 | 9 | const Dashboard = () => { 10 | const { authenticated } = useSelector((state: RootState) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | /* 14 | * Redirect to '/' if is not authenticated 15 | */ 16 | useEffect(() => { 17 | if (!authenticated) { 18 | dispatch(replace({ pathname: '/' })); 19 | } 20 | }, []); 21 | 22 | const pageTitle = () => { 23 | return 'Dashboard | reactGo'; 24 | }; 25 | 26 | const pageMeta = () => { 27 | return [ 28 | { name: 'description', content: 'A reactGo example of a dashboard page' }, 29 | ]; 30 | }; 31 | 32 | const pageLink = () => { 33 | return []; 34 | }; 35 | 36 | const getMetaData = () => ({ 37 | title: pageTitle(), 38 | meta: pageMeta(), 39 | link: pageLink(), 40 | }); 41 | 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default Dashboard; 50 | -------------------------------------------------------------------------------- /app_saga/pages/LoginOrRegister.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { replace } from 'connected-react-router'; 4 | import { RootState } from '../reducers'; 5 | 6 | import Page from './Page'; 7 | import LoginOrRegisterContainer from '../containers/LoginOrRegister'; 8 | 9 | const LoginOrRegister = () => { 10 | const { authenticated } = useSelector((state: RootState) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | /* 14 | * Redirect to '/' if is already logged in. 15 | */ 16 | useEffect(() => { 17 | if (authenticated) { 18 | dispatch(replace({ pathname: '/' })); 19 | } 20 | }, []); 21 | 22 | const pageTitle = () => { 23 | return 'LoginOrRegister | reactGo'; 24 | }; 25 | 26 | const pageMeta = () => { 27 | return [ 28 | { name: 'description', content: 'A reactGo example of a login or register page' }, 29 | ]; 30 | }; 31 | 32 | const pageLink = () => { 33 | return []; 34 | }; 35 | 36 | const getMetaData = () => ({ 37 | title: pageTitle(), 38 | meta: pageMeta(), 39 | link: pageLink(), 40 | }); 41 | 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default LoginOrRegister; 50 | -------------------------------------------------------------------------------- /app_saga/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | 4 | const About = () => { 5 | const pageTitle = () => { 6 | return '404 Not Found | reactGo'; 7 | }; 8 | 9 | const pageMeta = () => { 10 | return [ 11 | { name: 'description', content: '404 Not Found' }, 12 | ]; 13 | }; 14 | 15 | const pageLink = () => { 16 | return []; 17 | }; 18 | 19 | const getMetaData = () => ({ 20 | title: pageTitle(), 21 | meta: pageMeta(), 22 | link: pageLink(), 23 | }); 24 | 25 | return ( 26 | 27 |
Oops, this page doesn't exist!
28 |
29 | ); 30 | }; 31 | 32 | export default About; 33 | -------------------------------------------------------------------------------- /app_saga/pages/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | 4 | interface Props { 5 | title: string; 6 | link: Array<{}>; 7 | meta: Array<{}>; 8 | children: React.ReactNode; 9 | } 10 | 11 | const Page: FC = ({ 12 | title, link, meta, children, 13 | }) => { 14 | return ( 15 |
16 | 17 | {children} 18 |
19 | ); 20 | }; 21 | 22 | Page.defaultProps = { 23 | title: '', 24 | link: [], 25 | meta: [], 26 | }; 27 | 28 | export default Page; 29 | -------------------------------------------------------------------------------- /app_saga/pages/Vote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import VoteContainer from '../containers/Vote'; 4 | 5 | const Vote = () => { 6 | const pageTitle = () => { 7 | return 'Vote | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of a voting page' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Vote; 34 | -------------------------------------------------------------------------------- /app_saga/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as App } from './App'; 2 | export { default as Vote} from './Vote'; 3 | export { default as Dashboard } from './Dashboard'; 4 | export { default as LoginOrRegister } from './LoginOrRegister'; 5 | export { default as About } from './About'; 6 | export { default as NotFound } from './NotFound'; 7 | -------------------------------------------------------------------------------- /app_saga/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { History } from 'history'; 2 | import { combineReducers } from 'redux'; 3 | import { connectRouter } from 'connected-react-router'; 4 | 5 | import user from './user'; 6 | import topic from './topic'; 7 | import message from './message'; 8 | 9 | // Combine reducers with connectRouter which keeps track of router state 10 | const createRootReducer = (history: History) => combineReducers({ 11 | topic, 12 | user, 13 | message, 14 | router: connectRouter(history), 15 | }); 16 | 17 | export type RootState = ReturnType>; 18 | 19 | export default createRootReducer; 20 | -------------------------------------------------------------------------------- /app_saga/reducers/message.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux'; 2 | import * as types from '../types'; 3 | 4 | /* 5 | * Message store for global messages, i.e. Network messages / Redirect messages 6 | * that need to be communicated on the page itself. Ideally 7 | * messages/notifications should appear within the component to give the user 8 | * more context. - My 2 cents. 9 | */ 10 | export default function message(state = { 11 | message: '', 12 | type: 'SUCCESS' 13 | }, action: AnyAction) { 14 | switch (action.type) { 15 | case types.LOGIN_USER_REQUEST: 16 | case types.SIGNUP_USER_SUCCESS: 17 | return {...state, message: action.message, type: 'SUCCESS'}; 18 | case types.DISMISS_MESSAGE: 19 | return {...state, message: '', type: 'SUCCESS'}; 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app_saga/routes.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteConfig } from 'react-router-config'; 3 | import { Store } from 'redux'; 4 | 5 | import App from './containers/App'; 6 | import NotFound from './pages/NotFound'; 7 | import Vote from './pages/Vote'; 8 | import LoginOrRegister from './pages/LoginOrRegister'; 9 | import Dashboard from './pages/Dashboard'; 10 | import About from './pages/About'; 11 | import { getTopicsRequest } from './actions/topics'; 12 | 13 | const routes: RouteConfig[] = [{ 14 | component: App as React.ComponentType, 15 | routes: [{ 16 | path: '/', 17 | exact: true, 18 | component: Vote, 19 | fetchData: (store: Store) => { 20 | return store.dispatch(getTopicsRequest()); 21 | }, 22 | }, { 23 | path: '/login', 24 | component: LoginOrRegister, 25 | }, { 26 | path: '/dashboard', 27 | component: Dashboard, 28 | }, { 29 | path: '/about', 30 | component: About, 31 | }, { 32 | path: '*', 33 | component: NotFound 34 | }], 35 | }]; 36 | 37 | export default routes; 38 | -------------------------------------------------------------------------------- /app_saga/sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all, fork } from 'redux-saga/effects'; 2 | 3 | import topics from './topics'; 4 | import users from './users'; 5 | 6 | export default function* rootSaga() { 7 | yield all([ 8 | fork(topics), 9 | fork(users), 10 | ]); 11 | } 12 | -------------------------------------------------------------------------------- /app_saga/services/authentication.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface UserData { 4 | email: string, 5 | password: string, 6 | } 7 | export default () => { 8 | return { 9 | login: ({ email, password }: UserData) => axios.post('/sessions', { email, password }), 10 | signUp: ({ email, password }: UserData) => axios.post('/users', { email, password }), 11 | logOut: () => axios.delete('/sessions') 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /app_saga/services/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { apiEndpoint } from '../../config/app'; 4 | 5 | axios.defaults.baseURL = apiEndpoint; 6 | 7 | export { default as voteService } from './topics'; 8 | export { default as authService } from './authentication'; 9 | -------------------------------------------------------------------------------- /app_saga/services/topics.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Topic } from '../reducers/topic'; 3 | 4 | export default () => { 5 | return { 6 | getTopics: () => axios.get('/topic'), 7 | deleteTopic: ({ id }: { id: string }) => axios.delete(`/topic/${id}`), 8 | updateTopic: ({ id, data }: { id: string, data: { isFull: boolean, isIncrement: boolean } }) => axios.put(`/topic/${id}`, data), 9 | createTopic: ({ id, data }: { id: string, data: Topic }) => axios.post(`/topic/${id}`, data), 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /app_thunk/actions/messages.ts: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return: 0, no-else-return: 0 */ 2 | import * as types from '../types'; 3 | 4 | export function dismissMessage() { 5 | return { type: types.DISMISS_MESSAGE }; 6 | } 7 | 8 | export default { dismissMessage }; 9 | -------------------------------------------------------------------------------- /app_thunk/components/EntryBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { EntryBoxWrapper, Header, Input } from '../css/components/entrybox'; 4 | 5 | interface Props { 6 | onEntryChange: (value: string) => void; 7 | onEntrySave: (value: string) => void; 8 | topic: string; 9 | } 10 | 11 | // Takes callback functions from props and passes it down to TopicTextInput 12 | // Essentially this is passing the callback function two levels down from parent 13 | // to grandchild. To make it cleaner, you could consider: 14 | // 1. moving `connect` down to this component so you could mapStateToProps and dispatch 15 | // 2. Move TopicTextInput up to EntryBox so it's less confusing 16 | const EntryBox: FC = ({ onEntryChange, onEntrySave, topic }) => { 17 | return ( 18 | 19 |
Vote for your top hack idea
20 | 25 |
26 | ); 27 | }; 28 | 29 | export default EntryBox; 30 | -------------------------------------------------------------------------------- /app_thunk/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Topic } from '../reducers/topic'; 3 | 4 | import TopicItem from './TopicItem'; 5 | import { Header, List, MainSectionWrapper } from '../css/components/mainSection'; 6 | 7 | interface Props { 8 | topics: Topic[], 9 | onIncrement: (id: string) => void, 10 | onDecrement: (id: string) => void, 11 | onDestroy: (id: string) => void, 12 | } 13 | const MainSection: FC = ({ 14 | topics, onIncrement, onDecrement, onDestroy 15 | }) => { 16 | const topicItems = topics.map((topic, key) => { 17 | return ( 18 | 25 | ); 26 | }); 27 | 28 | return ( 29 | 30 |
Vote for your favorite hack day idea
31 | {topicItems} 32 |
33 | ); 34 | }; 35 | 36 | export default MainSection; 37 | -------------------------------------------------------------------------------- /app_thunk/components/Scoreboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Count, Item, Topic, ScoreboardWrapper, Header, List } from '../css/components/scoreboard'; 3 | import { Topic as ITopic } from '../reducers/topic'; 4 | 5 | interface Props { 6 | topics: ITopic[]; 7 | } 8 | const Scoreboard: FC = ({topics}) => { 9 | const topicListItems = topics.map((topic, key) => { 10 | return ( 11 | 12 | {topic.text} 13 | {topic.count} 14 | 15 | ); 16 | }); 17 | return ( 18 | 19 |
Vote count
20 | 21 | {topicListItems} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Scoreboard; 28 | -------------------------------------------------------------------------------- /app_thunk/components/TopicItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { Decrement, Destroy, Increment, Topic, TopicItemWrapper } from '../css/components/topicItem'; 4 | 5 | interface Props { 6 | text: string, 7 | id: string, 8 | incrementCount: (id: string) => void, 9 | decrementCount: (id: string) => void, 10 | destroyTopic: (id: string) => void, 11 | } 12 | const TopicItem: FC = ({ 13 | text, id, incrementCount, decrementCount, destroyTopic 14 | }) => { 15 | const onIncrement = () => { 16 | incrementCount(id); 17 | }; 18 | const onDecrement = () => { 19 | decrementCount(id); 20 | }; 21 | const onDestroy = () => { 22 | destroyTopic(id); 23 | }; 24 | 25 | return ( 26 | 27 | {text} 28 | 31 | + 32 | 33 | 36 | - 37 | 38 | 41 | {String.fromCharCode(215)} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default TopicItem; 48 | -------------------------------------------------------------------------------- /app_thunk/components/TopicTextForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from 'react'; 2 | 3 | interface Props { 4 | onEntrySave: (value: string) => void, 5 | onEntryChange: (value: string) => void, 6 | value: string, 7 | className?: string, 8 | placeholder: string, 9 | } 10 | const TopicTextForm: FC = ({ 11 | onEntrySave, onEntryChange, value, className, placeholder, 12 | }) => { 13 | /* 14 | * Invokes the callback passed in as onSave, allowing this component to be 15 | * used in different ways. I personally think this makes it more reusable. 16 | */ 17 | const onSave = useCallback(() => { 18 | onEntrySave(value); 19 | }, [value]); 20 | 21 | /* 22 | * Invokes the callback passed in as onSave, allowing this component to be 23 | * used in different ways. I personally think this makes it more reusable. 24 | * @param {object} event 25 | */ 26 | const onChange = useCallback((event) => { 27 | onEntryChange(event.currentTarget.value); 28 | }, []); 29 | 30 | /* 31 | * Be careful that value is a dependency for onSave function! 32 | * @param {object} event 33 | */ 34 | const onSubmit = useCallback((event) => { 35 | event.preventDefault(); 36 | onSave(); 37 | }, [value]); 38 | 39 | return ( 40 |
41 | 48 |
49 | ); 50 | }; 51 | 52 | export default TopicTextForm; 53 | -------------------------------------------------------------------------------- /app_thunk/containers/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Contribute, Description, Header } from '../css/components/about'; 4 | 5 | /* 6 | * Note: This is kept as a container-level component, 7 | * i.e. We should keep this as the container that does the data-fetching 8 | * and dispatching of actions if you decide to have any sub-components. 9 | */ 10 | const About = () => { 11 | return ( 12 |
13 |
About Ninja Ocean
14 | 15 |

16 | Imagine an ocean of ninjas. Now think of it as a metaphor. 17 |
18 | Seriously, we love good tech. React, redux, scala, Haskell, machine learning, you name it! 19 |

20 |
21 | 22 |

23 | Want to contribute? Help us out! 24 | If you think the code on   25 | this repo 26 |  could be improved, please create an issue  27 | here 28 | ! 29 |

30 |
31 |
32 | ); 33 | }; 34 | 35 | export default About; 36 | -------------------------------------------------------------------------------- /app_thunk/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import { renderRoutes, RouteConfig } from 'react-router-config'; 4 | import { Global } from '@emotion/react'; 5 | 6 | import { AppWrapper, global } from '../css/main'; 7 | import Navigation from './Navigation'; 8 | import Message from './Message'; 9 | 10 | interface Props { 11 | route: RouteConfig; 12 | } 13 | const App: FC = ({ route }) => ( 14 | 15 | 18 | 19 | 20 | 21 | {renderRoutes(route.routes)} 22 | 23 | 24 | ); 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /app_thunk/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | * Note: This is kept as a container-level component, 5 | * i.e. We should keep this as the container that does the data-fetching 6 | * and dispatching of actions if you decide to have any sub-components. 7 | */ 8 | const Dashboard = () =>
Welcome to the Dasboard. Stay tuned...
; 9 | 10 | export default Dashboard; 11 | -------------------------------------------------------------------------------- /app_thunk/containers/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { dismissMessage } from '../actions/messages'; 5 | import { MessageWrapper } from '../css/components/message'; 6 | import { RootState } from '../reducers'; 7 | 8 | const Message = () => { 9 | const { message, type } = useSelector((state) => state.message); 10 | const dispatch = useDispatch(); 11 | const dispatchDismissMessage = () => dispatch(dismissMessage()); 12 | 13 | return ( 14 | 0 || false} 17 | success={type === 'SUCCESS'} 18 | onClick={dispatchDismissMessage}> 19 | {message} 20 | 21 | ); 22 | }; 23 | 24 | export default Message; 25 | -------------------------------------------------------------------------------- /app_thunk/containers/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | import { logOut } from '../actions/users'; 6 | import { NavigationWrapper, Item, Logo } from '../css/components/navigation'; 7 | import { RootState } from '../reducers'; 8 | 9 | const LogOut = Item.withComponent('button') as React.ElementType; 10 | 11 | const Navigation = () => { 12 | const user = useSelector((state) => state.user); 13 | const dispatch = useDispatch(); 14 | const history = useHistory(); 15 | 16 | const dispatchLogOut = useCallback(() => { 17 | dispatch(logOut()); 18 | history.push('/'); 19 | }, []); 20 | 21 | // activeClassName issues https://github.com/ReactTraining/react-router/issues/6201 22 | return ( 23 | 24 | Ninja Ocean 25 | {user.authenticated ? ( 26 | Logout 27 | ) : ( 28 | Log in 29 | )} 30 | Dashboard 31 | About 32 | 33 | ); 34 | }; 35 | 36 | export default Navigation; 37 | -------------------------------------------------------------------------------- /app_thunk/containers/Vote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import EntryBox from '../components/EntryBox'; 5 | import MainSection from '../components/MainSection'; 6 | import Scoreboard from '../components/Scoreboard'; 7 | import { 8 | createTopic, typing, incrementCount, decrementCount, destroyTopic, 9 | } from '../actions/topics'; 10 | import { VoteWrapper } from '../css/components/vote'; 11 | import { RootState } from '../reducers'; 12 | 13 | const Vote = () => { 14 | const { topics, newTopic } = useSelector((state) => state.topic); 15 | const dispatch = useDispatch(); 16 | const dispatchCreateTopic = (data: string) => dispatch(createTopic(data)); 17 | const dispatchTyping = (data: string) => dispatch(typing(data)); 18 | const dispatchIncrementCount = (data: string) => dispatch(incrementCount(data)); 19 | const dispatchDecrementCount = (data: string) => dispatch(decrementCount(data)); 20 | const dispatchDestroyTopic = (data: string) => dispatch(destroyTopic(data)); 21 | 22 | return ( 23 | 24 | 29 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default Vote; 41 | -------------------------------------------------------------------------------- /app_thunk/css/common/button.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { small } from './typography'; 3 | 4 | export const button = css` 5 | ${small}; 6 | display: block; 7 | width: 100%; 8 | padding: 0.75rem 1.25rem; 9 | text-decoration: none; 10 | text-align: center; 11 | border-radius: 4px; 12 | border-style: solid; 13 | border-width: 2px; 14 | cursor: pointer; 15 | transition: 0.1s background-color ease; 16 | `; 17 | 18 | export const greenButton = css` 19 | ${button}; 20 | color: var(--colorWhite); 21 | background: var(--colorSalem); 22 | border-color: var(--colorSalem); 23 | 24 | &:hover { 25 | background: var(--colorDarkerSalem); 26 | border-color: var(--colorDarkerSalem); 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /app_thunk/css/common/card.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const card = css` 4 | border-radius: 4px; 5 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25); 6 | `; 7 | 8 | export const title = css` 9 | margin: 0; 10 | padding: 24px; 11 | border-radius: 4px 4px 0 0; 12 | `; 13 | -------------------------------------------------------------------------------- /app_thunk/css/common/input.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { normal } from './typography'; 3 | 4 | export const input = css` 5 | ${normal}; 6 | padding: 0.75rem 1rem; 7 | border: 2px solid var(--colorLoblolly); 8 | border-radius: 4px; 9 | width: 100%; 10 | 11 | background-color: var(--colorWhite); 12 | color: var(--colorLimedSpruce); 13 | margin-bottom: 0.75rem; 14 | `; 15 | -------------------------------------------------------------------------------- /app_thunk/css/common/layout.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const box = css` 4 | padding: 8px 24px; 5 | `; 6 | 7 | export const flexNotMoreThan200 = css` 8 | flex: 0 1 200px; 9 | `; 10 | -------------------------------------------------------------------------------- /app_thunk/css/common/typography.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const heading = css` 4 | font-size: 28px; 5 | font-weight: 700; 6 | line-height: 48px; 7 | `; 8 | 9 | export const subHeading = css` 10 | font-size: 21px; 11 | font-weight: 700; 12 | line-height: 24px; 13 | `; 14 | 15 | export const normal = css` 16 | font-size: 16px; 17 | font-weight: 400; 18 | line-height: 24px; 19 | `; 20 | 21 | export const small = css` 22 | font-size: 12px; 23 | font-weight: 400; 24 | line-height: 24px; 25 | `; 26 | -------------------------------------------------------------------------------- /app_thunk/css/components/about.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import { heading, normal, small } from '../common/typography'; 4 | 5 | export const Header = styled.h1` 6 | ${heading}; 7 | text-align: center; 8 | `; 9 | 10 | export const Description = styled.div` 11 | ${normal}; 12 | `; 13 | 14 | export const Contribute = styled.div` 15 | ${small}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_thunk/css/components/entrybox.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { heading } from '../common/typography'; 3 | import TopicTextForm from '../../components/TopicTextForm'; 4 | import { input } from '../common/input'; 5 | 6 | export const EntryBoxWrapper = styled.div` 7 | width: 100%; 8 | margin-bottom: 72px; 9 | `; 10 | 11 | export const Header = styled.h1` 12 | ${heading}; 13 | text-align: center; 14 | `; 15 | 16 | export const Input = styled(TopicTextForm)` 17 | ${heading}; 18 | ${input}; 19 | text-align: center; 20 | outline: none; 21 | `; 22 | -------------------------------------------------------------------------------- /app_thunk/css/components/mainSection.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | 5 | export const MainSectionWrapper = styled.div` 6 | ${card}; 7 | width: 50%; 8 | border-radius: var(--globalRadius); 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #ed193a; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | list-style: none; 20 | padding: 16px; 21 | `; 22 | -------------------------------------------------------------------------------- /app_thunk/css/components/message.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const MessageWrapper = styled.div<{ show: boolean; success: boolean; }>` 4 | display: none; 5 | padding: 12px 0; 6 | border-radius: var(--globalRadius); 7 | color: var(--colorBlack); 8 | font-size: var(--fontMedium); 9 | text-align: center; 10 | 11 | ${(props) => props.show && 'display: block;'}; 12 | ${(props) => props.success && ` 13 | background-color: var(--colorSalem); 14 | color: var(--colorWhite); 15 | `}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_thunk/css/components/navigation.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | export const NavigationWrapper = styled.nav` 5 | padding: 0 28px; 6 | `; 7 | 8 | export const Item = styled(NavLink)` 9 | display: inline-block; 10 | text-decoration: none; 11 | padding: 16px 32px; 12 | color: var(--colorBlack); 13 | background: transparent; 14 | border: none; 15 | font-family: var(--fontMontserrat); 16 | font-size: 16px; 17 | cursor: pointer; 18 | 19 | &.active { 20 | color: var(--colorDodgerBlue); 21 | } 22 | `; 23 | 24 | export const Logo = styled(Item)` 25 | font-size: var(--fontHuge); 26 | `; 27 | -------------------------------------------------------------------------------- /app_thunk/css/components/scoreboard.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | import { flexNotMoreThan200 } from '../common/layout'; 5 | 6 | export const ScoreboardWrapper = styled.div` 7 | ${card}; 8 | width: 40%; 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #0F9D58; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | padding: 16px; 20 | list-style: none; 21 | `; 22 | 23 | export const Item = styled.li` 24 | display: flex; 25 | justify-content: space-between; 26 | `; 27 | 28 | export const Topic = styled.span` 29 | ${flexNotMoreThan200}; 30 | font-size: var(--fontMedium); 31 | `; 32 | 33 | export const Count = styled.span``; 34 | -------------------------------------------------------------------------------- /app_thunk/css/components/topicItem.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { flexNotMoreThan200 } from '../common/layout'; 3 | 4 | export const TopicItemWrapper = styled.li` 5 | display: flex; 6 | justify-content: space-between; 7 | `; 8 | 9 | export const Topic = styled.span` 10 | ${flexNotMoreThan200}; 11 | font-size: var(--fontMedium); 12 | `; 13 | 14 | export const Button = styled.button` 15 | font-size: var(--fontMedium); 16 | width: 28px; 17 | height: 28px; 18 | border-radius: 50%; 19 | border: none; 20 | margin: 0 28px; 21 | color: #fff; 22 | `; 23 | 24 | export const Increment = styled(Button)` 25 | background-color: var(--colorSalem); 26 | `; 27 | 28 | export const Decrement = styled(Button)` 29 | background-color: var(--colorDodgerBlue); 30 | `; 31 | 32 | export const Destroy = styled(Button)` 33 | background-color: var(--colorPunch); 34 | `; 35 | -------------------------------------------------------------------------------- /app_thunk/css/components/vote.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const VoteWrapper = styled.div` 4 | display: flex; 5 | flex-flow: row wrap; 6 | justify-content: space-between; 7 | `; 8 | -------------------------------------------------------------------------------- /app_thunk/css/main.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { css } from '@emotion/react'; 3 | 4 | import { box } from './common/layout'; 5 | 6 | export const global = css` 7 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); 8 | :root { 9 | --globalRadius: 4px; 10 | --ViewTransitionIn: opacity .4s ease-in; 11 | --ViewTransitionOut: opacity .1s ease-out; 12 | --colorBlack: #000; 13 | --colorWhite: #fff; 14 | --colorDodgerBlue: #2196F3; 15 | --colorSalem: #0F9D58; 16 | --colorDarkerSalem: #0b6e3e; 17 | --colorPunch: #db4437; 18 | --colorLoblolly: #c3c8ce; 19 | --colorLimedSpruce: #333f48; 20 | --colorAthensGray: #f5f5f6; 21 | --colorMercury: #e3e3e3; 22 | --colorBombay: #b4bac1; 23 | --colorCrimson: #ed193a; 24 | --colorDarkerCrimson: color(var(--colorCrimson) blackness(+20%)); 25 | --fontMontserrat: 'Montserrat', Helvetica, Arial, sans-serif; 26 | --fontSmall: 12px; 27 | --fontMedium: 16px; 28 | --fontLarge: 21px; 29 | --fontHuge: 28px; 30 | } 31 | 32 | * { box-sizing: border-box; } 33 | 34 | body { 35 | margin: 0; 36 | font-family: var(--fontMontserrat); 37 | } 38 | `; 39 | 40 | export const AppWrapper = styled.div` 41 | ${box}; 42 | height: 100%; 43 | font-weight: normal; 44 | font-smoothing: antialiased; 45 | `; 46 | -------------------------------------------------------------------------------- /app_thunk/images/apple-ninja152-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_thunk/images/apple-ninja152-precomposed.png -------------------------------------------------------------------------------- /app_thunk/images/chrome-ninja192-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_thunk/images/chrome-ninja192-precomposed.png -------------------------------------------------------------------------------- /app_thunk/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_thunk/images/favicon.png -------------------------------------------------------------------------------- /app_thunk/images/ms-ninja144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_thunk/images/ms-ninja144-precomposed.png -------------------------------------------------------------------------------- /app_thunk/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import AboutContainer from '../containers/About'; 4 | 5 | const About = () => { 6 | const pageTitle = () => { 7 | return 'About | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of life' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default About; 34 | -------------------------------------------------------------------------------- /app_thunk/pages/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderRoutes } from 'react-router-config'; 3 | 4 | import Page from './Page'; 5 | import { link, meta, title } from './assets'; 6 | import routes from '../routes'; 7 | 8 | const App = () => ( 9 | 10 | {renderRoutes(routes)} 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /app_thunk/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { replace } from 'connected-react-router'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { RootState } from '../reducers'; 5 | 6 | import Page from './Page'; 7 | import DashboardContainer from '../containers/Dashboard'; 8 | 9 | const Dashboard = () => { 10 | const { authenticated } = useSelector((state) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | /* 14 | * Redirect to '/' if is not authenticated 15 | */ 16 | useEffect(() => { 17 | if (!authenticated) { 18 | dispatch(replace({ pathname: '/' })); 19 | } 20 | }, []); 21 | 22 | const pageTitle = () => { 23 | return 'Dashboard | reactGo'; 24 | }; 25 | 26 | const pageMeta = () => { 27 | return [ 28 | { name: 'description', content: 'A reactGo example of a dashboard page' }, 29 | ]; 30 | }; 31 | 32 | const pageLink = () => { 33 | return []; 34 | }; 35 | 36 | const getMetaData = () => ({ 37 | title: pageTitle(), 38 | meta: pageMeta(), 39 | link: pageLink(), 40 | }); 41 | 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default Dashboard; 50 | -------------------------------------------------------------------------------- /app_thunk/pages/LoginOrRegister.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { replace } from 'connected-react-router'; 4 | import { RootState } from '../reducers'; 5 | 6 | import Page from './Page'; 7 | import LoginOrRegisterContainer from '../containers/LoginOrRegister'; 8 | 9 | const LoginOrRegister = () => { 10 | const { authenticated } = useSelector((state) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | /* 14 | * Redirect to '/' if is already logged in. 15 | */ 16 | useEffect(() => { 17 | if (authenticated) { 18 | dispatch(replace({ pathname: '/' })); 19 | } 20 | }, []); 21 | 22 | const pageTitle = () => { 23 | return 'LoginOrRegister | reactGo'; 24 | }; 25 | 26 | const pageMeta = () => { 27 | return [ 28 | { name: 'description', content: 'A reactGo example of a login or register page' }, 29 | ]; 30 | }; 31 | 32 | const pageLink = () => { 33 | return []; 34 | }; 35 | 36 | const getMetaData = () => ({ 37 | title: pageTitle(), 38 | meta: pageMeta(), 39 | link: pageLink(), 40 | }); 41 | 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default LoginOrRegister; 50 | -------------------------------------------------------------------------------- /app_thunk/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | 4 | const About = () => { 5 | const pageTitle = () => { 6 | return '404 Not Found | reactGo'; 7 | }; 8 | 9 | const pageMeta = () => { 10 | return [ 11 | { name: 'description', content: '404 Not Found' }, 12 | ]; 13 | }; 14 | 15 | const pageLink = () => { 16 | return []; 17 | }; 18 | 19 | const getMetaData = () => ({ 20 | title: pageTitle(), 21 | meta: pageMeta(), 22 | link: pageLink(), 23 | }); 24 | 25 | return ( 26 | 27 |
Oops, this page doesn't exist!
28 |
29 | ); 30 | }; 31 | 32 | export default About; 33 | -------------------------------------------------------------------------------- /app_thunk/pages/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | 4 | interface Props { 5 | title: string; 6 | link: Array<{}>; 7 | meta: Array<{}>; 8 | children: React.ReactNode; 9 | } 10 | 11 | const Page: FC = ({ 12 | title, link, meta, children, 13 | }) => { 14 | return ( 15 |
16 | 17 | {children} 18 |
19 | ); 20 | }; 21 | 22 | export default Page; 23 | -------------------------------------------------------------------------------- /app_thunk/pages/Vote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import VoteContainer from '../containers/Vote'; 4 | 5 | const Vote = () => { 6 | const pageTitle = () => { 7 | return 'Vote | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of a voting page' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Vote; 34 | -------------------------------------------------------------------------------- /app_thunk/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as App } from './App'; 2 | export { default as Vote} from './Vote'; 3 | export { default as Dashboard } from './Dashboard'; 4 | export { default as LoginOrRegister } from './LoginOrRegister'; 5 | export { default as About } from './About'; 6 | export { default as NotFound } from './NotFound'; 7 | -------------------------------------------------------------------------------- /app_thunk/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Reducer } from 'redux'; 2 | import { History } from 'history'; 3 | import { connectRouter, RouterState } from 'connected-react-router'; 4 | 5 | import user from './user'; 6 | import topic, { Topic } from './topic'; 7 | import message from './message'; 8 | 9 | // Combine reducers with connectRouter which keeps track of router state 10 | const createRootReducer = (history: History) => combineReducers({ 11 | topic, 12 | user, 13 | message, 14 | router: connectRouter(history), 15 | }); 16 | 17 | export interface RootState { 18 | topic: { 19 | topics: Topic[]; 20 | newTopic: string; 21 | }, 22 | user: { 23 | authenticated: boolean; 24 | isWaiting: boolean; 25 | message: string; 26 | isLogin: boolean; 27 | }, 28 | message: { 29 | type: string; 30 | message: string; 31 | }, 32 | router: Reducer 33 | } 34 | 35 | export default createRootReducer; 36 | -------------------------------------------------------------------------------- /app_thunk/reducers/message.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux'; 2 | import * as types from '../types'; 3 | 4 | /* 5 | * Message store for global messages, i.e. Network messages / Redirect messages 6 | * that need to be communicated on the page itself. Ideally 7 | * messages/notifications should appear within the component to give the user 8 | * more context. - My 2 cents. 9 | */ 10 | function message(state = { 11 | message: '', 12 | type: 'SUCCESS' 13 | }, action: AnyAction) { 14 | switch (action.type) { 15 | case types.LOGIN_USER_REQUEST: 16 | case types.SIGNUP_USER_SUCCESS: 17 | return {...state, message: action.message, type: 'SUCCESS'}; 18 | case types.DISMISS_MESSAGE: 19 | return {...state, message: '', type: 'SUCCESS'}; 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | export default message; 26 | -------------------------------------------------------------------------------- /app_thunk/routes.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteConfig } from 'react-router-config'; 3 | import { Store } from 'redux'; 4 | import {ThunkDispatch} from 'redux-thunk'; 5 | 6 | import App from './containers/App'; 7 | import Vote from './pages/Vote'; 8 | import LoginOrRegister from './pages/LoginOrRegister'; 9 | import Dashboard from './pages/Dashboard'; 10 | import About from './pages/About'; 11 | import NotFound from './pages/NotFound'; 12 | import { getTopics } from './actions/topics'; 13 | 14 | const routes: RouteConfig[] = [{ 15 | component: App as React.ComponentType, 16 | routes: [{ 17 | path: '/', 18 | exact: true, 19 | component: Vote, 20 | fetchData: (store: Store) => { 21 | return (store.dispatch as ThunkDispatch<{}, {}, any>)(getTopics()); 22 | }, 23 | }, { 24 | path: '/login', 25 | component: LoginOrRegister, 26 | }, { 27 | path: '/dashboard', 28 | component: Dashboard, 29 | }, { 30 | path: '/about', 31 | component: About, 32 | }, { 33 | path: '*', 34 | component: NotFound, 35 | }], 36 | }]; 37 | 38 | export default routes; 39 | -------------------------------------------------------------------------------- /app_thunk/services/authentication.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface UserData { 4 | email: string, 5 | password: string, 6 | } 7 | export default () => { 8 | return { 9 | login: ({ email, password }: UserData) => axios.post('/sessions', { email, password }), 10 | signUp: ({ email, password }: UserData) => axios.post('/users', { email, password }), 11 | logOut: () => axios.delete('/sessions') 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /app_thunk/services/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { apiEndpoint } from '../../config/app'; 4 | 5 | axios.defaults.baseURL = apiEndpoint; 6 | 7 | export { default as voteService } from './topics'; 8 | export { default as authService } from './authentication'; 9 | -------------------------------------------------------------------------------- /app_thunk/services/topics.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Topic } from '../reducers/topic'; 3 | 4 | export default () => { 5 | return { 6 | getTopics: () => axios.get('/topic'), 7 | deleteTopic: ({ id }: { id: string }) => axios.delete(`/topic/${id}`), 8 | updateTopic: ({ id, data }: { id: string, data: { isFull: boolean, isIncrement: boolean } }) => axios.put(`/topic/${id}`, data), 9 | createTopic: ({ id, data }: { id: string, data: Topic }) => axios.post(`/topic/${id}`, data), 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /app_thunk/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose, Store } from 'redux'; 2 | import { History } from 'history'; 3 | import { routerMiddleware } from 'connected-react-router'; 4 | import thunk from 'redux-thunk'; 5 | import { createLogger } from 'redux-logger'; 6 | import { composeWithDevTools } from 'redux-devtools-extension'; 7 | import createRootReducer from '../reducers'; 8 | import { isClient, isDebug } from '../../config/app'; 9 | 10 | /* 11 | * @param {Object} initial state to bootstrap our stores with for server-side rendering 12 | * @param {History Object} a history object. We use `createMemoryHistory` for server-side rendering, 13 | * while using browserHistory for client-side 14 | * rendering. 15 | */ 16 | export default function configureStore(initialState: any, history: History) { 17 | // Installs hooks that always keep react-router and redux store in sync 18 | const middleware = [thunk, routerMiddleware(history)]; 19 | let store: Store; 20 | 21 | if (isClient && isDebug) { 22 | middleware.push(createLogger()); 23 | store = createStore(createRootReducer(history), initialState, composeWithDevTools( 24 | applyMiddleware(...middleware), 25 | )); 26 | } else { 27 | store = createStore(createRootReducer(history), initialState, compose(applyMiddleware(...middleware), (f: any) => f)); 28 | } 29 | 30 | return store; 31 | } 32 | -------------------------------------------------------------------------------- /app_toolkit/actions/users.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { push } from 'connected-react-router'; 3 | import { authService } from '../services'; 4 | import { getTopics } from './topics'; 5 | 6 | const logIn = createAsyncThunk( 7 | 'users/logIn', 8 | async (data, { dispatch, rejectWithValue }) => { 9 | try { 10 | await authService().login(data); 11 | dispatch(getTopics()); 12 | dispatch(push('/')); 13 | return 'You have been successfully logged in'; 14 | } catch (err) { 15 | return rejectWithValue('Oops! Invalid username or password'); 16 | } 17 | } 18 | ); 19 | 20 | const signUp = createAsyncThunk( 21 | 'users/signUp', 22 | async (data, { dispatch, rejectWithValue }) => { 23 | try { 24 | await authService().signUp(data); 25 | dispatch(push('/')); 26 | return 'You have successfully registered an account!'; 27 | } catch (err) { 28 | return rejectWithValue('Oops! Something went wrong when signing up'); 29 | } 30 | } 31 | ); 32 | 33 | const logOut = createAsyncThunk( 34 | 'users/logOut', 35 | async (data, { rejectWithValue }) => { 36 | try { 37 | const response = await authService().logOut(); 38 | return response.data; 39 | } catch (err) { 40 | return rejectWithValue(err.response.data); 41 | } 42 | } 43 | ); 44 | 45 | export { logOut, logIn, signUp }; 46 | export default { logOut, logIn, signUp }; 47 | -------------------------------------------------------------------------------- /app_toolkit/client.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { createBrowserHistory } from 'history'; 5 | import { ConnectedRouter } from 'connected-react-router'; 6 | 7 | import App from './pages/App'; 8 | import configureStore from './store/configureStore'; 9 | 10 | const history = createBrowserHistory(); 11 | export const store = configureStore(window.__INITIAL_STATE__, history); 12 | export type AppDispatch = typeof store.dispatch; 13 | 14 | render( 15 | 16 | 17 | 18 | 19 | , document.getElementById('app'), 20 | ); 21 | -------------------------------------------------------------------------------- /app_toolkit/components/EntryBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { EntryBoxWrapper, Header, Input } from '../css/components/entrybox'; 4 | 5 | interface Props { 6 | onEntryChange: (value: string) => void; 7 | onEntrySave: (value: string) => void; 8 | topic: string; 9 | } 10 | 11 | // Takes callback functions from props and passes it down to TopicTextInput 12 | // Essentially this is passing the callback function two levels down from parent 13 | // to grandchild. To make it cleaner, you could consider: 14 | // 1. moving `connect` down to this component so you could mapStateToProps and dispatch 15 | // 2. Move TopicTextInput up to EntryBox so it's less confusing 16 | const EntryBox: FC = ({ onEntryChange, onEntrySave, topic }) => { 17 | return ( 18 | 19 |
Vote for your top hack idea
20 | 25 |
26 | ); 27 | }; 28 | 29 | export default EntryBox; 30 | -------------------------------------------------------------------------------- /app_toolkit/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Topic } from '../reducers/topic'; 3 | 4 | import TopicItem from './TopicItem'; 5 | import { Header, List, MainSectionWrapper } from '../css/components/mainSection'; 6 | 7 | interface Props { 8 | topics: Topic[], 9 | onIncrement: (id: string) => void, 10 | onDecrement: (id: string) => void, 11 | onDestroy: (id: string) => void, 12 | } 13 | const MainSection: FC = ({ 14 | topics, onIncrement, onDecrement, onDestroy 15 | }) => { 16 | const topicItems = topics.map((topic, key) => { 17 | return ( 18 | 25 | ); 26 | }); 27 | 28 | return ( 29 | 30 |
Vote for your favorite hack day idea
31 | {topicItems} 32 |
33 | ); 34 | }; 35 | 36 | export default MainSection; 37 | -------------------------------------------------------------------------------- /app_toolkit/components/Scoreboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Count, Item, Topic, ScoreboardWrapper, Header, List } from '../css/components/scoreboard'; 3 | import { Topic as ITopic } from '../reducers/topic'; 4 | 5 | interface Props { 6 | topics: ITopic[]; 7 | } 8 | const Scoreboard: FC = ({topics}) => { 9 | const topicListItems = topics.map((topic, key) => { 10 | return ( 11 | 12 | {topic.text} 13 | {topic.count} 14 | 15 | ); 16 | }); 17 | return ( 18 | 19 |
Vote count
20 | 21 | {topicListItems} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Scoreboard; 28 | -------------------------------------------------------------------------------- /app_toolkit/components/TopicItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import { Decrement, Destroy, Increment, Topic, TopicItemWrapper } from '../css/components/topicItem'; 4 | 5 | interface Props { 6 | text: string, 7 | id: string, 8 | incrementCount: (id: string) => void, 9 | decrementCount: (id: string) => void, 10 | destroyTopic: (id: string) => void, 11 | } 12 | const TopicItem: FC = ({ 13 | text, id, incrementCount, decrementCount, destroyTopic 14 | }) => { 15 | const onIncrement = () => { 16 | incrementCount(id); 17 | }; 18 | const onDecrement = () => { 19 | decrementCount(id); 20 | }; 21 | const onDestroy = () => { 22 | destroyTopic(id); 23 | }; 24 | 25 | return ( 26 | 27 | {text} 28 | 31 | + 32 | 33 | 36 | - 37 | 38 | 41 | {String.fromCharCode(215)} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default TopicItem; 48 | -------------------------------------------------------------------------------- /app_toolkit/components/TopicTextForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from 'react'; 2 | 3 | interface Props { 4 | onEntrySave: (value: string) => void, 5 | onEntryChange: (value: string) => void, 6 | value: string, 7 | className?: string, 8 | placeholder: string, 9 | } 10 | const TopicTextForm: FC = ({ 11 | onEntrySave, onEntryChange, value, className, placeholder, 12 | }) => { 13 | /* 14 | * Invokes the callback passed in as onSave, allowing this component to be 15 | * used in different ways. I personally think this makes it more reusable. 16 | */ 17 | const onSave = useCallback(() => { 18 | if (value?.trim()) { 19 | onEntrySave(value); 20 | } 21 | }, [value]); 22 | 23 | /* 24 | * Invokes the callback passed in as onSave, allowing this component to be 25 | * used in different ways. I personally think this makes it more reusable. 26 | * @param {object} event 27 | */ 28 | const onChange = useCallback((event) => { 29 | onEntryChange(event.currentTarget.value); 30 | }, []); 31 | 32 | /* 33 | * Be careful that value is a dependency for onSave function! 34 | * @param {object} event 35 | */ 36 | const onSubmit = useCallback((event) => { 37 | event.preventDefault(); 38 | onSave(); 39 | }, [value]); 40 | 41 | return ( 42 |
43 | 50 |
51 | ); 52 | }; 53 | 54 | export default TopicTextForm; 55 | -------------------------------------------------------------------------------- /app_toolkit/containers/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Contribute, Description, Header } from '../css/components/about'; 4 | 5 | /* 6 | * Note: This is kept as a container-level component, 7 | * i.e. We should keep this as the container that does the data-fetching 8 | * and dispatching of actions if you decide to have any sub-components. 9 | */ 10 | const About = () => { 11 | return ( 12 |
13 |
About Ninja Ocean
14 | 15 |

16 | Imagine an ocean of ninjas. Now think of it as a metaphor. 17 |
18 | Seriously, we love good tech. React, redux, scala, Haskell, machine learning, you name it! 19 |

20 |
21 | 22 |

23 | Want to contribute? Help us out! 24 | If you think the code on   25 | this repo 26 |  could be improved, please create an issue  27 | here 28 | ! 29 |

30 |
31 |
32 | ); 33 | }; 34 | 35 | export default About; 36 | -------------------------------------------------------------------------------- /app_toolkit/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import { renderRoutes, RouteConfig } from 'react-router-config'; 4 | import { Global } from '@emotion/react'; 5 | 6 | import { AppWrapper, global } from '../css/main'; 7 | import Navigation from './Navigation'; 8 | import Message from './Message'; 9 | 10 | interface Props { 11 | route: RouteConfig; 12 | } 13 | const App: FC = ({ route }) => ( 14 | 15 | 18 | 19 | 20 | 21 | {renderRoutes(route.routes)} 22 | 23 | 24 | ); 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /app_toolkit/containers/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | * Note: This is kept as a container-level component, 5 | * i.e. We should keep this as the container that does the data-fetching 6 | * and dispatching of actions if you decide to have any sub-components. 7 | */ 8 | const Dashboard = () =>
Welcome to the Dasboard. Stay tuned...
; 9 | 10 | export default Dashboard; 11 | -------------------------------------------------------------------------------- /app_toolkit/containers/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { MessageWrapper } from '../css/components/message'; 5 | import { RootState } from '../reducers'; 6 | import messageSlice from '../reducers/message'; 7 | 8 | const Message = () => { 9 | const { message, type } = useSelector((state) => state.message); 10 | const dispatch = useDispatch(); 11 | const dispatchDismissMessage = () => dispatch(messageSlice.actions.dismissMessage()); 12 | 13 | return ( 14 | 0 : false} 17 | success={type === 'SUCCESS'} 18 | onClick={dispatchDismissMessage}> 19 | {message} 20 | 21 | ); 22 | }; 23 | 24 | export default Message; 25 | -------------------------------------------------------------------------------- /app_toolkit/containers/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useHistory } from 'react-router-dom'; 4 | 5 | import { logOut } from '../actions/users'; 6 | import { NavigationWrapper, Item, Logo } from '../css/components/navigation'; 7 | import { RootState } from '../reducers'; 8 | 9 | const LogOut = Item.withComponent('button') as React.ElementType; 10 | 11 | const Navigation = () => { 12 | const user = useSelector((state) => state.user); 13 | const dispatch = useDispatch(); 14 | const history = useHistory(); 15 | 16 | const dispatchLogOut = useCallback(() => { 17 | dispatch(logOut()); 18 | history.push('/'); 19 | }, []); 20 | 21 | // activeClassName issues https://github.com/ReactTraining/react-router/issues/6201 22 | return ( 23 | 24 | Ninja Ocean 25 | {user.authenticated ? ( 26 | Logout 27 | ) : ( 28 | Log in 29 | )} 30 | Dashboard 31 | About 32 | 33 | ); 34 | }; 35 | 36 | export default Navigation; 37 | -------------------------------------------------------------------------------- /app_toolkit/css/common/button.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { small } from './typography'; 3 | 4 | export const button = css` 5 | ${small}; 6 | display: block; 7 | width: 100%; 8 | padding: 0.75rem 1.25rem; 9 | text-decoration: none; 10 | text-align: center; 11 | border-radius: 4px; 12 | border-style: solid; 13 | border-width: 2px; 14 | cursor: pointer; 15 | transition: 0.1s background-color ease; 16 | `; 17 | 18 | export const greenButton = css` 19 | ${button}; 20 | color: var(--colorWhite); 21 | background: var(--colorSalem); 22 | border-color: var(--colorSalem); 23 | 24 | &:hover { 25 | background: var(--colorDarkerSalem); 26 | border-color: var(--colorDarkerSalem); 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /app_toolkit/css/common/card.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const card = css` 4 | border-radius: 4px; 5 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.25); 6 | `; 7 | 8 | export const title = css` 9 | margin: 0; 10 | padding: 24px; 11 | border-radius: 4px 4px 0 0; 12 | `; 13 | -------------------------------------------------------------------------------- /app_toolkit/css/common/input.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { normal } from './typography'; 3 | 4 | export const input = css` 5 | ${normal}; 6 | padding: 0.75rem 1rem; 7 | border: 2px solid var(--colorLoblolly); 8 | border-radius: 4px; 9 | width: 100%; 10 | 11 | background-color: var(--colorWhite); 12 | color: var(--colorLimedSpruce); 13 | margin-bottom: 0.75rem; 14 | `; 15 | -------------------------------------------------------------------------------- /app_toolkit/css/common/layout.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const box = css` 4 | padding: 8px 24px; 5 | `; 6 | 7 | export const flexNotMoreThan200 = css` 8 | flex: 0 1 200px; 9 | `; 10 | -------------------------------------------------------------------------------- /app_toolkit/css/common/typography.ts: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const heading = css` 4 | font-size: 28px; 5 | font-weight: 700; 6 | line-height: 48px; 7 | `; 8 | 9 | export const subHeading = css` 10 | font-size: 21px; 11 | font-weight: 700; 12 | line-height: 24px; 13 | `; 14 | 15 | export const normal = css` 16 | font-size: 16px; 17 | font-weight: 400; 18 | line-height: 24px; 19 | `; 20 | 21 | export const small = css` 22 | font-size: 12px; 23 | font-weight: 400; 24 | line-height: 24px; 25 | `; 26 | -------------------------------------------------------------------------------- /app_toolkit/css/components/about.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | import { heading, normal, small } from '../common/typography'; 4 | 5 | export const Header = styled.h1` 6 | ${heading}; 7 | text-align: center; 8 | `; 9 | 10 | export const Description = styled.div` 11 | ${normal}; 12 | `; 13 | 14 | export const Contribute = styled.div` 15 | ${small}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_toolkit/css/components/entrybox.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { heading } from '../common/typography'; 3 | import TopicTextForm from '../../components/TopicTextForm'; 4 | import { input } from '../common/input'; 5 | 6 | export const EntryBoxWrapper = styled.div` 7 | width: 100%; 8 | margin-bottom: 72px; 9 | `; 10 | 11 | export const Header = styled.h1` 12 | ${heading}; 13 | text-align: center; 14 | `; 15 | 16 | export const Input = styled(TopicTextForm)` 17 | ${heading}; 18 | ${input}; 19 | text-align: center; 20 | outline: none; 21 | `; 22 | -------------------------------------------------------------------------------- /app_toolkit/css/components/mainSection.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | 5 | export const MainSectionWrapper = styled.div` 6 | ${card}; 7 | width: 50%; 8 | border-radius: var(--globalRadius); 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #ed193a; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | list-style: none; 20 | padding: 16px; 21 | `; 22 | -------------------------------------------------------------------------------- /app_toolkit/css/components/message.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const MessageWrapper = styled.div<{ show: boolean; success: boolean; }>` 4 | display: none; 5 | padding: 12px 0; 6 | border-radius: var(--globalRadius); 7 | color: var(--colorBlack); 8 | font-size: var(--fontMedium); 9 | text-align: center; 10 | 11 | ${(props) => props.show && 'display: block;'}; 12 | ${(props) => props.success && ` 13 | background-color: var(--colorSalem); 14 | color: var(--colorWhite); 15 | `}; 16 | `; 17 | -------------------------------------------------------------------------------- /app_toolkit/css/components/navigation.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | export const NavigationWrapper = styled.nav` 5 | padding: 0 28px; 6 | `; 7 | 8 | export const Item = styled(NavLink)` 9 | display: inline-block; 10 | text-decoration: none; 11 | padding: 16px 32px; 12 | color: var(--colorBlack); 13 | background: transparent; 14 | border: none; 15 | font-family: var(--fontMontserrat); 16 | font-size: 16px; 17 | cursor: pointer; 18 | 19 | &.active { 20 | color: var(--colorDodgerBlue); 21 | } 22 | `; 23 | 24 | export const Logo = styled(Item)` 25 | font-size: var(--fontHuge); 26 | `; 27 | -------------------------------------------------------------------------------- /app_toolkit/css/components/scoreboard.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { card, title } from '../common/card'; 3 | import { subHeading } from '../common/typography'; 4 | import { flexNotMoreThan200 } from '../common/layout'; 5 | 6 | export const ScoreboardWrapper = styled.div` 7 | ${card}; 8 | width: 40%; 9 | `; 10 | 11 | export const Header = styled.h3` 12 | ${title}; 13 | ${subHeading}; 14 | background: #0F9D58; 15 | color: #fff; 16 | `; 17 | 18 | export const List = styled.ul` 19 | padding: 16px; 20 | list-style: none; 21 | `; 22 | 23 | export const Item = styled.li` 24 | display: flex; 25 | justify-content: space-between; 26 | `; 27 | 28 | export const Topic = styled.span` 29 | ${flexNotMoreThan200}; 30 | font-size: var(--fontMedium); 31 | `; 32 | 33 | export const Count = styled.span``; 34 | -------------------------------------------------------------------------------- /app_toolkit/css/components/topicItem.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { flexNotMoreThan200 } from '../common/layout'; 3 | 4 | export const TopicItemWrapper = styled.li` 5 | display: flex; 6 | justify-content: space-between; 7 | `; 8 | 9 | export const Topic = styled.span` 10 | ${flexNotMoreThan200}; 11 | font-size: var(--fontMedium); 12 | `; 13 | 14 | export const Button = styled.button` 15 | font-size: var(--fontMedium); 16 | width: 28px; 17 | height: 28px; 18 | border-radius: 50%; 19 | border: none; 20 | margin: 0 28px; 21 | color: #fff; 22 | `; 23 | 24 | export const Increment = styled(Button)` 25 | background-color: var(--colorSalem); 26 | `; 27 | 28 | export const Decrement = styled(Button)` 29 | background-color: var(--colorDodgerBlue); 30 | `; 31 | 32 | export const Destroy = styled(Button)` 33 | background-color: var(--colorPunch); 34 | `; 35 | -------------------------------------------------------------------------------- /app_toolkit/css/components/vote.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const VoteWrapper = styled.div` 4 | display: flex; 5 | flex-flow: row wrap; 6 | justify-content: space-between; 7 | `; 8 | -------------------------------------------------------------------------------- /app_toolkit/css/main.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { css } from '@emotion/react'; 3 | 4 | import { box } from './common/layout'; 5 | 6 | export const global = css` 7 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); 8 | :root { 9 | --globalRadius: 4px; 10 | --ViewTransitionIn: opacity .4s ease-in; 11 | --ViewTransitionOut: opacity .1s ease-out; 12 | --colorBlack: #000; 13 | --colorWhite: #fff; 14 | --colorDodgerBlue: #2196F3; 15 | --colorSalem: #0F9D58; 16 | --colorDarkerSalem: #0b6e3e; 17 | --colorPunch: #db4437; 18 | --colorLoblolly: #c3c8ce; 19 | --colorLimedSpruce: #333f48; 20 | --colorAthensGray: #f5f5f6; 21 | --colorMercury: #e3e3e3; 22 | --colorBombay: #b4bac1; 23 | --colorCrimson: #ed193a; 24 | --colorDarkerCrimson: color(var(--colorCrimson) blackness(+20%)); 25 | --fontMontserrat: 'Montserrat', Helvetica, Arial, sans-serif; 26 | --fontSmall: 12px; 27 | --fontMedium: 16px; 28 | --fontLarge: 21px; 29 | --fontHuge: 28px; 30 | } 31 | 32 | * { box-sizing: border-box; } 33 | 34 | body { 35 | margin: 0; 36 | font-family: var(--fontMontserrat); 37 | } 38 | `; 39 | 40 | export const AppWrapper = styled.div` 41 | ${box}; 42 | height: 100%; 43 | font-weight: normal; 44 | font-smoothing: antialiased; 45 | `; 46 | -------------------------------------------------------------------------------- /app_toolkit/images/apple-ninja152-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_toolkit/images/apple-ninja152-precomposed.png -------------------------------------------------------------------------------- /app_toolkit/images/chrome-ninja192-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_toolkit/images/chrome-ninja192-precomposed.png -------------------------------------------------------------------------------- /app_toolkit/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_toolkit/images/favicon.png -------------------------------------------------------------------------------- /app_toolkit/images/ms-ninja144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZeroCho/reactGo/232aab1705c13f2dff228f9724e7d9f4bb65f267/app_toolkit/images/ms-ninja144-precomposed.png -------------------------------------------------------------------------------- /app_toolkit/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import AboutContainer from '../containers/About'; 4 | 5 | const About = () => { 6 | const pageTitle = () => { 7 | return 'About | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of life' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default About; 34 | -------------------------------------------------------------------------------- /app_toolkit/pages/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderRoutes } from 'react-router-config'; 3 | 4 | import Page from './Page'; 5 | import { link, meta, title } from './assets'; 6 | import routes from '../routes'; 7 | 8 | const App = () => ( 9 | 10 | {renderRoutes(routes)} 11 | 12 | ); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /app_toolkit/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { replace } from 'connected-react-router'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { RootState } from '../reducers'; 5 | 6 | import Page from './Page'; 7 | import DashboardContainer from '../containers/Dashboard'; 8 | 9 | const Dashboard = () => { 10 | const { authenticated } = useSelector((state) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | /* 14 | * Redirect to '/' if is not authenticated 15 | */ 16 | useEffect(() => { 17 | if (!authenticated) { 18 | dispatch(replace({ pathname: '/' })); 19 | } 20 | }, []); 21 | 22 | const pageTitle = () => { 23 | return 'Dashboard | reactGo'; 24 | }; 25 | 26 | const pageMeta = () => { 27 | return [ 28 | { name: 'description', content: 'A reactGo example of a dashboard page' }, 29 | ]; 30 | }; 31 | 32 | const pageLink = () => { 33 | return []; 34 | }; 35 | 36 | const getMetaData = () => ({ 37 | title: pageTitle(), 38 | meta: pageMeta(), 39 | link: pageLink(), 40 | }); 41 | 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default Dashboard; 50 | -------------------------------------------------------------------------------- /app_toolkit/pages/LoginOrRegister.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { replace } from 'connected-react-router'; 4 | import { RootState } from '../reducers'; 5 | 6 | import Page from './Page'; 7 | import LoginOrRegisterContainer from '../containers/LoginOrRegister'; 8 | 9 | const LoginOrRegister = () => { 10 | const { authenticated } = useSelector((state) => state.user); 11 | const dispatch = useDispatch(); 12 | 13 | /* 14 | * Redirect to '/' if is already logged in. 15 | */ 16 | useEffect(() => { 17 | if (authenticated) { 18 | dispatch(replace({ pathname: '/' })); 19 | } 20 | }, [authenticated]); 21 | 22 | const pageTitle = () => { 23 | return 'LoginOrRegister | reactGo'; 24 | }; 25 | 26 | const pageMeta = () => { 27 | return [ 28 | { name: 'description', content: 'A reactGo example of a login or register page' }, 29 | ]; 30 | }; 31 | 32 | const pageLink = () => { 33 | return []; 34 | }; 35 | 36 | const getMetaData = () => ({ 37 | title: pageTitle(), 38 | meta: pageMeta(), 39 | link: pageLink(), 40 | }); 41 | 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default LoginOrRegister; 50 | -------------------------------------------------------------------------------- /app_toolkit/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | 4 | const About = () => { 5 | const pageTitle = () => { 6 | return '404 Not Found | reactGo'; 7 | }; 8 | 9 | const pageMeta = () => { 10 | return [ 11 | { name: 'description', content: '404 Not Found' }, 12 | ]; 13 | }; 14 | 15 | const pageLink = () => { 16 | return []; 17 | }; 18 | 19 | const getMetaData = () => ({ 20 | title: pageTitle(), 21 | meta: pageMeta(), 22 | link: pageLink(), 23 | }); 24 | 25 | return ( 26 | 27 |
Oops, this page doesn't exist!
28 |
29 | ); 30 | }; 31 | 32 | export default About; 33 | -------------------------------------------------------------------------------- /app_toolkit/pages/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | 4 | interface Props { 5 | title: string; 6 | link: Array<{}>; 7 | meta: Array<{}>; 8 | children: React.ReactNode; 9 | } 10 | 11 | const Page: FC = ({ 12 | title, link, meta, children, 13 | }) => { 14 | return ( 15 |
16 | 17 | {children} 18 |
19 | ); 20 | }; 21 | 22 | Page.defaultProps = { 23 | title: '', 24 | link: [], 25 | meta: [], 26 | }; 27 | 28 | export default Page; 29 | -------------------------------------------------------------------------------- /app_toolkit/pages/Vote.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import VoteContainer from '../containers/Vote'; 4 | 5 | const Vote = () => { 6 | const pageTitle = () => { 7 | return 'Vote | reactGo'; 8 | }; 9 | 10 | const pageMeta = () => { 11 | return [ 12 | { name: 'description', content: 'A reactGo example of a voting page' }, 13 | ]; 14 | }; 15 | 16 | const pageLink = () => { 17 | return []; 18 | }; 19 | 20 | const getMetaData = () => ({ 21 | title: pageTitle(), 22 | meta: pageMeta(), 23 | link: pageLink(), 24 | }); 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Vote; 34 | -------------------------------------------------------------------------------- /app_toolkit/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as App } from './App'; 2 | export { default as Vote} from './Vote'; 3 | export { default as Dashboard } from './Dashboard'; 4 | export { default as LoginOrRegister } from './LoginOrRegister'; 5 | export { default as About } from './About'; 6 | export { default as NotFound } from './NotFound'; 7 | -------------------------------------------------------------------------------- /app_toolkit/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { History } from 'history'; 3 | import { connectRouter } from 'connected-react-router'; 4 | 5 | import user from './user'; 6 | import topic from './topic'; 7 | import message from './message'; 8 | 9 | // Combine reducers with connectRouter which keeps track of router state 10 | const createRootReducer = (history: History) => combineReducers({ 11 | topic, 12 | user: user.reducer, 13 | message: message.reducer, 14 | router: connectRouter(history), 15 | }); 16 | 17 | export type RootState = ReturnType>; 18 | 19 | export default createRootReducer; 20 | -------------------------------------------------------------------------------- /app_toolkit/reducers/message.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { logIn, signUp } from '../actions/users'; 3 | /* 4 | * Message store for global messages, i.e. Network messages / Redirect messages 5 | * that need to be communicated on the page itself. Ideally 6 | * messages/notifications should appear within the component to give the user 7 | * more context. - My 2 cents. 8 | */ 9 | const messageSlice = createSlice({ 10 | name: 'message', 11 | initialState: { 12 | message: '', 13 | type: 'SUCCESS', 14 | }, 15 | reducers: { 16 | dismissMessage(state) { 17 | state.message = ''; 18 | state.type = 'SUCCESS'; 19 | }, 20 | }, 21 | extraReducers: (builder) => builder 22 | .addCase(logIn.fulfilled, (state, action) => { 23 | state.message = action.payload; 24 | state.type = 'SUCCESS'; 25 | }) 26 | .addCase(signUp.fulfilled, (state, action) => { 27 | state.message = action.payload; 28 | state.type = 'SUCCESS'; 29 | }), 30 | }); 31 | 32 | export default messageSlice; 33 | -------------------------------------------------------------------------------- /app_toolkit/routes.ts: -------------------------------------------------------------------------------- 1 | import { ThunkDispatch } from '@reduxjs/toolkit'; 2 | import React from 'react'; 3 | import { RouteConfig } from 'react-router-config'; 4 | 5 | import { store } from './client'; 6 | import App from './containers/App'; 7 | import Vote from './pages/Vote'; 8 | import LoginOrRegister from './pages/LoginOrRegister'; 9 | import Dashboard from './pages/Dashboard'; 10 | import About from './pages/About'; 11 | import NotFound from './pages/NotFound'; 12 | import { getTopics } from './actions/topics'; 13 | 14 | const routes: RouteConfig[] = [{ 15 | component: App as React.ComponentType, 16 | routes: [{ 17 | path: '/', 18 | exact: true, 19 | component: Vote, 20 | fetchData: (s: typeof store) => { 21 | return (s.dispatch as ThunkDispatch)(getTopics()); 22 | }, 23 | }, { 24 | path: '/login', 25 | component: LoginOrRegister, 26 | }, { 27 | path: '/dashboard', 28 | component: Dashboard, 29 | }, { 30 | path: '/about', 31 | component: About, 32 | }, { 33 | path: '*', 34 | component: NotFound, 35 | }], 36 | }]; 37 | 38 | export default routes; 39 | -------------------------------------------------------------------------------- /app_toolkit/services/authentication.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | interface UserData { 4 | email: string, 5 | password: string, 6 | } 7 | export default () => { 8 | return { 9 | login: ({ email, password }: UserData) => axios.post('/sessions', { email, password }), 10 | signUp: ({ email, password }: UserData) => axios.post('/users', { email, password }), 11 | logOut: () => axios.delete('/sessions') 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /app_toolkit/services/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { apiEndpoint } from '../../config/app'; 4 | 5 | axios.defaults.baseURL = apiEndpoint; 6 | 7 | export { default as voteService } from './topics'; 8 | export { default as authService } from './authentication'; 9 | -------------------------------------------------------------------------------- /app_toolkit/services/topics.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Topic } from '../reducers/topic'; 3 | 4 | export default () => { 5 | return { 6 | getTopics: () => axios.get('/topic'), 7 | deleteTopic: ({ id }: { id: string }) => axios.delete(`/topic/${id}`), 8 | updateTopic: ({ id, data }: { id: string, data: { isFull: boolean, isIncrement: boolean } }) => axios.put(`/topic/${id}`, data), 9 | createTopic: ({ id, data }: { id: string, data: Topic }) => axios.post(`/topic/${id}`, data), 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /app_toolkit/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, ThunkAction, Action, getDefaultMiddleware } from '@reduxjs/toolkit'; 2 | import { routerMiddleware } from 'connected-react-router'; 3 | import { History } from 'history'; 4 | import createRootReducer, { RootState } from '../reducers'; 5 | 6 | /* 7 | * @param {Object} initial state to bootstrap our stores with for server-side rendering 8 | * @param {History Object} a history object. We use `createMemoryHistory` for server-side rendering, 9 | * while using browserHistory for client-side 10 | * rendering. 11 | */ 12 | export type AppThunk = ThunkAction> 13 | 14 | export default (initialState: any, history: History) => configureStore({ 15 | reducer: createRootReducer(history), 16 | middleware: [...getDefaultMiddleware(), routerMiddleware(history)], 17 | preloadedState: initialState, 18 | }); 19 | -------------------------------------------------------------------------------- /config/app.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from './env'; 2 | 3 | export const isProduction = ENV === 'production'; 4 | export const isDebug = ENV === 'development'; 5 | export const isClient = typeof window !== 'undefined'; 6 | 7 | export const apiEndpoint = isDebug ? 'http://localhost:3000' : 'https://demo-reactgo.herokuapp.com'; 8 | // Replace with 'UA-########-#' or similar to enable tracking 9 | export const trackingID = null; 10 | -------------------------------------------------------------------------------- /config/dbTypes.ts: -------------------------------------------------------------------------------- 1 | export const DB_TYPES = { 2 | MONGO: 'MONGO', 3 | POSTGRES: 'POSTGRES', 4 | MYSQL: 'MYSQL', 5 | NONE: 'NONE' 6 | }; 7 | -------------------------------------------------------------------------------- /config/env.ts: -------------------------------------------------------------------------------- 1 | export const ENV = process.env.NODE_ENV as ('production' | 'test' | 'development') || 'development'; 2 | 3 | export const GOOGLE_ANALYTICS_ID = process.env.GOOGLE_ANALYTICS_ID || null; 4 | -------------------------------------------------------------------------------- /config/secrets.ts: -------------------------------------------------------------------------------- 1 | /** Important * */ 2 | /** You should not be committing this file to GitHub * */ 3 | /** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! * */ 4 | export const sessionSecret = process.env.SESSION_SECRET || 'Your Session Secret goes here'; 5 | export const google = { 6 | clientID: process.env.GOOGLE_CLIENTID || '62351010161-eqcnoa340ki5ekb9gvids4ksgqt9hf48.apps.googleusercontent.com', 7 | clientSecret: process.env.GOOGLE_SECRET || '6cKCWD75gHgzCvM4VQyR5_TU', 8 | callbackURL: process.env.GOOGLE_CALLBACK || '/auth/google/callback' 9 | }; 10 | 11 | /* To make sure everything referencing the session ID knows what it is called */ 12 | export const sessionId = 'sid'; 13 | -------------------------------------------------------------------------------- /config/serverEnv.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { DB_TYPES } from './dbTypes'; 3 | 4 | dotenv.config(); 5 | export const DB_TYPE = process.env.DB_TYPE || DB_TYPES.MONGO; 6 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | 1. Google Authentication does not work locally or on heroku! 2 | 1. Follow [these steps from Google](https://developers.google.com/identity/protocols/OpenIDConnect) to create your API keys on [Google Developers Console](https://console.developers.google.com/) 3 | 2. Under APIs & Auth, Copy your Client ID and Client Secret 4 | 5 | **Local dev** 6 | 7 | - For Google Auth to work locally, you need to do the following in your terminal before starting the server: 8 | 9 | ```bash 10 | export GOOGLE_CLIENTID=YOUR_CLIENTID 11 | export GOOGLE_SECRET=YOUR_SECRET 12 | ``` 13 | 14 | **Heroku** 15 | 16 | - Fret not! Heroku's covered [this](https://devcenter.heroku.com/articles/config-vars) pretty well: 17 | 18 | ```bash 19 | heroku config:set GOOGLE_CLIENTID=YOUR_CLIENTID 20 | heroku config:set GOOGLE_SECRET=YOUR_SECRET 21 | heroku config:set GOOGLE_CALLBACK=YOUR_CALLBACK 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/FAQ_KO.md: -------------------------------------------------------------------------------- 1 | 1. 로컬 환경이나 Heroku에서 구글 인증이 안 돼요! 2 | 1. [구글의 공식문서](https://developers.google.com/identity/protocols/OpenIDConnect) 를 따라 [Google Developers 콘솔](https://console.developers.google.com/) 에서 API 키를 생성하세요. 3 | 2. API & 인증 메뉴에서 Client ID와 Client Secret을 복사하세요. 4 | 5 | **로컬 환경** 6 | 7 | - 로컬 환경에서 구글 인증이 돌아가려면, 서버 시작 전에 다음 환경 변수를 설정해야 합니다. 8 | 9 | ```bash 10 | export GOOGLE_CLIENTID=복사한_CLIENTID 11 | export GOOGLE_SECRET=복사한_SECRET 12 | ``` 13 | 14 | **Heroku** 15 | 16 | - Heroku의 경우는 [여기에](https://devcenter.heroku.com/articles/config-vars) 환경 변수 설정법에 대해 나와 있습니다. 17 | 18 | ```bash 19 | heroku config:set GOOGLE_CLIENTID=YOUR_CLIENTID 20 | heroku config:set GOOGLE_SECRET=YOUR_SECRET 21 | heroku config:set GOOGLE_CALLBACK=YOUR_CALLBACK 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/apps.md: -------------------------------------------------------------------------------- 1 | ## Projects Built with `ReactGo` 2 | 3 | #### [TickTrade](http://ticktra.de) 4 | 5 | Tick Trade was developed by [@caranicas](http://github.com/caranicas). It is a classified site for listing sports tickets. It uses Reddit as a log in authority with the goal of connecting fans with tickets to fans that want tickets without having to pay a third party like stub hub. 6 | 7 | --- 8 | #### [YouTube Re Upload](http://ytreup.com) 9 | 10 | YT RE UP is a website for reuploading YouTube video fast and seamlessly. Connect safely with your YouTube account, paste a video link you want to reupload, and let the magic happen. Transfer videos from accounts never felt so easy. 11 | 12 | --- 13 | #### [Stillsports](https://www.stillsports.de) 14 | 15 | Stillsports is a website for sports enthusiasts developed by [tomas-st](http://github.com/tomas-st). You can browse products for running, walking, hiking etc. Stillsports is using reactGo and postgres and runs on Heroku. 16 | 17 | --- 18 | #### [ZeroCho.com](https://zerocho.com) 19 | 20 | ZeroCho.com is a website for a personal blog developed by [ZeroCho](http://github.com/ZeroCho). It provides a few lectures related to Javascript, React and Node.js in Korean. ZeroCho.com is using reactGo and MongoDB and runs on Heroku. 21 | 22 | --- 23 | **If you have a project you would like to add, please update this file and submit a pull request.** 24 | -------------------------------------------------------------------------------- /docs/deployment/Heroku.md: -------------------------------------------------------------------------------- 1 | ## Getting Started with Heroku 2 | 3 | Heroku 4 | ```bash 5 | heroku create 6 | 7 | # Deploy to Heroku server 8 | git push heroku master 9 | 10 | # Database on Heroku 11 | heroku addons:create mongohq 12 | # or 13 | heroku addons:create mongolab 14 | 15 | # OPTIONAL: 16 | 17 | # Rename if you need to 18 | heroku apps:rename 19 | 20 | # Open Link in browser 21 | heroku open 22 | 23 | ``` 24 | 25 | 26 | 27 | Note: 28 | 29 | 1. If you are working from a different machine and get `heroku does not appear to be a remote repository` message, be sure to run `git remote add heroku git@heroku.com:appname.git`. 30 | 2. For setting up Google Authentication for Heroku and local dev, read the FAQ section 31 | -------------------------------------------------------------------------------- /docs/deployment/Heroku_KO.md: -------------------------------------------------------------------------------- 1 | ## Heroku로 시작하기 2 | 3 | Heroku 4 | ```bash 5 | heroku create 6 | 7 | # 헤로쿠에 배포하기 8 | git push heroku master 9 | 10 | # 헤로쿠 MYSQL 데이터베이스 11 | heroku addons:create cleardb 12 | # 몽고DB를 쓰기 위해서는 MongoDB atlas를 사용해야 합니다. 13 | 14 | # 선택 사항: 15 | 16 | # 필요하면 앱 이름을 바꿀 수 있습니다. 17 | heroku apps:rename <새 이름> 18 | 19 | # 배포된 앱을 확인하세요. 20 | heroku open 21 | 22 | ``` 23 | 24 | 참고: 25 | 26 | 1. 다른 컴퓨터로 일할 때 `heroku does not appear to be a remote repository` 메시지를 본다면, `git remote add heroku git@heroku.com:앱이름.git`를 입력하세요. 27 | 2. Heroku와 로컬 환경에 구글 인증을 설정하길 원한다면 FAQ를 참조하세요. 28 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | ## MongoDB 2 | 3 | #### Install MongoDB as your database 4 | 5 | ```bash 6 | # Update brew formulae 7 | brew update 8 | # Install MongoDB 9 | brew install mongodb 10 | ``` 11 | 12 | If you hate MongoDB with a passion and would like to see a postgresql example, check [this](./databases.md) out! 13 | 14 | ## Build & Dev 15 | 16 | ### Installation 17 | 18 | #### Yarn 19 | ``` 20 | yarn install 21 | ``` 22 | 23 | #### npm 24 | ```bash 25 | # Install node modules - this includes those for production and development 26 | # You only need to do this once :) 27 | npm install 28 | ``` 29 | -------------------------------------------------------------------------------- /gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .tmp 4 | .idea 5 | public 6 | compiled 7 | *.swp 8 | .vscode 9 | .env 10 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "watch": [ 4 | "app/utils", 5 | "app/routes.jsx", 6 | "server", 7 | "webpack" 8 | ], 9 | "exec": "npm run build:dev && node compiled/server.dev.js", 10 | "ignore": ["**/*.js"] 11 | } 12 | -------------------------------------------------------------------------------- /server_mongo/db/connect.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { db } from './constants'; 3 | import loadModels from './models'; 4 | 5 | export default () => { 6 | // Find the appropriate database to connect to, default to localhost if not found. 7 | const connect = () => { 8 | mongoose.connect(db, { 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | useCreateIndex: true, 12 | useFindAndModify: false, 13 | }, (err) => { 14 | if (err) { 15 | console.log(`===> Error connecting to ${db}`); 16 | console.log(`Reason: ${err}`); 17 | } else { 18 | console.log(`===> Succeeded in connecting to ${db}`); 19 | } 20 | }); 21 | }; 22 | connect(); 23 | 24 | mongoose.connection.on('error', console.log); 25 | mongoose.connection.on('disconnected', connect); 26 | 27 | loadModels(); 28 | }; 29 | -------------------------------------------------------------------------------- /server_mongo/db/constants.ts: -------------------------------------------------------------------------------- 1 | export const db = process.env.MONGODB_URI || 'mongodb://localhost/ReactGo'; 2 | 3 | export default { 4 | db 5 | }; 6 | -------------------------------------------------------------------------------- /server_mongo/db/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import topics from './topics'; 2 | import users from './users'; 3 | 4 | export { topics, users }; 5 | 6 | export default { 7 | topics, 8 | users 9 | }; 10 | -------------------------------------------------------------------------------- /server_mongo/db/index.ts: -------------------------------------------------------------------------------- 1 | import connect from './connect'; 2 | import controllers from './controllers'; 3 | import passport from './passport'; 4 | import session from './session'; 5 | 6 | export { connect, controllers, passport, session }; 7 | -------------------------------------------------------------------------------- /server_mongo/db/models/index.ts: -------------------------------------------------------------------------------- 1 | export default function loadModels() { 2 | require('./topics'); 3 | require('./user'); 4 | } 5 | -------------------------------------------------------------------------------- /server_mongo/db/models/topics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Schema Definitions 3 | * 4 | */ 5 | import mongoose from 'mongoose'; 6 | 7 | const TopicSchema = new mongoose.Schema({ 8 | id: String, 9 | text: String, 10 | count: { type: Number, min: 0 }, 11 | date: { type: Date, default: Date.now } 12 | }); 13 | 14 | // Compiles the schema into a model, opening (or creating, if 15 | // nonexistent) the 'Topic' collection in the MongoDB database 16 | export default mongoose.model('Topic', TopicSchema); 17 | -------------------------------------------------------------------------------- /server_mongo/db/passport/deserializeUser.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | 3 | export default (id: string, done: (err: any, user?: any) => void) => { 4 | User.findById(id, (err, user) => { 5 | done(err, user); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /server_mongo/db/passport/index.ts: -------------------------------------------------------------------------------- 1 | import deserializeUser from './deserializeUser'; 2 | import google from './google'; 3 | import local from './local'; 4 | 5 | export { deserializeUser, google, local }; 6 | 7 | export default { 8 | deserializeUser, 9 | google, 10 | local, 11 | }; 12 | -------------------------------------------------------------------------------- /server_mongo/db/passport/local.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | 3 | export default (email: string, password: string, done: (err: any, user?: any, info?: { message: string }) => void) => { 4 | User.findOne({ email }, (findErr, user) => { 5 | if (!user) return done(null, false, { message: `There is no record of the email ${email}.` }); 6 | return user.comparePassword(password, (passErr, isMatch) => { 7 | if (isMatch) { 8 | return done(null, user); 9 | } 10 | return done(null, false, { message: 'Your email or password combination is not correct.' }); 11 | }); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /server_mongo/db/session.ts: -------------------------------------------------------------------------------- 1 | import session from 'express-session'; 2 | import connectMongo from 'connect-mongo'; 3 | import { db } from './constants'; 4 | 5 | const MongoStore = connectMongo(session); 6 | 7 | export default () => new MongoStore( 8 | { 9 | url: db, 10 | autoReconnect: true 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /server_mongo/db/unsupportedMessage.ts: -------------------------------------------------------------------------------- 1 | import { DB_TYPE } from '../../config/serverEnv'; 2 | 3 | export default (featureName: string) => `Attempted to use '${featureName}' but DB type '${DB_TYPE}' doesn't support it`; 4 | -------------------------------------------------------------------------------- /server_mongo/init/passport/index.ts: -------------------------------------------------------------------------------- 1 | /* Initializing passport.js */ 2 | import passport from 'passport'; 3 | import local from './local'; 4 | import google from './google'; 5 | import { passport as dbPassport } from '../../db'; 6 | import unsupportedMessage from '../../db/unsupportedMessage'; 7 | 8 | export default () => { 9 | // Configure Passport authenticated session persistence. 10 | // 11 | // In order to restore authentication state across HTTP requests, Passport needs 12 | // to serialize users into and deserialize users out of the session. The 13 | // typical implementation of this is as simple as supplying the user ID when 14 | // serializing, and querying the user record by ID from the database when 15 | // deserializing. 16 | 17 | if (dbPassport && dbPassport.deserializeUser) { 18 | passport.serializeUser((user: { id: string }, done) => { 19 | done(null, user.id); 20 | }); 21 | 22 | passport.deserializeUser(dbPassport.deserializeUser); 23 | } else { 24 | console.warn(unsupportedMessage('(de)serialize User')); 25 | } 26 | 27 | // use the following strategies 28 | local(); 29 | google(); 30 | }; 31 | -------------------------------------------------------------------------------- /server_mongo/init/passport/local.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Configuring local strategy to authenticate strategies 3 | Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js 4 | */ 5 | import passport from 'passport'; 6 | import { Strategy as LocalStrategy } from 'passport-local'; 7 | import { passport as dbPassport } from '../../db'; 8 | import unsupportedMessage from '../../db/unsupportedMessage'; 9 | 10 | export default () => { 11 | if (!dbPassport || !dbPassport.local || typeof dbPassport.local !== 'function') { 12 | console.warn(unsupportedMessage('passport-local')); 13 | return; 14 | } 15 | 16 | /* 17 | By default, LocalStrategy expects to find credentials in parameters named username and password. 18 | If your site prefers to name these fields differently, 19 | options are available to change the defaults. 20 | */ 21 | passport.use(new LocalStrategy({ 22 | usernameField: 'email' 23 | }, dbPassport.local)); 24 | }; 25 | -------------------------------------------------------------------------------- /server_mongo/render/static-assets/dev.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const createAppScript = () => ''; 4 | const createVendorScript = () => ''; 5 | 6 | const createAnalyticsSnippet = (id: string) => ` 11 | `; 12 | 13 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 14 | 15 | const createStylesheets = () => ''; 16 | 17 | export { 18 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 19 | }; 20 | -------------------------------------------------------------------------------- /server_mongo/render/static-assets/index.ts: -------------------------------------------------------------------------------- 1 | const createStaticAssets = process.env.NODE_ENV === 'production' ? require('./prod') : require('./dev'); 2 | 3 | export default createStaticAssets; 4 | -------------------------------------------------------------------------------- /server_mongo/render/static-assets/prod.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const assets = require('../../../public/assets/manifest.json'); 4 | 5 | const createAppScript = () => ``; 6 | const createVendorScript = () => ``; 7 | 8 | const createAnalyticsSnippet = (id: string) => ` 13 | `; 14 | 15 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 16 | 17 | const createStylesheets = () => ` 18 | 19 | 20 | `; 21 | 22 | export { 23 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 24 | }; 25 | -------------------------------------------------------------------------------- /server_mysql/db/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, controllers, passport } from './sequelize'; 2 | import session from './session'; 3 | 4 | export { connect, controllers, passport, session }; 5 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/connect.ts: -------------------------------------------------------------------------------- 1 | import { sequelize } from './models'; 2 | 3 | export default () => { 4 | sequelize 5 | .sync() 6 | .then(() => { 7 | console.log('Successfully connected to sequelize database'); 8 | }, (err: Error) => { 9 | console.log('Unable to connect to the sequelize database: ', err); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/constants.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from '../../../config/env'; 2 | import sequelizeConfig from './sequelize_config'; 3 | 4 | const config = sequelizeConfig[ENV]; 5 | 6 | export const db = process.env[config.use_env_variable] || `${config.dialect}://${config.username}:${config.password}@${config.host}/${config.database}`; 7 | 8 | export default { 9 | db 10 | }; 11 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import topics from './topics'; 2 | import users from './users'; 3 | 4 | export default { 5 | topics, 6 | users 7 | }; 8 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | export { default as connect } from './connect'; 2 | export { default as controllers } from './controllers'; 3 | export { default as passport } from './passport'; 4 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/migrations/20160416222221-add-topics.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'Topics', { 5 | id: { 6 | type: DataTypes.STRING, 7 | primaryKey: true 8 | }, 9 | text: { 10 | type: DataTypes.STRING 11 | }, 12 | count: { 13 | type: DataTypes.INTEGER 14 | }, 15 | date: { 16 | type: DataTypes.DATE, 17 | defaultValue: DataTypes.fn('NOW') 18 | } 19 | } 20 | ); 21 | }, 22 | 23 | down(queryInterface) { 24 | return queryInterface.dropTable('Topics'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/migrations/20160416222345-add-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'Users', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | email: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | }, 14 | password: { 15 | type: DataTypes.STRING 16 | }, 17 | name: { 18 | type: DataTypes.STRING, 19 | defaultValue: '' 20 | }, 21 | gender: { 22 | type: DataTypes.STRING, 23 | defaultValue: '' 24 | }, 25 | location: { 26 | type: DataTypes.STRING, 27 | defaultValue: '' 28 | }, 29 | website: { 30 | type: DataTypes.STRING, 31 | defaultValue: '' 32 | }, 33 | picture: { 34 | type: DataTypes.STRING, 35 | defaultValue: '' 36 | }, 37 | resetPasswordToken: { 38 | type: DataTypes.STRING 39 | }, 40 | resetPasswordExpires: { 41 | type: DataTypes.DATE 42 | } 43 | } 44 | ).then(() => queryInterface.addIndex( 45 | 'Users', 46 | [DataTypes.fn('lower', DataTypes.col('email'))], 47 | { 48 | indexName: 'users_email', 49 | indicesType: 'unique' 50 | } 51 | )); 52 | }, 53 | 54 | down(queryInterface) { 55 | return queryInterface.dropTable('Users'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/migrations/20160416222416-add-tokens.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'Tokens', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | kind: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | }, 14 | accessToken: { 15 | type: DataTypes.STRING, 16 | allowNull: false 17 | }, 18 | userId: { 19 | type: DataTypes.INTEGER, 20 | references: { 21 | model: 'Users', 22 | key: 'id' 23 | } 24 | } 25 | } 26 | ); 27 | }, 28 | 29 | down(queryInterface) { 30 | return queryInterface.dropTable('Tokens'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/migrations/20160416222449-add-google-id-to-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | queryInterface.addColumn('Users', 'google', DataTypes.STRING); 4 | }, 5 | 6 | down(queryInterface) { 7 | queryInterface.removeColumn('Users', 'google'); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/migrations/20160416222520-add-sessions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'session', { 5 | sid: { 6 | type: DataTypes.STRING, 7 | primaryKey: true 8 | }, 9 | sess: { 10 | type: DataTypes.JSON 11 | }, 12 | expire: { 13 | type: DataTypes.DATE 14 | } 15 | } 16 | ); 17 | }, 18 | 19 | down(queryInterface) { 20 | return queryInterface.dropTable('session'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/models/index.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import sequelizeConfig from '../sequelize_config'; 3 | import { ENV } from '../../../../config/env'; 4 | import tokenModel from './tokens'; 5 | import topicModel from './topics'; 6 | import userModel from './users'; 7 | 8 | const config = sequelizeConfig[ENV]; 9 | 10 | const dbUrl = process.env[config.use_env_variable]; 11 | 12 | const sequelize = dbUrl ? new Sequelize(dbUrl) : new Sequelize(config.database, config.username, config.password, config); 13 | 14 | const db = { 15 | User: userModel, 16 | Token: tokenModel, 17 | Topic: topicModel, 18 | } as const; 19 | export type dbType = typeof db; 20 | 21 | Object.keys(db).forEach((key) => { 22 | const model = db[key as keyof dbType]; 23 | model.initWithSequelize(sequelize); 24 | }); 25 | 26 | Object.keys(db).forEach((key) => { 27 | const model = db[key as keyof dbType]; 28 | if (model.associate) { 29 | model.associate(db); 30 | } 31 | }); 32 | 33 | export { db as Models, sequelize }; 34 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/models/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | import { dbType } from './index'; 3 | 4 | class Token extends Model { 5 | static initWithSequelize(sequelize: Sequelize) { 6 | return Token.init({ 7 | kind: { 8 | type: DataTypes.STRING, 9 | allowNull: false 10 | }, 11 | accessToken: { 12 | type: DataTypes.INTEGER, 13 | allowNull: false 14 | }, 15 | userId: { 16 | type: DataTypes.INTEGER, 17 | references: { 18 | model: 'Users', 19 | key: 'id' 20 | } 21 | } 22 | }, { 23 | sequelize, 24 | modelName: 'Token', 25 | tableName: 'tokens', 26 | timestamps: false, 27 | }); 28 | } 29 | 30 | static associate = (models: dbType) => { 31 | models.Token.belongsTo(models.User, { 32 | foreignKey: 'userId' 33 | }); 34 | }; 35 | } 36 | 37 | export default Token; 38 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/models/topics.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | class Topic extends Model { 4 | static initWithSequelize(sequelize: Sequelize) { 5 | return Topic.init({ 6 | id: { 7 | type: DataTypes.STRING, 8 | primaryKey: true 9 | }, 10 | text: DataTypes.STRING, 11 | count: { 12 | type: DataTypes.INTEGER, 13 | validate: { 14 | min: 0 15 | } 16 | }, 17 | date: { 18 | type: DataTypes.DATE, 19 | defaultValue: Sequelize.fn('NOW') 20 | } 21 | }, { 22 | sequelize, 23 | modelName: 'Topic', 24 | tableName: 'topics', 25 | timestamps: false, 26 | }); 27 | } 28 | 29 | static associate() {} 30 | } 31 | 32 | export default Topic; 33 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/passport/deserializeUser.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/users'; 2 | 3 | export default (id: number, done: (error: any, user?: User | null) => void) => { 4 | User.findByPk(id).then((user) => { 5 | done(null, user); 6 | }).catch(done); 7 | }; 8 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/passport/index.ts: -------------------------------------------------------------------------------- 1 | import deserializeUser from './deserializeUser'; 2 | import google from './google'; 3 | import local from './local'; 4 | 5 | export default { 6 | deserializeUser, 7 | google, 8 | local, 9 | }; 10 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/passport/local.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/users'; 2 | 3 | export default async (email: string, password: string, done: (err: any, user?: any, info?: { message: string }) => void) => { 4 | try { 5 | const user = await User.findOne({ where: { email } }); 6 | if (!user) return done(null, false, { message: `There is no record of the email ${email}.` }); 7 | const result = await user.comparePassword(password); 8 | console.log('result', result); 9 | if (result) done(null, user); 10 | else done(null, false, { message: 'Your email/password combination is incorrect.' }); 11 | } catch (err) { 12 | console.log(err); 13 | done(null, false, { message: 'Something went wrong trying to authenticate' }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /server_mysql/db/sequelize/sequelize_config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | type SequelizeConfig = { 6 | development: any; 7 | test: any; 8 | production: any; 9 | }; 10 | 11 | const exportedConfig: SequelizeConfig = { 12 | development: { 13 | username: process.env.MYSQLUSER || 'root', 14 | password: process.env.MYSQLPASS || null, 15 | database: process.env.MYSQLDB || 'reactgo_development', 16 | host: '127.0.0.1', 17 | dialect: 'mysql' 18 | }, 19 | test: { 20 | username: process.env.MYSQLUSER || 'root', 21 | password: process.env.MYSQLPASS || null, 22 | database: 'reactgo_test', 23 | host: '127.0.0.1', 24 | dialect: 'mysql' 25 | }, 26 | production: { 27 | username: process.env.MYSQLUSER || 'root', 28 | password: process.env.MYSQLPASS || null, 29 | database: process.env.MYSQLDB || 'reactgo_production', 30 | host: process.env.MYSQLHOST || '127.0.0.1', 31 | dialect: 'mysql' 32 | } 33 | } as const; 34 | 35 | export default exportedConfig; 36 | -------------------------------------------------------------------------------- /server_mysql/db/session.ts: -------------------------------------------------------------------------------- 1 | import * as session from 'express-session'; 2 | import sessionSequelize from 'connect-session-sequelize'; 3 | import { sequelize } from './sequelize/models'; 4 | 5 | const SequelizeStore = sessionSequelize(session.Store); 6 | 7 | export default () => new SequelizeStore({ 8 | db: sequelize, 9 | }); 10 | -------------------------------------------------------------------------------- /server_mysql/db/unsupportedMessage.ts: -------------------------------------------------------------------------------- 1 | import { DB_TYPE } from '../../config/serverEnv'; 2 | 3 | export default (featureName: string) => `Attempted to use '${featureName}' but DB type '${DB_TYPE}' doesn't support it`; 4 | -------------------------------------------------------------------------------- /server_mysql/init/passport/index.ts: -------------------------------------------------------------------------------- 1 | /* Initializing passport.js */ 2 | import passport from 'passport'; 3 | import local from './local'; 4 | import google from './google'; 5 | import { passport as dbPassport } from '../../db'; 6 | import User from '../../db/sequelize/models/users'; 7 | import unsupportedMessage from '../../db/unsupportedMessage'; 8 | 9 | export default () => { 10 | // Configure Passport authenticated session persistence. 11 | // 12 | // In order to restore authentication state across HTTP requests, Passport needs 13 | // to serialize users into and deserialize users out of the session. The 14 | // typical implementation of this is as simple as supplying the user ID when 15 | // serializing, and querying the user record by ID from the database when 16 | // deserializing. 17 | 18 | if (dbPassport && dbPassport.deserializeUser) { 19 | passport.serializeUser((user: User, done) => { 20 | done(null, user.getDataValue('id')); 21 | }); 22 | 23 | passport.deserializeUser(dbPassport.deserializeUser); 24 | } else { 25 | console.warn(unsupportedMessage('(de)serialize User')); 26 | } 27 | 28 | // use the following strategies 29 | local(); 30 | google(); 31 | }; 32 | -------------------------------------------------------------------------------- /server_mysql/init/passport/local.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Configuring local strategy to authenticate strategies 3 | Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js 4 | */ 5 | import passport from 'passport'; 6 | import { Strategy as LocalStrategy } from 'passport-local'; 7 | import { passport as dbPassport } from '../../db'; 8 | import unsupportedMessage from '../../db/unsupportedMessage'; 9 | 10 | export default () => { 11 | if (!dbPassport || !dbPassport.local || typeof dbPassport.local !== 'function') { 12 | console.warn(unsupportedMessage('passport-local')); 13 | return; 14 | } 15 | 16 | /* 17 | By default, LocalStrategy expects to find credentials in parameters named username and password. 18 | If your site prefers to name these fields differently, 19 | options are available to change the defaults. 20 | */ 21 | passport.use(new LocalStrategy({ 22 | usernameField: 'email', 23 | passwordField: 'password', 24 | }, dbPassport.local)); 25 | }; 26 | -------------------------------------------------------------------------------- /server_mysql/render/static-assets/dev.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const createAppScript = () => ''; 4 | const createVendorScript = () => ''; 5 | 6 | const createAnalyticsSnippet = (id: string) => ` 11 | `; 12 | 13 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 14 | 15 | const createStylesheets = () => ''; 16 | 17 | export { 18 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 19 | }; 20 | -------------------------------------------------------------------------------- /server_mysql/render/static-assets/index.ts: -------------------------------------------------------------------------------- 1 | const createStaticAssets = process.env.NODE_ENV === 'production' ? require('./prod') : require('./dev'); 2 | 3 | export default createStaticAssets; 4 | -------------------------------------------------------------------------------- /server_mysql/render/static-assets/prod.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const assets = require('../../../public/assets/manifest.json'); 4 | 5 | const createAppScript = () => ``; 6 | const createVendorScript = () => ``; 7 | 8 | const createAnalyticsSnippet = (id: string) => ` 13 | `; 14 | 15 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 16 | 17 | const createStylesheets = () => ` 18 | 19 | 20 | `; 21 | 22 | export { 23 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 24 | }; 25 | -------------------------------------------------------------------------------- /server_none/db/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import topics from './topics'; 2 | import users from './users'; 3 | 4 | export { topics, users }; 5 | 6 | export default { 7 | topics, 8 | users 9 | }; 10 | -------------------------------------------------------------------------------- /server_none/db/controllers/topics.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | /** 4 | * List 5 | */ 6 | export function all(req: Request, res: Response) { 7 | res.json([]); 8 | } 9 | 10 | /** 11 | * Add a Topic 12 | */ 13 | export function add(req: Request, res: Response) { 14 | res.send('ok'); 15 | } 16 | 17 | /** 18 | * Update a topic 19 | */ 20 | export function update(req: Request, res: Response) { 21 | res.send('ok'); 22 | } 23 | 24 | /** 25 | * Remove a topic 26 | */ 27 | export function remove(req: Request, res: Response) { 28 | res.send('ok'); 29 | } 30 | 31 | export default { 32 | all, 33 | add, 34 | update, 35 | remove 36 | }; 37 | -------------------------------------------------------------------------------- /server_none/db/controllers/users.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import passport from 'passport'; 3 | 4 | /** 5 | * POST /login 6 | */ 7 | export function login(req: Request, res: Response, next: NextFunction) { 8 | // Do email and password validation for the server 9 | passport.authenticate('local', (authErr, user, info) => { 10 | if (authErr) return next(authErr); 11 | if (!user) { 12 | return res.sendStatus(401); 13 | } 14 | if (info) { 15 | return res.status(401).json(info); 16 | } 17 | // Passport exposes a login() function on req (also aliased as 18 | // logIn()) that can be used to establish a login session 19 | return req.logIn(user, (loginErr) => { 20 | if (loginErr) return res.sendStatus(401); 21 | return res.sendStatus(200); 22 | }); 23 | })(req, res, next); 24 | } 25 | 26 | /** 27 | * POST /logout 28 | */ 29 | export function logout(req: Request, res: Response) { 30 | req.logout(); 31 | res.sendStatus(200); 32 | } 33 | 34 | /** 35 | * POST /signup 36 | * Create a new local account 37 | */ 38 | export async function signUp(req: Request, res: Response, next: NextFunction) { 39 | res.send('ok'); 40 | } 41 | 42 | export default { 43 | login, 44 | logout, 45 | signUp, 46 | }; 47 | -------------------------------------------------------------------------------- /server_none/db/index.ts: -------------------------------------------------------------------------------- 1 | import { MemoryStore } from 'express-session'; 2 | 3 | const connect = () => {}; 4 | const session = () => new MemoryStore(); 5 | 6 | export { connect, session }; 7 | export { default as passport } from './passport'; 8 | export { default as controllers } from './controllers'; 9 | -------------------------------------------------------------------------------- /server_none/db/passport/deserializeUser.ts: -------------------------------------------------------------------------------- 1 | export default (id: string, done: (err: any, user?: any) => void) => { 2 | done(null, id); 3 | }; 4 | -------------------------------------------------------------------------------- /server_none/db/passport/google.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | interface GoogleProfile { 4 | id: string; 5 | displayName: string; 6 | _json: { 7 | gender: string; 8 | picture: string; 9 | emails: Array<{ value: string }> 10 | } 11 | } 12 | 13 | type DoneFunction = (err: any, user?: any, info?: { message: string }) => void; 14 | 15 | export default (req: Request, accessToken: string, refreshToken: string, profile: GoogleProfile, done: DoneFunction) => { 16 | done(null, profile); 17 | }; 18 | -------------------------------------------------------------------------------- /server_none/db/passport/index.ts: -------------------------------------------------------------------------------- 1 | import deserializeUser from './deserializeUser'; 2 | import google from './google'; 3 | import local from './local'; 4 | 5 | export { deserializeUser, google, local }; 6 | 7 | export default { 8 | deserializeUser, 9 | google, 10 | local, 11 | }; 12 | -------------------------------------------------------------------------------- /server_none/db/passport/local.ts: -------------------------------------------------------------------------------- 1 | export default (email: string, password: string, done: (err: any, user?: any, info?: { message: string }) => void) => { 2 | done(null, { email, password }); 3 | }; 4 | -------------------------------------------------------------------------------- /server_none/db/unsupportedMessage.ts: -------------------------------------------------------------------------------- 1 | import { DB_TYPE } from '../../config/serverEnv'; 2 | 3 | export default (featureName: string) => `Attempted to use '${featureName}' but DB type '${DB_TYPE}' doesn't support it`; 4 | -------------------------------------------------------------------------------- /server_none/init/passport/index.ts: -------------------------------------------------------------------------------- 1 | /* Initializing passport.js */ 2 | import passport from 'passport'; 3 | import local from './local'; 4 | import google from './google'; 5 | import { passport as dbPassport } from '../../db'; 6 | import unsupportedMessage from '../../db/unsupportedMessage'; 7 | 8 | export default () => { 9 | // Configure Passport authenticated session persistence. 10 | // 11 | // In order to restore authentication state across HTTP requests, Passport needs 12 | // to serialize users into and deserialize users out of the session. The 13 | // typical implementation of this is as simple as supplying the user ID when 14 | // serializing, and querying the user record by ID from the database when 15 | // deserializing. 16 | 17 | if (dbPassport && dbPassport.deserializeUser) { 18 | passport.serializeUser((user: { id: string }, done) => { 19 | done(null, user.id); 20 | }); 21 | 22 | passport.deserializeUser(dbPassport.deserializeUser); 23 | } else { 24 | console.warn(unsupportedMessage('(de)serialize User')); 25 | } 26 | 27 | // use the following strategies 28 | local(); 29 | google(); 30 | }; 31 | -------------------------------------------------------------------------------- /server_none/init/passport/local.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Configuring local strategy to authenticate strategies 3 | Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js 4 | */ 5 | import passport from 'passport'; 6 | import { Strategy as LocalStrategy } from 'passport-local'; 7 | import { passport as dbPassport } from '../../db'; 8 | import unsupportedMessage from '../../db/unsupportedMessage'; 9 | 10 | export default () => { 11 | if (!dbPassport || !dbPassport.local || typeof dbPassport.local !== 'function') { 12 | console.warn(unsupportedMessage('passport-local')); 13 | return; 14 | } 15 | 16 | /* 17 | By default, LocalStrategy expects to find credentials in parameters named username and password. 18 | If your site prefers to name these fields differently, 19 | options are available to change the defaults. 20 | */ 21 | passport.use(new LocalStrategy({ 22 | usernameField: 'email' 23 | }, dbPassport.local)); 24 | }; 25 | -------------------------------------------------------------------------------- /server_none/render/static-assets/dev.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const createAppScript = () => ''; 4 | const createVendorScript = () => ''; 5 | 6 | const createAnalyticsSnippet = (id: string) => ` 11 | `; 12 | 13 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 14 | 15 | const createStylesheets = () => ''; 16 | 17 | export { 18 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 19 | }; 20 | -------------------------------------------------------------------------------- /server_none/render/static-assets/index.ts: -------------------------------------------------------------------------------- 1 | const createStaticAssets = process.env.NODE_ENV === 'production' ? require('./prod') : require('./dev'); 2 | 3 | export default createStaticAssets; 4 | -------------------------------------------------------------------------------- /server_none/render/static-assets/prod.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const assets = require('../../../public/assets/manifest.json'); 4 | 5 | const createAppScript = () => ``; 6 | const createVendorScript = () => ``; 7 | 8 | const createAnalyticsSnippet = (id: string) => ` 13 | `; 14 | 15 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 16 | 17 | const createStylesheets = () => ` 18 | 19 | 20 | `; 21 | 22 | export { 23 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 24 | }; 25 | -------------------------------------------------------------------------------- /server_pg/db/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, controllers, passport } from './sequelize'; 2 | import session from './session'; 3 | 4 | export { connect, controllers, passport, session }; 5 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/connect.ts: -------------------------------------------------------------------------------- 1 | import { sequelize } from './models'; 2 | 3 | export default () => { 4 | sequelize 5 | .sync() 6 | .then(() => { 7 | console.log('Successfully connected to sequelize database'); 8 | }, (err: Error) => { 9 | console.log('Unable to connect to the sequelize database: ', err); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/constants.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from '../../../config/env'; 2 | import sequelizeConfig from './sequelize_config'; 3 | 4 | const config = sequelizeConfig[ENV]; 5 | 6 | export const db = process.env[config.use_env_variable] || `${config.dialect}://${config.username}:${config.password}@${config.host}/${config.database}`; 7 | 8 | export default { 9 | db 10 | }; 11 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import topics from './topics'; 2 | import users from './users'; 3 | 4 | export default { 5 | topics, 6 | users 7 | }; 8 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | export { default as connect } from './connect'; 2 | export { default as controllers } from './controllers'; 3 | export { default as passport } from './passport'; 4 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/migrations/20160416222221-add-topics.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'Topics', { 5 | id: { 6 | type: DataTypes.STRING, 7 | primaryKey: true 8 | }, 9 | text: { 10 | type: DataTypes.STRING 11 | }, 12 | count: { 13 | type: DataTypes.INTEGER 14 | }, 15 | date: { 16 | type: DataTypes.DATE, 17 | defaultValue: DataTypes.fn('NOW') 18 | } 19 | } 20 | ); 21 | }, 22 | 23 | down(queryInterface) { 24 | return queryInterface.dropTable('Topics'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/migrations/20160416222345-add-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'Users', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | email: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | }, 14 | password: { 15 | type: DataTypes.STRING 16 | }, 17 | name: { 18 | type: DataTypes.STRING, 19 | defaultValue: '' 20 | }, 21 | gender: { 22 | type: DataTypes.STRING, 23 | defaultValue: '' 24 | }, 25 | location: { 26 | type: DataTypes.STRING, 27 | defaultValue: '' 28 | }, 29 | website: { 30 | type: DataTypes.STRING, 31 | defaultValue: '' 32 | }, 33 | picture: { 34 | type: DataTypes.STRING, 35 | defaultValue: '' 36 | }, 37 | resetPasswordToken: { 38 | type: DataTypes.STRING 39 | }, 40 | resetPasswordExpires: { 41 | type: DataTypes.DATE 42 | } 43 | } 44 | ).then(() => queryInterface.addIndex( 45 | 'Users', 46 | [DataTypes.fn('lower', DataTypes.col('email'))], 47 | { 48 | indexName: 'users_email', 49 | indicesType: 'unique' 50 | } 51 | )); 52 | }, 53 | 54 | down(queryInterface) { 55 | return queryInterface.dropTable('Users'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/migrations/20160416222416-add-tokens.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'Tokens', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | kind: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | }, 14 | accessToken: { 15 | type: DataTypes.STRING, 16 | allowNull: false 17 | }, 18 | userId: { 19 | type: DataTypes.INTEGER, 20 | references: { 21 | model: 'Users', 22 | key: 'id' 23 | } 24 | } 25 | } 26 | ); 27 | }, 28 | 29 | down(queryInterface) { 30 | return queryInterface.dropTable('Tokens'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/migrations/20160416222449-add-google-id-to-users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | queryInterface.addColumn('Users', 'google', DataTypes.STRING); 4 | }, 5 | 6 | down(queryInterface) { 7 | queryInterface.removeColumn('Users', 'google'); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/migrations/20160416222520-add-sessions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, DataTypes) { 3 | return queryInterface.createTable( 4 | 'session', { 5 | sid: { 6 | type: DataTypes.STRING, 7 | primaryKey: true 8 | }, 9 | sess: { 10 | type: DataTypes.JSON 11 | }, 12 | expire: { 13 | type: DataTypes.DATE 14 | } 15 | } 16 | ); 17 | }, 18 | 19 | down(queryInterface) { 20 | return queryInterface.dropTable('session'); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/models/index.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import sequelizeConfig from '../sequelize_config'; 3 | import { ENV } from '../../../../config/env'; 4 | import tokenModel from './tokens'; 5 | import topicModel from './topics'; 6 | import userModel from './users'; 7 | 8 | const config = sequelizeConfig[ENV]; 9 | 10 | const dbUrl = process.env[config.use_env_variable]; 11 | 12 | const sequelize = dbUrl ? new Sequelize(dbUrl) : new Sequelize(config.database, config.username, config.password, config); 13 | 14 | const db = { 15 | User: userModel, 16 | Token: tokenModel, 17 | Topic: topicModel, 18 | } as const; 19 | export type dbType = typeof db; 20 | 21 | Object.keys(db).forEach((key) => { 22 | const model = db[key as keyof dbType]; 23 | model.initWithSequelize(sequelize); 24 | }); 25 | 26 | Object.keys(db).forEach((key) => { 27 | const model = db[key as keyof dbType]; 28 | if (model.associate) { 29 | model.associate(db); 30 | } 31 | }); 32 | 33 | export { db as Models, sequelize }; 34 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/models/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | import { dbType } from './index'; 3 | 4 | class Token extends Model { 5 | static initWithSequelize(sequelize: Sequelize) { 6 | return Token.init({ 7 | kind: { 8 | type: DataTypes.STRING, 9 | allowNull: false 10 | }, 11 | accessToken: { 12 | type: DataTypes.INTEGER, 13 | allowNull: false 14 | }, 15 | userId: { 16 | type: DataTypes.INTEGER, 17 | references: { 18 | model: 'Users', 19 | key: 'id' 20 | } 21 | } 22 | }, { 23 | sequelize, 24 | modelName: 'Token', 25 | tableName: 'tokens', 26 | timestamps: false, 27 | }); 28 | } 29 | 30 | static associate = (models: dbType) => { 31 | models.Token.belongsTo(models.User, { 32 | foreignKey: 'userId' 33 | }); 34 | }; 35 | } 36 | 37 | export default Token; 38 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/models/topics.ts: -------------------------------------------------------------------------------- 1 | import { Model, Sequelize, DataTypes } from 'sequelize'; 2 | 3 | class Topic extends Model { 4 | static initWithSequelize(sequelize: Sequelize) { 5 | return Topic.init({ 6 | id: { 7 | type: DataTypes.STRING, 8 | primaryKey: true 9 | }, 10 | text: DataTypes.STRING, 11 | count: { 12 | type: DataTypes.INTEGER, 13 | validate: { 14 | min: 0 15 | } 16 | }, 17 | date: { 18 | type: DataTypes.DATE, 19 | defaultValue: Sequelize.fn('NOW') 20 | } 21 | }, { 22 | sequelize, 23 | modelName: 'Topic', 24 | tableName: 'topics', 25 | timestamps: false, 26 | }); 27 | } 28 | 29 | static associate() {} 30 | } 31 | 32 | export default Topic; 33 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/passport/deserializeUser.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/users'; 2 | 3 | export default (id: number, done: (error: any, user?: User | null) => void) => { 4 | User.findByPk(id).then((user) => { 5 | done(null, user); 6 | }).catch(done); 7 | }; 8 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/passport/index.ts: -------------------------------------------------------------------------------- 1 | import deserializeUser from './deserializeUser'; 2 | import google from './google'; 3 | import local from './local'; 4 | 5 | export default { 6 | deserializeUser, 7 | google, 8 | local, 9 | }; 10 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/passport/local.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/users'; 2 | 3 | export default async (email: string, password: string, done: (err: any, user?: any, info?: { message: string }) => void) => { 4 | try { 5 | const user = await User.findOne({ where: { email } }); 6 | if (!user) return done(null, false, { message: `There is no record of the email ${email}.` }); 7 | const result = await user.comparePassword(password); 8 | console.log('result', result); 9 | if (result) done(null, user); 10 | else done(null, false, { message: 'Your email/password combination is incorrect.' }); 11 | } catch (err) { 12 | console.log(err); 13 | done(null, false, { message: 'Something went wrong trying to authenticate' }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /server_pg/db/sequelize/sequelize_config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config(); 4 | 5 | type SequelizeConfig = { 6 | development: any; 7 | test: any; 8 | production: any; 9 | }; 10 | 11 | const exportedConfig: SequelizeConfig = { 12 | development: { 13 | username: process.env.PGUSER || 'root', 14 | password: process.env.PGPASS || null, 15 | database: process.env.PGDB || 'reactgo_development', 16 | host: '127.0.0.1', 17 | dialect: 'postgres' 18 | }, 19 | test: { 20 | username: process.env.PGUSER || 'root', 21 | password: process.env.PGPASS || null, 22 | database: 'reactgo_test', 23 | host: '127.0.0.1', 24 | dialect: 'postgres' 25 | }, 26 | production: { 27 | use_env_variable: 'POSTGRES_DB_URL', 28 | username: process.env.PGUSER || 'root', 29 | password: process.env.PGPASS || null, 30 | database: process.env.PGDB || 'reactgo_production', 31 | host: process.env.PGHOST || '127.0.0.1', 32 | dialect: 'postgres' 33 | } 34 | } as const; 35 | 36 | export default exportedConfig; 37 | -------------------------------------------------------------------------------- /server_pg/db/session.ts: -------------------------------------------------------------------------------- 1 | import session from 'express-session'; 2 | import connectPostgres from 'connect-pg-simple'; 3 | import { db } from './sequelize/constants'; 4 | 5 | const PGStore = connectPostgres(session); 6 | 7 | export default () => new PGStore({ 8 | conString: db, 9 | }); 10 | -------------------------------------------------------------------------------- /server_pg/db/unsupportedMessage.ts: -------------------------------------------------------------------------------- 1 | import { DB_TYPE } from '../../config/serverEnv'; 2 | 3 | export default (featureName: string) => `Attempted to use '${featureName}' but DB type '${DB_TYPE}' doesn't support it`; 4 | -------------------------------------------------------------------------------- /server_pg/init/passport/index.ts: -------------------------------------------------------------------------------- 1 | /* Initializing passport.js */ 2 | import passport from 'passport'; 3 | import local from './local'; 4 | import google from './google'; 5 | import { passport as dbPassport } from '../../db'; 6 | import unsupportedMessage from '../../db/unsupportedMessage'; 7 | 8 | export default () => { 9 | // Configure Passport authenticated session persistence. 10 | // 11 | // In order to restore authentication state across HTTP requests, Passport needs 12 | // to serialize users into and deserialize users out of the session. The 13 | // typical implementation of this is as simple as supplying the user ID when 14 | // serializing, and querying the user record by ID from the database when 15 | // deserializing. 16 | 17 | if (dbPassport && dbPassport.deserializeUser) { 18 | passport.serializeUser((user: { id: string }, done) => { 19 | done(null, user.id); 20 | }); 21 | 22 | passport.deserializeUser(dbPassport.deserializeUser); 23 | } else { 24 | console.warn(unsupportedMessage('(de)serialize User')); 25 | } 26 | 27 | // use the following strategies 28 | local(); 29 | google(); 30 | }; 31 | -------------------------------------------------------------------------------- /server_pg/init/passport/local.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Configuring local strategy to authenticate strategies 3 | Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js 4 | */ 5 | import passport from 'passport'; 6 | import { Strategy as LocalStrategy } from 'passport-local'; 7 | import { passport as dbPassport } from '../../db'; 8 | import unsupportedMessage from '../../db/unsupportedMessage'; 9 | 10 | export default () => { 11 | if (!dbPassport || !dbPassport.local || typeof dbPassport.local !== 'function') { 12 | console.warn(unsupportedMessage('passport-local')); 13 | return; 14 | } 15 | 16 | /* 17 | By default, LocalStrategy expects to find credentials in parameters named username and password. 18 | If your site prefers to name these fields differently, 19 | options are available to change the defaults. 20 | */ 21 | passport.use(new LocalStrategy({ 22 | usernameField: 'email', 23 | passwordField: 'password', 24 | }, dbPassport.local)); 25 | }; 26 | -------------------------------------------------------------------------------- /server_pg/render/static-assets/dev.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const createAppScript = () => ''; 4 | const createVendorScript = () => ''; 5 | 6 | const createAnalyticsSnippet = (id: string) => ` 11 | `; 12 | 13 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 14 | 15 | const createStylesheets = () => ''; 16 | 17 | export { 18 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 19 | }; 20 | -------------------------------------------------------------------------------- /server_pg/render/static-assets/index.ts: -------------------------------------------------------------------------------- 1 | const createStaticAssets = process.env.NODE_ENV === 'production' ? require('./prod') : require('./dev'); 2 | 3 | export default createStaticAssets; 4 | -------------------------------------------------------------------------------- /server_pg/render/static-assets/prod.ts: -------------------------------------------------------------------------------- 1 | import { GOOGLE_ANALYTICS_ID } from '../../../config/env'; 2 | 3 | const assets = require('../../../public/assets/manifest.json'); 4 | 5 | const createAppScript = () => ``; 6 | const createVendorScript = () => ``; 7 | 8 | const createAnalyticsSnippet = (id: string) => ` 13 | `; 14 | 15 | const createTrackingScript = () => (GOOGLE_ANALYTICS_ID ? createAnalyticsSnippet(GOOGLE_ANALYTICS_ID) : ''); 16 | 17 | const createStylesheets = () => ` 18 | 19 | 20 | `; 21 | 22 | export { 23 | createAppScript, createVendorScript, createTrackingScript, createStylesheets 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "dom", 6 | "esnext" 7 | ], 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react", 15 | "typeRoots": ["./types"] 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "compiled", 20 | "public", 21 | "**/*.js" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /types/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: any; 3 | export = value; 4 | } 5 | declare module '*.svg' { 6 | const content: any; 7 | export = content; 8 | } 9 | -------------------------------------------------------------------------------- /types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import User from '../../server/db/sequelize/models/users'; 2 | 3 | declare module 'express-serve-static-core' { 4 | interface Request { 5 | user: User; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | declare global { 3 | interface Error {} 4 | interface Window { 5 | __INITIAL_STATE__: any; 6 | } 7 | interface NodeModule { 8 | hot: any; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webpack/externals.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const externalModules = fs.readdirSync('node_modules') 4 | .filter(x => ['.bin'].indexOf(x) === -1) 5 | .reduce((acc, cur) => Object.assign(acc, { [cur]: 'commonjs ' + cur }), {}); 6 | 7 | export default externalModules; 8 | 9 | -------------------------------------------------------------------------------- /webpack/paths.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | /* 4 | * __dirname is changed after webpack-ed to another directory 5 | * so process.cwd() is used instead to determine the correct base directory 6 | * Read more: https://nodejs.org/api/process.html#process_process_cwd 7 | */ 8 | const CURRENT_WORKING_DIR = process.cwd(); 9 | 10 | export default { 11 | app: path.resolve(CURRENT_WORKING_DIR, 'app'), 12 | assets: path.resolve(CURRENT_WORKING_DIR, 'public', 'assets'), 13 | compiled: path.resolve(CURRENT_WORKING_DIR, 'compiled'), 14 | public: '/assets/', // use absolute path for css-loader? 15 | modules: path.resolve(CURRENT_WORKING_DIR, 'node_modules') 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /webpack/plugins.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import ManifestPlugin from 'webpack-manifest-plugin'; 4 | 5 | export default ({ production = false, browser = false } = {}) => { 6 | const bannerOptions = { raw: true, banner: 'require("source-map-support").install();' }; 7 | 8 | if (!production && !browser) { 9 | return [ 10 | new webpack.EnvironmentPlugin(['NODE_ENV']), 11 | new webpack.BannerPlugin(bannerOptions) 12 | ]; 13 | } 14 | if (!production && browser) { 15 | return [ 16 | new webpack.EnvironmentPlugin(['NODE_ENV']), 17 | new webpack.HotModuleReplacementPlugin(), 18 | ]; 19 | } 20 | if (production && !browser) { 21 | return [ 22 | new webpack.EnvironmentPlugin(['NODE_ENV']), 23 | new webpack.BannerPlugin(bannerOptions), 24 | ]; 25 | } 26 | if (production && browser) { 27 | return [ 28 | new webpack.EnvironmentPlugin(['NODE_ENV']), 29 | new MiniCssExtractPlugin({ 30 | filename: '[contenthash].css', 31 | }), 32 | new ManifestPlugin({ 33 | fileName: 'manifest.json', 34 | publicPath: '' 35 | }) 36 | ]; 37 | } 38 | return []; 39 | }; 40 | -------------------------------------------------------------------------------- /webpack/resolve.ts: -------------------------------------------------------------------------------- 1 | import PATHS from './paths'; 2 | 3 | export default { 4 | modules: [PATHS.app, PATHS.modules], 5 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.css'], 6 | alias: { 7 | 'react-dom': '@hot-loader/react-dom' 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /webpack/rules/image.ts: -------------------------------------------------------------------------------- 1 | const PATHS = require('../paths'); 2 | 3 | export default ({ limit = 10000 } = {}) => ({ 4 | test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, 5 | loader: 'url-loader', 6 | options: { name: '[hash].[ext]', limit, esModule: false }, 7 | include: PATHS.app 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /webpack/rules/index.ts: -------------------------------------------------------------------------------- 1 | import image from './image'; 2 | import typescript from './typescript'; 3 | import css from './css'; 4 | 5 | export default ({ production = false, browser = false } = {}) => ( 6 | [ 7 | typescript({ production }), 8 | css({ production, browser }), 9 | image() 10 | ] 11 | ); 12 | --------------------------------------------------------------------------------