Dwayne Harris 4 years ago
parent
commit
81b2c4d01e
  1. 3
      .vscode/settings.json
  2. 22
      package-lock.json
  3. 1
      package.json
  4. 31
      src/app/components/avatar-editor.tsx
  5. 2
      src/app/components/controls/button.tsx
  6. 11
      src/app/components/controls/cover-image-field.tsx
  7. 114
      src/app/components/controls/file-field.tsx
  8. 11
      src/app/components/controls/icon-image-field.tsx
  9. 11
      src/app/components/controls/image-field.tsx
  10. 2
      src/app/components/controls/password-field.tsx
  11. 2
      src/app/components/controls/select-field.tsx
  12. 2
      src/app/components/controls/static-field.tsx
  13. 2
      src/app/components/controls/text-field.tsx
  14. 8
      src/app/components/help-text.tsx
  15. 2
      src/app/components/logo.tsx
  16. 2
      src/app/components/pages/admin-apps.tsx
  17. 2
      src/app/components/pages/admin-groups.tsx
  18. 10
      src/app/components/pages/view-user.tsx
  19. 22
      src/app/components/slider.tsx
  20. 10
      src/app/components/user-apps.tsx
  21. 190
      src/app/styles/app.css
  22. 2
      src/app/utils/index.ts
  23. 4
      src/server/server.ts

3
.vscode/settings.json

@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

22
package-lock.json

@ -1451,6 +1451,11 @@
}
}
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
@ -5370,6 +5375,14 @@
"prop-types": "^15.6.2"
}
},
"react-avatar-editor": {
"version": "12.0.0-beta.0",
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-12.0.0-beta.0.tgz",
"integrity": "sha512-7vrkqjmXDCZuBRpRsrldeN0/BAW1/rx/k+5WE1AhvQMMGXYGwy1GY3YF97okzgBYwZV3OocGyx8oauOcTz2xAw==",
"requires": {
"react-draggable": "^4.1.0"
}
},
"react-dom": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
@ -5381,6 +5394,15 @@
"scheduler": "^0.18.0"
}
},
"react-draggable": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.2.0.tgz",
"integrity": "sha512-5wFq//gEoeTYprnd4ze8GrFc+Rbnx+9RkOMR3vk4EbWxj02U6L6T3yrlKeiw4X5CtjD2ma2+b3WujghcXNRzkw==",
"requires": {
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
}
},
"react-is": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz",

1
package.json

@ -56,6 +56,7 @@
"lodash": "^4.17.15",
"moment": "^2.24.0",
"react": "^16.12.0",
"react-avatar-editor": "^12.0.0-beta.0",
"react-dom": "^16.12.0",
"react-redux": "^7.1.3",
"react-router-dom": "^5.1.2",

31
src/app/components/avatar-editor.tsx

@ -0,0 +1,31 @@
import React, { FC, useRef, useState } from 'react'
import Editor from 'react-avatar-editor'
import Slider from '../components/slider'
interface Props {
file: File
width: number
height: number
}
const AvatarEditor: FC<Props> = ({ file, width, height }) => {
const ref = useRef<Editor>(null)
const [border, setBorder] = useState(50)
const [scale, setScale] = useState(1.2)
return (
<div>
<Editor
ref={ref}
image={file}
width={width}
height={height}
border={border}
scale={scale} />
<Slider value={border} onChange={setBorder} />
<Slider value={scale} onChange={setScale} />
</div>
)
}
export default AvatarEditor

2
src/app/components/controls/button.tsx

@ -19,7 +19,7 @@ const Button: FC<Props> = ({ text, icon, loading, color, backgroundColor, onClic
const content = () => (
<>
{icon &&
<span className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={icon} />
</span>
}

11
src/app/components/controls/cover-image-field.tsx

@ -9,7 +9,16 @@ interface Props {
}
const CoverImageField: FC<Props> = ({ name, label = 'Cover Image', help = 'Approx 400 x 200. Max 5 MBs.' }) => {
return <FileField name={name} label={label} help={help} previewWidth={200} maxSize={MEDIA_COVER_MAX_SIZE} />
return (
<FileField
name={name}
label={label}
width={400}
height={200}
help={help}
previewWidth={200}
maxSize={MEDIA_COVER_MAX_SIZE} />
)
}
export default CoverImageField

114
src/app/components/controls/file-field.tsx

@ -1,4 +1,4 @@
import React, { FC, ChangeEvent, useState } from 'react'
import React, { FC, ChangeEvent, useState, useRef } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUpload } from '@fortawesome/free-solid-svg-icons'
@ -14,10 +14,13 @@ import { AppState, AppThunkDispatch, SasResponse, NotificationType } from '../..
import Progress from '../../components/progress'
import FieldLabel from '../../components/controls/field-label'
import AvatarEditor from '../../components/avatar-editor'
interface Props {
name: string
label: string
width: number
height: number
help?: string
previewWidth?: number
maxSize?: number
@ -28,56 +31,69 @@ const FileField: FC<Props> = props => {
const value = useSelector<AppState, string>(state => getFieldValue<string>(state, props.name, ''))
const dispatch = useDispatch<AppThunkDispatch>()
const { name, label, help, previewWidth = 128, maxSize = MEDIA_DEFAULT_MAX_SIZE } = props
const [progress, setProgress] = useState(0)
const [file, setFile] = useState<File | undefined>()
const [uploading, setUploading] = useState(false)
const [uploaded, setUploaded] = useState(false)
const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const file = event.target.files[0]
const {
name,
label,
width,
height,
help,
previewWidth = 128,
maxSize = MEDIA_DEFAULT_MAX_SIZE,
} = props
if (file.size > maxSize) {
const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files[0]) {
if (event.target.files[0].size > maxSize) {
const maxSizeString = Math.round(maxSize / 1024 / 1024)
dispatch(showNotification(NotificationType.Error, `Files must be less than ${maxSizeString} MBs`))
return
}
const ext = file.name.substring(file.name.lastIndexOf('.'))
const { sas, blobUrl, id } = await apiFetch<SasResponse>({ path: '/v1/sas' })
const filename = `${id}${ext}`
setUploading(true)
try {
const blockBlobClient = new BlockBlobClient(`${blobUrl}/${filename}?${sas}`, new AnonymousCredential())
await blockBlobClient.uploadBrowserData(file, {
onProgress: p => {
setProgress((p.loadedBytes / file.size) * 100)
}
})
await apiFetch({
path: '/v1/media',
method: 'post',
body: {
name: filename,
size: file.size,
type: file.type,
originalName: file.name,
}
})
dispatch(setFieldValue(name, `${blobUrl}/${filename}`))
setUploaded(true)
} catch (err) {
console.error(err)
dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
}
setFile(event.target.files[0])
}
}
const handleUpload = async () => {
if (!file) return
setUploading(true)
const ext = file.name.substring(file.name.lastIndexOf('.'))
const { sas, blobUrl, id } = await apiFetch<SasResponse>({ path: '/v1/sas' })
const filename = `${id}${ext}`
const url = `${blobUrl}/${filename}`
try {
const blockBlobClient = new BlockBlobClient(`${url}?${sas}`, new AnonymousCredential())
await blockBlobClient.uploadBrowserData(file, {
onProgress: p => {
setProgress((p.loadedBytes / file.size) * 100)
}
})
await apiFetch({
path: '/v1/media',
method: 'post',
body: {
name: filename,
size: file.size,
type: file.type,
originalName: file.name,
}
})
setUploading(false)
dispatch(setFieldValue(name, url))
setUploaded(true)
} catch (err) {
console.error(err)
dispatch(showNotification(NotificationType.Error, `Upload error: ${err}`))
}
setUploading(false)
}
const handleDelete = async () => {
@ -91,6 +107,18 @@ const FileField: FC<Props> = props => {
dispatch(setFieldValue(name, ''))
}
if (file) {
return (
<div className="field">
<FieldLabel>{label} (<span style={{ color: theme.red }}>Unsaved</span>)</FieldLabel>
<AvatarEditor
file={file}
width={width}
height={height} />
</div>
)
}
if (uploading) {
return (
<div className="field">
@ -107,9 +135,7 @@ const FileField: FC<Props> = props => {
<div style={{ padding: '1rem 0px' }}>
<img src={value} style={{ width: previewWidth }} />
<div style={{ color: theme.secondary, fontSize: '0.8rem' }}>
{value.split('/').pop()}
&nbsp;&nbsp;
(<a style={{ color: theme.red }} onClick={() => handleDelete()}>Delete</a>)
<a style={{ color: theme.red }} onClick={() => handleDelete()}>Delete</a>
</div>
</div>
}
@ -117,9 +143,9 @@ const FileField: FC<Props> = props => {
<div>
<label className="file-input" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<input type="file" name={name} onChange={handleChange} />
<div className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={faUpload} />
</div>
</span>
<span>Choose a file...</span>
</label>
<p className="help" style={{ color: theme.text }}>{help}</p>

11
src/app/components/controls/icon-image-field.tsx

@ -9,7 +9,16 @@ interface Props {
}
const IconImageField: FC<Props> = ({ name, label = 'Icon Image', help = 'Approx 32 x 32. Max 1 MB.' }) => {
return <FileField name={name} label={label} help={help} previewWidth={32} maxSize={MEDIA_ICON_MAX_SIZE} />
return (
<FileField
name={name}
label={label}
width={32}
height={32}
help={help}
previewWidth={32}
maxSize={MEDIA_ICON_MAX_SIZE} />
)
}
export default IconImageField

11
src/app/components/controls/image-field.tsx

@ -9,7 +9,16 @@ interface Props {
}
const ImageField: FC<Props> = ({ name, label = 'Image', help = 'Approx 128 x 128. Max 5 MBs.' }) => {
return <FileField name={name} label={label} help={help} previewWidth={64} maxSize={MEDIA_DEFAULT_MAX_SIZE} />
return (
<FileField
name={name}
label={label}
width={128}
height={128}
help={help}
previewWidth={64}
maxSize={MEDIA_DEFAULT_MAX_SIZE} />
)
}
export default ImageField

2
src/app/components/controls/password-field.tsx

@ -79,7 +79,7 @@ const PasswordField: FC<Props> = ({
<div className="field">
<FieldLabel>Password</FieldLabel>
<div className="control-container">
<div className="icon" style={{ backgroundColor: color, color: theme.primaryAlternate }}>
<div className="control-icon" style={{ backgroundColor: color, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={faKey} />
</div>
<div className="control">

2
src/app/components/controls/select-field.tsx

@ -39,7 +39,7 @@ const SelectField: FC<Props> = ({
<FieldLabel>{label}</FieldLabel>
<div className="control-container">
{icon &&
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<div className="control-icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={icon} />
</div>
}

2
src/app/components/controls/static-field.tsx

@ -18,7 +18,7 @@ const StaticField: FC<Props> = ({ label, value, icon }) => {
<FieldLabel>{label}</FieldLabel>
<div className="control-container">
{icon &&
<div className="icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<div className="control-icon" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={icon} />
</div>
}

2
src/app/components/controls/text-field.tsx

@ -54,7 +54,7 @@ const TextField: FC<Props> = ({
<FieldLabel>{label}</FieldLabel>
<div className="control-container">
{icon &&
<div className="icon" style={{ backgroundColor: color, color: theme.primaryAlternate }}>
<div className="control-icon" style={{ backgroundColor: color, color: theme.primaryAlternate }}>
<FontAwesomeIcon icon={icon} />
</div>
}

8
src/app/components/help-text.tsx

@ -1,11 +1,9 @@
import React, { FC } from 'react'
import { useTheme } from '../hooks'
const Notification: FC = ({ children }) => {
const HelpText: FC = ({ children }) => {
const theme = useTheme()
return (
<p className="help" style={{ color: theme.secondary }}>{children}</p>
)
return <p className="help" style={{ color: theme.secondary }}>{children}</p>
}
export default Notification
export default HelpText

2
src/app/components/logo.tsx

@ -7,7 +7,7 @@ const Logo: FC = () => {
const history = useHistory()
return (
<div className="logo" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate, cursor: 'pointer' }} onClick={() => history.push('/')}>
<div className="logo" style={{ backgroundColor: theme.primary, color: theme.primaryAlternate }} onClick={() => history.push('/')}>
F
</div>
)

2
src/app/components/pages/admin-apps.tsx

@ -45,7 +45,7 @@ const AdminApps: FC = () => {
}
useEffect(() => {
setTitle('Admin \\ Apps')
setTitle('Admin / Apps')
const init = async () => {
try {

2
src/app/components/pages/admin-groups.tsx

@ -35,7 +35,7 @@ const AdminGroups: FC = () => {
}
useEffect(() => {
setTitle('Admin \\ Groups')
setTitle('Admin / Groups')
const init = async () => {
try {

10
src/app/components/pages/view-user.tsx

@ -120,7 +120,7 @@ const ViewUser: FC = () => {
<div className="buttons">
{subscribed &&
<button style={{ backgroundColor: theme.red, color: 'white' }} onClick={() => dispatch(unsubscribe(user.id))}>
<span className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={faUserMinus} />
</span>
<span>Unsusbcribe</span>
@ -129,7 +129,7 @@ const ViewUser: FC = () => {
{subscriptionPending &&
<button style={{ backgroundColor: theme.blue, color: 'white' }}>
<span className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={faUserClock} />
</span>
<span>Pending</span>
@ -138,7 +138,7 @@ const ViewUser: FC = () => {
{self && !isSelf && !subscribed && !subscriptionPending &&
<button style={{ backgroundColor: theme.green, color: 'white' }} onClick={() => dispatch(subscribe(user.id))}>
<span className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Subscribe</span>
@ -147,7 +147,7 @@ const ViewUser: FC = () => {
{!isSelf &&
<button style={{ backgroundColor: theme.red, color: 'white' }}>
<span className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block</span>
@ -156,7 +156,7 @@ const ViewUser: FC = () => {
{user.group && !isGroup &&
<button style={{ backgroundColor: theme.red, color: 'white' }}>
<span className="icon">
<span className="button-icon">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block Community: {user.group.name}</span>

22
src/app/components/slider.tsx

@ -0,0 +1,22 @@
import React, { FC } from 'react'
import { useTheme } from '../hooks'
interface Props {
value: number
onChange: (value: number) => void
}
const Slider: FC<Props> = ({ value, onChange }) => {
const theme = useTheme()
return (
<input
type="range"
min="0"
max="100"
style={{ backgroundColor: '#ccc' }}
value={value}
onChange={e => onChange(parseInt(e.target.value, 10))} />
)
}
export default Slider

10
src/app/components/user-apps.tsx

@ -1,23 +1,25 @@
import React, { FC, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'
import { setTitle } from '../utils'
const UserApps: FC = () => {
const history = useHistory()
useEffect(() => {
setTitle('Your Apps')
}, [])
return (
<div>
<Link className="button is-primary" to="/apps/new">
<span className="icon is-small">
<button onClick={() => history.push('/apps/new')}>
<span className="button-icon">
<FontAwesomeIcon icon={faPlusCircle} />
</span>
<span>Create a new App</span>
</Link>
</button>
</div>
)
}

190
src/app/styles/app.css

@ -2,11 +2,21 @@
@import "../../../node_modules/normalize.css/normalize.css";
:root {
--default-border: 1px solid;
--default-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
--input-padding: 0.5rem 0.75rem;
--content-width: 600px;
--menu-width: 270px;
--transition-duration: 1s;
--color-primary: #000;
--color-alternate: #ddd;
--color-secondary: #333;
--color-background-primary: #fff;
--color-background-secondary: #eee;
--color-text: #555;
--color-red: #ff1a1a;
--color-green: #00802b;
--color-blue: #005ce6;
}
html {
@ -15,49 +25,28 @@ html {
font-weight: 300;
}
body,
div,
h1,
h2,
input,
textarea,
select,
label,
button,
section,
p.help,
div.icon {
transition: color 1s;
}
div,
input,
textarea,
select,
button,
section,
div.content,
div.menu,
div.icon {
transition: background-color 1s, border-color 1s;
}
body {
background-color: var(--color-background-primary);
font-family: var(--default-font);
line-height: 1.2;
margin: 0px;
padding: 0px;
transition: background-color var(--transition-duration);
}
input, textarea, select {
border: var(--default-border);
background-color: var(--color-background-secondary);
border: solid 1px var(--color-primary);
border-radius: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: var(--color-text);
font-family: var(--default-font);
font-size: 0.9rem;
margin: 0px;
outline: none;
padding: var(--input-padding);
transition: background-color var(--transition-duration), border var(--transition-duration);
width: 100%;
}
@ -77,13 +66,47 @@ input[type="checkbox"] {
width: initial;
}
input[type="range"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: var(--color-background-secondary);
border-radius: 5px;
display: block;
height: 10px;
outline: none;
width: 8rem;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: var(--color-secondary);
border-radius: 50%;
cursor: pointer;
height: 20px;
width: 20px;
}
input[type="range"]::-moz-range-thumb {
border-radius: 50%;
cursor: pointer;
height: 20px;
width: 20px;
}
h1 {
color: var(--color-primary);
font-size: 2rem;
margin: 0.5rem 0;
transition: color var(--transition-duration);
}
h2 {
color: var(--color-primary);
font-size: 1.2rem;
transition: color var(--transition-duration);
}
h1 + h2 {
@ -94,12 +117,31 @@ a {
text-decoration: none;
}
a:link {
color: var(--color-primary);
transition: color var(--transition-duration);
}
a:hover {
color: var(--color-secondary);
transition: color var(--transition-duration);
}
a:visited {
color: var(--color-secondary);
transition: color var(--transition-duration);
}
hr {
border: 1px solid;
border: 1px solid var(--color-primary);
color: var(--color-primary);
margin: 1rem 0px;
transition: border var(--transition-duration), color var(--transition-duration);
}
main {
background-color: var(--color-background-primary);
color: var(--color-text);
bottom: 0;
display: flex;
justify-content: center;
@ -108,10 +150,14 @@ main {
position: absolute;
right: 0;
top: 0;
transition: background-color var(--transition-duration), color var(--transition-duration);
}
section {
background-color: var(--color-background-primary);
color: var(--color-text);
padding: 1rem;
transition: background-color var(--transition-duration), color var(--transition-duration);
}
iframe {
@ -129,6 +175,17 @@ button, label.file-input {
font-weight: 700;
padding: 0.5rem 1rem;
min-width: 100px;
transition: background-color var(--transition-duration), color var(--transition-duration);
}
button.primary {
background-color: var(--color-primary);
color: var(--color-alternate);
}
button.secondary {
background-color: var(--color-secondary);
color: var(--color-secondary);
}
div.buttons {
@ -145,7 +202,10 @@ div.logo {
--size: 40px;
--padding-top: 8px;
background-color: var(--color-primary);
border-radius: 90px;
color: var(--color-alternate);
cursor: pointer;
font-size: 20px;
font-weight: bold;
height: calc(var(--size) - var(--padding-top));
@ -153,17 +213,21 @@ div.logo {
padding-top: var(--padding-top);
position: fixed;
text-align: center;
transition: background-color var(--transition-duration), color var(--transition-duration);
width: var(--size);
}
div.content-container {
background-color: var(--color-background-primary);
width: var(--content-width);
transition: background-color var(--transition-duration);
}
div.content {
border-left: var(--default-border);
border-right: var(--default-border);
border-left: solid 1px var(--color-background-secondary);
border-right: solid 1px var(--color-background-primary);
padding-bottom: 3rem;
transition: border-left var(--transition-duration), border-right var(--transition-duration);
}
div.menu-container {
@ -173,12 +237,15 @@ div.menu-container {
}
div.menu {
background-color: var(--color-background-secondary);
border-color: var(--color-background-primary);
bottom: 0;
display: flex;
flex-direction: column;
margin: 0px;
position: fixed;
top: 0;
transition: background-color var(--transition-duration), border-color var(--transition-duration);
width: var(--menu-width);
}
@ -192,8 +259,10 @@ div.menu > nav > div {
}
div.spinner {
color: var(--color-primary);
padding: 1rem;
text-align: center;
transition: color var(--transition-duration);
}
.icon {
@ -202,9 +271,11 @@ div.spinner {
}
footer {
color: var(--color-text);
font-size: 0.8rem;
padding: 0.9rem;
text-align: center;
transition: color var(--transition-duration);
}
table {
@ -212,8 +283,16 @@ table {
width: 100%;
}
table tr {
transition: background-color var(--transition-duration);
}
table tr:nth-child(even) {
background-color: transparent !important;
background-color: transparent;
}
table tr:nth-child(odd) {
background-color: var(--color-background-secondary);
}
table td {
@ -228,14 +307,18 @@ span.tag {
}
div.tabs {
background-color: var(--color-background-secondary);
border-radius: 20px;
display: flex;
font-size: 1rem;
justify-content: space-around;
transition: background-color var(--transition-duration);
}
div.tabs > div {
border-color: var(--color-primary);
padding: 0.5rem;
transition: border-color var(--transition-duration);
}
div.tabs > div.active {
@ -243,16 +326,20 @@ div.tabs > div.active {
}
div.progress {
border: 1px solid;
background-color: var(--color-background-secondary);
border: 1px solid var(--color-primary);
height: 1rem;
margin: 1rem;
max-height: 100px;
min-height: 10px;
padding: 0px;
transition: background-color var(--transition-duration), border var(--transition-duration);
}
div.progress > div {
background-color: var(--color-secondary);
height: 100%;
transition: background-color var(--transition-duration);
}
div.field {
@ -260,6 +347,7 @@ div.field {
}
div.field label {
color: var(--color-secondary);
display: block;
font-weight: 700;
margin-bottom: 0.5rem;
@ -274,12 +362,15 @@ div.control-container {
padding: 0.5rem 0px;
}
div.control-container > div.icon {
div.control-icon {
background-color: var(--color-primary);
color: var(--color-alternate);
margin: 0px;
padding: var(--input-padding);
transition: background-color var(--transition-duration), color var(--transition-duration);
}
div.icon > svg {
div.control-icon > svg {
vertical-align: middle;
}
@ -288,8 +379,10 @@ div.control {
}
p.help {
color: var(--color-primary);
font-size: 0.8rem;
margin-top: -0.25rem;
transition: color var(--transition-duration);
}
div.search {
@ -333,12 +426,16 @@ nav.level > div {
}
nav.level p.label {
color: var(--color-secondary);
font-size: 0.9rem;
font-weight: bold;
transition: color var(--transition-duration);
}
nav.level p.content {
color: var(--color-text);
font-size: 1.1rem;
transition: color var(--transition-duration);
}
p.label + p.content {
@ -352,25 +449,40 @@ div.member {
}
div.composer-container {
border-top: var(--default-border);
border-bottom: var(--default-border);
border-top: solid 1px var(--color-background-secondary);
border-bottom: solid 1px var(--color-background-secondary);
transition: border-top var(--transition-duration), border-bottom var(--transition-duration);
}
div.composer-empty, div.composer-error {
font-size: 0.8rem;
text-align: center;
padding: 1.5rem;
transition: background-color var(--transition-duration), color var(--transition-duration);
}
div.composer-empty {
background-color: var(--color-background-secondary);
color: var(--color-secondary);
}
div.composer-error {
background-color: var(--color-background-secondary);
color: var(--color-red);
}
div.installations {
background-color: var(--color-background-secondary);
display: flex;
padding: 0.5rem;
transition: background-color var(--transition-duration);
}
div.installations > div {
border-right: var(--default-border);
border-right: solid 1px var(--color-background-primary);
padding: 0.5rem;
text-align: center;
transition: border-right var(--transition-duration);
}
div.installations > div > p {

2
src/app/utils/index.ts

@ -5,7 +5,7 @@ export const objectToQuerystring = (obj: object) => Object.entries(obj).filter((
export function setTitle(title: string, decorate: boolean = true) {
if (decorate) {
document.title = `${title} \\ Flexor`
document.title = `${title} / Flexor`
} else {
document.title = title
}

4
src/server/server.ts

@ -29,11 +29,9 @@ server.get('/*', {}, (_, reply) => {
const port = parseInt(process.env.PORT!, 10)
server.listen(port, (err, address) => {
server.listen(port, err => {
if (err) {
server.log.error(err)
process.exit(1)
}
server.log.info(`✊🏾 Flexor Web listening at ${address}`)
})
Loading…
Cancel
Save