summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md10
-rw-r--r--cli/app/sql/common.py1
-rw-r--r--cli/app/sql/models/graph.py2
-rw-r--r--cli/app/sql/models/page.py2
-rw-r--r--cli/app/sql/versions/202006011944_adding_uploads.py37
-rw-r--r--cli/app/sql/versions/202006021608_creating_database.py (renamed from cli/app/sql/versions/202006011943_adding_database.py)20
-rw-r--r--frontend/app.js20
-rw-r--r--frontend/common/app.css37
-rw-r--r--frontend/common/form.component.js8
-rw-r--r--frontend/common/form.css61
-rw-r--r--frontend/common/header.component.js14
-rw-r--r--frontend/common/index.js1
-rw-r--r--frontend/index.js1
-rw-r--r--frontend/views/index.js3
-rw-r--r--frontend/views/index/components/graph.form.js149
-rw-r--r--frontend/views/index/containers/graph.edit.js53
-rw-r--r--frontend/views/index/containers/graph.index.js28
-rw-r--r--frontend/views/index/containers/graph.new.js44
-rw-r--r--frontend/views/index/index.container.js33
-rw-r--r--frontend/views/index/index.css18
-rw-r--r--frontend/views/upload/index.js3
-rw-r--r--frontend/views/upload/upload.container.js2
-rw-r--r--static/index.html13
23 files changed, 458 insertions, 102 deletions
diff --git a/README.md b/README.md
index e6d5690..38a303e 100644
--- a/README.md
+++ b/README.md
@@ -13,19 +13,14 @@ npm install
(this should work on Linux as well but let me know if it doesn't)
-Set up the database:
-
-```
-./cli.py db upgrade head
-```
-
## running the site
-Before running the commands, enter the client directory and load the Conda environment:
+Before running the commands, enter the client directory, load the Conda environment, and make sure the database is current:
```
cd cli
conda activate swimmer
+./cli.py db upgrade head
```
Then build the frontend and run the Flask server:
@@ -50,4 +45,5 @@ Generate a new migration if you've modified the database:
```
./cli.py db revision --autogenerate -m 'describe the changes'
+./cli.py db upgrade head
```
diff --git a/cli/app/sql/common.py b/cli/app/sql/common.py
index d007e23..c8bd557 100644
--- a/cli/app/sql/common.py
+++ b/cli/app/sql/common.py
@@ -21,7 +21,6 @@ from app.settings import app_cfg
os.makedirs(app_cfg.DIR_DATABASE, exist_ok=True)
connection_url = "sqlite:///{}".format(os.path.join(app_cfg.DIR_DATABASE, 'swimmer.sqlite3'))
-print(connection_url)
engine = create_engine(connection_url, encoding="utf-8", pool_recycle=3600)
diff --git a/cli/app/sql/models/graph.py b/cli/app/sql/models/graph.py
index 1f553e9..0d3fdab 100644
--- a/cli/app/sql/models/graph.py
+++ b/cli/app/sql/models/graph.py
@@ -17,6 +17,7 @@ class Graph(Base):
id = Column(Integer, primary_key=True)
path = Column(String(64, convert_unicode=True), nullable=False)
title = Column(String(64, convert_unicode=True), nullable=False)
+ username = Column(String(32, convert_unicode=True), nullable=False)
description = Column(Text(convert_unicode=True), nullable=False)
settings = Column(JSON, default={}, nullable=True)
created_at = Column(UtcDateTime(), default=utcnow())
@@ -29,6 +30,7 @@ class Graph(Base):
'id': self.id,
'path': self.path,
'title': self.title,
+ 'username': self.username,
'description': self.description,
'settings': self.settings,
'created_at': self.created_at,
diff --git a/cli/app/sql/models/page.py b/cli/app/sql/models/page.py
index f23db8d..22fcc96 100644
--- a/cli/app/sql/models/page.py
+++ b/cli/app/sql/models/page.py
@@ -16,6 +16,7 @@ class Page(Base):
graph_id = Column(Integer, ForeignKey('graph.id'), nullable=True)
path = Column(String(64, convert_unicode=True), nullable=False)
title = Column(String(64, convert_unicode=True), nullable=False)
+ username = Column(String(32, convert_unicode=True), nullable=False)
description = Column(Text(convert_unicode=True), nullable=False)
settings = Column(JSON, default={}, nullable=True)
created_at = Column(UtcDateTime(), default=utcnow())
@@ -29,6 +30,7 @@ class Page(Base):
'graph_id': self.graph_id,
'path': self.path,
'title': self.title,
+ 'username': self.username,
'description': self.description,
'settings': self.settings,
'created_at': self.created_at,
diff --git a/cli/app/sql/versions/202006011944_adding_uploads.py b/cli/app/sql/versions/202006011944_adding_uploads.py
deleted file mode 100644
index f09f013..0000000
--- a/cli/app/sql/versions/202006011944_adding_uploads.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""adding uploads
-
-Revision ID: 5b926731a4ac
-Revises: 7acd0c82a048
-Create Date: 2020-06-01 19:44:30.400513
-
-"""
-from alembic import op
-import sqlalchemy as sa
-import sqlalchemy_utc
-
-
-# revision identifiers, used by Alembic.
-revision = '5b926731a4ac'
-down_revision = '7acd0c82a048'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.create_table('upload',
- sa.Column('id', sa.Integer(), nullable=False),
- sa.Column('sha256', sa.String(length=256), nullable=False),
- sa.Column('fn', sa.String(length=256), nullable=False),
- sa.Column('ext', sa.String(length=4, _expect_unicode=True), nullable=False),
- sa.Column('username', sa.String(length=16, _expect_unicode=True), nullable=False),
- sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True),
- sa.PrimaryKeyConstraint('id')
- )
- # ### end Alembic commands ###
-
-
-def downgrade():
- # ### commands auto generated by Alembic - please adjust! ###
- op.drop_table('upload')
- # ### end Alembic commands ###
diff --git a/cli/app/sql/versions/202006011943_adding_database.py b/cli/app/sql/versions/202006021608_creating_database.py
index 4e5b0e6..7c6e54a 100644
--- a/cli/app/sql/versions/202006011943_adding_database.py
+++ b/cli/app/sql/versions/202006021608_creating_database.py
@@ -1,8 +1,8 @@
-"""adding database
+"""creating database
-Revision ID: 7acd0c82a048
+Revision ID: 453787357254
Revises:
-Create Date: 2020-06-01 19:43:53.359855
+Create Date: 2020-06-02 16:08:43.195875
"""
from alembic import op
@@ -11,7 +11,7 @@ import sqlalchemy_utc
# revision identifiers, used by Alembic.
-revision = '7acd0c82a048'
+revision = '453787357254'
down_revision = None
branch_labels = None
depends_on = None
@@ -23,17 +23,28 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('path', sa.String(length=64, _expect_unicode=True), nullable=False),
sa.Column('title', sa.String(length=64, _expect_unicode=True), nullable=False),
+ sa.Column('username', sa.String(length=32, _expect_unicode=True), nullable=False),
sa.Column('description', sa.Text(_expect_unicode=True), nullable=False),
sa.Column('settings', sa.JSON(), nullable=True),
sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True),
sa.Column('updated_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
+ op.create_table('upload',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('sha256', sa.String(length=256), nullable=False),
+ sa.Column('fn', sa.String(length=256), nullable=False),
+ sa.Column('ext', sa.String(length=4, _expect_unicode=True), nullable=False),
+ sa.Column('username', sa.String(length=16, _expect_unicode=True), nullable=False),
+ sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True),
+ sa.PrimaryKeyConstraint('id')
+ )
op.create_table('page',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('graph_id', sa.Integer(), nullable=True),
sa.Column('path', sa.String(length=64, _expect_unicode=True), nullable=False),
sa.Column('title', sa.String(length=64, _expect_unicode=True), nullable=False),
+ sa.Column('username', sa.String(length=32, _expect_unicode=True), nullable=False),
sa.Column('description', sa.Text(_expect_unicode=True), nullable=False),
sa.Column('settings', sa.JSON(), nullable=True),
sa.Column('created_at', sqlalchemy_utc.sqltypes.UtcDateTime(timezone=True), nullable=True),
@@ -62,5 +73,6 @@ def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('tile')
op.drop_table('page')
+ op.drop_table('upload')
op.drop_table('graph')
# ### end Alembic commands ###
diff --git a/frontend/app.js b/frontend/app.js
index 38a50c6..da8dccb 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -4,7 +4,7 @@ import { Route } from 'react-router'
import { Header } from './common'
-import actions from './actions'
+// import actions from './actions'
import * as views from './views'
@@ -22,17 +22,15 @@ export default class App extends Component {
render() {
return (
<ConnectedRouter history={this.props.history}>
- <div>
+ <div className='app'>
<Header />
- <div className='app'>
- <div className='body'>
- {viewList}
- <Route exact key='root' path='/' render={() => {
- // redirect to search!!
- setTimeout(() => this.props.history.push('/search/'), 10)
- return null
- }} />
- </div>
+ <div className='body'>
+ {viewList}
+ <Route exact key='root' path='/' render={() => {
+ // redirect to index!!
+ setTimeout(() => this.props.history.push('/index'), 10)
+ return null
+ }} />
</div>
</div>
</ConnectedRouter>
diff --git a/frontend/common/app.css b/frontend/common/app.css
index a235507..41aa54e 100644
--- a/frontend/common/app.css
+++ b/frontend/common/app.css
@@ -2,15 +2,17 @@
html, body {
margin: 0;
padding: 0;
- min-width: 100%;
- min-height: 100%;
+ width: 100%;
+ height: 100%;
}
body {
- background: #fff;
- color: #000;
+ background: #000;
+ color: #ddd;
overflow-y: scroll;
font-family: 'Roboto', sans-serif;
font-size: 0.875rem;
+ height: 100%;
+ width: 100%;
}
.gray {
color: #888;
@@ -18,6 +20,21 @@ body {
/* layout */
+.container {
+ height: 100%;
+ width: 100%;
+}
+.app {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+}
+.app .body {
+ display: flex;
+ flex-grow: 1;
+}
+
.row {
display: flex;
flex-direction: row;
@@ -30,9 +47,7 @@ body {
.row > div:last-child {
margin-right: 0;
}
-.body section > div:last-child {
- flex-grow: 1;
-}
+
.row.menubar {
justify-content: flex-end;
@@ -59,7 +74,7 @@ header {
flex-direction: row;
justify-content: space-between;
align-items: center;
- background: #11f;
+ background: rgba(64,64,64,0.5);
color: white;
}
header b {
@@ -151,8 +166,12 @@ p {
/* links */
+b {
+ color: #fff;
+}
a {
- color: #11f;
+ text-decoration: underline;
+ color: #8df;
}
/* menu button */
diff --git a/frontend/common/form.component.js b/frontend/common/form.component.js
index fb8acbe..36369b5 100644
--- a/frontend/common/form.component.js
+++ b/frontend/common/form.component.js
@@ -10,10 +10,18 @@ export const TextInput = props => (
onChange={props.onChange}
name={props.name}
value={props.data[props.name]}
+ autoComplete={props.autoComplete}
/>
</label>
)
+export const LabelDescription = props => (
+ <label className={'text description'}>
+ <span>{props.title}</span>
+ <span>{props.children}</span>
+ </label>
+)
+
export const NumberInput = props => (
<label className={props.error ? 'error' : 'text'}>
<span>{props.title}</span>
diff --git a/frontend/common/form.css b/frontend/common/form.css
index f3ba85d..5b8f1e3 100644
--- a/frontend/common/form.css
+++ b/frontend/common/form.css
@@ -49,13 +49,24 @@ form .error input[type=password] {
border-color: #f11;
}
+/* form field descriptions */
+
+form label.description {
+ cursor: normal;
+ font-size: small;
+ color: #ddd;
+}
+
/* text input */
input[type=text],
input[type=number],
input[type=password] {
padding: 0.5rem;
- border: 1px solid #888;
+ border: 1px solid #ddd;
+ color: #fff;
+ background: #111;
+ font-family: 'Roboto', sans-serif;
font-size: 0.875rem;
width: 20rem;
border-radius: 0.125rem;
@@ -64,19 +75,24 @@ input[type=password] {
input[type=text]:focus,
input[type=number]:focus,
input[type=password]:focus {
- border: 1px solid #000;
+ border: 1px solid #8ff;
+ background: #000;
}
textarea {
width: 35rem;
height: 20rem;
padding: 0.5rem;
- border: 1px solid #888;
+ border: 1px solid #ddd;
+ font-family: 'Roboto', sans-serif;
+ background: #111;
+ color: #fff;
font-size: 0.875rem;
border-radius: 0.125rem;
}
textarea:focus {
- border: 1px solid #000;
+ border: 1px solid #8ff;
+ background: #000;
}
/* checkbox */
@@ -114,7 +130,7 @@ input[type="checkbox"]:after {
left: 0;
width: 0.75rem;
height: 0.75rem;
- border: 0.0625rem solid #888;
+ border: 0.0625rem solid #ddd;
content: "";
background-color: #fff;
background-repeat: no-repeat;
@@ -141,7 +157,7 @@ input[type="checkbox"]:checked:after {
min-width: auto;
background: #fff;
border-radius: 0.125rem;
- border: 1px solid #888;
+ border: 1px solid #ddd;
padding: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
@@ -165,14 +181,14 @@ input[type="checkbox"]:checked:after {
height: 0;
border-left: 0.375rem solid transparent;
border-right: 0.375rem solid transparent;
- border-top: 0.375rem solid #888;
+ border-top: 0.375rem solid #ddd;
}
.select.focus {
- border-color: #11f;
+ border-color: #fff;
background: #f4f4ff;
}
.select.focus:after {
- border-top-color: #11f;
+ border-top-color: #fff;
}
.select:hover {
background-color: #f4f4ff;
@@ -192,21 +208,22 @@ input[type="checkbox"]:checked:after {
button {
position: relative;
- background: #fff;
+ background: #333;
border-radius: 0.125rem;
- border: 1px solid #888;
+ color: #ddd;
+ border: 1px solid;
padding: 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
font-family: 'Roboto', sans-serif;
font-size: 0.875rem;
cursor: pointer;
- text-transform: uppercase;
+ /*text-transform: uppercase;*/
transition: all 0.1s;
}
button:hover {
- background-color: #f0f0ff;
- border-color: #1111ff;
+ background-color: #000;
+ border-color: #fff;
}
button.process {
padding-left: 1.5rem;
@@ -227,9 +244,9 @@ button.process:focus:after {
border-left-color: #11f;
}
button:focus {
- background: #f4f4ff;
- border-color: #11f;
- color: #11f;
+ background: #000;
+ border-color: #fff;
+ color: #fff;
outline: 0;
}
button:disabled {
@@ -245,13 +262,15 @@ button:disabled:after {
margin-right: 0.75rem;
}
button.submit {
- color: #1111ff;
- border-color: #1111ff;
+ border-color: #d8f;
+ color: #fff;
+ background: #111;
}
+button.submit:focus,
button.submit:hover {
+ border-color: #fff;
color: #fff;
- background: #1111ff;
- border-color: #1111ff;
+ background: #222;
}
/* file upload, should always be inside a container */
diff --git a/frontend/common/header.component.js b/frontend/common/header.component.js
index a4777dc..5cb15b5 100644
--- a/frontend/common/header.component.js
+++ b/frontend/common/header.component.js
@@ -1,6 +1,6 @@
-import React from 'react';
-import { bindActionCreators } from 'redux';
-import { connect } from 'react-redux';
+import React from 'react'
+// import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { session } from '../session'
@@ -11,8 +11,6 @@ function Header(props) {
<Link to="/" className="logo"><b>swimmer</b></Link>
</div>
<div>
- <Link to="/collection/">Collections</Link>
- <Link to="/dashboard/">Dashboard</Link>
<span className='username' onClick={() => changeUsername()}>
{' → '}{props.username}
</span>
@@ -34,9 +32,9 @@ const mapStateToProps = (state) => ({
auth: state.auth,
username: session.get('username'),
isAuthenticated: state.auth.isAuthenticated,
-});
+})
const mapDispatchToProps = (dispatch) => ({
-});
+})
-export default connect(mapStateToProps, mapDispatchToProps)(Header);
+export default connect(mapStateToProps, mapDispatchToProps)(Header)
diff --git a/frontend/common/index.js b/frontend/common/index.js
index 9e81c99..9a11eba 100644
--- a/frontend/common/index.js
+++ b/frontend/common/index.js
@@ -5,6 +5,7 @@ export {
export {
Select, Checkbox, FileInput, FileInputField,
TextInput, NumberInput, TextArea, SubmitButton,
+ LabelDescription,
} from './form.component'
export {
Loader, Swatch, Dot, Columns, Statistic, Detections, Progress
diff --git a/frontend/index.js b/frontend/index.js
index 94cac35..6f1a0a5 100644
--- a/frontend/index.js
+++ b/frontend/index.js
@@ -7,6 +7,7 @@ import App from './app'
import { store, history } from './store'
const container = document.createElement('div')
+container.classList.add('container')
document.body.appendChild(container)
ReactDOM.render(
diff --git a/frontend/views/index.js b/frontend/views/index.js
index 5f88c96..69bb70e 100644
--- a/frontend/views/index.js
+++ b/frontend/views/index.js
@@ -1 +1,2 @@
-export { Container as upload } from './upload'
+export { default as index } from './index/index.container'
+export { default as upload } from './upload/upload.container'
diff --git a/frontend/views/index/components/graph.form.js b/frontend/views/index/components/graph.form.js
new file mode 100644
index 0000000..ef546ec
--- /dev/null
+++ b/frontend/views/index/components/graph.form.js
@@ -0,0 +1,149 @@
+import React, { Component } from 'react'
+import { Link } from 'react-router-dom'
+
+import { session } from '../../../session'
+
+import { TextInput, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from '../../../common'
+
+const newGraph = () => ({
+ path: '',
+ title: '',
+ username: session('username'),
+ description: '',
+})
+
+export default class GraphForm extends Component {
+ state = {
+ title: "",
+ submitTitle: "",
+ data: { ...newGraph() },
+ errorFields: new Set([]),
+ }
+
+ componentDidMount() {
+ const { data, isNew } = this.props
+ const title = isNew ? 'new graph' : 'editing ' + data.title
+ const submitTitle = isNew ? "Create Graph" : "Save Changes"
+ this.setState({
+ title,
+ submitTitle,
+ errorFields: new Set([]),
+ data: {
+ ...newGraph(),
+ ...data
+ },
+ })
+ }
+
+ handleChange(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]: value,
+ }
+ })
+ }
+
+ handleSelect(name, value) {
+ const { errorFields } = this.state
+ if (errorFields.has(name)) {
+ errorFields.delete(name)
+ }
+ this.setState({
+ errorFields,
+ data: {
+ ...this.state.data,
+ [name]: value,
+ }
+ })
+ }
+
+ handleSubmit(e) {
+ e.preventDefault()
+ const { isNew, onSubmit } = this.props
+ const { data } = this.state
+ const requiredKeys = "title username".split(" ")
+ const validKeys = "title username notes archived".split(" ")
+ const validData = validKeys.reduce((a,b) => { a[b] = data[b]; return a }, {})
+ const errorFields = requiredKeys.filter(key => !validData[key])
+ if (errorFields.length) {
+ console.log('error', errorFields, validData)
+ this.setState({ errorFields: new Set(errorFields) })
+ } else {
+ if (isNew) {
+ // side effect: set username if we're creating a new graph
+ session.set('username', data.username)
+ } else {
+ validData.id = data.id
+ }
+ console.log('submit', validData)
+ onSubmit(validData)
+ }
+ }
+
+ render() {
+ const { isNew } = this.props
+ const { title, submitTitle, errorFields, data } = this.state
+ return (
+ <div className='form'>
+ <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"
+ />
+ <LabelDescription>
+ {data.path
+ ? 'Project URLs will be: /' + data.path + '/example'
+ : 'Enter the base path for this project.'}
+ </LabelDescription>
+ <TextInput
+ title="Title"
+ name="title"
+ required
+ data={data}
+ error={errorFields.has('title')}
+ onChange={this.handleChange.bind(this)}
+ autoComplete="off"
+ />
+ <TextInput
+ title="Author"
+ name="username"
+ required
+ data={data}
+ error={errorFields.has('username')}
+ onChange={this.handleChange.bind(this)}
+ autoComplete="off"
+ />
+ <TextArea
+ title="Description"
+ name="description"
+ data={data}
+ onChange={this.handleChange.bind(this)}
+ />
+ <SubmitButton
+ title={submitTitle}
+ onClick={this.handleSubmit.bind(this)}
+ />
+ {!!errorFields.size &&
+ <label>
+ <span></span>
+ <span>Please complete the required fields =)</span>
+ </label>
+ }
+ </form>
+ </div>
+ )
+ }
+}
diff --git a/frontend/views/index/containers/graph.edit.js b/frontend/views/index/containers/graph.edit.js
new file mode 100644
index 0000000..2f8c7fb
--- /dev/null
+++ b/frontend/views/index/containers/graph.edit.js
@@ -0,0 +1,53 @@
+import React, { Component } from 'react'
+import { Link } from 'react-router-dom'
+import { connect } from 'react-redux'
+
+import { history } from '../../../store'
+import actions from '../../../actions'
+
+import { Loader } from '../../../common'
+
+import GraphForm from '../components/graph.form'
+
+class GraphEdit extends Component {
+ componentDidMount() {
+ actions.graph.show(this.props.match.params.id)
+ }
+
+ handleSubmit(data) {
+ actions.graph.update(data)
+ .then(response => {
+ // response
+ console.log(response)
+ history.push('/graph/' + data.id + '/show/')
+ })
+ }
+
+ render() {
+ const { show } = this.props.graph
+ if (show.loading || !show.res) {
+ return (
+ <div className='form'>
+ <h1>Loading...</h1>
+ <Loader />
+ </div>
+ )
+ }
+ return (
+ <GraphForm
+ data={show.res}
+ onSubmit={this.handleSubmit.bind(this)}
+ />
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ graph: state.graph,
+})
+
+const mapDispatchToProps = dispatch => ({
+ // searchActions: bindActionCreators({ ...searchActions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(GraphEdit)
diff --git a/frontend/views/index/containers/graph.index.js b/frontend/views/index/containers/graph.index.js
new file mode 100644
index 0000000..b18c768
--- /dev/null
+++ b/frontend/views/index/containers/graph.index.js
@@ -0,0 +1,28 @@
+import React, { Component } from 'react'
+import { Link } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+// import actions from '../../actions'
+// import * as uploadActions from './upload.actions'
+
+class GraphIndex extends Component {
+ render() {
+ return (
+ <div className='graphIndex'>
+ <b>welcome, swimmer</b>
+ <Link to='/index/new'>+ new project</Link>
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ // upload: state.upload,
+})
+
+const mapDispatchToProps = dispatch => ({
+ // uploadActions: bindActionCreators({ ...uploadActions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(GraphIndex)
diff --git a/frontend/views/index/containers/graph.new.js b/frontend/views/index/containers/graph.new.js
new file mode 100644
index 0000000..186f8f7
--- /dev/null
+++ b/frontend/views/index/containers/graph.new.js
@@ -0,0 +1,44 @@
+import React, { Component } from 'react'
+import { Link } from 'react-router-dom'
+import { connect } from 'react-redux'
+
+import { history } from '../../../store'
+import actions from '../../../actions'
+
+import GraphForm from '../components/graph.form'
+
+class GraphNew extends Component {
+ handleSubmit(data) {
+ console.log(data)
+ actions.graph.create(data)
+ .then(res => {
+ console.log(res)
+ if (res.res && res.res.id) {
+ history.push('/graph/' + res.res.name)
+ }
+ })
+ .catch(err => {
+ console.error('error')
+ })
+ }
+
+ render() {
+ return (
+ <GraphForm
+ isNew
+ data={{}}
+ onSubmit={this.handleSubmit.bind(this)}
+ />
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ graph: state.graph,
+})
+
+const mapDispatchToProps = dispatch => ({
+ // searchActions: bindActionCreators({ ...searchActions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(GraphNew)
diff --git a/frontend/views/index/index.container.js b/frontend/views/index/index.container.js
new file mode 100644
index 0000000..7805e83
--- /dev/null
+++ b/frontend/views/index/index.container.js
@@ -0,0 +1,33 @@
+import React, { Component } from 'react'
+import { Route } from 'react-router-dom'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+
+import './index.css'
+
+// import actions from '../../actions'
+// import * as uploadActions from './upload.actions'
+
+import GraphIndex from './containers/graph.index'
+import GraphNew from './containers/graph.new'
+
+class Container extends Component {
+ render() {
+ return (
+ <div className='index'>
+ <Route exact path='/index/new' component={GraphNew} />
+ <Route exact path='/index' component={GraphIndex} />
+ </div>
+ )
+ }
+}
+
+const mapStateToProps = state => ({
+ // upload: state.upload,
+})
+
+const mapDispatchToProps = dispatch => ({
+ // uploadActions: bindActionCreators({ ...uploadActions }, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(Container)
diff --git a/frontend/views/index/index.css b/frontend/views/index/index.css
new file mode 100644
index 0000000..48c5abc
--- /dev/null
+++ b/frontend/views/index/index.css
@@ -0,0 +1,18 @@
+* {
+
+}
+
+.index > div {
+ margin: 1rem;
+ padding: 1rem;
+ max-height: calc(100% - 2rem);
+ overflow: scroll;
+ background: rgba(64,12,64,0.9);
+}
+.graphIndex {
+ display: flex;
+ flex-direction: column;
+}
+.graphIndex > * {
+ margin-bottom: 0.5rem;
+} \ No newline at end of file
diff --git a/frontend/views/upload/index.js b/frontend/views/upload/index.js
deleted file mode 100644
index b1e9ebe..0000000
--- a/frontend/views/upload/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as Container } from './upload.container'
-
-import './upload.css'
diff --git a/frontend/views/upload/upload.container.js b/frontend/views/upload/upload.container.js
index c163b92..0096b4f 100644
--- a/frontend/views/upload/upload.container.js
+++ b/frontend/views/upload/upload.container.js
@@ -3,6 +3,8 @@ import { Route, Link } from 'react-router-dom'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
+import './upload.css'
+
import actions from '../../actions'
import * as uploadActions from './upload.actions'
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..47eae19
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8">
+ <title>Swimmer</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
+ <body>
+<script>
+ var s = document.createElement('script');
+ s.setAttribute('src', '/static/js/dist/bundle.js?' + Date.now())
+ document.body.appendChild(s)
+</script>
+</html>