Dwayne Harris
5 years ago
92 changed files with 3069 additions and 2935 deletions
-
3.vscode/settings.json
-
2160package-lock.json
-
27package.json
-
21postcss-preset-env.d.ts
-
6src/actions/apps.ts
-
4src/actions/authentication.ts
-
13src/actions/menu.ts
-
8src/actions/registration.ts
-
24src/actions/theme.ts
-
44src/components/app-info.tsx
-
23src/components/app-list-item.tsx
-
50src/components/app.tsx
-
31src/components/composer.tsx
-
39src/components/controls/button.tsx
-
9src/components/controls/checkbox-field.tsx
-
0src/components/controls/cover-image-field.tsx
-
11src/components/controls/field-label.tsx
-
87src/components/controls/file-field.tsx
-
0src/components/controls/icon-image-field.tsx
-
0src/components/controls/image-field.tsx
-
108src/components/controls/password-field.tsx
-
28src/components/controls/primary-button.tsx
-
28src/components/controls/secondary-button.tsx
-
47src/components/controls/select-field.tsx
-
77src/components/controls/text-field.tsx
-
61src/components/controls/textarea-field.tsx
-
61src/components/controls/theme-field.tsx
-
21src/components/create-group-form.tsx
-
31src/components/create-group-step.tsx
-
23src/components/create-user-form.tsx
-
28src/components/create-user-step.tsx
-
21src/components/footer.tsx
-
26src/components/form-notification.tsx
-
103src/components/forms/password-field.tsx
-
71src/components/forms/text-field.tsx
-
57src/components/forms/textarea-field.tsx
-
49src/components/group-info.tsx
-
8src/components/group-invitations.tsx
-
4src/components/group-list-item.tsx
-
2src/components/group-logs.tsx
-
11src/components/help-text.tsx
-
9src/components/horizontal-rule.tsx
-
24src/components/level.tsx
-
16src/components/logo.tsx
-
27src/components/member-list-item.tsx
-
78src/components/navigation-menu.tsx
-
2src/components/notification-container.tsx
-
19src/components/notification.tsx
-
19src/components/page-header.tsx
-
9src/components/pages/about.tsx
-
13src/components/pages/apps.tsx
-
92src/components/pages/create-app.tsx
-
41src/components/pages/developers.tsx
-
138src/components/pages/edit-app.tsx
-
133src/components/pages/group-admin.tsx
-
28src/components/pages/groups.tsx
-
25src/components/pages/home.tsx
-
19src/components/pages/loading.tsx
-
46src/components/pages/login.tsx
-
29src/components/pages/register-group.tsx
-
16src/components/pages/register.tsx
-
153src/components/pages/self.tsx
-
122src/components/pages/view-app.tsx
-
125src/components/pages/view-group.tsx
-
48src/components/pages/view-post.tsx
-
177src/components/pages/view-user.tsx
-
8src/components/post-list.tsx
-
24src/components/post.tsx
-
16src/components/search.tsx
-
13src/components/section.tsx
-
92src/components/self-info.tsx
-
24src/components/spinner.tsx
-
9src/components/subtitle.tsx
-
6src/components/timeline.tsx
-
9src/components/title.tsx
-
42src/components/user-info.tsx
-
32src/components/user.tsx
-
5src/hooks/index.ts
-
22src/reducers/menu.ts
-
30src/reducers/theme.ts
-
3src/selectors/menu.ts
-
6src/selectors/theme.ts
-
4src/store/index.ts
-
367src/styles/app.css
-
207src/styles/app.scss
-
47src/styles/spinner.css
-
62src/styles/spinner.scss
-
78src/themes.ts
-
23src/types/index.ts
-
17src/types/store.ts
-
5src/utils/index.ts
-
22webpack.config.ts
@ -0,0 +1,3 @@ |
|||
{ |
|||
"typescript.tsdk": "node_modules\\typescript\\lib" |
|||
} |
2160
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,21 @@ |
|||
declare module 'postcss-preset-env' { |
|||
import { |
|||
plugin, Plugin, ParserInput, |
|||
Result, LazyResult, Root, ProcessOptions |
|||
} from 'postcss'; |
|||
|
|||
interface PluginOptions { |
|||
stage?: number; |
|||
features?: any; |
|||
browsers?: string; |
|||
insertBefore?: any; |
|||
insertAfter?: any; |
|||
autoprefixer?: any; |
|||
preserve?: boolean; |
|||
importFrom?: string; |
|||
exportTo?: string; |
|||
} |
|||
|
|||
const PostcssPresetEnv: Plugin<PluginOptions>; |
|||
export default PostcssPresetEnv; |
|||
} |
@ -1,13 +0,0 @@ |
|||
import { Action } from 'redux' |
|||
|
|||
export interface SetCollapsedAction extends Action { |
|||
type: 'MENU_SET_COLLAPSED' |
|||
payload: boolean |
|||
} |
|||
|
|||
export type MenuActions = SetCollapsedAction |
|||
|
|||
export const setCollapsed = (collapsed: boolean): SetCollapsedAction => ({ |
|||
type: 'MENU_SET_COLLAPSED', |
|||
payload: collapsed |
|||
}) |
@ -0,0 +1,24 @@ |
|||
import { Action } from 'redux' |
|||
import { ColorScheme } from '../types' |
|||
|
|||
export interface SetThemeAction extends Action { |
|||
type: 'THEME_SET_THEME' |
|||
payload: string |
|||
} |
|||
|
|||
export interface SetColorSchemeAction extends Action { |
|||
type: 'THEME_SET_COLOR_SCHEME' |
|||
payload: ColorScheme |
|||
} |
|||
|
|||
export type ThemeActions = SetThemeAction | SetColorSchemeAction |
|||
|
|||
export const setTheme = (name: string): SetThemeAction => ({ |
|||
type: 'THEME_SET_THEME', |
|||
payload: name, |
|||
}) |
|||
|
|||
export const setColorScheme = (scheme: ColorScheme): SetColorSchemeAction => ({ |
|||
type: 'THEME_SET_COLOR_SCHEME', |
|||
payload: scheme, |
|||
}) |
@ -1,44 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
import moment from 'moment' |
|||
|
|||
import { App } from 'src/types' |
|||
|
|||
interface Props { |
|||
app: App |
|||
} |
|||
|
|||
const AppInfo: FC<Props> = ({ app }) => ( |
|||
<nav className="level"> |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Users</p> |
|||
<p className="title">{app.users}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Rating</p> |
|||
<p className="title">{app.rating}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
{app.companyName && |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Company</p> |
|||
<p className="title">{app.companyName}</p> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Updated</p> |
|||
<p className="title">{moment(app.updated).format('MMMM Do, YYYY')}</p> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
) |
|||
|
|||
export default AppInfo |
@ -0,0 +1,39 @@ |
|||
import React, { FC, MouseEventHandler } from 'react' |
|||
import noop from 'lodash/noop' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
|
|||
import Spinner from 'src/components/spinner' |
|||
|
|||
export interface Props { |
|||
text: string |
|||
icon?: IconDefinition |
|||
loading?: boolean |
|||
color: string |
|||
backgroundColor: string |
|||
onClick?: MouseEventHandler |
|||
} |
|||
|
|||
const Button: FC<Props> = ({ text, icon, loading, color, backgroundColor, onClick = noop }) => { |
|||
const isLoading = loading === undefined ? false : loading |
|||
|
|||
const content = () => ( |
|||
<> |
|||
{icon && |
|||
<span className="icon"> |
|||
<FontAwesomeIcon icon={icon} /> |
|||
</span> |
|||
} |
|||
<span>{text}</span> |
|||
</> |
|||
) |
|||
|
|||
return ( |
|||
<button style={{ backgroundColor, color }} disabled={loading} onClick={onClick}> |
|||
{isLoading && <Spinner color={color} />} |
|||
{!isLoading && content()} |
|||
</button> |
|||
) |
|||
} |
|||
|
|||
export default Button |
@ -0,0 +1,11 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const FieldLabel: FC = ({ children }) => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<label style={{ color: theme.secondary }}>{children}</label> |
|||
) |
|||
} |
|||
|
|||
export default FieldLabel |
@ -0,0 +1,108 @@ |
|||
import React, { FC, ReactNode } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import zxcvbn from 'zxcvbn' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons' |
|||
import { useTheme } from 'src/hooks' |
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState, FormNotification, NotificationType } from 'src/types' |
|||
|
|||
import FieldLabel from 'src/components/controls/field-label' |
|||
import FormNotificationComponent from 'src/components/form-notification' |
|||
|
|||
export interface Props { |
|||
placeholder?: string |
|||
userInputs?: string[] |
|||
showStrength?: boolean |
|||
} |
|||
|
|||
const PasswordField: FC<Props> = ({ |
|||
placeholder, |
|||
userInputs = [], |
|||
showStrength = true, |
|||
}) => { |
|||
const theme = useTheme() |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', '')) |
|||
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, 'password')) |
|||
const dispatch = useDispatch() |
|||
|
|||
let icon: IconDefinition | undefined |
|||
let passwordMessage: ReactNode | undefined |
|||
let successState = false |
|||
let errorState = false |
|||
|
|||
if (value && showStrength) { |
|||
const { score } = zxcvbn(value, userInputs) |
|||
|
|||
switch (score) { |
|||
case 0: |
|||
errorState = true |
|||
icon = faExclamationTriangle |
|||
passwordMessage = <span>Strength: <span style={{ color: theme.red }}>Unusable</span></span> |
|||
break |
|||
case 1: |
|||
errorState = true |
|||
passwordMessage = <span>Strength: <span style={{ color: theme.red }}>Not good</span></span> |
|||
break |
|||
case 2: |
|||
passwordMessage = <span>Strength: <span>OK</span></span> |
|||
break |
|||
case 3: |
|||
successState = true |
|||
passwordMessage = <span>Strength: <span style={{ color: theme.green }}>Good!</span></span> |
|||
break |
|||
case 4: |
|||
successState = true |
|||
icon = faCheckCircle |
|||
passwordMessage = <span>Strength: <span style={{ color: theme.green }}>LIT</span></span> |
|||
break |
|||
} |
|||
} |
|||
|
|||
if (notification) { |
|||
switch (notification.type) { |
|||
case NotificationType.Success: |
|||
errorState = false |
|||
successState = true |
|||
case NotificationType.Error: |
|||
errorState = true |
|||
successState = false |
|||
} |
|||
} |
|||
|
|||
const color = successState ? theme.green : errorState ? theme.red : theme.backgroundSecondary |
|||
|
|||
const helpText = () => { |
|||
if (notification) return <FormNotificationComponent notification={notification} /> |
|||
if (passwordMessage) return <p className="help">{passwordMessage}</p> |
|||
} |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<FieldLabel>Password</FieldLabel> |
|||
<div className="control-container"> |
|||
<div className="icon"> |
|||
<FontAwesomeIcon icon={faKey} /> |
|||
</div> |
|||
<div className="control"> |
|||
<input |
|||
style={{ backgroundColor: theme.backgroundPrimary, borderColor: color, color: theme.text }} |
|||
type="password" |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={e => dispatch(setFieldValue('password', e.target.value))} /> |
|||
</div> |
|||
{icon && |
|||
<div className="icon"> |
|||
<FontAwesomeIcon icon={icon} /> |
|||
</div> |
|||
} |
|||
</div> |
|||
{helpText()} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default PasswordField |
@ -0,0 +1,28 @@ |
|||
import React, { FC, MouseEventHandler } from 'react' |
|||
import noop from 'lodash/noop' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
import Button from 'src/components/controls/button' |
|||
|
|||
export interface Props { |
|||
text: string |
|||
icon?: IconDefinition |
|||
loading?: boolean |
|||
onClick?: MouseEventHandler |
|||
} |
|||
|
|||
const PrimaryButton: FC<Props> = ({ text, icon, loading, onClick = noop }) => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<Button |
|||
text={text} |
|||
icon={icon} |
|||
loading={loading} |
|||
color={theme.primaryAlternate} |
|||
backgroundColor={theme.primary} |
|||
onClick={onClick} /> |
|||
) |
|||
} |
|||
|
|||
export default PrimaryButton |
@ -0,0 +1,28 @@ |
|||
import React, { FC, MouseEventHandler } from 'react' |
|||
import noop from 'lodash/noop' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
import Button from 'src/components/controls/button' |
|||
|
|||
export interface Props { |
|||
text: string |
|||
icon?: IconDefinition |
|||
loading?: boolean |
|||
onClick?: MouseEventHandler |
|||
} |
|||
|
|||
const PrimaryButton: FC<Props> = ({ text, icon, loading, onClick = noop }) => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<Button |
|||
text={text} |
|||
icon={icon} |
|||
loading={loading} |
|||
color={theme.backgroundSecondary} |
|||
backgroundColor={theme.secondary} |
|||
onClick={onClick} /> |
|||
) |
|||
} |
|||
|
|||
export default PrimaryButton |
@ -0,0 +1,77 @@ |
|||
import React, { FC, FocusEventHandler } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import noop from 'lodash/noop' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
import { useTheme } from 'src/hooks' |
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState, FormNotification, NotificationType } from 'src/types' |
|||
|
|||
import FieldLabel from 'src/components/controls/field-label' |
|||
import FormNotificationComponent from 'src/components/form-notification' |
|||
import HelpText from 'src/components/help-text' |
|||
|
|||
interface Props { |
|||
name: string |
|||
label: string |
|||
type?: 'text' | 'email' |
|||
placeholder?: string |
|||
help?: string |
|||
icon?: IconDefinition |
|||
onBlur?: FocusEventHandler<HTMLInputElement> |
|||
} |
|||
|
|||
const TextField: FC<Props> = ({ |
|||
name, |
|||
label, |
|||
type = 'text', |
|||
placeholder, |
|||
help, |
|||
icon, |
|||
onBlur = noop, |
|||
}) => { |
|||
const theme = useTheme() |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, '')) |
|||
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name)) |
|||
const dispatch = useDispatch() |
|||
|
|||
let color = theme.secondary |
|||
|
|||
if (notification) { |
|||
switch (notification.type) { |
|||
case NotificationType.Error: |
|||
color = theme.red |
|||
break |
|||
case NotificationType.Success: |
|||
color = theme.green |
|||
break |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<FieldLabel>{label}</FieldLabel> |
|||
<div className="control-container"> |
|||
{icon && |
|||
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}> |
|||
<FontAwesomeIcon icon={icon} /> |
|||
</div> |
|||
} |
|||
<div className="control"> |
|||
<input |
|||
style={{ backgroundColor: theme.backgroundSecondary, borderColor: color, color: theme.text }} |
|||
type={type} |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={e => dispatch(setFieldValue(name, e.target.value))} |
|||
onBlur={onBlur} /> |
|||
</div> |
|||
</div> |
|||
{(!notification && help) && <HelpText>{help}</HelpText>} |
|||
{notification && <FormNotificationComponent notification={notification} />} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default TextField |
@ -0,0 +1,61 @@ |
|||
import React, { FC, FocusEventHandler } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import noop from 'lodash/noop' |
|||
import { useTheme } from 'src/hooks' |
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { AppState, FormNotification, NotificationType } from 'src/types' |
|||
|
|||
import FieldLabel from 'src/components/controls/field-label' |
|||
import FormNotificationComponent from 'src/components/form-notification' |
|||
|
|||
export interface Props { |
|||
name: string |
|||
label: string |
|||
placeholder?: string |
|||
onBlur?: FocusEventHandler |
|||
} |
|||
|
|||
const TextField: FC<Props> = ({ |
|||
name, |
|||
label, |
|||
placeholder, |
|||
onBlur = noop, |
|||
}) => { |
|||
const theme = useTheme() |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, '')) |
|||
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name)) |
|||
const dispatch = useDispatch() |
|||
|
|||
let color = theme.secondary |
|||
|
|||
if (notification) { |
|||
switch (notification.type) { |
|||
case NotificationType.Error: |
|||
color = theme.red |
|||
break |
|||
case NotificationType.Success: |
|||
color = theme.green |
|||
break |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<FieldLabel>{label}</FieldLabel> |
|||
<div className="control-container"> |
|||
<div className="control"> |
|||
<textarea |
|||
style={{ backgroundColor: theme.backgroundSecondary, borderColor: color, color: theme.text }} |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={e => dispatch(setFieldValue(name, e.target.value))} |
|||
onBlur={onBlur} /> |
|||
</div> |
|||
</div> |
|||
{notification && <FormNotificationComponent notification={notification} />} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default TextField |
@ -0,0 +1,61 @@ |
|||
import React, { FC, useState, useEffect } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import capitalize from 'lodash/capitalize' |
|||
import { useTheme } from 'src/hooks' |
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { setTheme } from 'src/actions/theme' |
|||
import { getFieldValue } from 'src/selectors/forms' |
|||
import { getThemeName } from 'src/selectors/theme' |
|||
import { AppState, Theme, ColorScheme } from 'src/types' |
|||
|
|||
import FieldLabel from 'src/components/controls/field-label' |
|||
import themes from 'src/themes' |
|||
|
|||
export interface Props { |
|||
name: string |
|||
label: string |
|||
} |
|||
|
|||
const ThemeField: FC<Props> = ({ name, label }) => { |
|||
const currentTheme = useTheme() |
|||
const currentThemeName = useSelector<AppState, string>(getThemeName) |
|||
const [previousThemeName, setPreviousThemeName] = useState('') |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, '')) |
|||
const dispatch = useDispatch() |
|||
|
|||
const themeList = Object.entries(themes).map(([name, schemes]) => { |
|||
return [name, schemes[ColorScheme.Light]] as [string, Theme] |
|||
}) |
|||
|
|||
const handleMouseEnter = (name: string) => { |
|||
dispatch(setTheme(name)) |
|||
} |
|||
|
|||
const handleMouseLeave = () => { |
|||
dispatch(setTheme(previousThemeName)) |
|||
} |
|||
|
|||
useEffect(() => { |
|||
setPreviousThemeName(currentThemeName) |
|||
}, []) |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<FieldLabel>{label}</FieldLabel> |
|||
<div className="control"> |
|||
<div className="theme-picker"> |
|||
{themeList.map(([themeName, theme]) => ( |
|||
<div |
|||
style={{ backgroundColor: theme.primary, borderColor: themeName === value ? currentTheme.red : currentTheme.secondary }} |
|||
onMouseEnter={() => handleMouseEnter(themeName)} |
|||
onMouseLeave={() => handleMouseLeave()} |
|||
onClick={() => dispatch(setFieldValue(name, themeName))}></div> |
|||
))} |
|||
</div> |
|||
<div style={{ color: currentTheme.primary }}>{capitalize(currentThemeName)}</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default ThemeField |
@ -1,20 +1,23 @@ |
|||
import React, { FC } from 'react' |
|||
import { Link } from 'react-router-dom' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const Divider: FC = () => <> ⚬ </> |
|||
|
|||
const Footer: FC = () => ( |
|||
<footer> |
|||
<div className="content has-text-centered has-text-white is-size-7"> |
|||
<Link className="has-text-white is-inline-block" to="/">Home</Link> |
|||
const Footer: FC = () => { |
|||
const theme = useTheme() |
|||
|
|||
return ( |
|||
<footer style={{ color: theme.text }}> |
|||
<Link style={{ color: theme.primary }} to="/">Home</Link> |
|||
<Divider /> |
|||
<Link className="has-text-white is-inline-block" to="/developers">Developers</Link> |
|||
<Link style={{ color: theme.primary }} to="/developers">Developers</Link> |
|||
<Divider /> |
|||
<Link className="has-text-white is-inline-block" to="/about">About</Link> |
|||
<Link style={{ color: theme.primary }} to="/about">About</Link> |
|||
|
|||
<p>© 2019 Flexor.cc</p> |
|||
</div> |
|||
</footer> |
|||
) |
|||
</footer> |
|||
) |
|||
} |
|||
|
|||
export default Footer |
@ -0,0 +1,26 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
import { NotificationType, FormNotification } from 'src/types' |
|||
|
|||
interface Props { |
|||
notification: FormNotification |
|||
} |
|||
|
|||
const Notification: FC<Props> = ({ notification }) => { |
|||
const theme = useTheme() |
|||
let color = theme.text |
|||
|
|||
switch (notification.type) { |
|||
case NotificationType.Error: |
|||
color = theme.red |
|||
break |
|||
case NotificationType.Success: |
|||
color = theme.green |
|||
} |
|||
|
|||
return ( |
|||
<p className="help" style={{ color }}>{notification.message}</p> |
|||
) |
|||
} |
|||
|
|||
export default Notification |
@ -1,103 +0,0 @@ |
|||
import React, { FC, ReactNode } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import classNames from 'classnames' |
|||
import zxcvbn from 'zxcvbn' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
import { faKey, faExclamationTriangle, faCheckCircle } from '@fortawesome/free-solid-svg-icons' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { notificationTypeToClassName } from 'src/utils' |
|||
import { AppState, FormNotification, ClassDictionary } from 'src/types' |
|||
|
|||
export interface Props { |
|||
placeholder?: string |
|||
userInputs?: string[] |
|||
showStrength?: boolean |
|||
} |
|||
|
|||
const PasswordField: FC<Props> = ({ |
|||
placeholder, |
|||
userInputs = [], |
|||
showStrength = true, |
|||
}) => { |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, 'password', '')) |
|||
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, 'password')) |
|||
|
|||
const dispatch = useDispatch() |
|||
|
|||
const inputClassDictionary: ClassDictionary = { input: true } |
|||
const controlClassDictionary: ClassDictionary = { control: true, 'has-icons-left': true } |
|||
const helpClassDictionary: ClassDictionary = { help: true } |
|||
let icon: IconDefinition | undefined |
|||
let passwordMessage: ReactNode | undefined |
|||
|
|||
if (value && showStrength) { |
|||
const { score } = zxcvbn(value, userInputs) |
|||
|
|||
switch (score) { |
|||
case 0: |
|||
inputClassDictionary['is-danger'] = true |
|||
controlClassDictionary['has-icons-right'] = true |
|||
icon = faExclamationTriangle |
|||
passwordMessage = <span>Strength: <span className="has-text-danger">Unusable</span></span> |
|||
break |
|||
case 1: |
|||
inputClassDictionary['is-danger'] = true |
|||
passwordMessage = <span>Strength: <span className="has-text-danger">Not good</span></span> |
|||
break |
|||
case 2: |
|||
inputClassDictionary['is-warning'] = true |
|||
passwordMessage = <span>Strength: <span className="has-text-warning">OK</span></span> |
|||
break |
|||
case 3: |
|||
inputClassDictionary['is-success'] = true |
|||
passwordMessage = <span>Strength: <span className="has-text-success">Good!</span></span> |
|||
break |
|||
case 4: |
|||
inputClassDictionary['is-success'] = true |
|||
controlClassDictionary['has-icons-right'] = true |
|||
icon = faCheckCircle |
|||
passwordMessage = <span>Strength: <span className="has-text-success">LIT</span></span> |
|||
break |
|||
} |
|||
} |
|||
|
|||
if (notification) { |
|||
const ncn = notificationTypeToClassName(notification.type) |
|||
|
|||
helpClassDictionary[ncn] = true |
|||
inputClassDictionary[ncn] = true |
|||
} |
|||
|
|||
const helpText = () => { |
|||
if (notification) return <p className={classNames(helpClassDictionary)}>{notification.message}</p> |
|||
if (passwordMessage) return <p className="help">{passwordMessage}</p> |
|||
} |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<label className="label">Password</label> |
|||
<div className={classNames(controlClassDictionary)}> |
|||
<input |
|||
className={classNames(inputClassDictionary)} |
|||
type="password" |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={e => dispatch(setFieldValue('password', e.target.value))} /> |
|||
<span className="icon is-small is-left"> |
|||
<FontAwesomeIcon icon={faKey} /> |
|||
</span> |
|||
{icon && |
|||
<span className="icon is-small is-right"> |
|||
<FontAwesomeIcon icon={icon} /> |
|||
</span> |
|||
} |
|||
</div> |
|||
{helpText()} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default PasswordField |
@ -1,71 +0,0 @@ |
|||
import React, { FC, FocusEventHandler } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import classNames from 'classnames' |
|||
import noop from 'lodash/noop' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { IconDefinition } from '@fortawesome/fontawesome-common-types' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { notificationTypeToClassName } from 'src/utils' |
|||
import { AppState, FormNotification, ClassDictionary } from 'src/types' |
|||
|
|||
interface Props { |
|||
name: string |
|||
label: string |
|||
type?: 'text' | 'email' |
|||
placeholder?: string |
|||
help?: string |
|||
icon?: IconDefinition |
|||
onBlur?: FocusEventHandler<HTMLInputElement> |
|||
} |
|||
|
|||
const TextField: FC<Props> = ({ |
|||
name, |
|||
label, |
|||
type = 'text', |
|||
placeholder, |
|||
help, |
|||
icon, |
|||
onBlur = noop, |
|||
}) => { |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, '')) |
|||
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name)) |
|||
|
|||
const dispatch = useDispatch() |
|||
|
|||
const controlClassDictionary = { control: true, 'has-icons-left': !!icon } |
|||
const helpClassDictionary: ClassDictionary = { help: true } |
|||
const inputClassDictionary: ClassDictionary = { input: true } |
|||
|
|||
if (notification) { |
|||
const ncn = notificationTypeToClassName(notification.type) |
|||
|
|||
helpClassDictionary[ncn] = true |
|||
inputClassDictionary[ncn] = true |
|||
} |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<label className="label">{label}</label> |
|||
<div className={classNames(controlClassDictionary)}> |
|||
<input |
|||
className={classNames(inputClassDictionary)} |
|||
type={type} |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={e => dispatch(setFieldValue(name, e.target.value))} |
|||
onBlur={onBlur} /> |
|||
{icon && |
|||
<span className="icon is-small is-left"> |
|||
<FontAwesomeIcon icon={icon} /> |
|||
</span> |
|||
} |
|||
</div> |
|||
{(!notification && help) && <p className="help">{help}</p>} |
|||
{notification && <p className={classNames(helpClassDictionary)}>{notification.message}</p>} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default TextField |
@ -1,57 +0,0 @@ |
|||
import React, { FC, FocusEventHandler } from 'react' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import classNames from 'classnames' |
|||
import noop from 'lodash/noop' |
|||
|
|||
import { setFieldValue } from 'src/actions/forms' |
|||
import { getFieldValue, getFieldNotification } from 'src/selectors/forms' |
|||
import { notificationTypeToClassName } from 'src/utils' |
|||
import { AppState, FormNotification, ClassDictionary } from 'src/types' |
|||
|
|||
export interface Props { |
|||
name: string |
|||
label: string |
|||
placeholder?: string |
|||
onBlur?: FocusEventHandler |
|||
} |
|||
|
|||
const TextField: FC<Props> = ({ |
|||
name, |
|||
label, |
|||
placeholder, |
|||
onBlur = noop, |
|||
}) => { |
|||
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, name, '')) |
|||
const notification = useSelector<AppState, FormNotification | undefined>(state => getFieldNotification(state, name)) |
|||
|
|||
const dispatch = useDispatch() |
|||
|
|||
const helpClassDictionary: ClassDictionary = { help: true } |
|||
const inputClassDictionary: ClassDictionary = { textarea: true } |
|||
|
|||
if (notification) { |
|||
const ncn = notificationTypeToClassName(notification.type) |
|||
|
|||
helpClassDictionary[ncn] = true |
|||
inputClassDictionary[ncn] = true |
|||
} |
|||
|
|||
return ( |
|||
<div className="field"> |
|||
<label className="label">{label}</label> |
|||
<div className="control"> |
|||
<textarea |
|||
className={classNames(inputClassDictionary)} |
|||
placeholder={placeholder} |
|||
value={value} |
|||
onChange={e => dispatch(setFieldValue(name, e.target.value))} |
|||
onBlur={onBlur} /> |
|||
</div> |
|||
{notification && |
|||
<p className={classNames(helpClassDictionary)}>{notification.message}</p> |
|||
} |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default TextField |
@ -1,49 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
import moment from 'moment' |
|||
|
|||
import { Group } from 'src/types' |
|||
|
|||
interface Props { |
|||
group: Group |
|||
} |
|||
|
|||
const GroupInfo: FC<Props> = ({ group }) => ( |
|||
<nav className="level"> |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Members</p> |
|||
<p className="title">{group.members}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Posts</p> |
|||
<p className="title">{group.posts}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading has-text-success">Awards</p> |
|||
<p className="title">{group.posts}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Points</p> |
|||
<p className="title">{group.points}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Created</p> |
|||
<p className="title is-size-5">{moment(group.created).format('MMMM Do, YYYY')}</p> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
) |
|||
|
|||
export default GroupInfo |
@ -0,0 +1,11 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const Notification: FC = ({ children }) => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<p className="help" style={{ color: theme.secondary }}>{children}</p> |
|||
) |
|||
} |
|||
|
|||
export default Notification |
@ -0,0 +1,9 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const HorizontalRule: FC = () => { |
|||
const theme = useTheme() |
|||
return <hr style={{ borderColor: theme.backgroundSecondary, color: theme.backgroundSecondary }} /> |
|||
} |
|||
|
|||
export default HorizontalRule |
@ -0,0 +1,24 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
import { LevelItem } from 'src/types' |
|||
|
|||
interface Props { |
|||
items: LevelItem[] |
|||
} |
|||
|
|||
const Level: FC<Props> = ({ items }) => { |
|||
const theme = useTheme() |
|||
|
|||
return ( |
|||
<nav className="level"> |
|||
{items.map(item => ( |
|||
<div> |
|||
{item.label && <p className="label" style={{ color: theme.secondary }}>{item.label}</p>} |
|||
<p className="content" style={{ color: theme.text }}>{item.content}</p> |
|||
</div> |
|||
))} |
|||
</nav> |
|||
) |
|||
} |
|||
|
|||
export default Level |
@ -0,0 +1,16 @@ |
|||
import React, { FC } from 'react' |
|||
import { useHistory } from 'react-router-dom' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const Logo: FC = () => { |
|||
const theme = useTheme() |
|||
const history = useHistory() |
|||
|
|||
return ( |
|||
<div className="logo" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate, cursor: 'pointer' }} onClick={() => history.push('/')}> |
|||
F |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Logo |
@ -1,26 +1,66 @@ |
|||
import React, { FC } from 'react' |
|||
import { Link } from 'react-router-dom' |
|||
import { useSelector, useDispatch } from 'react-redux' |
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
|||
import { faStream, faPaperPlane } from '@fortawesome/free-solid-svg-icons' |
|||
import { faStream, faPaperPlane, faSun, faMoon } from '@fortawesome/free-solid-svg-icons' |
|||
import { useTheme } from 'src/hooks' |
|||
import { setColorScheme } from 'src/actions/theme' |
|||
import { getColorScheme } from 'src/selectors/theme' |
|||
import { AppState, ColorScheme } from 'src/types' |
|||
|
|||
const NavigationMenu: FC = () => ( |
|||
<div id="navigation"> |
|||
<div> |
|||
<span className="icon has-text-white"> |
|||
<FontAwesomeIcon icon={faStream} /> |
|||
</span> |
|||
|
|||
<Link className="has-text-white" to="/">Timeline</Link> |
|||
</div> |
|||
const NavigationMenu: FC = () => { |
|||
const theme = useTheme() |
|||
const scheme = useSelector<AppState, ColorScheme>(getColorScheme) |
|||
const dispatch = useDispatch() |
|||
|
|||
<div> |
|||
<span className="icon has-text-white"> |
|||
<FontAwesomeIcon icon={faPaperPlane} /> |
|||
</span> |
|||
|
|||
<Link className="has-text-white" to="/apps">Apps</Link> |
|||
</div> |
|||
</div> |
|||
) |
|||
const switchColorSchemeItem = () => { |
|||
switch (scheme) { |
|||
case ColorScheme.Light: |
|||
return ( |
|||
<a style={{ color: theme.primary, cursor: 'pointer' }} onClick={() => dispatch(setColorScheme(ColorScheme.Dark))}> |
|||
<span className="icon" style={{ color: theme.primary }}> |
|||
<FontAwesomeIcon icon={faMoon} /> |
|||
</span> |
|||
|
|||
<span>Dark Mode</span> |
|||
</a> |
|||
) |
|||
case ColorScheme.Dark: |
|||
return ( |
|||
<a style={{ color: theme.primary, cursor: 'pointer' }} onClick={() => dispatch(setColorScheme(ColorScheme.Light))}> |
|||
<span className="icon" style={{ color: theme.primary }}> |
|||
<FontAwesomeIcon icon={faSun} /> |
|||
</span> |
|||
|
|||
<span>Light Mode</span> |
|||
</a> |
|||
) |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<nav> |
|||
<div> |
|||
<span className="icon" style={{ color: theme.primary }}> |
|||
<FontAwesomeIcon icon={faStream} /> |
|||
</span> |
|||
|
|||
<Link style={{ color: theme.primary }} to="/">Timeline</Link> |
|||
</div> |
|||
|
|||
<div> |
|||
<span className="icon" style={{ color: theme.primary }}> |
|||
<FontAwesomeIcon icon={faPaperPlane} /> |
|||
</span> |
|||
|
|||
<Link style={{ color: theme.primary }} to="/apps">Apps</Link> |
|||
</div> |
|||
|
|||
<div> |
|||
{switchColorSchemeItem()} |
|||
</div> |
|||
</nav> |
|||
) |
|||
} |
|||
|
|||
export default NavigationMenu |
@ -1,19 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
|
|||
interface Props { |
|||
title: string |
|||
subtitle?: string |
|||
} |
|||
|
|||
const PageHeader: FC<Props> = ({ title, subtitle }) => ( |
|||
<section className="hero is-dark is-bold"> |
|||
<div className="hero-body"> |
|||
<div className="container"> |
|||
<h1 className="title">{title}</h1> |
|||
{subtitle && <h2 className="subtitle">{subtitle}</h2>} |
|||
</div> |
|||
</div> |
|||
</section> |
|||
) |
|||
|
|||
export default PageHeader |
@ -1,15 +1,16 @@ |
|||
import React, { FC } from 'react' |
|||
|
|||
import PageHeader from 'src/components/page-header' |
|||
import { useTheme } from 'src/hooks' |
|||
import Title from 'src/components/title' |
|||
import Spinner from 'src/components/spinner' |
|||
|
|||
const Loading: FC = () => ( |
|||
<div> |
|||
<PageHeader title="Loading..." /> |
|||
<div className="main-content"> |
|||
<Spinner /> |
|||
const Loading: FC = () => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<div> |
|||
<Title>Loading...</Title> |
|||
<Spinner color={theme.primary} /> |
|||
</div> |
|||
</div> |
|||
) |
|||
) |
|||
} |
|||
|
|||
export default Loading |
@ -0,0 +1,16 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const App: FC = () => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<div className="search"> |
|||
<input |
|||
style={{ backgroundColor: theme.backgroundPrimary, borderColor: theme.primary, color: theme.text }} |
|||
type="text" |
|||
placeholder="Search" /> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default App |
@ -0,0 +1,13 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const Section: FC = ({ children }) => { |
|||
const theme = useTheme() |
|||
return ( |
|||
<section style={{ backgroundColor: theme.backgroundPrimary }}> |
|||
{children} |
|||
</section> |
|||
) |
|||
} |
|||
|
|||
export default Section |
@ -1,16 +1,20 @@ |
|||
import React, { FC } from 'react' |
|||
|
|||
const Spinner: FC = () => ( |
|||
interface Props { |
|||
color: string |
|||
} |
|||
|
|||
const Spinner: FC<Props> = ({ color }) => ( |
|||
<div className="sk-cube-grid"> |
|||
<div className="sk-cube sk-cube1"></div> |
|||
<div className="sk-cube sk-cube2"></div> |
|||
<div className="sk-cube sk-cube3"></div> |
|||
<div className="sk-cube sk-cube4"></div> |
|||
<div className="sk-cube sk-cube5"></div> |
|||
<div className="sk-cube sk-cube6"></div> |
|||
<div className="sk-cube sk-cube7"></div> |
|||
<div className="sk-cube sk-cube8"></div> |
|||
<div className="sk-cube sk-cube9"></div> |
|||
<div className="sk-cube sk-cube1" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube2" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube3" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube4" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube5" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube6" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube7" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube8" style={{ backgroundColor: color }}></div> |
|||
<div className="sk-cube sk-cube9" style={{ backgroundColor: color }}></div> |
|||
</div> |
|||
) |
|||
|
|||
|
@ -0,0 +1,9 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const Subtitle: FC = ({ children }) => { |
|||
const theme = useTheme() |
|||
return <h2 className="subtitle" style={{ color: theme.secondary }}>{children}</h2> |
|||
} |
|||
|
|||
export default Subtitle |
@ -0,0 +1,9 @@ |
|||
import React, { FC } from 'react' |
|||
import { useTheme } from 'src/hooks' |
|||
|
|||
const Title: FC = ({ children }) => { |
|||
const theme = useTheme() |
|||
return <h1 style={{ color: theme.primary }}>{children}</h1> |
|||
} |
|||
|
|||
export default Title |
@ -1,42 +0,0 @@ |
|||
import React, { FC } from 'react' |
|||
import moment from 'moment' |
|||
|
|||
import { User } from 'src/types' |
|||
|
|||
interface Props { |
|||
user: User |
|||
} |
|||
|
|||
const UserInfo: FC<Props> = ({ user }) => ( |
|||
<nav className="level"> |
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Posts</p> |
|||
<p className="title">{user.posts}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading has-text-success">Awards</p> |
|||
<p className="title">{user.awards}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Points</p> |
|||
<p className="title">{user.points}</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="level-item has-text-centered"> |
|||
<div> |
|||
<p className="heading">Joined</p> |
|||
<p className="title is-size-5">{moment(user.created).format('MMMM Do, YYYY')}</p> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
) |
|||
|
|||
export default UserInfo |
@ -1,22 +0,0 @@ |
|||
import { Reducer } from 'redux' |
|||
|
|||
import { MenuActions } from '../actions/menu' |
|||
import { MenuState } from '../types' |
|||
|
|||
const initialState: MenuState = { |
|||
collapsed: false, |
|||
} |
|||
|
|||
const reducer: Reducer<MenuState, MenuActions> = (state = initialState, action) => { |
|||
switch (action.type) { |
|||
case 'MENU_SET_COLLAPSED': |
|||
return { |
|||
...state, |
|||
collapsed: action.payload, |
|||
} |
|||
default: |
|||
return state |
|||
} |
|||
} |
|||
|
|||
export default reducer |
@ -0,0 +1,30 @@ |
|||
import { Reducer } from 'redux' |
|||
|
|||
import { ThemeActions } from '../actions/theme' |
|||
import { ThemeState, ColorScheme } from '../types' |
|||
|
|||
const initialState: ThemeState = { |
|||
scheme: ColorScheme.Light, |
|||
name: 'blue', |
|||
} |
|||
|
|||
const reducer: Reducer<ThemeState, ThemeActions> = (state = initialState, action) => { |
|||
switch (action.type) { |
|||
case 'THEME_SET_THEME': { |
|||
return { |
|||
...state, |
|||
name: action.payload, |
|||
} |
|||
} |
|||
case 'THEME_SET_COLOR_SCHEME': { |
|||
return { |
|||
...state, |
|||
scheme: action.payload, |
|||
} |
|||
} |
|||
default: |
|||
return state |
|||
} |
|||
} |
|||
|
|||
export default reducer |
@ -1,3 +0,0 @@ |
|||
import { AppState } from '../types' |
|||
|
|||
export const getCollapsed = (state: AppState) => state.menu.collapsed |
@ -0,0 +1,6 @@ |
|||
import themes from 'src/themes' |
|||
import { AppState } from 'src/types' |
|||
|
|||
export const getTheme = (state: AppState) => themes[state.theme.name][state.theme.scheme] |
|||
export const getThemeName = (state: AppState) => state.theme.name |
|||
export const getColorScheme = (state: AppState) => state.theme.scheme |
@ -0,0 +1,367 @@ |
|||
@charset "utf-8"; |
|||
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap'); |
|||
|
|||
:root { |
|||
--default-border: 1px solid; |
|||
--default-font: 'Source Sans Pro', sans-serif; |
|||
--input-padding: 0.5rem; |
|||
--content-width: 600px; |
|||
--menu-width: 270px; |
|||
} |
|||
|
|||
html { |
|||
font-family: var(--default-font); |
|||
font-size: 18px; |
|||
} |
|||
|
|||
body, div, h1, h2, input, select, textarea, label, button, p.help, section, div.icon { |
|||
transition: color 1s; |
|||
} |
|||
|
|||
input, select, textarea, button, div, div.content, div.menu, section, div.icon { |
|||
transition: background-color 1s, border-color 1s; |
|||
} |
|||
|
|||
body { |
|||
font-family: var(--default-font); |
|||
margin: 0px; |
|||
padding: 0px; |
|||
} |
|||
|
|||
input, textarea, select { |
|||
border: var(--default-border); |
|||
box-sizing: border-box; |
|||
font-family: var(--default-font); |
|||
font-size: 1rem; |
|||
padding: var(--input-padding); |
|||
width: 100%; |
|||
} |
|||
|
|||
input[type="file"] { |
|||
display: none; |
|||
} |
|||
|
|||
h1 { |
|||
font-size: 2rem; |
|||
margin: 0.5rem 0; |
|||
} |
|||
|
|||
h2 { |
|||
font-size: 1.2rem; |
|||
} |
|||
|
|||
h1 + h2 { |
|||
margin-top: -0.5rem; |
|||
} |
|||
|
|||
a { |
|||
text-decoration: none; |
|||
} |
|||
|
|||
hr { |
|||
margin: 1rem 0px; |
|||
} |
|||
|
|||
main { |
|||
bottom: 0; |
|||
display: flex; |
|||
justify-content: center; |
|||
left: 0; |
|||
padding: 0px; |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
} |
|||
|
|||
section { |
|||
padding: 1rem; |
|||
} |
|||
|
|||
iframe { |
|||
border: none; |
|||
overflow: hidden; |
|||
width: 100%; |
|||
} |
|||
|
|||
button, label.file-input { |
|||
border: none; |
|||
border-radius: 8px; |
|||
cursor: pointer; |
|||
font-size: 0.8rem; |
|||
font-weight: 700; |
|||
padding: 0.5rem 1rem; |
|||
min-width: 100px; |
|||
} |
|||
|
|||
div.logo-container { |
|||
padding-top: 1rem; |
|||
width: 60px; |
|||
} |
|||
|
|||
div.logo { |
|||
--size: 40px; |
|||
--padding-top: 7px; |
|||
|
|||
border-radius: 90px; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
height: calc(var(--size) - var(--padding-top)); |
|||
margin: 10px; |
|||
padding-top: var(--padding-top); |
|||
position: fixed; |
|||
text-align: center; |
|||
width: var(--size); |
|||
} |
|||
|
|||
div.content { |
|||
border-left: var(--default-border); |
|||
border-right: var(--default-border); |
|||
width: var(--content-width); |
|||
} |
|||
|
|||
div.menu-container { |
|||
margin: 0px; |
|||
padding: 0px; |
|||
width: var(--menu-width); |
|||
} |
|||
|
|||
div.menu { |
|||
bottom: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin: 0px; |
|||
position: fixed; |
|||
top: 0; |
|||
width: var(--menu-width); |
|||
} |
|||
|
|||
div.menu > nav { |
|||
flex-grow: 1; |
|||
padding: 1rem; |
|||
} |
|||
|
|||
div.menu > nav > div { |
|||
margin-bottom: 0.9rem; |
|||
} |
|||
|
|||
.icon { |
|||
display: inline-block; |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
footer { |
|||
font-size: 0.8rem; |
|||
padding: 0.9rem; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.field { |
|||
margin: 1rem 0px; |
|||
} |
|||
|
|||
div.field div.label { |
|||
font-weight: 700; |
|||
} |
|||
|
|||
div.control-container { |
|||
display: flex; |
|||
padding: 0.5rem 0px; |
|||
} |
|||
|
|||
div.control-container > div.icon { |
|||
margin: 0px; |
|||
padding: var(--input-padding); |
|||
} |
|||
|
|||
div.control { |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
p.help { |
|||
font-size: 0.8rem; |
|||
} |
|||
|
|||
div.search { |
|||
padding: 10px; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.notification-container { |
|||
bottom: 10px; |
|||
position: fixed; |
|||
left: 10px; |
|||
width: 40%; |
|||
} |
|||
|
|||
nav.level { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
nav.level > div { |
|||
text-align: center; |
|||
} |
|||
|
|||
nav.level p.label { |
|||
font-size: 0.9rem; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
nav.level p.content { |
|||
font-size: 1.1rem; |
|||
} |
|||
|
|||
p.label + p.content { |
|||
margin-top: -10px; |
|||
} |
|||
|
|||
div.member { |
|||
border: var(--default-border); |
|||
margin-right: 10px; |
|||
min-width: 150px; |
|||
padding: 1rem; |
|||
} |
|||
|
|||
div.composer-container { |
|||
border-top: var(--default-border); |
|||
border-bottom: var(--default-border); |
|||
} |
|||
|
|||
div.composer-empty, div.composer-empty { |
|||
font-size: 0.8rem; |
|||
text-align: center; |
|||
padding: 1.5rem; |
|||
} |
|||
|
|||
div.installations { |
|||
display: flex; |
|||
padding: 0.5rem; |
|||
} |
|||
|
|||
div.installations > div { |
|||
border-right: var(--default-border); |
|||
padding: 0.5rem; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.installations > div > p { |
|||
font-size: 0.6rem; |
|||
font-weight: bold; |
|||
margin: 0px; |
|||
padding: 0px; |
|||
} |
|||
|
|||
div.user-info, div.group-info { |
|||
align-items: center; |
|||
display: flex; |
|||
} |
|||
|
|||
div.user-info div.image, div.group-info, div.image { |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
div.user-info { |
|||
display: flex; |
|||
padding: 1.5rem 1rem; |
|||
} |
|||
|
|||
div.group-info { |
|||
display: flex; |
|||
} |
|||
|
|||
div.post { |
|||
border-top: var(--default-border); |
|||
border-bottom: var(--default-border); |
|||
margin: 1rem 0px; |
|||
} |
|||
|
|||
div.post div.post-content { |
|||
padding: 1rem; |
|||
} |
|||
|
|||
div.post div.cover { |
|||
cursor: pointer; |
|||
font-weight: bold; |
|||
padding: 2rem; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.post-info { |
|||
border-top: var(--default-border); |
|||
display: flex; |
|||
font-size: 0.8rem; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
div.post-info > div { |
|||
padding: 0.8rem; |
|||
} |
|||
|
|||
div.user { |
|||
align-items: flex-start; |
|||
display: flex; |
|||
} |
|||
|
|||
div.user div.group div.image { |
|||
display: inline-block; |
|||
margin-right: 5px; |
|||
vertical-align: sub; |
|||
} |
|||
|
|||
div.theme-picker { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
margin: 0.5rem 0px; |
|||
} |
|||
|
|||
div.theme-picker > div { |
|||
--size: 25px; |
|||
|
|||
border: 2px solid; |
|||
height: var(--size); |
|||
margin-bottom: 10px; |
|||
margin-right: 10px; |
|||
width: var(--size); |
|||
} |
|||
|
|||
div.theme-picker + div { |
|||
font-size: 0.8rem; |
|||
font-weight: 700; |
|||
margin-top: -10px; |
|||
} |
|||
|
|||
div.cover-image { |
|||
width: var(--content-width); |
|||
} |
|||
|
|||
div.cover-image img { |
|||
width: var(--content-width); |
|||
} |
|||
|
|||
div.header { |
|||
display: flex; |
|||
} |
|||
|
|||
div.cover-image + div.header { |
|||
margin-top: -20px; |
|||
} |
|||
|
|||
div.header img { |
|||
width: 128px; |
|||
} |
|||
|
|||
div.app-list-item { |
|||
display: flex; |
|||
margin: 1rem 0px; |
|||
padding: 1rem; |
|||
} |
|||
|
|||
div.app-list-item p { |
|||
font-size: 0.8rem; |
|||
padding: 0.5rem; |
|||
padding-top: 0px; |
|||
} |
|||
|
|||
div.app-list-item img { |
|||
width: 64px; |
|||
} |
@ -1,207 +0,0 @@ |
|||
@charset "utf-8"; |
|||
|
|||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700&display=swap'); |
|||
|
|||
// Colors |
|||
$orange: hsl(14, 100%, 53%); |
|||
$yellow: hsl(48, 100%, 67%); |
|||
$green: hsl(141, 65%, 31%); |
|||
$turquoise: hsl(171, 100%, 41%); |
|||
$cyan: hsl(204, 86%, 53%); |
|||
$blue: hsl(217, 72%, 30%); |
|||
$purple: hsl(271, 63%, 32%); |
|||
$red: hsl(348, 71%, 42%); |
|||
$grey: hsl(0, 0%, 48%); |
|||
$grey-light: hsl(0, 0%, 71%); |
|||
$grey-lighter: hsl(0, 0%, 86%); |
|||
$white-ter: hsl(0, 0%, 96%); |
|||
$white-bis: hsl(0, 0%, 98%); |
|||
|
|||
$family-sans-serif: "Open Sans", sans-serif; |
|||
$primary: $blue; |
|||
$body-background-color: $white-ter; |
|||
$body-size: 14px; |
|||
|
|||
@import "../../node_modules/bulma/sass/utilities/_all.sass"; |
|||
@import "../../node_modules/bulma/sass/base/_all.sass"; |
|||
@import "../../node_modules/bulma/sass/form/_all.sass"; |
|||
@import "../../node_modules/bulma/sass/grid/columns.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/button.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/icon.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/notification.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/other.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/table.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/tag.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/title.sass"; |
|||
@import "../../node_modules/bulma/sass/elements/progress.sass"; |
|||
@import "../../node_modules/bulma/sass/layout/hero.sass"; |
|||
@import "../../node_modules/bulma/sass/components/level.sass"; |
|||
@import "../../node_modules/bulma/sass/components/media.sass"; |
|||
@import "../../node_modules/bulma/sass/components/tabs.sass"; |
|||
|
|||
div#main-menu { |
|||
background: linear-gradient(135deg, $primary, darken($primary, 20%)); |
|||
bottom: 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
position: fixed; |
|||
right: 0; |
|||
top: 0; |
|||
} |
|||
|
|||
div#header, div#navigation { |
|||
padding: $size-normal; |
|||
} |
|||
|
|||
div.main-content { |
|||
padding: $size-normal; |
|||
} |
|||
|
|||
div.centered-content { |
|||
background-color: $white; |
|||
border-radius: $radius; |
|||
margin: 1rem auto; |
|||
padding: 2rem; |
|||
width: 95%; |
|||
|
|||
div.centered-content-icon { |
|||
border-radius: 100px; |
|||
margin: auto; |
|||
margin-top: -20px; |
|||
text-align: center; |
|||
width: 3rem; |
|||
} |
|||
} |
|||
|
|||
div.centered-content-narrow { |
|||
@extend div.centered-content; |
|||
width: 75%; |
|||
} |
|||
|
|||
div#navigation { |
|||
flex-grow: 1; |
|||
|
|||
div { |
|||
margin: 1rem 0px; |
|||
} |
|||
} |
|||
|
|||
footer { |
|||
padding: $size-normal; |
|||
} |
|||
|
|||
div#notification-container { |
|||
bottom: 10px; |
|||
position: fixed; |
|||
left: 10px; |
|||
width: 40%; |
|||
} |
|||
|
|||
div.group-list-item, div.app-list-item { |
|||
background-color: $white; |
|||
border-radius: 15px; |
|||
margin: 10px 0px; |
|||
padding: 20px; |
|||
} |
|||
|
|||
div.member { |
|||
border: solid 1px $grey-lighter; |
|||
margin-right: 10px; |
|||
min-width: 150px; |
|||
padding: 1rem; |
|||
} |
|||
|
|||
div.invitation-options { |
|||
display: flex; |
|||
} |
|||
|
|||
div.invitation-options > div { |
|||
margin-right: 20px; |
|||
} |
|||
|
|||
article#user-info { |
|||
padding: 20px; |
|||
} |
|||
|
|||
div.composer-container { |
|||
background-color: white; |
|||
border: solid 1px $primary; |
|||
|
|||
div.composer { |
|||
color: $primary; |
|||
font-weight: bold; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.composer-empty { |
|||
padding: 3rem; |
|||
} |
|||
|
|||
div.installations { |
|||
background-color: $white-bis; |
|||
|
|||
div { |
|||
border: solid 2px $white-bis; |
|||
cursor: pointer; |
|||
margin: 10px; |
|||
padding: 5px 15px; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.selected { |
|||
border: solid 2px $green; |
|||
} |
|||
} |
|||
} |
|||
|
|||
div.user { |
|||
display: flex; |
|||
|
|||
div.avatar { |
|||
margin-top: 6px; |
|||
margin-right: 10px; |
|||
} |
|||
} |
|||
|
|||
div.post-list { |
|||
margin: 10px; |
|||
} |
|||
|
|||
div.post { |
|||
background-color: white; |
|||
margin: 10px 0; |
|||
} |
|||
|
|||
div.post p { |
|||
padding: 20px; |
|||
} |
|||
|
|||
div.post > div.cover { |
|||
background: linear-gradient(135deg, $white-ter, $grey-light); |
|||
cursor: pointer; |
|||
font-size: 1.1rem; |
|||
font-weight: bold; |
|||
padding: 30px; |
|||
text-align: center; |
|||
} |
|||
|
|||
div.post-info { |
|||
border-top: solid 1px $grey-lighter; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
padding: 0 20px; |
|||
} |
|||
|
|||
div.post-info > div { |
|||
padding: 10px; |
|||
} |
|||
|
|||
div.attachment { |
|||
padding: 10px; |
|||
|
|||
p.caption { |
|||
color: $grey; |
|||
font-size: 1rem; |
|||
margin-top: -15px; |
|||
} |
|||
} |
@ -0,0 +1,47 @@ |
|||
.sk-cube-grid { |
|||
width: 30px; |
|||
height: 30px; |
|||
margin: 10px auto; |
|||
} |
|||
|
|||
.sk-cube-grid .sk-cube { |
|||
width: 33%; |
|||
height: 33%; |
|||
float: left; |
|||
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; |
|||
} |
|||
|
|||
.sk-cube-grid .sk-cube1 { |
|||
animation-delay: 0.2s; } |
|||
.sk-cube-grid .sk-cube2 { |
|||
animation-delay: 0.3s; } |
|||
.sk-cube-grid .sk-cube3 { |
|||
animation-delay: 0.4s; } |
|||
.sk-cube-grid .sk-cube4 { |
|||
animation-delay: 0.1s; } |
|||
.sk-cube-grid .sk-cube5 { |
|||
animation-delay: 0.2s; } |
|||
.sk-cube-grid .sk-cube6 { |
|||
animation-delay: 0.3s; } |
|||
.sk-cube-grid .sk-cube7 { |
|||
animation-delay: 0s; } |
|||
.sk-cube-grid .sk-cube8 { |
|||
animation-delay: 0.1s; } |
|||
.sk-cube-grid .sk-cube9 { |
|||
animation-delay: 0.2s; } |
|||
|
|||
@-webkit-keyframes sk-cubeGridScaleDelay { |
|||
0%, 70%, 100% { |
|||
transform: scale3D(1, 1, 1); |
|||
} 35% { |
|||
transform: scale3D(0, 0, 1); |
|||
} |
|||
} |
|||
|
|||
@keyframes sk-cubeGridScaleDelay { |
|||
0%, 70%, 100% { |
|||
transform: scale3D(1, 1, 1); |
|||
} 35% { |
|||
transform: scale3D(0, 0, 1); |
|||
} |
|||
} |
@ -1,62 +0,0 @@ |
|||
.sk-cube-grid { |
|||
width: 30px; |
|||
height: 30px; |
|||
margin: 10px auto; |
|||
} |
|||
|
|||
.sk-cube-grid .sk-cube { |
|||
width: 33%; |
|||
height: 33%; |
|||
background-color: #000; |
|||
float: left; |
|||
-webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; |
|||
animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; |
|||
} |
|||
|
|||
.sk-cube-grid .sk-cube1 { |
|||
-webkit-animation-delay: 0.2s; |
|||
animation-delay: 0.2s; } |
|||
.sk-cube-grid .sk-cube2 { |
|||
-webkit-animation-delay: 0.3s; |
|||
animation-delay: 0.3s; } |
|||
.sk-cube-grid .sk-cube3 { |
|||
-webkit-animation-delay: 0.4s; |
|||
animation-delay: 0.4s; } |
|||
.sk-cube-grid .sk-cube4 { |
|||
-webkit-animation-delay: 0.1s; |
|||
animation-delay: 0.1s; } |
|||
.sk-cube-grid .sk-cube5 { |
|||
-webkit-animation-delay: 0.2s; |
|||
animation-delay: 0.2s; } |
|||
.sk-cube-grid .sk-cube6 { |
|||
-webkit-animation-delay: 0.3s; |
|||
animation-delay: 0.3s; } |
|||
.sk-cube-grid .sk-cube7 { |
|||
-webkit-animation-delay: 0s; |
|||
animation-delay: 0s; } |
|||
.sk-cube-grid .sk-cube8 { |
|||
-webkit-animation-delay: 0.1s; |
|||
animation-delay: 0.1s; } |
|||
.sk-cube-grid .sk-cube9 { |
|||
-webkit-animation-delay: 0.2s; |
|||
animation-delay: 0.2s; } |
|||
|
|||
@-webkit-keyframes sk-cubeGridScaleDelay { |
|||
0%, 70%, 100% { |
|||
-webkit-transform: scale3D(1, 1, 1); |
|||
transform: scale3D(1, 1, 1); |
|||
} 35% { |
|||
-webkit-transform: scale3D(0, 0, 1); |
|||
transform: scale3D(0, 0, 1); |
|||
} |
|||
} |
|||
|
|||
@keyframes sk-cubeGridScaleDelay { |
|||
0%, 70%, 100% { |
|||
-webkit-transform: scale3D(1, 1, 1); |
|||
transform: scale3D(1, 1, 1); |
|||
} 35% { |
|||
-webkit-transform: scale3D(0, 0, 1); |
|||
transform: scale3D(0, 0, 1); |
|||
} |
|||
} |
@ -0,0 +1,78 @@ |
|||
import { ThemeCollection, ColorScheme } from 'src/types' |
|||
|
|||
const themes: ThemeCollection = { |
|||
'blue': { |
|||
[ColorScheme.Light]: { |
|||
primary: '#3b42f4', |
|||
primaryAlternate: '#fff', |
|||
secondary: '#6165e0', |
|||
backgroundPrimary: '#fff', |
|||
backgroundSecondary: '#e6eeff', |
|||
text: '#555', |
|||
red: '#ff1a1a', |
|||
green: '#00802b', |
|||
blue: '#005ce6', |
|||
}, |
|||
[ColorScheme.Dark]: { |
|||
primary: '#e5e6fe', |
|||
primaryAlternate: '#3b42f4', |
|||
secondary: '#fff', |
|||
text: '#ddd', |
|||
backgroundPrimary: '#000', |
|||
backgroundSecondary: '#333', |
|||
red: '#ff1a1a', |
|||
green: '#00802b', |
|||
blue: '#005ce6', |
|||
}, |
|||
}, |
|||
'orange': { |
|||
[ColorScheme.Light]: { |
|||
primary: '#ff8000', |
|||
primaryAlternate: '#fff', |
|||
secondary: '#ffb84d', |
|||
backgroundPrimary: '#fff', |
|||
backgroundSecondary: '#ffe6cc', |
|||
text: '#555', |
|||
red: '#ff1a1a', |
|||
green: '#00802b', |
|||
blue: '#005ce6', |
|||
}, |
|||
[ColorScheme.Dark]: { |
|||
primary: '#ffe6cc', |
|||
primaryAlternate: '#ff8000', |
|||
secondary: '#fff', |
|||
text: '#ddd', |
|||
backgroundPrimary: '#000', |
|||
backgroundSecondary: '#333', |
|||
red: '#ff1a1a', |
|||
green: '#00802b', |
|||
blue: '#005ce6', |
|||
}, |
|||
}, |
|||
'green': { |
|||
[ColorScheme.Light]: { |
|||
primary: '#004d00', |
|||
primaryAlternate: '#fff', |
|||
secondary: '#336600', |
|||
backgroundPrimary: '#fff', |
|||
backgroundSecondary: '#e6ffe6', |
|||
text: '#555', |
|||
red: '#ff1a1a', |
|||
green: '#00802b', |
|||
blue: '#005ce6', |
|||
}, |
|||
[ColorScheme.Dark]: { |
|||
primary: '#e6ffe6', |
|||
primaryAlternate: '#39ac39', |
|||
secondary: '#fff', |
|||
text: '#ddd', |
|||
backgroundPrimary: '#000', |
|||
backgroundSecondary: '#333', |
|||
red: '#ff1a1a', |
|||
green: '#00802b', |
|||
blue: '#005ce6', |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
export default themes |
Write
Preview
Loading…
Cancel
Save
Reference in new issue