Compare commits

..

2 Commits

Author SHA1 Message Date
a915b36967 implemented user login and logout 2019-07-08 14:31:51 -04:00
337b12ad29 WIP refactor notes, ProjectView 2019-07-08 09:35:18 -04:00
45 changed files with 180 additions and 2316 deletions

View File

@ -1,18 +1,19 @@
you need postgres!
# TODO for refactoring
- localhost needs to be trusted
- update renderProject to renderProjectView
- update renderProjects to renderProjectsView
- create ProjectsView, ProjectTasksView, UserProjectsView
```
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
```
- create TasksView, UserTasksView
- create a user
`sudo -u postgres createuser --interactive`
# User Stories
- database: tasks-mockup
`createdb tasks-backend`
As a user, I want to:
- seed database
`npm run seed`
- View all available projects (should we assume all is public for now?)
- View all available tasks
- View all available profiles (e.g., to find potential collaborators)
- View a single project with its tasks (including unassigned)
- View a single profile with tasks assigned (to see my tasks, to see anothers' tasks)
- Hide completed tasks

745
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,17 @@
{
"name": "tasks-mockup",
"name": "my-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.19.0",
"concurrently": "^4.0.1",
"cors": "^2.8.5",
"express": "^4.17.1",
"morgan": "^1.9.1",
"nodemon": "^1.19.1",
"pg": "^7.11.0",
"sequelize": "^5.8.11",
"bootstrap-imageupload": "^1.1.3",
"http-proxy-middleware": "^0.19.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"uuid": "^3.3.2"
"react-scripts": "3.0.1"
},
"scripts": {
"start": "concurrently \"react-scripts start\" \"nodemon server\"",
"seed": "node server/db/seed.js",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"

View File

@ -1,3 +0,0 @@
.replies {
margin-left: 1em;
}

View File

@ -25,8 +25,7 @@
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
<link rel="stylesheet" type="text/css" href="css/custom.css" />
<title>Anarchy Planet</title>
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,56 +0,0 @@
const router = require('express').Router()
const { Project, Task, Article, Tag } = require('../db/models')
module.exports = router
/* CREATE */
router.post('/', async (req, res, next) => {
try {
const article = await Article.create(req.body)
res.json(article)
} catch (err) {
next(err)
}
})
/* READ */
router.get('/', async (req, res, next) => {
try {
const articles = await Article.findAll({
include: { model: Tag },
})
res.send(articles)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const article = await Article.findByPk(req.params.id)
res.json(article)
} catch (err) {
next(err)
}
})
/* UPDATE */
router.put('/:id', async (req, res, next) => {
try {
const article = await Article.findByPk(req.params.id)
await article.update(req.body)
res.json(article)
} catch (err) {
next(err)
}
})
/* DELETE */
router.post('/:id/delete', async (req, res, next) => {
try {
const article = await Article.findByPk(req.params.id)
await article.destroy({ force: true })
res.json(article)
} catch (err) {
next(err)
}
})

View File

@ -1,35 +0,0 @@
const router = require('express').Router()
const { Comment, User, Vote } = require('../db/models')
module.exports = router
router.get('/', async (req, res, next) => {
try {
const comments = await Comment.findAll({
include: ['replies', 'user'],
})
res.send(comments)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const comment = await Comment.findByPk(req.params.id, {
attributes: ['id', 'text'],
})
res.json(comment)
} catch (err) {
next(err)
}
})
router.post('/', async (req, res, next) => {
try {
const comment = await Comment.create(req.body)
res.json(comment)
} catch (err) {
next(err)
}
})

View File

@ -1,27 +0,0 @@
const router = require('express').Router()
module.exports = router
const ascii = require('../ascii')
router.use('/tasks', require('./tasks'))
router.use('/projects', require('./projects'))
router.use('/articles', require('./articles'))
router.use('/tags', require('./tags'))
router.use('/comments', require('./comments'))
router.use('/votes', require('./votes'))
router.use('/users', require('./users'))
router.get('/', async (req, res, next) => {
try {
res.json({ ascii })
} catch (err) {
console.log(err)
next()
}
})
router.use((req, res, next) => {
const error = new Error(`Not Found: ${req.url}`)
console.log(error.message)
error.status = 404
next()
})

View File

@ -1,59 +0,0 @@
const router = require('express').Router()
const { Project, Task, Article, Tag } = require('../db/models')
module.exports = router
/* CREATE */
router.post('/', async (req, res, next) => {
try {
const project = await Project.create(req.body)
res.json(project)
} catch (err) {
next(err)
}
})
/* READ */
router.get('/', async (req, res, next) => {
try {
const projects = await Project.findAll()
res.send(projects)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const project = await Project.findByPk(req.params.id)
res.json(project)
} catch (err) {
next(err)
}
})
/* UPDATE */
router.put('/:id', async (req, res, next) => {
try {
const project = await Project.findByPk(req.params.id)
await project.update(req.body)
res.json(project)
} catch (err) {
next(err)
}
})
/* DELETE */
router.post('/:id/delete', async (req, res, next) => {
try {
const project = await Project.findByPk(req.params.id)
await project.destroy({ force: true })
await Task.destroy({
where: {
projectId: req.params.id,
},
})
res.json(project)
} catch (err) {
next(err)
}
})

View File

@ -1,100 +0,0 @@
const router = require('express').Router()
const { Project, Task, Article, Tag } = require('../db/models')
module.exports = router
/* CREATE */
router.post('/', async (req, res, next) => {
try {
// find or create tag and add article relation
var tag = await Tag.findOne({
where: { name: req.body.name },
include: { model: Article },
})
if (!tag) {
const newTag = { name: req.body.name }
tag = await Tag.create(newTag, { include: { model: Article } })
}
tag.addArticle(req.body.articleId)
// find selected article and add tag relation
var article = await Article.findByPk(req.body.articleId, {
include: { model: Tag },
})
article.addTag(tag.id)
// update article and tag after adding the relation
article = await Article.findByPk(req.body.articleId, {
include: { model: Tag },
})
tag = await Tag.findOne({
where: { name: req.body.name },
include: { model: Article },
})
res.json({ tag: tag, article: article })
} catch (err) {
next(err)
}
})
/* READ */
router.get('/', async (req, res, next) => {
try {
const tags = await Tag.findAll({
include: { model: Article },
})
res.send(tags)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const tag = await Tag.findByPk(req.params.id)
res.json(tag)
} catch (err) {
next(err)
}
}) // remove tag from article
/* UPDATE */ router.put('/:id', async (req, res, next) => {
try {
//const tag = await Tag.findByPk(req.params.id)
//await tag.update(req.body)
//res.json(tag)
// find or create tag and add article relation
var tag = await Tag.findByPk(req.body.id, {
include: { model: Article },
})
tag.removeArticle(req.body.articleId)
// find selected article and add tag relation
var article = await Article.findByPk(req.body.articleId, {
include: { model: Tag },
})
article.removeTag(tag.id)
// update article and tag after adding the relation
article = await Article.findByPk(req.body.articleId, {
include: { model: Tag },
})
tag = await Tag.findByPk(req.body.id, {
include: { model: Article },
})
res.json({ tag: tag, article: article })
} catch (err) {
next(err)
}
})
/* DELETE */
router.post('/:id/delete', async (req, res, next) => {
try {
const tag = await Tag.findByPk(req.params.id)
await tag.destroy({ force: true })
res.json(tag)
} catch (err) {
next(err)
}
})

View File

@ -1,61 +0,0 @@
const router = require('express').Router()
const { Project, Task, Article, Tag } = require('../db/models')
module.exports = router
/* CREATE */
router.post('/', async (req, res, next) => {
try {
const task = await Task.create(req.body)
if (req.body.projectId) {
const project = await Project.findOne({
where: { id: req.body.projectId },
})
await task.setProject(project.id)
}
res.json(task)
} catch (err) {
next(err)
}
})
/* READ */
router.get('/', async (req, res, next) => {
try {
const tasks = await Task.findAll()
res.send(tasks)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const task = await Task.findByPk(req.params.id)
res.json(task)
} catch (err) {
next(err)
}
})
/* UPDATE */
router.put('/:id', async (req, res, next) => {
try {
const task = await Task.findByPk(req.params.id)
await task.update(req.body)
res.json(task)
} catch (err) {
next(err)
}
})
/* DELETE */
router.post('/:id/delete', async (req, res, next) => {
try {
const task = await Task.findByPk(req.params.id)
await task.destroy({ force: true })
res.json(task)
} catch (err) {
next(err)
}
})

View File

@ -1,54 +0,0 @@
const router = require('express').Router()
const { User } = require('../db/models')
module.exports = router
/* CREATE */
router.post('/', async (req, res, next) => {
try {
const task = await User.create(req.body)
res.json(task)
} catch (err) {
next(err)
}
})
/* READ */
router.get('/', async (req, res, next) => {
try {
const tasks = await User.findAll()
res.send(tasks)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const task = await User.findByPk(req.params.id)
res.json(task)
} catch (err) {
next(err)
}
})
/* UPDATE */
router.put('/:id', async (req, res, next) => {
try {
const task = await User.findByPk(req.params.id)
await task.update(req.body)
res.json(task)
} catch (err) {
next(err)
}
})
/* DELETE */
router.post('/:id/delete', async (req, res, next) => {
try {
const task = await User.findByPk(req.params.id)
task.destroy({ force: true })
res.json(task)
} catch (err) {
next(err)
}
})

View File

@ -1,53 +0,0 @@
const router = require('express').Router()
const { Comment, User, Vote } = require('../db/models')
module.exports = router
router.get('/', async (req, res, next) => {
try {
const votes = await Vote.findAll()
res.send(votes)
} catch (err) {
next(err)
}
})
router.post('/', async (req, res, next) => {
const {userId, commentId, upvote, downvote} = req.body
try {
const votes = await Vote.create({userId, commentId, upvote, downvote})
res.send(votes)
} catch (err) {
next(err)
}
})
router.get('/:id', async (req, res, next) => {
try {
const vote = await Vote.findByPk(+req.params.id)
res.json(vote)
} catch (err) {
next(err)
}
})
router.post('/:id/delete', async (req, res, next) => {
try {
const vote = await Vote.findByPk(+req.params.id)
await vote.destroy()
res.json(vote)
} catch (err) {
next(err)
}
})
router.put('/:id/update', async (req, res, next) => {
const upvote = req.body.downvote
const downvote = req.body.upvote
try {
const vote = await Vote.findByPk(+req.params.id)
await vote.update({upvote, downvote})
res.json(vote)
} catch (err) {
next(err)
}
})

View File

@ -1,26 +0,0 @@
const ascii = String.raw`
. .
* . . . . *
. . . . . .
o . .
. . . .
0 .
. . , , ,
. \ . .
. . \ ,
. o . . .
. . . \ , .
#\##\# . .
# #O##\### .
. . #*# #\##\### .
. ##*# #\##\## .
. . ##*# #o##\# .
. *# #\# . .
\ . .
____^/\___^--____/\____O______________/\/\---/\___________--
/\^ ^ ^ ^ ^^ ^ '\ ^ ^
-- - -- - - --- __ ^
-- __ ___-- ^ ^`
module.exports = ascii

View File

@ -1,18 +0,0 @@
const Sequelize = require('sequelize')
const pkg = require('../../package.json')
const databaseName = pkg.name
const createDB = () => {
const db = new Sequelize(
process.env.DATABASE_URL || `postgres://localhost:5432/${databaseName}`,
{
logging: false,
},
)
return db
}
const db = createDB()
module.exports = db

View File

@ -1,6 +0,0 @@
const db = require('./db')
// register models
require('./models')
module.exports = db

View File

@ -1,15 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Article = db.define('articles', {
title: {
type: Sequelize.STRING,
allowNull: false,
},
text: {
type: Sequelize.TEXT,
allowNull: true,
},
})
module.exports = Article

View File

@ -1,11 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Comment = db.define('comments', {
text: {
type: Sequelize.TEXT,
allowNull: false,
},
})
module.exports = Comment

View File

@ -1,43 +0,0 @@
const Task = require('./task')
const Project = require('./project')
const User = require('./user')
const Article = require('./article')
const Tag = require('./tag')
const Comment = require('./comment')
const Vote = require('./vote')
User.hasMany(Article)
User.hasMany(Comment)
User.hasMany(Vote)
Project.hasMany(Task)
Task.belongsTo(Project)
Article.hasMany(Comment)
Article.belongsToMany(Tag, { through: 'articleTags' })
Article.belongsTo(User)
Tag.belongsToMany(Article, { through: 'articleTags' })
Comment.belongsTo(Article)
Comment.belongsTo(User)
Comment.hasMany(Vote)
Comment.belongsTo(Comment, { as: 'parent' })
Comment.hasMany(Comment, {
as: { singular: 'reply', plural: 'replies' },
foreignKey: 'parentId',
})
Vote.belongsTo(Comment)
Vote.belongsTo(User)
User.belongsToMany(Project, { through: 'projectUser' })
Project.hasMany(User)
Task.belongsToMany(User, { through: 'userTask' })
User.hasMany(Task)
module.exports = {
Task,
Project,
User,
Article,
Tag,
Comment,
Vote,
}

View File

@ -1,11 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Project = db.define('projects', {
name: {
type: Sequelize.STRING,
allowNull: false,
},
})
module.exports = Project

View File

@ -1,11 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Tag = db.define('tags', {
name: {
type: Sequelize.STRING,
allowNull: false,
},
})
module.exports = Tag

View File

@ -1,16 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Task = db.define('tasks', {
desc: {
type: Sequelize.STRING,
allowNull: false,
},
completed: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
})
module.exports = Task

View File

@ -1,20 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const User = db.define('users', {
name: {
type: Sequelize.STRING,
allowNull: false,
},
email: {
type: Sequelize.STRING,
},
avatar: {
type: Sequelize.STRING,
defaultValue: 'default-user-img.png',
},
})
module.exports = User

View File

@ -1,9 +0,0 @@
const Sequelize = require('sequelize')
const db = require('../db')
const Vote = db.define('votes', {
upvote: Sequelize.INTEGER,
downvote: Sequelize.INTEGER,
})
module.exports = Vote

View File

@ -1,102 +0,0 @@
const db = require('../db')
const { Task, Project, User, Article, Tag, Comment, Vote } = require('./models')
const testTasks = [
{
desc: 'make app',
completed: false,
projectId: 1,
},
{
desc: 'update backend',
completed: false,
projectId: 1,
},
{
desc: 'eat dinner',
completed: false,
projectId: 1,
},
]
const testProjects = [{ name: 'Anarchy Planet' }, { name: 'Tilde' }]
const testUsers = [
{ name: 'nn', email: 'nn@ap.org' },
{ name: 'dn', email: 'dn@ap.org' },
]
const testArticles = [{ title: 'latest news', text: 'waddup?!' }]
const testTags = [{ name: 'dox' }]
const tc1 = { text: 'pretty good' }
const tc2 = { text: 'yeah!' }
const tc3 = { text: 'could be more detailed tho' }
const tc4 = { text: 'BUT THE INSECTS' }
const tc5 = { text: 'HAHAHAHAHA' }
const tc6 = { text: 'oh shut up' }
const testVotes = [{ upvote: 0, downvote: 1 }]
async function runSeed() {
await db.sync({ force: true })
console.log('db synced!')
console.log('seeding...')
try {
const p1 = await Project.create(testProjects[0])
const p2 = await Project.create(testProjects[1])
const u1 = await User.create(testUsers[0])
const u2 = await User.create(testUsers[1])
const t1 = await Task.create(testTasks[0])
const t2 = await Task.create(testTasks[1])
const t3 = await Task.create(testTasks[2])
await p1.addTask(t1)
await p1.addTask(t2)
await p2.addTask(t3)
await u1.addTasks([t1, t2])
await u2.addTask(t3)
const a1 = await Article.create(testArticles[0])
const t4 = await Tag.create(testTags[0])
const c1 = await Comment.create(tc1)
const c2 = await Comment.create(tc2)
const c3 = await Comment.create(tc3)
const c4 = await Comment.create(tc4)
const c5 = await Comment.create(tc5)
const c6 = await Comment.create(tc6)
const v1 = await Vote.create(testVotes[0])
await a1.setUser(u1)
await a1.addTag(t4)
await a1.addComments(c1, c2, c3, c4, c5, c6)
await c1.setUser(u2)
await c2.setUser(u1)
await c3.setUser(u2)
await c4.setUser(u1)
await c5.setUser(u1)
await c6.setUser(u2)
await c1.addVote(v1)
await c1.addReply(2)
await c2.addReplies([c3, c4])
await c5.setParent(c4)
await c6.setParent(c1)
console.log('seeded successfully')
} catch (err) {
console.error(err)
process.exitCode = 1
} finally {
console.log('closing db connection')
await db.close()
console.log('db connection closed')
}
}
runSeed()

View File

@ -1,34 +0,0 @@
const express = require('express')
const path = require('path')
const app = express()
const morgan = require('morgan')
const ascii = require('./ascii')
const cors = require('cors')
const port = process.env.PORT || 1337
app.use(morgan('tiny'))
app.use(cors())
// body parsing middleware
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(require('body-parser').text())
app.use('/api', require('./api'))
if (process.env.NODE_ENV === 'production') {
// Express will serve up production assets
app.use(express.static(path.join(__dirname, 'dist')))
}
app.use('/', express.static(path.resolve(__dirname, '..', 'build')))
// error handling endware
app.use((err, req, res, next) => {
console.error(err)
console.error(err.stack)
res.status(err.status || 500).send(err.message || 'Internal server error.')
next()
})
app.listen(port, () => {
console.log('\n' + ascii + '\n')
console.log(`Doin' haxor stuff on port ${port}`)
})

View File

@ -1,15 +1,6 @@
import React from 'react'
import axios from 'axios'
import {
createArticle,
updateArticle,
deleteArticle,
selectArticle,
} from './controllers/articles'
import { addTag, selectTag, removeTag } from './controllers/tags'
import {
createTask,
updateTask,
@ -28,10 +19,8 @@ import {
import { updateProfile } from './controllers/profile'
import {
Login,
About,
Articles,
Article,
UpdateArticle,
Tasks,
UpdateTask,
Profile,
@ -45,16 +34,15 @@ console.log('Using API at ' + API)
const defaultState = {
loading: true,
user: { name: 'Scott' },
user: {},
tasks: [],
projects: [],
component: 'projects',
// TODO try to get rid of:
search: '',
newArticle: '',
newTask: '',
newProject: '',
project: {},
newTag: '',
}
class App extends React.Component {
@ -65,12 +53,7 @@ class App extends React.Component {
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.createArticle = createArticle.bind(this)
this.updateArticle = updateArticle.bind(this)
this.deleteArticle = deleteArticle.bind(this)
this.addTag = addTag.bind(this)
this.removeTag = removeTag.bind(this)
this.login = this.login.bind(this)
this.createTask = createTask.bind(this)
this.updateTask = updateTask.bind(this)
@ -82,22 +65,10 @@ class App extends React.Component {
this.updateProfile = updateProfile.bind(this)
this.selectArticle = selectArticle.bind(this)
this.selectTag = selectTag.bind(this)
this.selectTask = selectTask.bind(this)
this.selectProject = selectProject.bind(this)
}
async fetchArticles() {
try {
const { data } = await axios.get(API + '/api/articles')
return data
} catch (error) {
console.log(error)
this.setState({ error })
}
}
async fetchTasks() {
try {
const { data } = await axios.get(API + '/api/tasks')
@ -118,52 +89,23 @@ class App extends React.Component {
}
}
async fetchComments() {
async login(userData) {
try {
const { data } = await axios.get(API + '/api/comments')
return data
} catch (error) {
console.log(error)
this.setState({ error })
}
}
async fetchTags() {
try {
const { data } = await axios.get(API + '/api/tags')
return data
} catch (error) {
console.log(error)
this.setState({ error })
}
}
async fetchVotes() {
try {
const { data } = await axios.get(API + '/api/votes')
return data
} catch (error) {
console.log(error)
this.setState({ error })
const { data } = await axios.post(API + '/login', userData)
console.log('login retrieved data:', data)
if (data.name) {
this.setState({ user: data })
this.navigate('projects')
} else this.setState({ error: 'no user found.' })
} catch (err) {
console.log(err)
}
}
async fetchData() {
const articles = await this.fetchArticles()
const tags = await this.fetchTags()
const tasks = await this.fetchTasks()
const projects = await this.fetchProjects()
const comments = await this.fetchComments()
const votes = await this.fetchVotes()
await this.setState({
loading: false,
articles,
tags,
tasks,
projects,
comments,
votes,
})
await this.setState({ loading: false, tasks, projects })
}
componentDidMount() {
@ -186,9 +128,9 @@ class App extends React.Component {
return <div>Loading...</div>
}
renderError() {
if (!this.state.error) {
return <div>There was an error.</div>
} else if (this.state.error.message === 'Network Error') {
const { error } = this.state
if (error) {
if (this.state.error.message === 'Network Error') {
alert(`Failed to reach backend at\n${this.state.error.config.url}.`)
} else {
return (
@ -205,6 +147,8 @@ class App extends React.Component {
)
}
}
return <div>Uncaught error. You probably forgot to render something.</div>
}
renderProfile() {
return (
<Profile
@ -215,43 +159,6 @@ class App extends React.Component {
/>
)
}
renderArticles() {
return (
<Articles
navigate={this.navigate}
handleChange={this.handleChange}
createArticle={this.createArticle}
selectArticle={this.selectArticle}
deleteArticle={this.deleteArticle}
selectTag={this.selectTag}
{...this.state}
/>
)
}
renderArticle() {
return (
<Article
navigate={this.navigate}
addTag={this.addTag}
removeTag={this.removeTag}
deleteArticle={this.deleteArticle}
selectTag={this.selectTag}
handleChange={this.handleChange}
{...this.state}
/>
)
}
renderUpdateArticle() {
return (
<UpdateArticle
handleChange={this.handleChange}
updateArticle={this.updateArticle}
navigate={this.navigate}
selectTag={this.selectTag}
{...this.state}
/>
)
}
renderProjects() {
return (
<Projects
@ -283,6 +190,10 @@ class App extends React.Component {
renderAbout() {
return <About {...this.state} />
}
renderLogin() {
return <Login login={this.login} {...this.state} />
}
renderTasks(filtered, completed) {
return (
<Tasks
@ -330,12 +241,6 @@ class App extends React.Component {
? this.renderLoading()
: this.state.error
? this.renderError()
: this.state.component === 'articles'
? this.renderArticles()
: this.state.component === 'article'
? this.renderArticle()
: this.state.component === 'update-article'
? this.renderUpdateArticle()
: this.state.component === 'tasks'
? this.renderTasks(filtered, completed)
: this.state.component === 'task'
@ -348,6 +253,8 @@ class App extends React.Component {
? this.renderProjects()
: this.state.component === 'project'
? this.renderProject()
: this.state.component === 'login'
? this.renderLogin()
: this.renderError()
return (

View File

@ -1,26 +0,0 @@
import React from 'react'
const CollapsedComment = props => {
return (
<li key={props.id}>
<div className="media-body ml-3 py-2 my-auto">
<img className="px-2" alt="profile" src="anon.jpeg" height={16} />
<a href="#user">username</a>
<small className="px-3">One day ago</small>
<span
className="a"
style={{
fontVariant: 'small-caps',
color: 'blue',
cursor: 'pointer',
}}
onClick={() => props.toggleCollapse(props.id)}
>
expand
</span>
</div>
</li>
)
}
export default CollapsedComment

View File

@ -1,47 +0,0 @@
import React from 'react'
const ExpandedComment = props => {
return (
<li
className=""
key={props.id}
onMouseEnter={() => props.showCollapse(props.id, true)}
onMouseLeave={() => props.showCollapse(props.id, false)}
>
<div className="media bg bg-dark">
<img alt="profile" src={props.user.avatar} height={64} />
<div className="media-body ml-3 my-auto">
<a className="" href="#user">
{props.user.name}
</a>
</div>
<div className="white">
<small>{props.createdAt}</small>
{props.replies.length > 0 ? (
<span
className="a ml-3"
style={{
fontVariant: 'small-caps',
color: 'blue',
cursor: 'pointer',
display: props.displayCollapse || 'none',
}}
onClick={() => props.toggleCollapse(props.id, !props.collapsed)}
>
{props.collapsed ? 'expand' : 'collapse'}
</span>
) : (
''
)}
<a className="float-right ml-2" href="#replies">
reply
</a>
</div>
</div>
<div className="">{props.text}</div>
</li>
)
}
export default ExpandedComment

View File

@ -1,10 +0,0 @@
import React from 'react'
import ExpandedComment from './ExpandedComment'
import CollapsedComment from './CollapsedComment'
const SingleComment = props => {
if (props.hidden) return <CollapsedComment {...props} />
return <ExpandedComment {...props} />
}
export default SingleComment

View File

@ -1,92 +0,0 @@
import React from 'react'
import SingleComment from './SingleComment'
import uuid from 'uuid'
class ThreadList extends React.Component {
constructor() {
super()
this.toggleCollapse = this.toggleCollapse.bind(this)
this.showCollapse = this.showCollapse.bind(this)
}
showCollapse(id, state) {
this.setState({ displayCollapse: 'block' })
const otherComments = this.props.comments.filter(c => c.id !== id)
const comment = this.props.comments.find(c => c.id === id)
comment['displayCollapse'] = state
//console.log('displayed collapse for: ', comment)
this.setState({ comments: otherComments.concat(comment) })
}
toggleCollapse(id, state) {
const otherComments = this.props.comments.filter(c => c.id !== id)
var parent = this.props.comments.find(c => c.id === id)
console.log('toggling', parent)
parent['collapsed'] = state
if (!state) {
// unhide parent when unhiding thread
parent['hidden'] = false
}
this.setState({ comments: otherComments.concat(parent) })
parent.replies.map(comment => this.toggleReplies(comment.id, state))
}
toggleReplies(id, state) {
const otherComments = this.props.comments.filter(c => c.id !== id)
var comment = this.props.comments.find(c => c.id === id)
comment['hidden'] = state
this.setState({ comments: otherComments.concat(comment) })
if (comment.replies) {
comment.replies.map(c => this.toggleReplies(c.id, state))
}
}
render() {
const comments = this.props.comments.filter(
// only show comments for this parent / thread
c =>
(!c.parentId && !this.props.threadLevel) ||
c.parentId === this.props.threadLevel,
)
if (comments.length === 0) {
return ''
}
console.log('showing comments for thread', this.props.threadLevel, comments)
return (
<ul className="list-unstyled">
{comments.map(comment => {
// TODO comments aren't sorted by time
if (comment.replies && comment.replies.length === 0) {
return (
<div className="ml-3" key={uuid()}>
<SingleComment
threadLevel={this.props.threadLevel}
showCollapse={this.showCollapse}
toggleCollapse={this.toggleCollapse}
{...comment}
/>
</div>
)
} else {
return (
<div className="ml-3" key={uuid()}>
<SingleComment
threadLevel={this.props.threadLevel}
showCollapse={this.showCollapse}
toggleCollapse={this.toggleCollapse}
{...comment}
/>
<ThreadList
threadLevel={comment.id}
showCollapse={this.showCollapse}
comments={this.props.comments}
/>
</div>
)
}
})}
</ul>
)
}
}
export default ThreadList

View File

@ -0,0 +1,23 @@
import React from 'react'
import Tasks from './tasks'
function Project(props) {
// if (!props.selectedProjectId) {
// props.navigate('projects')
// return null
// }
// const project = props.projects.find(p => p.id === props.selectedProjectId)
// const filtered = props.tasks.filter(t => t.projectId === props.selectedProjectId && !t.completed)
// const completed = props.tasks.filter(
// t => (t.projectId === props.selectedProjectId && t.completed === true) || null,
// )
return (
<div>
<h2>{props.project.name}</h2>
<Tasks filtered={props.filtered} completed={props.completed} {...props} />
</div>
)
}
export default Project

View File

@ -1,38 +0,0 @@
import React from 'react'
import Tags from './tags'
import Comments from './Comments'
class Article extends React.Component {
constructor(props) {
super(props)
const article = props.articles.find(t => t.id === props.selectedArticleId)
this.state = article
}
render() {
return (
<div>
<Tags {...this.props} />
<h2>{this.state.title}</h2>
<div className="mb-3">{this.state.text}</div>
<div>
<button
className="btn btn-primary"
onClick={() => this.props.navigate('articles')}
>
Back
</button>
<button
className="btn btn-primary ml-1"
onClick={() => this.props.navigate('update-article')}
>
Edit
</button>
</div>
<h2 className="mt-3">Comments</h2>
<Comments {...this.props} />
</div>
)
}
}
export default Article

View File

@ -1,83 +0,0 @@
import React from 'react'
function Articles(props) {
var articles = props.articles || []
if (props.selectedTagId) {
articles = props.articles.filter(a =>
filterArticles(a, props.selectedTagId),
)
}
function filterArticles(article, tagId) {
if (article.tags && article.tags.filter(t => t.id === tagId).length !== 0) {
return article
}
}
return (
<div>
<h3 className="mt-4">Articles</h3>
{props.selectedTagId ? (
<button
className="btn btn-primary mb-1"
onClick={() => props.selectTag()}
>
Show all
</button>
) : (
''
)}
<ul className="list-group">
{articles.map(article => (
<li className="list-group-item" key={article.id}>
<button
className="btn btn-outline-danger mr-1"
onClick={() => props.deleteArticle(article.id)}
>
X
</button>
<button
className="btn ml-1"
onClick={() => props.selectArticle(article.id)}
>
{article.title}{' '}
</button>
<button className="btn ml-1 float-right">
{props.comments.map(c => c.articleId === article.id).length}{' '}
comment(s)
</button>
{article.tags &&
article.tags.map(tag => (
<button
className="btn btn-outline-success mr-1 float-right"
key={article.id + tag.id}
onClick={e => props.selectTag(tag.id)}
>
{tag.name}
</button>
))}
</li>
))}
<li className="list-group-item">
<form className="form-group row" onSubmit={props.createArticle}>
<input
className="form-control col input-sm"
placeholder="Create Article"
name="newArticle"
value={props.newArticle}
onChange={props.handleChange}
/>
<button
type="submit"
className="form-control col col-sm-2 ml-1 btn btn-primary"
onClick={props.createArticle}
>
Save
</button>
</form>
</li>
</ul>
</div>
)
}
export default Articles

View File

@ -1,12 +1,8 @@
export { default as Login } from './login'
export { default as Navbar } from './navbar'
export { default as About } from './about'
export { default as Articles } from './articles'
export { default as Article } from './article'
export { default as UpdateArticle } from './update-article'
export { default as Tags } from './tags'
export { default as Tasks } from './tasks'
export { default as UpdateTask } from './update-task'
export { default as Profile } from './profile'
export { default as Projects } from './projects'
export { default as Project } from './project'
export { default as Comments } from './Comments'

62
src/components/login.js Normal file
View File

@ -0,0 +1,62 @@
import React from 'react'
const initialState = { email: '', password: '' }
class Login extends React.Component {
constructor() {
super()
this.state = initialState
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(e) {
this.setState({ [e.target.name]: e.target.value })
}
handleSubmit(e) {
e.preventDefault()
this.setState(initialState)
const email = e.target.email.value
const password = e.target.password.value // hash dis b4 send on frontend
return this.props.login({ email, password })
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div className="form-group row">
<label htmlFor="email" className="col-sm-2 col-form-label">
Email
</label>
<div className="col-sm-10">
<input
type="email"
className="form-control"
id="email"
placeholder="Email"
/>
</div>
</div>
<div className="form-group row">
<label htmlFor="password" className="col-sm-2 col-form-label">
Password
</label>
<div className="col-sm-10">
<input
type="password"
className="form-control"
id="password"
placeholder="Password"
/>
</div>
</div>
<button type="submit" onSubmit={this.handleSubmit}>
Log In
</button>
</form>
)
}
}
export default Login

View File

@ -3,6 +3,7 @@ import React from 'react'
function Navbar(props) {
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light">
{props.user && props.user.name ? (
<a
className="navbar-brand"
href="#profile"
@ -10,6 +11,15 @@ function Navbar(props) {
>
{`Hello, ${props.user.name}!`}
</a>
) : (
<a
className="navbar-brand"
href="#login"
onClick={() => props.navigate('login')}
>
Log in
</a>
)}
<button
className="navbar-toggler"
type="button"
@ -23,23 +33,13 @@ function Navbar(props) {
</button>
<div className="collapse navbar-collapse" id="navbarText">
<ul className="navbar-nav mr-auto">
<li
className={`nav-item ${
props.component === 'about' ? 'active' : ''
}`}
onClick={() => props.navigate('articles')}
>
<a className="nav-link" href="#articles">
Articles
</a>
</li>
<li
className={`nav-item ${
props.component === 'project' ? 'active' : ''
}`}
onClick={() => props.navigate('projects')}
onClick={() => props.navigate('project')}
>
<a className="nav-link" href="#projects">
<a className="nav-link" href="#project">
Projects
</a>
</li>
@ -63,6 +63,19 @@ function Navbar(props) {
About
</a>
</li>
{props.user && props.user.name ? (
<li className="nav-item" onClick={() => props.logout()}>
<a className="nav-link" href="#logout">
Logout
</a>
</li>
) : (
<li className="nav-item" onClick={() => props.navigate('login')}>
<a className="nav-link" href="#login">
Login
</a>
</li>
)}
</ul>
</div>
</nav>

View File

@ -14,7 +14,6 @@ function Project(props) {
return (
<div>
<span className="btn btn-primary" onClick={() => props.navigate('projects')}>Back</span>
<h2>{project.name}</h2>
<Tasks filtered={filtered} completed={completed} {...props} />
</div>

View File

@ -1,42 +0,0 @@
import React from 'react'
function Tags(props) {
const article = props.articles.filter(
a => a.id === props.selectedArticleId,
)[0]
return (
<div>
{article.tags &&
article.tags.map(tag => (
<span key={tag.id}>
<button
className="btn btn-outline-success ml-1"
onClick={e => props.selectTag(tag.id)}
>
{tag.name}
</button>
<button
className="btn btn-danger mr-1"
onClick={e => props.removeTag(tag.id)}
>
-
</button>
</span>
))}
<form className="mt-2 mb-2" onSubmit={props.addTag}>
<input
className="col input-sm col-sm-3 ml-3 mr-1"
placeholder="Add tag"
name="newTag"
value={props.newTag}
onChange={props.handleChange}
/>
<button className="col btn btn-primary col-sm-1" onClick={props.addTag}>
+
</button>
</form>
</div>
)
}
export default Tags

View File

@ -1,78 +0,0 @@
import React from 'react'
class UpdateArticle extends React.Component {
constructor(props) {
super(props)
const article = props.articles.find(t => t.id === props.selectedArticleId)
this.state = article
this.handleChange = this.handleChange.bind(this)
if (!props.selectedArticleId) {
props.navigate('articles')
}
}
handleChange(e) {
this.setState({ [e.target.name]: e.target.value })
}
render() {
return (
<div>
<button className="btn" onClick={() => this.props.navigate('article')}>
back
</button>
<form>
<h2 className="form-group row">Article {this.state.id}</h2>
<div className="form-group row">
<input
type="text"
className="form-control mb-1"
name="title"
value={this.state.title}
onChange={this.handleChange}
/>
</div>
<div className="form-group row">
<textarea
type="text"
className="form-control"
name="text"
rows="25"
value={this.state.text || ''}
onChange={this.handleChange}
/>
</div>
<div className="form-group row">
<label htmlFor="createdAt" className="col-sm-2 col-form-label">
Created:
</label>
<div className="col-sm-10">
<input
className="form-control"
name="createdAt"
value={this.state.createdAt}
readOnly
/>
</div>
</div>
<div className="form-group row">
<div className="col-sm-10">
<button
type="submit"
className="btn btn-primary"
onClick={e => this.props.updateArticle(e, this.state)}
>
Save
</button>
</div>
</div>
</form>
</div>
)
}
}
export default UpdateArticle

View File

@ -1,85 +0,0 @@
import axios from 'axios'
const API = process.env.REACT_APP_API || 'http://localhost:1337'
export async function createArticle(e) {
e.preventDefault()
if (this.state.newArticle === '') {
alert('You forgot the title.')
return
}
const article = { title: this.state.newArticle }
try {
const { data, error } = await axios.post(API + '/api/articles', article)
if (error) {
alert(`Received error when creating article ${article.name}: ${error}`)
} else if (data.id) {
this.setState({
articles: this.state.articles.concat(data),
newArticle: '',
})
console.log(`Added article: `, data)
} else {
alert(`Failed to add article '${article}'.`)
}
} catch (e) {
alert(`Failed to add article '${this.state.newArticle}'.`)
}
}
export function selectArticle(selectedArticleId) {
this.setState({ selectedArticleId })
this.navigate('article')
}
export async function updateArticle(e, updatedArticle) {
e.preventDefault()
const oldArticle = this.state.articles.find(t => t.id === updatedArticle.id)
if (JSON.stringify(oldArticle) === JSON.stringify(updatedArticle)) {
this.navigate('articles')
return
}
try {
const { data, error } = await axios.put(
API + `/api/articles/${oldArticle.id}`,
updatedArticle,
)
if (error) {
alert('Received error when updating article: ', error)
} else if (data.title) {
const oldArticles = this.state.articles.filter(t => t.id !== updatedArticle.id)
this.setState({ articles: oldArticles.concat(updatedArticle) })
console.log('Successfully updated article:', data)
this.navigate('articles')
} else {
console.log('Received malformed data when updating article: ', data)
}
} catch (e) {
alert(`Updating article failed: ${e}`)
}
}
export async function deleteArticle(id) {
const article = this.state.articles.find(p => p.id === id)
if (!article.id) {
alert(`Could not find article with id ${id}.`)
return
}
try {
const { data, error } = await axios.post(
API + `/api/articles/${article.id}/delete`,
)
if (error) {
alert(`Received error when deleting article ${article.title}: ${error}`)
} else if (data.title) {
console.log('Deleted article:', data)
this.setState({
articles: this.state.articles.filter(p => p.id !== article.id),
})
this.navigate('articles')
} else {
alert(`Failed to delete article '${article.title}'.`)
}
} catch (e) {
alert(`Failed to delete article '${article.title}': ${e}`)
}
}

View File

@ -1,20 +0,0 @@
import axios from 'axios'
const API = process.env.REACT_APP_API || 'http://localhost:1337'
export async function replyToComment(e) {
e.preventDefault()
const user = {} // TODO update properties from form
console.log(e.target)
try {
const { data, error } = await axios.put(API + `/api/user`, user)
console.log('Trying to update user profile: ', user)
if (error) {
alert('Received error when updating user profile: ', error)
} else if (data) {
this.setState({ user: data })
console.log('Successfully updated user profile:', data)
}
} catch (e) {
alert(`Updating user profile failed: ${e}`)
}
}

View File

@ -14,7 +14,7 @@ export async function createProject(e) {
alert(`Received error when creating project ${project.name}: ${error}`)
} else if (data.id) {
this.setState({
// projects: this.state.projects.concat(data),
projects: this.state.projects.concat(data),
newProject: '',
})
console.log(`Added project: `, data)

View File

@ -1,95 +0,0 @@
import axios from 'axios'
const API = process.env.REACT_APP_API || 'http://localhost:1337'
export async function addTag(e) {
e.preventDefault()
if (this.state.newTag === '') {
return alert('Refusing to add an empty tag.')
}
const tag = {
name: this.state.newTag,
articleId: this.state.selectedArticleId,
}
try {
const { data, error } = await axios.post(API + '/api/tags', tag)
if (error) {
alert(`Received error when adding tag ${tag.name} to article: ${error}`)
} else if (data.tag && data.article) {
const oldTags = this.state.tags.filter(t => t.id !== data.tag.id)
const oldArticles = this.state.articles.filter(
a => a.id !== data.article.id,
)
this.setState({
tags: oldTags.concat(data.tag),
articles: oldArticles.concat(data.article),
newTag: '',
})
console.log(`Added tag to article: `, data)
} else {
alert(`Failed to add tag to article '${tag}'.`)
}
} catch (e) {
alert(`Failed to add tag to article '${this.state.newTag}'.`)
}
}
export function selectTag(selectedTagId) {
this.setState({ selectedTagId })
this.navigate('articles')
}
export async function removeTag(id) {
const tag = this.state.tags.find(p => p.id === id)
if (!tag.id) {
alert(`Could not find tag with id ${id}.`)
return
}
tag['articleId'] = this.state.selectedArticleId
try {
const { data, error } = await axios.put(
API + `/api/tags/${(tag, id)}}`,
tag,
)
if (error) {
alert(`Received error when removing tag ${tag.name}: ${error}`)
} else if (data.tag && data.article) {
console.log(data)
const oldTags = this.state.tags.filter(t => t.id !== data.tag.id)
const oldArticles = this.state.articles.filter(
a => a.id !== data.article.id,
)
this.setState({
tags: oldTags.concat(data.tag),
articles: oldArticles.concat(data.article),
})
console.log('Removed tag from article:', data)
} else {
alert(`Failed to remove tag '${tag.name}' from article .`)
}
} catch (e) {
alert(`Failed to remove tag '${tag.name} from article ': ${e}`)
}
}
export async function deleteTag(id) {
const tag = this.state.tags.find(p => p.id === id)
if (!tag.id) {
alert(`Could not find tag with id ${id}.`)
return
}
try {
const { data, error } = await axios.post(API + `/api/tags/${tag.id}/delete`)
if (error) {
alert(`Received error when deleting tag ${tag.name}: ${error}`)
} else if (data.name) {
console.log('Deleted tag:', data)
this.setState({
tags: this.state.tags.filter(p => p.id !== tag.id),
})
} else {
alert(`Failed to delete tag '${tag.name}'.`)
}
} catch (e) {
alert(`Failed to delete tag '${tag.name}': ${e}`)
}
}

View File

@ -7,6 +7,7 @@ export async function createTask(e) {
alert("Why don't you enter a name for this task?")
return
}
const newTask = {
desc: this.state.newTask,
projectId: this.state.selectedProjectId,
@ -15,6 +16,7 @@ export async function createTask(e) {
alert('Task description is empty.')
return
}
try {
console.log('Adding task: ', newTask)
const { data, error } = await axios.post(API + '/api/tasks', newTask)
@ -45,6 +47,8 @@ export async function updateTask(e, updatedTask) {
if (error) {
alert('Received error when updating task: ', error)
} else if (data.desc && data.projectId) {
const oldTasks = this.state.tasks.filter(t => t.id !== updatedTask.id)
this.setState({ tasks: oldTasks.concat(updatedTask) })
console.log('Successfully updated task:', data)
} else {
console.log('Received malformed data when updating task: ', data)