summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--frontend/common/form.component.js22
-rw-r--r--frontend/common/form.css32
-rw-r--r--frontend/common/index.js2
-rw-r--r--frontend/views/graph/graph.css62
-rw-r--r--frontend/views/page/components/tile.form.js255
-rw-r--r--frontend/views/page/components/tile.new.js2
-rw-r--r--frontend/views/page/page.container.js8
-rw-r--r--static/img/check.svg4
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