diff --git a/staff/johnny-rojas/socialcode/api/README.MD b/staff/johnny-rojas/socialcode/api/README.MD new file mode 100644 index 000000000..827441b14 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/README.MD @@ -0,0 +1,83 @@ +- list users + +```sh +πŸ– curl http://localhost:8080/users -v +``` + +- register user + +```sh +πŸ– curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d '{"name":"Pepito","surname":"Grillo","email":"pepito@grillo.com","username":"pepitogrillo","password":"123123123","passwordRepeat":"123123123"}' -v +``` + +```js +const xhr = new XMLHttpRequest + +xhr.onload = () => { + debugger + + if (xhr.status === 201) { + console.log('user registered') + + return + } + + const { error, message } = JSON.parse(xhr.response) + + console.error(error, message) +} + +xhr.open('POST', 'http://localhost:8080/users') + +const body = {name:'Peter',surname:'Grillo',email:'pepito@grillo.com',bodyname:'pepitogrillo',password:'123123123', passwordRepeat:'123123123'} + +const json = JSON.stringify(body) + +xhr.setRequestHeader('Content-Type', 'application/json') +xhr.send(json) +``` + +- authenticate user + +```sh +πŸ– curl -X POST http://localhost:8080/users/auth -H "Content-Type: application/json" -d '{"username":"pepitogrillo","password":"123123123"}' -v +``` + +```js +const xhr = new XMLHttpRequest + +xhr.onload = () => { + debugger + + if (xhr.status === 200) { + console.log('user authenticated') + + return + } + + const { error, message } = JSON.parse(xhr.response) + + console.error(error, message) +} + +xhr.open('POST', 'http://localhost:8080/users/auth') + +const body = {username:'pepitogrillo',password:'123123123'} + +const json = JSON.stringify(body) + +xhr.setRequestHeader('Content-Type', 'application/json') +xhr.send(json) +``` + +- list posts + +```sh +πŸ– curl http://localhost:8080/posts -v +``` + +- create post + +```sh +πŸ– curl -X POST http://localhost:8080/posts -H "Authorization: Basic Flash" -H "Content-Type: application/json" -d '{"title":"blah","image":"https://upload.wikimedia.org/wikipedia/commons/1/1d/Blah_Blah_Blah.jpg","description":"blah blah"}' -v +``` \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/data/index.js b/staff/johnny-rojas/socialcode/api/data/index.js new file mode 100644 index 000000000..1044b0046 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/data/index.js @@ -0,0 +1,150 @@ +import fs from 'fs' +import { SystemError } from '../errors.js' + +const data = {} + +data.findUser = (condition, callback) => { + fs.readFile('./data/users.json', 'utf8', (error, json) => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + if (!json) json = '[]' + + const users = JSON.parse(json) + + const user = users.find(condition) + + callback(null, user) + }) +} + +data.insertUser = (user, callback) => { + fs.readFile('./data/users.json', 'utf8', (error, json) => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + if (!json) json = '[]' + + const users = JSON.parse(json) + + users.push(user) + + const newJson = JSON.stringify(users) + + fs.writeFile('./data/users.json', newJson, error => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + callback(null) + }) + }) +} + +data.findPosts = (condition, callback) => { + fs.readFile('./data/posts.json', 'utf8', (error, json) => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + if (!json) json = '[]' + + const posts = JSON.parse(json) + + const filtered = posts.filter(condition) + + callback(null, filtered) + }) +} + +data.findPost = (condition, callback) => { + fs.readFile('./data/posts.json', 'utf8', (error, json) => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + if (!json) json = '[]' + + const posts = JSON.parse(json) + + const post = posts.find(condition) + + callback(null, post) + }) +} + +data.insertPost = (post, callback) => { + fs.readFile('./data/posts.json', 'utf8', (error, json) => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + if (!json) json = '[]' + + const posts = JSON.parse(json) + + post.id = `${Math.random().toString().slice(2)}-${Date.now()}` + + posts.push(post) + + const newJson = JSON.stringify(posts) + + fs.writeFile('./data/posts.json', newJson, error => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + callback(null) + }) + }) +} + +data.deletePost = (condition, callback) => { + fs.readFile('./data/posts.json', 'utf8', (error, json) => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + if (!json) json = '[]' + + const posts = JSON.parse(json) + + const index = posts.findIndex(condition) + + if (index > -1) { + posts.splice(index, 1) + + const newJson = JSON.stringify(posts) + + fs.writeFile('./data/posts.json', newJson, error => { + if (error) { + callback(new SystemError(error.message)) + + return + } + + callback(null) + }) + } else callback(null) + }) +} + +export default data \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/data/index.test.js b/staff/johnny-rojas/socialcode/api/data/index.test.js new file mode 100644 index 000000000..78114d7db --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/data/index.test.js @@ -0,0 +1,56 @@ +import data from './index.js' + +// data.findUser(user => user.surname === 'Grillo', (error, user) => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log(user) +// }) + +// data.insertUser({ name: 'James', surname: 'Hook', email: 'james@hook.com', username: 'jameshook', password: '123123123' }, error => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log('user inserted') +// }) + +// data.findPosts(post => post.date.includes('T19'), (error, posts) => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log(posts) +// }) + +// data.insertPost({ +// author: 'jameshook', +// title: 'smile 2', +// image: 'https://m.media-amazon.com/images/I/41xsPjrM-pL._AC_UF350,350_QL50_.jpg', description: 'hi 2', +// date: new Date().toISOString() +// }, error => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log('post inserted') +// }) + +data.deletePost(post => post.title === 'smile 2', error => { + if (error) { + console.error(error) + + return + } + + console.log('post deleted') +}) \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/data/post.json b/staff/johnny-rojas/socialcode/api/data/post.json deleted file mode 100644 index e69de29bb..000000000 diff --git a/staff/johnny-rojas/socialcode/api/data/posts.json b/staff/johnny-rojas/socialcode/api/data/posts.json new file mode 100644 index 000000000..2cdacae97 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/data/posts.json @@ -0,0 +1 @@ +[{"author":"Freakazoid","title":"Freakazoid freakazoid!","image":"https://amblin.com/wp-content/uploads/2019/09/freakazoid_1995_photo_hero.jpg","description":"Qui a coupΓ© le fromage","date":"2024-05-29T20:56:42.584Z","id":"8418584534790448-1717016202588"},{"author":"Flash","title":"CMIYC","image":"https://media.giphy.com/media/Tte4rCQALbiwAYXvPR/giphy.gif?cid=790b76112rg5mygn5f9ugptai4j8mmw22p8hbpu51upxtczb&ep=v1_gifs_search&rid=giphy.gif&ct=g","description":"Fast fast fast...","date":"2024-05-29T21:03:08.272Z","id":"6274552332766918-1717016588274"},{"author":"ValHallen","title":"Concert to nigth ⚑️🎸","image":"https://64.media.tumblr.com/b81ff95da2e4c645bc05ffccfaf80992/tumblr_ncn6pfY3VY1tzqospo1_500.gif","description":"Keep awake for the info🀘🏽","date":"2024-05-29T21:06:31.878Z","id":"1793392930363531-1717016791880"}] \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/data/user.json b/staff/johnny-rojas/socialcode/api/data/user.json deleted file mode 100644 index e69de29bb..000000000 diff --git a/staff/johnny-rojas/socialcode/api/data/users.json b/staff/johnny-rojas/socialcode/api/data/users.json new file mode 100644 index 000000000..7f7df59ed --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/data/users.json @@ -0,0 +1 @@ +[{"name":"Barry","surname":"Allen","email":"flash@rapido.com","username":"Flash","password":"1234"},{"name":"Lorem","surname":"Ipsum","email":"lorem@ipsum.com","username":"Lorem","password":"1234"},{"name":"Dexter","surname":"Douglas","email":"dexter@freak.com","username":"Freakazoid","password":"1234"},{"name":"Val","surname":"Hallen","email":"val@hallen.com","username":"ValHallen","password":"1234"}] \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/errors.js b/staff/johnny-rojas/socialcode/api/errors.js new file mode 100644 index 000000000..7e3463120 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/errors.js @@ -0,0 +1,39 @@ +class ContentError extends Error { + constructor(message) { + super(message) + + //this.name = ContentError.name + this.name = this.constructor.name + } +} + +class MatchError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class DuplicityError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class SystemError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +export { + ContentError, + MatchError, + DuplicityError, + SystemError +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/index.js b/staff/johnny-rojas/socialcode/api/index.js index e69de29bb..864a602ac 100644 --- a/staff/johnny-rojas/socialcode/api/index.js +++ b/staff/johnny-rojas/socialcode/api/index.js @@ -0,0 +1,125 @@ +import express from 'express' +import fs from 'fs' +import logic from './logic/index.js' + +const api = express() + +api.use(express.static('public')) + +const jsonBodyParser = express.json({ strict: true, type: 'application/json' }) + +api.get('/', (req, res) => res.send('Hello, World!')) + +api.post('/users', jsonBodyParser, (req, res) => { + const { name, surname, email, username, password, passwordRepeat } = req.body + + try { + logic.registerUser(name, surname, email, username, password, passwordRepeat, error => { + if (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + + return + } + + res.status(201).send() + }) + } catch (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + } +}) + +api.post('/users/auth', jsonBodyParser, (req, res) => { + const { username, password } = req.body + + try { + logic.authenticateUser(username, password, error => { + if (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + + return + } + + res.send() + }) + } catch (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + } +}) + +api.get('/users/:targetUsername', (req, res) => { + const username = req.headers.authorization.slice(6) + + const { targetUsername } = req.params + + try { + logic.getUserName(username, targetUsername, (error, name) => { + if (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + + return + } + + res.json(name) + }) + } catch (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + } +}) + +api.get('/posts', (req, res) => { + try { + logic.getAllPosts((error, posts) => { + if (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + + return + } + + res.json(posts) + }) + } catch (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + } +}) + +api.post('/posts', jsonBodyParser, (req, res) => { + const username = req.headers.authorization.slice(6) + + const { title, image, description } = req.body + + try { + logic.createPost(username, title, image, description, error => { + if (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + + return + } + + res.status(201).send() + }) + } catch (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + } +}) + +api.delete('/posts/:postId', (req, res) => { + const username = req.headers.authorization.slice(6) + + const { postId } = req.params + + try { + logic.deletePost(username, postId, error => { + if (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + + return + } + + res.status(204).send() + }) + } catch (error) { + res.status(500).json({ error: error.constructor.name, message: error.message }) + } +}) + +api.listen(8080, () => console.log('api is up')) \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/logic/index.js b/staff/johnny-rojas/socialcode/api/logic/index.js new file mode 100644 index 000000000..55e4ee4b3 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/logic/index.js @@ -0,0 +1,259 @@ +import data from '../data/index.js' +import { ContentError, DuplicityError, MatchError } from '../errors.js' + +const logic = {} + +const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +const USERNAME_REGEX = /^[\w-]+$/ +const PASSWORD_REGEX = /^[\w-$%&=\[\]\{\}\<\>\(\)]{4,}$/ + +const NAME_REGEX = /^[a-zA-Z=\[\]\{\}\<\>\(\)]{1,}$/ + +const ID_REGEX = /^[0-9]+-[0-9]+$/ + +logic.registerUser = (name, surname, email, username, password, passwordRepeat, callback) => { + if (!NAME_REGEX.test(name)) + throw new ContentError('name is not valid') + + if (!NAME_REGEX.test(surname)) + throw new ContentError('surname is not valid') + + if (!EMAIL_REGEX.test(email)) + throw new ContentError('email is not valid') + + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (!PASSWORD_REGEX.test(password)) + throw new ContentError('password is not valid') + + if (password !== passwordRepeat) + throw new MatchError('passwords don\'t match') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + data.findUser(user => user.email === email || user.username === username, (error, user) => { + if (error) { + callback(error) + + return + } + + if (user) { + callback(new DuplicityError('user already exists')) + + return + } + + const newUser = { + name: name, + surname: surname, + email: email, + username: username, + password: password + } + + data.insertUser(newUser, error => { + if (error) { + callback(error) + + return + } + + callback(null) + }) + }) +} + +logic.authenticateUser = (username, password, callback) => { + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (!PASSWORD_REGEX.test(password)) + throw new ContentError('password is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + data.findUser(user => user.username === username, (error, user) => { + if (error) { + callback(error) + + return + } + + if (!user) { + callback(new MatchError('user not found')) + + return + } + + if (user.password !== password) { + callback(new MatchError('wrong password')) + + return + } + + callback(null) + }) +} + +logic.getUserName = (username, targetUsername, callback) => { + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (!USERNAME_REGEX.test(targetUsername)) + throw new ContentError('targetUsername is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + data.findUser(user => user.username === username, (error, user) => { + if (error) { + callback(error) + + return + } + + if (!user) { + callback(new MatchError('user not found')) + + return + } + + data.findUser(user => user.username === targetUsername, (error, targetUser) => { + if (error) { + callback(error) + + return + } + + if (!targetUser) { + callback(new MatchError('targetUser not found')) + + return + } + + callback(null, targetUser.name) + }) + }) +} + +logic.getAllPosts = callback => { + data.findPosts(() => true, (error, posts) => { + if (error) { + callback(error) + + return + } + + callback(null, posts.reverse()) + }) +} + +logic.createPost = (username, title, image, description, callback) => { + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (typeof title !== 'string' || !title.length || title.length > 50) + throw new ContentError('title is not valid') + + if (typeof image !== 'string' || !image.startsWith('http')) + throw new ContentError('image is not valid') + + if (typeof description !== 'string' || !description.length || description.length > 200) + throw new ContentError('description is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + data.findUser(user => user.username === username, (error, user) => { + if (error) { + callback(error) + + return + } + + if (!user) { + callback(new MatchError('user not found')) + + return + } + + const post = { + author: username, + title, + image, + description, + date: new Date().toISOString() + } + + data.insertPost(post, error => { + if (error) { + callback(error) + + return + } + + callback(null) + }) + }) +} + +logic.deletePost = (username, postId, callback) => { + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (!ID_REGEX.test(postId)) + throw new ContentError('postId is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + data.findUser(user => user.username === username, (error, user) => { + if (error) { + callback(error) + + return + } + + if (!user) { + callback(new MatchError('user not found')) + + return + } + + data.findPost(post => post.id === postId, (error, post) => { + if (error) { + callback(error) + + return + } + + if (!post) { + callback(new MatchError('post not found')) + + return + } + + if (post.author !== username) { + callback(new MatchError('post author does not match user')) + + return + } + + data.deletePost(post => post.id === postId, error => { + if (error) { + callback(error) + + return + } + + callback(null) + }) + }) + }) +} + +export default logic \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/logic/index.test.js b/staff/johnny-rojas/socialcode/api/logic/index.test.js new file mode 100644 index 000000000..747886bd5 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/logic/index.test.js @@ -0,0 +1,71 @@ +import logic from './index.js' + +// try { +// logic.registerUser('Peter', 'Pan', 'peter@pan.com', 'peterpan', '123123123', '123123123', error => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log('user registered') +// }) +// } catch (error) { +// console.error(error) +// } + +// try { +// logic.authenticateUser('pepitogrillo', '123123123', error => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log('user authenticated') +// }) +// } catch (error) { +// console.error(error) +// } + +// try { +// logic.getAllPosts((error, posts) => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log('posts retrieved', posts) +// }) +// } catch (error) { +// console.error(error) +// } + +// try { +// logic.createPost('peterpan', 'hello world', 'https://miro.medium.com/v2/resize:fit:1024/1*OohqW5DGh9CQS4hLY5FXzA.png', 'console.log("hello world")', error => { +// if (error) { +// console.error(error) + +// return +// } + +// console.log('post created') +// }) +// } catch (error) { +// console.error(error) +// } + +try { + logic.getUserName('peterpan', 'pepitogrillo', (error, name) => { + if (error) { + console.error(error) + + return + } + + console.log('user name retrieved', name) + }) +} catch (error) { + console.error(error) +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/package.json b/staff/johnny-rojas/socialcode/api/package.json index c61a1a290..b9c1588c3 100644 --- a/staff/johnny-rojas/socialcode/api/package.json +++ b/staff/johnny-rojas/socialcode/api/package.json @@ -2,8 +2,11 @@ "name": "api", "version": "1.0.0", "description": "", + "type": "module", "main": "index.js", "scripts": { + "start": "node .", + "inspect": "node --inspect-brk .", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -12,4 +15,4 @@ "dependencies": { "express": "^4.19.2" } -} +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/component.js b/staff/johnny-rojas/socialcode/api/public/app/components/component.js new file mode 100644 index 000000000..2a6e32315 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/component.js @@ -0,0 +1,66 @@ +class Component { + constructor(tagNameOrContainer = 'div') { + if (typeof tagNameOrContainer === 'string') + this.container = document.createElement(tagNameOrContainer) + else if (tagNameOrContainer instanceof HTMLElement || tagNameOrContainer instanceof HTMLDocument) + this.container = tagNameOrContainer + else + throw new Error('tagNameOrContainer is not a tagName or container') + + this.children = [] + } + + add(child) { + if (!(child instanceof Component)) throw new TypeError('child is not component') + + this.children.push(child) + + this.container.appendChild(child.container) + } + + remove(child) { + if (!(child instanceof Component)) throw new TypeError('child is not component') + + const index = this.children.indexOf(child) + + if (index > -1) + this.children.splice(index, 1) + + if (this.container.contains(child.container)) + this.container.removeChild(child.container) + } + + removeAll() { + const children = this.children.concat() + + children.forEach(child => this.remove(child)) + } + + setText(text) { + this.container.innerText = text + } + + setId(id) { + this.container.id = id + } + + addClass(clazz) { + this.container.classList.add(clazz) + } + + removeClass(clazz) { + this.container.classList.remove(clazz) + } + + onClick(listener) { + this.container.addEventListener('click', listener) + } + + onKeyDown(listener) { + this.container.addEventListener('keydown', listener) + } + + onKeyUp(listener) { + this.container.addEventListener('keyup', listener) + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Button.css b/staff/johnny-rojas/socialcode/api/public/app/components/core/Button.css new file mode 100644 index 000000000..48eb34839 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Button.css @@ -0,0 +1,6 @@ +.Button { + padding: .4rem; + background-color: transparent; + font-size: 1rem; + border: 1px solid var(--first-color); +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Button.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Button.js new file mode 100644 index 000000000..5069cf264 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Button.js @@ -0,0 +1,13 @@ +class Button extends Component { + constructor(text) { + super('button') + + this.addClass('Button') + + if (text) this.setText(text) + } + + setType(type) { + this.container.type = type + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Field.css b/staff/johnny-rojas/socialcode/api/public/app/components/core/Field.css new file mode 100644 index 000000000..e3f02e0cd --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Field.css @@ -0,0 +1,5 @@ +.Field { + display: flex; + flex-direction: column; + margin: .25rem 0; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Field.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Field.js new file mode 100644 index 000000000..7729d4553 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Field.js @@ -0,0 +1,28 @@ +class Field extends Component { + constructor(id, type, text) { + super('div') + + this.addClass('Field') + + const label = new Label + label.setText(text) + label.setFor(id) + + const input = new Input + input.setId(id) + input.setType(type) + + this.add(label) + this.add(input) + } + + setPlaceholder(placeholder) { + this.children[1].setPlaceholder(placeholder) + } + + getValue() { + const input = this.children[1] + + return input.getValue() + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Form.css b/staff/johnny-rojas/socialcode/api/public/app/components/core/Form.css new file mode 100644 index 000000000..ba571f556 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Form.css @@ -0,0 +1,5 @@ +.Form { + display: flex; + flex-direction: column; + align-items: center; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Form.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Form.js new file mode 100644 index 000000000..eb056648a --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Form.js @@ -0,0 +1,15 @@ +class Form extends Component { + constructor() { + super('form') + + this.addClass('Form') + } + + onSubmit(listener) { + this.container.addEventListener('submit', listener) + } + + clear() { + this.container.reset() + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Heading.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Heading.js new file mode 100644 index 000000000..d9126f291 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Heading.js @@ -0,0 +1,5 @@ +class Heading extends Component { + constructor(level) { + super('h' + level) + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Image.css b/staff/johnny-rojas/socialcode/api/public/app/components/core/Image.css new file mode 100644 index 000000000..ec2796226 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Image.css @@ -0,0 +1,3 @@ +.Image { + width: 100%; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Image.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Image.js new file mode 100644 index 000000000..bb7e94d16 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Image.js @@ -0,0 +1,15 @@ +class Image extends Component { + constructor() { + super('img') + + this.addClass('Image') + } + + setUrl(url) { + this.container.src = url + } + + setAltText(altText) { + this.container.alt = altText + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Input.css b/staff/johnny-rojas/socialcode/api/public/app/components/core/Input.css new file mode 100644 index 000000000..a39cb75dd --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Input.css @@ -0,0 +1,6 @@ +.Input { + padding: .4rem; + background-color: transparent; + font-size: 1rem; + border: 1px solid var(--first-color); +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Input.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Input.js new file mode 100644 index 000000000..0e3879a59 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Input.js @@ -0,0 +1,19 @@ +class Input extends Component { + constructor() { + super('input') + + this.addClass('Input') + } + + setType(type) { + this.container.type = type + } + + setPlaceholder(placeholder) { + this.container.placeholder = placeholder + } + + getValue() { + return this.container.value + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Label.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Label.js new file mode 100644 index 000000000..709e341d1 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Label.js @@ -0,0 +1,9 @@ +class Label extends Component { + constructor() { + super('label') + } + + setFor(id) { + this.container.htmlFor = id + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/Link.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/Link.js new file mode 100644 index 000000000..22ba7745e --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/Link.js @@ -0,0 +1,15 @@ +class Link extends Component { + constructor() { + super('a') + + this.setUrl('') + } + + setUrl(url) { + this.container.href = url + } + + setTarget(target) { + this.container.target = target + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/SubmitButton.css b/staff/johnny-rojas/socialcode/api/public/app/components/core/SubmitButton.css new file mode 100644 index 000000000..9d84fe380 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/SubmitButton.css @@ -0,0 +1,3 @@ +.SubmitButton { + margin: .5rem 0; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/core/SubmitButton.js b/staff/johnny-rojas/socialcode/api/public/app/components/core/SubmitButton.js new file mode 100644 index 000000000..7c2b361c8 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/core/SubmitButton.js @@ -0,0 +1,10 @@ +class SubmitButton extends Button { + constructor(text) { + super() + + this.addClass('SubmitButton') + + this.setType('submit') + this.setText(text) + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/library/FormWithFeedback.css b/staff/johnny-rojas/socialcode/api/public/app/components/library/FormWithFeedback.css new file mode 100644 index 000000000..be2db1a2f --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/library/FormWithFeedback.css @@ -0,0 +1,11 @@ +.FormWithFeedback { + padding: 1rem; +} + +.FormWithFeedback .Feedback { + color: tomato; +} + +.FormWithFeedback .Feedback.success { + color: greenyellow; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/components/library/FormWithFeedback.js b/staff/johnny-rojas/socialcode/api/public/app/components/library/FormWithFeedback.js new file mode 100644 index 000000000..8394bb9e7 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/components/library/FormWithFeedback.js @@ -0,0 +1,30 @@ +class FormWithFeedback extends Form { + constructor() { + super() + + this.addClass('FormWithFeedback') + + const feedbackPanel = new Component('p') + feedbackPanel.addClass('Feedback') + + this.feedbackPanel = feedbackPanel + } + + setFeedback(message, level) { + if (level === 'success') + this.feedbackPanel.addClass('success') + + this.feedbackPanel.setText(message) + + this.add(this.feedbackPanel) + } + + clear() { + super.clear() + + this.feedbackPanel.setText('') + this.feedbackPanel.removeClass('success') + + this.remove(this.feedbackPanel) + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/data.js b/staff/johnny-rojas/socialcode/api/public/app/data.js new file mode 100644 index 000000000..02a1b5b6c --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/data.js @@ -0,0 +1,73 @@ +const data = {} + +data.findUser = callback => { + let usersJson = localStorage.users + + if (!usersJson) usersJson = '[]' + + const users = JSON.parse(usersJson) + + const user = users.find(callback) + + return user +} + +data.insertUser = user => { + let usersJson = localStorage.users + + if (!usersJson) usersJson = '[]' + + const users = JSON.parse(usersJson) + + users.push(user) + + usersJson = JSON.stringify(users) + + localStorage.users = usersJson +} + +data.findPosts = callback => { + let postsJson = localStorage.posts + + if (!postsJson) postsJson = '[]' + + const posts = JSON.parse(postsJson) + + const filtered = posts.filter(callback) + + return filtered +} + +data.insertPost = post => { + let postsJson = localStorage.posts + + if (!postsJson) postsJson = '[]' + + const posts = JSON.parse(postsJson) + + post.id = `${Math.random().toString().slice(2)}-${Date.now()}` + + posts.push(post) + + postsJson = JSON.stringify(posts) + + localStorage.posts = postsJson +} + +data.deletePost = callback => { + let postsJson = localStorage.posts + + if (!postsJson) postsJson = '[]' + + const posts = JSON.parse(postsJson) + + const index = posts.findIndex(callback) + + if (index > -1) { + posts.splice(index, 1) + + postsJson = JSON.stringify(posts) + + localStorage.posts = postsJson + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/errors.js b/staff/johnny-rojas/socialcode/api/public/app/errors.js new file mode 100644 index 000000000..33bd07fe1 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/errors.js @@ -0,0 +1,39 @@ +class ContentError extends Error { + constructor(message) { + super(message) + + //this.name = ContentError.name + this.name = this.constructor.name + } +} + +class MatchError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class DuplicityError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class SystemError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +const errors = { + ContentError, + MatchError, + DuplicityError, + SystemError +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/global.css b/staff/johnny-rojas/socialcode/api/public/app/global.css new file mode 100644 index 000000000..2e65f97d5 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/global.css @@ -0,0 +1,21 @@ +@import url('https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap'); + +:root { + --first-color: #18171C; + --second-color: #FEFFFA; +} + +* { + font-family: Raleway; + color: var(--first-color); +} + +body { + background-color: var(--second-color); + margin: 0; +} + +Button { + border-radius: 10%; + margin: 3px; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/components/Confirm.css b/staff/johnny-rojas/socialcode/api/public/app/home/components/Confirm.css new file mode 100644 index 000000000..fd1b97804 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/components/Confirm.css @@ -0,0 +1,11 @@ +.Confirm { + padding: .4rem; + background-color: var(--second-color); + font-size: 1rem; + border: 1px solid var(--first-color); + position: fixed; + top: 0; + left: 0; + width: 200px; + height: 100px; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/components/Confirm.js b/staff/johnny-rojas/socialcode/api/public/app/home/components/Confirm.js new file mode 100644 index 000000000..932e0c52d --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/components/Confirm.js @@ -0,0 +1,35 @@ +class Confirm extends Component { + constructor() { + super('div') // TODO make it a dialog + + this.addClass('Confirm') + + const question = new Component('p') + + this.question = question + + const cancelButton = new Button('Cancel') + + cancelButton.onClick(() => this.onCancelListener()) + + const confirmButton = new Button('Confirm') + + confirmButton.onClick(() => this.onConfirmListener()) + + this.add(question) + this.add(cancelButton) + this.add(confirmButton) + } + + setText(text) { + this.question.setText(text) + } + + onCancel(listener) { + this.onCancelListener = listener + } + + onConfirm(listener) { + this.onConfirmListener = listener + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/components/CreatePostForm.css b/staff/johnny-rojas/socialcode/api/public/app/home/components/CreatePostForm.css new file mode 100644 index 000000000..42bbe3cb6 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/components/CreatePostForm.css @@ -0,0 +1,17 @@ +.CreatePostForm { + position: fixed; + bottom: 3rem; + width: 55%; + left: 50%; + transform: translateX(-50%); + box-sizing: border-box; + background-color: rgba(255, 255, 255, 0.5); + backdrop-filter: blur(10px); + padding: 1rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.Cancel{ + border: 0 !important; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/components/CreatePostForm.js b/staff/johnny-rojas/socialcode/api/public/app/home/components/CreatePostForm.js new file mode 100644 index 000000000..ba6deafbe --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/components/CreatePostForm.js @@ -0,0 +1,93 @@ +class CreatePostForm extends FormWithFeedback { + constructor() { + super() + + this.addClass('CreatePostForm') + + const titleField = new Field('title', 'text', 'Title') + titleField.setPlaceholder('title') + + const imageField = new Field('image', 'text', 'Image') + imageField.setPlaceholder('image') + + const descriptionField = new Field('description', 'text', 'Description') + descriptionField.setPlaceholder('description') + + const cancelButton = new Button('Cancel') + cancelButton.addClass('Cancel') + cancelButton.setType('button') + + cancelButton.onClick(event => { + event.preventDefault() + + this.clear() + + this.onCancelClickListener() + }) + + + const submitButton = new SubmitButton('Create') + + this.add(titleField) + this.add(imageField) + this.add(descriptionField) + this.add(cancelButton) + this.add(submitButton) + + this.onSubmit(event => { + event.preventDefault() + + const title = this.getTitle() + const image = this.getImage() + const description = this.getDescription() + + try { + logic.createPost(title, image, description, error => { + if (error) { + if (error instanceof ContentError) + this.setFeedback(error.message + ', please, correct it') + else + this.setFeedback('sorry, there was an error, please try again later') + + return + } + + this.clear() + + this.onPostCreatedListener() + }) + } catch (error) { + if (error instanceof ContentError) + this.setFeedback(error.message + ', please, correct it') + else + this.setFeedback('sorry, there was an error, please try again later') + } + }) + } + + getTitle() { + const titleField = this.children[0] + + return titleField.getValue() + } + + getImage() { + const imageField = this.children[1] + + return imageField.getValue() + } + + getDescription() { + const descriptionField = this.children[2] + + return descriptionField.getValue() + } + + onCancelClick(listener) { + this.onCancelClickListener = listener + } + + onPostCreated(listener) { + this.onPostCreatedListener = listener + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/components/Post.js b/staff/johnny-rojas/socialcode/api/public/app/home/components/Post.js new file mode 100644 index 000000000..1ca392dd4 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/components/Post.js @@ -0,0 +1,61 @@ +class Post extends Component { + constructor(post) { + super('article') + + const authorTitle = new Component('p') + authorTitle.setText(post.author) + + const postTitle = new Component('h2') + postTitle.setText(post.title) + + const postImage = new Image + postImage.setUrl(post.image) + + const postDescription = new Component('p') + postDescription.setText(post.description) + + const postDate = new Component('time') + postDate.setText(post.date) + + this.add(authorTitle) + this.add(postTitle) + this.add(postImage) + this.add(postDescription) + this.add(postDate) + + if (post.author === logic.getLoggedInUsername()) { + const deleteButton = new Button('Delete') + + deleteButton.onClick(() => { + const confirm = new Confirm + confirm.setText('Delete the post?') + + confirm.onConfirm(() => { + try { + logic.deletePost(post.id, error => { + if (error) { + alert(error.message) + + return + } + + this.onPostDeletedListener() + }) + } catch (error) { + alert(error.message) + } + }) + + confirm.onCancel(() => this.remove(confirm)) + + this.add(confirm) + }) + + this.add(deleteButton) + } + } + + onPostDeleted(listener) { + this.onPostDeletedListener = listener + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/components/PostList.js b/staff/johnny-rojas/socialcode/api/public/app/home/components/PostList.js new file mode 100644 index 000000000..64fd5276c --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/components/PostList.js @@ -0,0 +1,37 @@ +class PostList extends Component { + constructor() { + super('section') + + this.load() + } + + load() { + this.removeAll() + + try { + logic.getAllPosts((error, posts) => { + if (error) { + console.error(error) + + // TODO show feedback to the user in a better way + alert(error.message) + + return + } + + posts.forEach(post => { + const post2 = new Post(post) + + post2.onPostDeleted(() => this.load()) + + this.add(post2) + }) + }) + } catch (error) { + console.error(error) + + // TODO show feedback to the user in a better way + alert(error.message) + } + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/index.css b/staff/johnny-rojas/socialcode/api/public/app/home/index.css new file mode 100644 index 000000000..0164285ff --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/index.css @@ -0,0 +1,87 @@ +.View { + display: flex; + flex-direction: column; + align-items: center; + margin: 4rem 2rem; +} + +.Header { + display: flex; + gap: 1rem; + width: 100%; + justify-content: flex-end; + position: fixed; + top: 0; + padding: .5rem; + box-sizing: border-box; + border-bottom: 1px solid var(--first-color); + background-color: var(--second-color); +} + +article { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + padding: 2rem 0; +} + +.LogoutButton { + border: 0 !important; + text-decoration: underline; +} + +.Post { + width: 100%; + + max-width: 600px; + + margin-bottom: 1rem; + +} + +.Delete { + border: 0 !important; +} + + +.Footer { + width: 100%; + display: flex; + justify-content: center; + position: fixed; + bottom: 0; + padding: .5rem 0; + border-top: 1px solid var(--first-color); + background-color: var(--second-color); +} + +.Main { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + + padding-top: 4rem; + + padding-bottom: 4rem; + +} + + +.Image { + width: 100%; + + max-width: 55%; + +} + + +.Author { + text-decoration: underline; +} + +.Header .HeaderSC { + margin-right: 505px; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/index.html b/staff/johnny-rojas/socialcode/api/public/app/home/index.html new file mode 100644 index 000000000..7ded2c0e6 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/index.html @@ -0,0 +1,51 @@ + + + + + + + Home + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/home/index.js b/staff/johnny-rojas/socialcode/api/public/app/home/index.js new file mode 100644 index 000000000..472575f06 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/home/index.js @@ -0,0 +1,63 @@ +if (!logic.isUserLoggedIn()) + location.href = '../login' + +const view = new Component(document.body) +view.addClass('View') + +const header = new Component('header') +header.addClass('Header') + +view.add(header) + +logic.getUserName((error, userName) => { + if (error) { + alert(error.message) + + return + } + + usernameTitle.setText(userName) +}) + +const usernameTitle = new Heading(3) +header.add(usernameTitle) + +const logoutButton = new Button +logoutButton.setText('Logout') + +logoutButton.onClick(() => { + logic.logoutUser() + + location.href = '../login' +}) + +header.add(logoutButton) + +const main = new Component('main') + +view.add(main) + +const postList = new PostList +main.add(postList) + +const createPostForm = new CreatePostForm + +createPostForm.onPostCreated(() => { + main.remove(createPostForm) + + postList.load() +}) + +createPostForm.onCancelClick(() => main.remove(createPostForm)) + +const footer = new Component('footer') +footer.addClass('Footer') + +view.add(footer) + +const addPostButton = new Button +addPostButton.setText('+') + +addPostButton.onClick(() => main.add(createPostForm)) + +footer.add(addPostButton) \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/logic.js b/staff/johnny-rojas/socialcode/api/public/app/logic.js new file mode 100644 index 000000000..0d1455749 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/logic.js @@ -0,0 +1,230 @@ +const logic = {} + +const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +const USERNAME_REGEX = /^[\w-]+$/ +const PASSWORD_REGEX = /^[\w-$%&=\[\]\{\}\<\>\(\)]{4,}$/ + +const NAME_REGEX = /^[a-zA-Z=\[\]\{\}\<\>\(\)]{1,}$/ + +const ID_REGEX = /^[0-9]+-[0-9]+$/ + +logic.registerUser = (name, surname, email, username, password, passwordRepeat, callback) => { + if (!NAME_REGEX.test(name)) + throw new ContentError('name is not valid') + + if (!NAME_REGEX.test(surname)) + throw new ContentError('surname is not valid') + + if (!EMAIL_REGEX.test(email)) + throw new ContentError('email is not valid') + + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (!PASSWORD_REGEX.test(password)) + throw new ContentError('password is not valid') + + if (password !== passwordRepeat) + throw new MatchError('passwords don\'t match') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + const xhr = new XMLHttpRequest + + xhr.onload = () => { + if (xhr.status === 201) { + callback(null) + + return + } + + const { error, message } = JSON.parse(xhr.response) + + const constructor = errors[error] + + callback(new constructor(message)) + } + + xhr.open('POST', 'http://localhost:8080/users') + + const body = { name, surname, email, username, password, passwordRepeat } + + const json = JSON.stringify(body) + + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(json) +} + +logic.loginUser = (username, password, callback) => { + if (!USERNAME_REGEX.test(username)) + throw new ContentError('username is not valid') + + if (!PASSWORD_REGEX.test(password)) + throw new ContentError('password is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + const xhr = new XMLHttpRequest + + xhr.onload = () => { + if (xhr.status === 200) { + sessionStorage.username = username + + callback(null) + + return + } + + const { error, message } = JSON.parse(xhr.response) + + const constructor = errors[error] + + callback(new constructor(message)) + } + + xhr.open('POST', 'http://localhost:8080/users/auth') + + const body = { username, password } + + const json = JSON.stringify(body) + + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(json) +} + +logic.isUserLoggedIn = () => !!sessionStorage.username + +logic.logoutUser = () => delete sessionStorage.username + +logic.getUserName = callback => { + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + const xhr = new XMLHttpRequest + + xhr.onload = () => { + if (xhr.status === 200) { + const name = JSON.parse(xhr.response) + + callback(null, name) + + return + } + + const { error, message } = JSON.parse(xhr.response) + + const constructor = errors[error] + + callback(new constructor(message)) + } + + xhr.open('GET', `http://localhost:8080/users/${sessionStorage.username}`) + + xhr.setRequestHeader('Authorization', `Basic ${sessionStorage.username}`) + xhr.send() +} + +logic.getAllPosts = callback => { + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + const xhr = new XMLHttpRequest + + xhr.onload = () => { + if (xhr.status === 200) { + const posts = JSON.parse(xhr.response) + + callback(null, posts) + + return + } + + const { error, message } = JSON.parse(xhr.response) + + const constructor = errors[error] + + callback(new constructor(message)) + } + + xhr.open('GET', 'http://localhost:8080/posts') + + xhr.send() +} + +logic.createPost = (title, image, description, callback) => { + if (typeof title !== 'string' || !title.length || title.length > 50) + throw new ContentError('title is not valid') + + if (typeof image !== 'string' || !image.startsWith('http')) + throw new ContentError('image is not valid') + + if (typeof description !== 'string' || !description.length || description.length > 200) + throw new ContentError('description is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + const xhr = new XMLHttpRequest + + xhr.onload = () => { + if (xhr.status === 201) { + callback(null) + + return + } + + const { error, message } = JSON.parse(xhr.response) + + const constructor = errors[error] + + callback(new constructor(message)) + } + + xhr.open('POST', 'http://localhost:8080/posts') + + xhr.setRequestHeader('Authorization', `Basic ${sessionStorage.username}`) + + const body = { + title, + image, + description + } + + const json = JSON.stringify(body) + + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(json) +} + +logic.getLoggedInUsername = () => sessionStorage.username + +logic.deletePost = (postId, callback) => { + if (!ID_REGEX.test(postId)) + throw new ContentError('postId is not valid') + + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') + + const xhr = new XMLHttpRequest + + xhr.onload = () => { + if (xhr.status === 204) { + callback(null) + + return + } + + const { error, message } = JSON.parse(xhr.response) + + const constructor = errors[error] + + callback(new constructor(message)) + } + + xhr.open('DELETE', `http://localhost:8080/posts/${postId}`) + + xhr.setRequestHeader('Authorization', `Basic ${sessionStorage.username}`) + xhr.send() +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/login/LoginForm.js b/staff/johnny-rojas/socialcode/api/public/app/login/LoginForm.js new file mode 100644 index 000000000..e8a17d473 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/login/LoginForm.js @@ -0,0 +1,63 @@ +class LoginForm extends FormWithFeedback { + constructor() { + super() + + this.addClass('LoginForm') + + const usernameField = new Field('username', 'text', 'Username') + + const passwordField = new Field('password', 'password', 'Password') + + const submitButton = new SubmitButton('Login') + + this.add(usernameField) + this.add(passwordField) + this.add(submitButton) + + this.onSubmit(event => { + event.preventDefault() + + const username = this.getUsername() + const password = this.getPassword() + + try { + logic.loginUser(username, password, error => { + if (error) { + this.setFeedback(error.message + ', please, correct it') + + return + } + + this.clear() + + this.setFeedback('user successfully logged in', 'success') + + this.onLoggedInListener() + }) + } catch (error) { + if (error instanceof ContentError) + this.setFeedback(error.message + ', please, correct it') + else if (error instanceof MatchError) + this.setFeedback('wrong credentials') + else + this.setFeedback('sorry, there was an error, please try again later') + } + }) + } + + getUsername() { + const usernameField = this.children[0] + + return usernameField.getValue() + } + + getPassword() { + const passwordField = this.children[1] + + return passwordField.getValue() + } + + onLoggedIn(listener) { + this.onLoggedInListener = listener + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/login/index.css b/staff/johnny-rojas/socialcode/api/public/app/login/index.css new file mode 100644 index 000000000..9eab357e2 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/login/index.css @@ -0,0 +1,5 @@ +.View { + display: flex; + flex-direction: column; + align-items: center; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/login/index.html b/staff/johnny-rojas/socialcode/api/public/app/login/index.html new file mode 100644 index 000000000..fd5f50fab --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/login/index.html @@ -0,0 +1,46 @@ + + + + + + + Login + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/login/index.js b/staff/johnny-rojas/socialcode/api/public/app/login/index.js new file mode 100644 index 000000000..1ed75970f --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/login/index.js @@ -0,0 +1,24 @@ +if (logic.isUserLoggedIn()) + location.href = '../home' + +const view = new Component(document.body) +view.addClass('View') + +const title = new Heading(1) +title.setText('Login') + +const loginForm = new LoginForm + +loginForm.onLoggedIn(() => setTimeout(() => location.href = '../home', 0)) + +const registerLink = new Link +registerLink.setText('Register') +registerLink.onClick(event => { + event.preventDefault() + + setTimeout(() => location.href = '../register', 0) +}) + +view.add(title) +view.add(loginForm) +view.add(registerLink) \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/register/RegisterForm.js b/staff/johnny-rojas/socialcode/api/public/app/register/RegisterForm.js new file mode 100644 index 000000000..dadfbbe7e --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/register/RegisterForm.js @@ -0,0 +1,111 @@ +class RegisterForm extends FormWithFeedback { + constructor() { + super() + + this.addClass('RegisterForm') + + const nameField = new Field('name', 'text', 'Name') + nameField.setPlaceholder('name') + + const surnameField = new Field('surname', 'text', 'Surname') + surnameField.setPlaceholder('surname') + + const emailField = new Field('email', 'email', 'E-mail') + emailField.setPlaceholder('name@example.com') + + const usernameField = new Field('username', 'text', 'Username') + usernameField.setPlaceholder('username') + + const passwordField = new Field('password', 'password', 'Password') + passwordField.setPlaceholder('password') + + const passwordRepeatField = new Field('password', 'password', 'Password repeat') + passwordRepeatField.setPlaceholder('repeat password') + + const submitButton = new SubmitButton('Register') + + this.add(nameField) + this.add(surnameField) + this.add(emailField) + this.add(usernameField) + this.add(passwordField) + this.add(passwordRepeatField) + this.add(submitButton) + + this.onSubmit(event => { + event.preventDefault() + + const name = this.getName() + const surname = this.getSurname() + const email = this.getEmail() + const username = this.getUsername() + const password = this.getPassword() + const passwordRepeat = this.getPasswordRepeat() + + try { + logic.registerUser(name, surname, email, username, password, passwordRepeat, error => { + if (error) { + this.setFeedback(error.message + ', please, correct it') + + return + } + + this.clear() + + this.setFeedback('user successfully registered', 'success') + + this.onRegisteredListener() + }) + } catch (error) { + if (error instanceof ContentError) + this.setFeedback(error.message + ', please, correct it') + else if (error instanceof MatchError) + this.setFeedback(error.message + ', please, retype them') + else if (error instanceof DuplicityError) + this.setFeedback(error.message + ', please, enter new one') + else + this.setFeedback('sorry, there was an error, please try again later') + } + }) + } + + getName() { + const nameField = this.children[0] + + return nameField.getValue() + } + + getSurname() { + const surnameField = this.children[1] + + return surnameField.getValue() + } + + getEmail() { + const emailField = this.children[2] + + return emailField.getValue() + } + + getUsername() { + const usernameField = this.children[3] + + return usernameField.getValue() + } + + getPassword() { + const passwordField = this.children[4] + + return passwordField.getValue() + } + + getPasswordRepeat() { + const passwordFieldRepeat = this.children[5] + + return passwordFieldRepeat.getValue() + } + + onRegistered(listener) { + this.onRegisteredListener = listener + } +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/register/index.css b/staff/johnny-rojas/socialcode/api/public/app/register/index.css new file mode 100644 index 000000000..9eab357e2 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/register/index.css @@ -0,0 +1,5 @@ +.View { + display: flex; + flex-direction: column; + align-items: center; +} \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/register/index.html b/staff/johnny-rojas/socialcode/api/public/app/register/index.html new file mode 100644 index 000000000..0f43d0648 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/register/index.html @@ -0,0 +1,46 @@ + + + + + + + Register + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/staff/johnny-rojas/socialcode/api/public/app/register/index.js b/staff/johnny-rojas/socialcode/api/public/app/register/index.js new file mode 100644 index 000000000..2a12bc393 --- /dev/null +++ b/staff/johnny-rojas/socialcode/api/public/app/register/index.js @@ -0,0 +1,24 @@ +if (logic.isUserLoggedIn()) + location.href = '../home' + +const view = new Component(document.body) +view.addClass('View') + +const title = new Heading(1) +title.setText('Register') +title.onClick(() => alert('By clicking on this title you wont get anything .P')) + +const registerForm = new RegisterForm +registerForm.onRegistered(() => setTimeout(() => location.href = '../login', 1000)) + +const loginLink = new Link +loginLink.setText('Login') +loginLink.onClick(event => { + event.preventDefault() + + setTimeout(() => location.href = '../login', 500) +}) + +view.add(title) +view.add(registerForm) +view.add(loginLink) \ No newline at end of file