diff --git a/config/local.json b/config/config.json similarity index 100% rename from config/local.json rename to config/config.json diff --git a/package-lock.json b/package-lock.json index cff3018..39ab1b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,12 @@ "@types/node": "*" } }, + "@types/classnames": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz", + "integrity": "sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==", + "dev": true + }, "@types/clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz", @@ -1513,6 +1519,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", diff --git a/package.json b/package.json index 7119ea0..a30c1ad 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "deploy:prod": "run-s deploy:batch:prod deploy:config:prod" }, "devDependencies": { + "@types/classnames": "^2.2.9", "@types/html-webpack-plugin": "^3.2.1", "@types/lodash": "^4.14.138", "@types/mini-css-extract-plugin": "^0.8.0", @@ -41,9 +42,11 @@ "webpack-dev-server": "^3.8.0" }, "dependencies": { + "@fortawesome/fontawesome-common-types": "^0.2.22", "@fortawesome/fontawesome-svg-core": "^1.2.22", "@fortawesome/free-solid-svg-icons": "^5.10.2", "@fortawesome/react-fontawesome": "^0.1.4", + "classnames": "^2.2.6", "lodash": "^4.17.15", "normalizr": "^3.4.1", "react": "^16.9.0", diff --git a/src/actions/directory.ts b/src/actions/directory.ts index 3a6636f..79297ce 100644 --- a/src/actions/directory.ts +++ b/src/actions/directory.ts @@ -43,7 +43,7 @@ export const setContinuation = (continuation: string): SetContinuationAction => payload: continuation, }) -export const fetchGroup = (id: string) => { +export const fetchGroup = (id: string): ThunkAction, AppState, void, AnyAction> => { return async (dispatch: ThunkDispatch) => { dispatch(startRequest(FETCH_GROUP_ID)) @@ -52,8 +52,8 @@ export const fetchGroup = (id: string) => { dispatch(setEntity('group', group)) dispatch(finishRequest(FETCH_GROUP_ID, true)) } catch (err) { - console.error(err) dispatch(finishRequest(FETCH_GROUP_ID, false)) + throw err } } } @@ -75,8 +75,8 @@ export const fetchGroups = (sort?: string, continuation?: string): ThunkAction

= ({ menuCollapsed, fetching }) => {

- +
diff --git a/src/components/app/index.ts b/src/components/app/index.ts index 638fa0c..94c2e1e 100644 --- a/src/components/app/index.ts +++ b/src/components/app/index.ts @@ -1,7 +1,7 @@ import { connect } from 'react-redux' -import { getMenuCollapsed, getFetching } from '../../selectors' -import { AppState } from '../../types' +import { getMenuCollapsed, getFetching } from 'src/selectors' +import { AppState } from 'src/types' import App from './app' diff --git a/src/components/notification-container/index.ts b/src/components/notification-container/index.ts index b85a49f..acf74b4 100644 --- a/src/components/notification-container/index.ts +++ b/src/components/notification-container/index.ts @@ -1,9 +1,9 @@ import { Dispatch } from 'redux' import { connect } from 'react-redux' -import { setNotificationAuto, removeNotification } from '../../actions/notifications' -import { getNotifications } from '../../selectors' -import { AppState } from '../../types' +import { setNotificationAuto, removeNotification } from 'src/actions/notifications' +import { getNotifications } from 'src/selectors' +import { AppState } from 'src/types' import NotificationContainer from './notification-container' diff --git a/src/components/notification-container/notification-container.tsx b/src/components/notification-container/notification-container.tsx index e8386c2..0e39b93 100644 --- a/src/components/notification-container/notification-container.tsx +++ b/src/components/notification-container/notification-container.tsx @@ -1,5 +1,5 @@ import React, { FC } from 'react' -import { Notification as INotification } from '../../types' +import { Notification as INotification } from 'src/types' import Notification from '../notification' diff --git a/src/components/notification/index.tsx b/src/components/notification/index.tsx index d7a8214..edfd169 100644 --- a/src/components/notification/index.tsx +++ b/src/components/notification/index.tsx @@ -1,5 +1,8 @@ import React, { FC, MouseEventHandler } from 'react' -import { NotificationType } from '../../types' +import classNames from 'classnames' + +import { notificationTypeToClassName } from 'src/utils' +import { NotificationType } from 'src/types' interface Props { id: string @@ -9,19 +12,11 @@ interface Props { dismiss: (id: string) => void } -const getClassName = (type: NotificationType) => { - switch (type) { - case 'info': return 'is-info' - case 'success': return 'is-success' - case 'error': return 'is-danger' - } -} - const Notification: FC = ({ id, type, auto, setAuto, dismiss, children }) => { - const classnames = [ - 'notification', - getClassName(type), - ].join(' ') + const classnames = classNames({ + notification: true, + [notificationTypeToClassName(type)]: true, + }) const handleDismiss: MouseEventHandler = e => { e.stopPropagation() diff --git a/src/components/pages/directory/directory.tsx b/src/components/pages/directory/directory.tsx index 677de55..f74ea1a 100644 --- a/src/components/pages/directory/directory.tsx +++ b/src/components/pages/directory/directory.tsx @@ -1,7 +1,6 @@ import React, { FC, useEffect } from 'react' import { Link } from 'react-router-dom' -import { Entity } from '../../../types' - +import { Entity } from 'src/types' import './directory.scss' interface Props { diff --git a/src/components/pages/directory/index.ts b/src/components/pages/directory/index.ts index dd467d2..68e8c76 100644 --- a/src/components/pages/directory/index.ts +++ b/src/components/pages/directory/index.ts @@ -2,9 +2,9 @@ import { connect } from 'react-redux' import { ThunkDispatch } from 'redux-thunk' import { AnyAction } from 'redux' -import { fetchGroups } from '../../../actions/directory' -import { getDirectoryGroups } from '../../../selectors' -import { AppState } from '../../../types' +import { fetchGroups } from 'src/actions/directory' +import { getDirectoryGroups } from 'src/selectors' +import { AppState } from 'src/types' import Directory from './directory' diff --git a/src/components/pages/register-group/index.ts b/src/components/pages/register-group/index.ts new file mode 100644 index 0000000..d413526 --- /dev/null +++ b/src/components/pages/register-group/index.ts @@ -0,0 +1,29 @@ +import { AnyAction } from 'redux' +import { ThunkDispatch } from 'redux-thunk' +import { connect } from 'react-redux' + +import { fetchGroup } from 'src/actions/directory' +import { getEntity } from 'src/selectors' +import { AppState } from 'src/types' + +import RegisterGroup, { Props } from './register-group' + +const mapStateToProps = (state: AppState, ownProps: Props) => ({ + group: getEntity(state, 'group', ownProps.match.params.id), +}) + +const mapDispatchToProps = (dispatch: ThunkDispatch, ownProps: Props) => ({ + fetchGroup: async () => { + try { + await dispatch(fetchGroup(ownProps.match.params.id)) + } catch (err) { + console.error('Expected error') + console.error(err) + } + }, +}) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(RegisterGroup) diff --git a/src/components/pages/register-group/register-group.tsx b/src/components/pages/register-group/register-group.tsx new file mode 100644 index 0000000..f50c159 --- /dev/null +++ b/src/components/pages/register-group/register-group.tsx @@ -0,0 +1,36 @@ +import React, { FC, useEffect } from 'react' +import { RouteComponentProps } from 'react-router' + +import { Entity } from 'src/types' + +interface Params { + id: string +} + +export interface Props extends RouteComponentProps { + group?: Entity + fetchGroup: () => void +} + +const RegisterGroup: FC = ({ group, fetchGroup }) => { + useEffect(() => { + fetchGroup() + }, []) + + if (!group) { + return ( +
+

Community Not Found

+
+ ) + } + + return ( +
+

{group.name}

+

Create a new Account.

+
+ ) +} + +export default RegisterGroup diff --git a/src/components/pages/register/index.ts b/src/components/pages/register/index.ts deleted file mode 100644 index fc61df8..0000000 --- a/src/components/pages/register/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux' -import Register from './register' - -const mapStateToProps = () => { -} - -export default connect( - mapStateToProps -)(Register) diff --git a/src/components/pages/register/register.tsx b/src/components/pages/register/register.tsx deleted file mode 100644 index 6694c81..0000000 --- a/src/components/pages/register/register.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { FC, useEffect } from 'react' -import { RouteComponentProps } from 'react-router' - -import { Entity } from '../../../types' - -interface Params { - group: string -} - -type Props = RouteComponentProps & { - group?: Entity -} - -const Register: FC = ({ group, match }) => { - useEffect(() => { - - }) - - return ( -
-

Create a new Account

-
- ) -} - -export default Register diff --git a/src/components/register-form/index.ts b/src/components/register-form/index.ts new file mode 100644 index 0000000..e8a0ba9 --- /dev/null +++ b/src/components/register-form/index.ts @@ -0,0 +1,24 @@ +import { Dispatch } from 'redux' +import { connect } from 'react-redux' + +import { initForm, initField } from 'src/actions/forms' +import { AppState } from 'src/types' + +import RegisterForm from './register-form' + +const mapStateToProps = (state: AppState) => ({ +}) + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + initForm: () => { + dispatch(initForm()) + dispatch(initField('username')) + dispatch(initField('name')) + dispatch(initField('email')) + } +}) + +export default connect( + mapStateToProps, + mapDispatchToProps +)(RegisterForm) diff --git a/src/components/register-form/register-form.tsx b/src/components/register-form/register-form.tsx new file mode 100644 index 0000000..17cf44a --- /dev/null +++ b/src/components/register-form/register-form.tsx @@ -0,0 +1,21 @@ +import React, { FC, useEffect } from 'react' +import TextField from '../text-field' +import './register-form.scss' + +interface Props { + initForm: () => void +} + +const RegisterForm: FC = ({ initForm }) => { + useEffect(() => { + initForm() + }, []) + + return ( +
+ +
+ ) +} + +export default RegisterForm diff --git a/src/components/text-field/index.ts b/src/components/text-field/index.ts new file mode 100644 index 0000000..a8af9c6 --- /dev/null +++ b/src/components/text-field/index.ts @@ -0,0 +1,15 @@ +import { connect } from 'react-redux' + +import { getFieldValue, getFieldNotification } from 'src/selectors' +import { AppState } from 'src/types' + +import TextField, { Props } from './text-field' + +const mapStateToProps = (state: AppState, ownProps: Props) => ({ + value: getFieldValue(state, ownProps.name), + notification: getFieldNotification(state, ownProps.name), +}) + +export default connect( + mapStateToProps +)(TextField) diff --git a/src/components/text-field/text-field.tsx b/src/components/text-field/text-field.tsx new file mode 100644 index 0000000..85f5f54 --- /dev/null +++ b/src/components/text-field/text-field.tsx @@ -0,0 +1,49 @@ +import React, { FC } from 'react' +import classNames from 'classnames' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { IconDefinition } from '@fortawesome/fontawesome-common-types' + +import { notificationTypeToClassName } from 'src/utils' +import { FormNotification } from 'src/types' + +import './register-form.scss' + +export interface Props { + name: string + label: string + placeholder?: string + icon?: IconDefinition + value?: string + notification?: FormNotification +} + +const RegisterForm: FC = ({ label, placeholder, icon, value, notification }) => { + const controlClassNames = classNames({ control: true, 'has-icons-left': !!icon }) + let helpClassNames: string | undefined + + if (notification) { + helpClassNames = classNames({ + help: true, + [notificationTypeToClassName(notification.type)]: true, + }) + } + + return ( +
+ +
+ + {icon && + + + + } +
+ {notification && +

{notification.message}

+ } +
+ ) +} + +export default RegisterForm diff --git a/src/components/user-info/index.ts b/src/components/user-info/index.ts index ee54496..afd3706 100644 --- a/src/components/user-info/index.ts +++ b/src/components/user-info/index.ts @@ -1,7 +1,7 @@ import { connect } from 'react-redux' -import { getAuthenticated } from '../../selectors' -import { AppState } from '../../types' +import { getAuthenticated } from 'src/selectors' +import { AppState } from 'src/types' import UserInfo from './user-info' diff --git a/src/components/user-info/user-info.tsx b/src/components/user-info/user-info.tsx index e532ba6..3a33e83 100644 --- a/src/components/user-info/user-info.tsx +++ b/src/components/user-info/user-info.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react' import { Link } from 'react-router-dom' -import { User } from '../../types' +import { User } from 'src/types' import './user-info.scss' @@ -32,7 +32,7 @@ const UserInfo: FC = ({ authenticated, user }) => {

Log In
- Sign Up + Register

diff --git a/src/selectors/index.ts b/src/selectors/index.ts index 83bc0d2..77d05ca 100644 --- a/src/selectors/index.ts +++ b/src/selectors/index.ts @@ -7,6 +7,25 @@ import { AppState, Entity } from '../types' export const getEntityStore = (state: AppState) => state.entities +export const getEntity = (state: AppState, type: string, id: string) => { + const store = getEntityStore(state) + const collection = store[type] + + return collection ? collection[id] : undefined +} + +export const getForm = (state: AppState) => state.forms.form + +export const getFieldValue = (state: AppState, name: string) => { + const field = getForm(state)[name] + return field.value +} + +export const getFieldNotification = (state: AppState, name: string) => { + const field = getForm(state)[name] + return field.notification +} + export const getMenuCollapsed = (state: AppState) => state.menu.collapsed export const getAuthenticated = (state: AppState) => state.authentication.authenticated export const getNotifications = (state: AppState) => state.notifications diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..21e2dc8 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,9 @@ +import { NotificationType } from '../types' + +export function notificationTypeToClassName(type: NotificationType): string { + switch (type) { + case 'info': return 'is-info' + case 'success': return 'is-success' + case 'error': return 'is-danger' + } +} diff --git a/webpack.config.ts b/webpack.config.ts index c18ea3b..f4ef136 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -2,7 +2,7 @@ import { Configuration } from 'webpack' import HtmlWebpackPlugin from 'html-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' -import localConfig from './config/local.json' +import c from './config/config.json' const config: Configuration = { mode: 'development', @@ -17,15 +17,17 @@ const config: Configuration = { }, devServer: { contentBase: `${__dirname}/dist`, + historyApiFallback: true, before: app => { app.get('/config.json', (req, res) => { - res.json(localConfig) + res.json(c) }) }, }, resolve: { extensions: ['.ts', '.tsx', '.js'], alias: { + src: `${__dirname}/src`, actions: `${__dirname}/src/actions/`, components: `${__dirname}/src/components/`, reducers: `${__dirname}/src/reducers/`,