diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2020-06-03 19:27:29 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2020-06-03 19:27:29 +0200 |
| commit | e52dcc61ed433980d760a050ff14852a05676b96 (patch) | |
| tree | 8af65e8261971bfba5130aae20df557b7bb1c54e | |
| parent | 3e9de575501fd1945b8341f7a4e3a89b73e3cb03 (diff) | |
image/text form
| -rw-r--r-- | frontend/common/form.component.js | 22 | ||||
| -rw-r--r-- | frontend/common/form.css | 32 | ||||
| -rw-r--r-- | frontend/common/index.js | 2 | ||||
| -rw-r--r-- | frontend/views/graph/graph.css | 62 | ||||
| -rw-r--r-- | frontend/views/page/components/tile.form.js | 255 | ||||
| -rw-r--r-- | frontend/views/page/components/tile.new.js | 2 | ||||
| -rw-r--r-- | frontend/views/page/page.container.js | 8 | ||||
| -rw-r--r-- | static/img/check.svg | 4 |
8 files changed, 316 insertions, 71 deletions
diff --git a/frontend/common/form.component.js b/frontend/common/form.component.js index 36369b5..4f52a5e 100644 --- a/frontend/common/form.component.js +++ b/frontend/common/form.component.js @@ -3,13 +3,14 @@ import { courtesyS } from '../util' export const TextInput = props => ( <label className={props.error ? 'error' : 'text'}> - <span>{props.title}</span> + {props.title && <span>{props.title}</span>} <input type="text" required={props.required} onChange={props.onChange} name={props.name} value={props.data[props.name]} + placeholder={props.placeholder} autoComplete={props.autoComplete} /> </label> @@ -40,7 +41,7 @@ export const NumberInput = props => ( export const TextArea = props => ( <label className={props.error ? 'textarea error' : 'textarea'}> - <span>{props.title}</span> + {props.title && <span>{props.title}</span>} <textarea onChange={props.onChange} name={props.name} @@ -50,7 +51,7 @@ export const TextArea = props => ( ) export const Checkbox = props => ( - <label> + <label className="checkbox"> <input type="checkbox" name={props.name} @@ -62,6 +63,21 @@ export const Checkbox = props => ( </label> ) +export const Radio = props => { + return ( + <label className="radio"> + <input + type="radio" + name={props.name} + value={props.value} + checked={props.value === props.currentValue} + onChange={() => props.onChange(props.name, props.value)} + /> + <span>{props.label}</span> + </label> + ) +} + export class Select extends Component { state = { focused: false, diff --git a/frontend/common/form.css b/frontend/common/form.css index 8643065..54ac2d9 100644 --- a/frontend/common/form.css +++ b/frontend/common/form.css @@ -78,7 +78,7 @@ input[type=password] { input[type=text]:focus, input[type=number]:focus, input[type=password]:focus { - border: 1px solid #8ff; + border: 1px solid #84f; background: #000; } @@ -94,7 +94,7 @@ textarea { border-radius: 0.125rem; } textarea:focus { - border: 1px solid #8ff; + border: 1px solid #84f; background: #000; } @@ -118,13 +118,13 @@ input[type=checkbox]:hover + span { color: #000; } input[type=checkbox]:focus + span { - color: #11f; + color: #84f; } input[type="checkbox"]:checked + span { color: #000; } input[type="checkbox"]:focus:checked + span { - color: #11f; + color: #84f; } input[type="checkbox"]:after { @@ -143,11 +143,11 @@ input[type="checkbox"]:after { border-radius: 0.125rem; } input[type=checkbox]:focus:after { - border-color: #11f; + border-color: #84f; } input[type="checkbox"]:checked:after { - border-color: #11f; - background-color: #11f; + border-color: #84f; + background-color: #84f; background-image: url(/static/img/check.svg); background-size: cover; } @@ -158,7 +158,7 @@ input[type="checkbox"]:checked:after { position: relative; width: 9rem; min-width: auto; - background: #fff; + background: #111; border-radius: 0.125rem; border: 1px solid #ddd; padding: 0.5rem; @@ -187,14 +187,14 @@ input[type="checkbox"]:checked:after { border-top: 0.375rem solid #ddd; } .select.focus { - border-color: #fff; - background: #f4f4ff; + border-color: #84f; + background: #000; } .select.focus:after { - border-top-color: #fff; + border-top-color: #84f; } .select:hover { - background-color: #f4f4ff; + background-color: #000; } .select div { width: calc(100% - 1.025rem); @@ -244,7 +244,7 @@ button.process:after { border-left: 0.375rem solid #888; } button.process:focus:after { - border-left-color: #11f; + border-left-color: #84f; } button:focus { background: #000; @@ -299,14 +299,14 @@ input[type=file] { .copyButton { border-color: transparent; - color: #11f; + color: #84f; font-size: 0.675rem; padding: 0.25rem; margin-left: 0.25rem; } .desktop .copyButton:hover { - border-color: #11f; + border-color: #84f; } .copyButton.copied { - color: #11f; + color: #84f; }
\ No newline at end of file diff --git a/frontend/common/index.js b/frontend/common/index.js index 9a11eba..d33442a 100644 --- a/frontend/common/index.js +++ b/frontend/common/index.js @@ -3,7 +3,7 @@ export { MenuButton, SmallMenuButton, MenuRoute, } from './menubutton.component' export { - Select, Checkbox, FileInput, FileInputField, + Select, Checkbox, Radio, FileInput, FileInputField, TextInput, NumberInput, TextArea, SubmitButton, LabelDescription, } from './form.component' diff --git a/frontend/views/graph/graph.css b/frontend/views/graph/graph.css index 73ac103..6facb9b 100644 --- a/frontend/views/graph/graph.css +++ b/frontend/views/graph/graph.css @@ -15,11 +15,14 @@ /* Sidebar boxes */ -.box { - width: 15rem; +.sidebar { position: absolute; top: 1rem; right: 1rem; + z-index: 2000; +} +.box { + width: 15rem; padding: 0.5rem; background: rgba(64,12,64,0.9); border: 2px solid #000; @@ -36,6 +39,30 @@ align-items: flex-start; margin-bottom: 0.25rem; } +.box form label.checkbox { + flex-direction: row; + justify-content: flex-start; + align-items: center; +} +.box form input[type="checkbox"] { + margin-left: 0rem; +} +.box form input[type=checkbox] + span { + color: #ddd; +} +.box form input[type=checkbox]:hover + span { + color: #fff; +} +.box form input[type="checkbox"]:after { + border-color: #84f; +} +.box form input[type="checkbox"]:checked:after { + border-color: #84f; + background-color: #84f; +} +.box form input[type=checkbox]:hover + span { + color: #84f; +} .box input[type=text], .box input[type=number], .box input[type=password] { @@ -48,10 +75,37 @@ height: 5rem; border-color: #888; } -.box .text.description span:first-child { - display: none; +.box .select { + padding: 0.25rem; + margin-right: 0; +} +.box .selects label { + flex-direction: row; + width: 6.5rem; + margin-right: 0.5rem; + min-width: auto; +} +.box form textarea { + padding: 0.25rem; } +.box form .pair label span { + min-width: 3rem; + padding: 0.25rem 0; +} +.box .pair label { + flex-direction: row; + width: 6rem; + margin-right: 1rem; + min-width: auto; +} +.box .pair input[type=text] { + width: 3rem; +} +.box .position { + font-size: smaller; + margin-bottom: 0.25rem; +} /* Graph handles */ .handle { diff --git a/frontend/views/page/components/tile.form.js b/frontend/views/page/components/tile.form.js index 6b9d0a7..9bd8f14 100644 --- a/frontend/views/page/components/tile.form.js +++ b/frontend/views/page/components/tile.form.js @@ -3,38 +3,79 @@ import { Link } from 'react-router-dom' import { session } from '../../../session' -import { TextInput, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' +import { TextInput, NumberInput, Select, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' -const newTile = (data) => ({ - path: '', - title: '', - username: session('username'), - description: '', +const SELECT_TYPES = [ + "image", "text" +].map(s => ({ name: s, label: s })) + +const ALIGNMENTS = [ + "top_left", "top center", "top_right", + "center_left", "center_center", "center_right", + "bottom_left", "bottom_center", "bottom_right", +].map(align => ({ + name: align, + label: align === 'center_center' + ? 'center' + : align.replace('_', ' ') + })) + +const REQUIRED_KEYS = { + image: ['url'], + text: ['content', 'width', 'height'], +} + +// target_page_id = Column(Integer, ForeignKey('page.id'), nullable=True) + +const newImage = (data) => ({ settings: { - x: 0.05, y: 0.05, + ...newPosition(), + tile: false, + url: "", }, + type: 'image', ...data, }) +const newText = (data) => ({ + settings: { + ...newPosition(), + tile: false, + url: "", + width: 200, + height: 200, + }, + type: 'text', + ...data, +}) + +const newPosition = () => ({ + x: 0, y: 0, + width: 0, height: 0, + rotation: 0, scale: 1, + align: "center_center", +}) + export default class TileForm extends Component { state = { title: "", submitTitle: "", - data: { ...newTile() }, + data: { ...newText() }, errorFields: new Set([]), } componentDidMount() { - const { graph, page, data, isNew } = this.props - const title = isNew ? 'new tile' : 'editing ' + data.title + const { graph, page, isNew } = this.props + const title = isNew ? 'new tile' : 'editing tile' const submitTitle = isNew ? "Create Tile" : "Save Changes" this.setState({ title, submitTitle, errorFields: new Set([]), data: { - ...newTile({ graph_id: graph.id, page_id: page.id }), - ...data, + ...(this.props.data || this.state.data), + graph_id: graph.id, + page_id: page.id, }, }) } @@ -45,15 +86,45 @@ export default class TileForm extends Component { if (errorFields.has(name)) { errorFields.delete(name) } - let sanitizedValue = value - if (name === 'path') { - sanitizedValue = sanitizedValue.toLowerCase().replace(/ /, '-').replace(/[!@#$%^&*()[\]{}]/, '-').replace(/-+/, '-') + if (name === 'type') { + const { graph, page } = this.props + let newData; + switch(type) { + case 'image': + newData = newImage({ graph_id: graph.id, page_id: page.id }) + break + case 'text': + newData = newText({ graph_id: graph.id, page_id: page.id }) + break + } + this.setState({ + errorFields, + data: newData, + }) + } + this.setState({ + errorFields, + data: { + ...this.state.data, + [name]: value, + } + }) + } + + handleSettingsChange(e) { + const { errorFields } = this.state + const { name, value } = e.target + if (errorFields.has(name)) { + errorFields.delete(name) } this.setState({ errorFields, data: { ...this.state.data, - [name]: sanitizedValue, + settings: { + ...this.state.settings, + [name]: value, + } } }) } @@ -72,12 +143,29 @@ export default class TileForm extends Component { }) } + handleSettingsSelect(name, value) { + const { errorFields } = this.state + if (errorFields.has(name)) { + errorFields.delete(name) + } + this.setState({ + errorFields, + data: { + ...this.state.data, + settings: { + ...this.state.data.settings, + [name]: value, + } + } + }) + } + handleSubmit(e) { e.preventDefault() const { isNew, onSubmit } = this.props const { data } = this.state - const requiredKeys = "path title".split(" ") - const validKeys = "graph_id path title username description settings".split(" ") + const requiredKeys = REQUIRED_KEYS[data.type] + const validKeys = "graph_id username settings".split(" ") const validData = validKeys.reduce((a,b) => { a[b] = data[b]; return a }, {}) const errorFields = requiredKeys.filter(key => !validData[key]) if (errorFields.length) { @@ -96,36 +184,38 @@ export default class TileForm extends Component { } render() { - const { graph, isNew } = this.props + const { isNew } = this.props const { title, submitTitle, errorFields, data } = this.state + if (!data) return return ( <div className='box'> <h1>{title}</h1> <form onSubmit={this.handleSubmit.bind(this)}> - <TextInput - title="Path" - name="path" - required - data={data} - error={errorFields.has('path')} - onChange={this.handleChange.bind(this)} - autoComplete="off" - /> - <TextInput - title="Title" - name="title" - required - data={data} - error={errorFields.has('title')} - onChange={this.handleChange.bind(this)} - autoComplete="off" - /> - <TextArea - title="Description" - name="description" - data={data} - onChange={this.handleChange.bind(this)} - /> + <div className="row selects"> + <Select + name='type' + selected={data.type} + options={SELECT_TYPES} + title='' + onChange={this.handleSelect.bind(this)} + /> + <Select + name='align' + selected={data.settings.align} + options={ALIGNMENTS} + title='' + onChange={this.handleSettingsSelect.bind(this)} + /> + </div> + + {this.renderPositionInfo()} + + {data.type === 'image' + ? this.renderImageForm() + : data.type === 'text' + ? this.renderTextForm() + : ""} + <SubmitButton title={submitTitle} onClick={this.handleSubmit.bind(this)} @@ -140,4 +230,83 @@ export default class TileForm extends Component { </div> ) } + + renderPositionInfo() { + const { x, y, width, height, rotation, scale } = this.state.data.settings + return ( + <div className='position'> + {''}{parseInt(x)}{', '} + {''}{parseInt(y)}{' '} + {parseInt(width)}{'x'}{parseInt(height)}{' '} + {parseInt(rotation * 180 / Math.PI)}{'\u00B0 '} + {'x'}{scale.toFixed(0.2)} + </div> + ) + } + + renderImageForm() { + // const { isNew } = this.props + const { data } = this.state + return ( + <div> + <TextInput + title="" + placeholder='http://' + name="url" + required + data={data.settings} + error={errorFields.has('url')} + onChange={this.handleSettingsChange.bind(this)} + autoComplete="off" + /> + <Checkbox + label="Tiled" + name="tile" + data={data.settings} + onChange={this.handleSettingsSelect.bind(this)} + autoComplete="off" + /> + </div> + ) + + } + + renderTextForm() { + const { data } = this.state + return ( + <div> + <TextArea + title="" + name="content" + required + data={data.settings} + error={errorFields.has('content')} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <div className='row pair'> + <NumberInput + title="Width" + name="width" + data={data.settings} + min={10} + max={1200} + error={errorFields.has('width')} + onChange={this.handleSettingsChange.bind(this)} + autoComplete="off" + /> + <NumberInput + title="Height" + name="height" + data={data.settings} + min={10} + max={1200} + error={errorFields.has('height')} + onChange={this.handleSettingsChange.bind(this)} + autoComplete="off" + /> + </div> + </div> + ) + } } diff --git a/frontend/views/page/components/tile.new.js b/frontend/views/page/components/tile.new.js index cb5eaa8..4e88729 100644 --- a/frontend/views/page/components/tile.new.js +++ b/frontend/views/page/components/tile.new.js @@ -29,7 +29,7 @@ class TileNew extends Component { isNew graph={this.props.graph.show.res} page={this.props.page.show.res} - data={{}} + data={null} onSubmit={this.handleSubmit.bind(this)} /> ) diff --git a/frontend/views/page/page.container.js b/frontend/views/page/page.container.js index bbb1f3a..6993632 100644 --- a/frontend/views/page/page.container.js +++ b/frontend/views/page/page.container.js @@ -66,9 +66,11 @@ class PageContainer extends Component { <PageHeader /> <div className='body'> <PageEditor /> - {this.props.graph.editor.editingPage && <PageEdit />} - {this.props.page.editor.addingTile && <TileNew />} - {this.props.page.editor.editingTile && <TileEdit />} + <div className='sidebar'> + {this.props.graph.editor.editingPage && <PageEdit />} + {this.props.page.editor.addingTile && <TileNew />} + {this.props.page.editor.editingTile && <TileEdit />} + </div> </div> </div> ) diff --git a/static/img/check.svg b/static/img/check.svg new file mode 100644 index 0000000..7560292 --- /dev/null +++ b/static/img/check.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<svg viewBox="0 0 24 24" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" > + <path fill="none" stroke="#fff" stroke-width="4" d="M1.73,12.91 8.1,19.28 22.79,4.59" /> +</svg>
\ No newline at end of file |
