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
-
42src/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
-
127src/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 React, { FC } from 'react' |
||||
import { Link } from 'react-router-dom' |
import { Link } from 'react-router-dom' |
||||
|
import { useTheme } from 'src/hooks' |
||||
|
|
||||
const Divider: FC = () => <> ⚬ </> |
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 /> |
<Divider /> |
||||
<Link className="has-text-white is-inline-block" to="/developers">Developers</Link> |
|
||||
|
<Link style={{ color: theme.primary }} to="/developers">Developers</Link> |
||||
<Divider /> |
<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> |
<p>© 2019 Flexor.cc</p> |
||||
</div> |
|
||||
</footer> |
|
||||
) |
|
||||
|
</footer> |
||||
|
) |
||||
|
} |
||||
|
|
||||
export default 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 React, { FC } from 'react' |
||||
import { Link } from 'react-router-dom' |
import { Link } from 'react-router-dom' |
||||
|
import { useSelector, useDispatch } from 'react-redux' |
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
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 |
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 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' |
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> |
||||
</div> |
|
||||
) |
|
||||
|
) |
||||
|
} |
||||
|
|
||||
export default Loading |
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' |
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-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> |
</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