WIP
This commit is contained in:
committed by
Ulises Gascon
parent
91ba1f5054
commit
9dd233c371
@@ -0,0 +1 @@
|
||||
{ "presets": ["@babel/preset-env"] }
|
||||
@@ -0,0 +1 @@
|
||||
20.11.0
|
||||
@@ -0,0 +1,10 @@
|
||||
[{
|
||||
"id": 1,
|
||||
"message": "Hello World! This is my first Whisper. Yay! 🎉"
|
||||
}, {
|
||||
"id": 2,
|
||||
"message": "It is raining now... 🌧️"
|
||||
}, {
|
||||
"id": 3,
|
||||
"message": "I am learning Node.js and I love it! 🙌"
|
||||
}]
|
||||
@@ -0,0 +1,7 @@
|
||||
import { app } from './server.js'
|
||||
|
||||
const port = 3000
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Running in http://localhost:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
modulePathIgnorePatterns: ['<rootDir>/node_test/']
|
||||
}
|
||||
Generated
+9943
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "nodejs-for-beginners",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"standard": {
|
||||
"env": [ "jest" ],
|
||||
"ignore": [ "public/*.js" ]
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.2",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.24.1",
|
||||
"jest": "^29.7.0",
|
||||
"standard": "^17.1.0",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// == Selectors ==
|
||||
const whispers = document.getElementById('whispers')
|
||||
const whisperCreateButton = document.getElementById('whisper-create')
|
||||
|
||||
// == Event Listeners ==
|
||||
whispers.addEventListener('click', event => {
|
||||
if(event.target.tagName === 'BUTTON') {
|
||||
const button = event.target
|
||||
const article = event.target.closest('article')
|
||||
const action = button.dataset.action
|
||||
const id = article.dataset.id
|
||||
const message = article.querySelector('p').innerText
|
||||
if(action === 'edit') requestUserEdit(id, message)
|
||||
if(action === 'delete') requestUserDelete(id)
|
||||
}
|
||||
})
|
||||
|
||||
whisperCreateButton.addEventListener('click', event => {
|
||||
const message = prompt("What's your whisper?")
|
||||
if(message) {
|
||||
createWhisper(message)
|
||||
.then(refreshAllUI)
|
||||
}
|
||||
})
|
||||
|
||||
// === Functions ==
|
||||
// -- API --
|
||||
const fetchAllWhispers = () => fetch('http://localhost:3000/api/v1/whisper').then((response) => response.json())
|
||||
const deleteWhisper = (id) => fetch(`http://localhost:3000/api/v1/whisper/${id}`, { method: 'DELETE' })
|
||||
const updateWhisper = (id, message) => fetch(`http://localhost:3000/api/v1/whisper/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) })
|
||||
const createWhisper = (message) => fetch('http://localhost:3000/api/v1/whisper', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) })
|
||||
|
||||
// -- UI --
|
||||
const refreshWhispers = data => whispers.innerHTML = data
|
||||
.reverse()
|
||||
.map(whisper => {
|
||||
return `
|
||||
<article data-id="${whisper.id}">
|
||||
<div class="actions">
|
||||
<button data-action="edit">✏️</button>
|
||||
<button data-action="delete">❌</button>
|
||||
</div>
|
||||
<p>${whisper.message}</p>
|
||||
</article>`
|
||||
}).join('')
|
||||
|
||||
const refreshAllUI = () => fetchAllWhispers().then(refreshWhispers)
|
||||
|
||||
const requestUserEdit = (id, message) => {
|
||||
const newMessage = prompt("Edit the Whisper", message);
|
||||
if(newMessage && newMessage !== message) {
|
||||
updateWhisper(id, newMessage)
|
||||
.then(refreshAllUI)
|
||||
}
|
||||
console.log("Request User Edit", id, message)
|
||||
}
|
||||
const requestUserDelete = (id) => {
|
||||
const confirmation = confirm("Are you sure you want to delete this whisper?");
|
||||
if(confirmation) {
|
||||
deleteWhisper(id)
|
||||
.then(refreshAllUI)
|
||||
}
|
||||
}
|
||||
|
||||
// == Initialization ==
|
||||
refreshAllUI()
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Whispering | Home</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="menu">
|
||||
<ul>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="#">Whispering</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<button type="button" class="block" id="whisper-create">Spread a whisper 🤭</button>
|
||||
<div id="whispers"></div>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -0,0 +1,110 @@
|
||||
html,
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.menu ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: #343e8b;
|
||||
}
|
||||
|
||||
.menu li {
|
||||
float: right;
|
||||
margin-right: 4%;
|
||||
font-weight: bold;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.menu li:last-child {
|
||||
float: left;
|
||||
margin-left: 4%;
|
||||
}
|
||||
|
||||
.menu a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
figcaption {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#whispers {
|
||||
border-top: 1px solid #ccc;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
article {
|
||||
background: #f8f6f6;
|
||||
border: 2px solid #343e8b;
|
||||
margin: 1.5em 10px;
|
||||
padding: 0 10px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
article p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
article .actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
article .actions > button {
|
||||
cursor: pointer;
|
||||
margin: 3px;
|
||||
border: none;
|
||||
background-color: #f8f6f6;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
width: 60%;
|
||||
border: none;
|
||||
background-color: #343e8b;
|
||||
padding: 0.5em 10px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import express from 'express'
|
||||
import bodyParser from 'body-parser'
|
||||
import { getAll, getById, create, updateById, deleteById } from './store.js'
|
||||
|
||||
const app = express()
|
||||
app.use(express.static('public'))
|
||||
app.use(bodyParser.json())
|
||||
app.set('view engine', 'ejs')
|
||||
|
||||
app.get('/about', async (req, res) => {
|
||||
const whispers = await getAll()
|
||||
res.render('about', { whispers })
|
||||
})
|
||||
|
||||
app.get('/api/v1/whisper', async (req, res) => {
|
||||
const whispers = await getAll()
|
||||
res.json(whispers)
|
||||
})
|
||||
|
||||
app.get('/api/v1/whisper/:id', async (req, res) => {
|
||||
const id = parseInt(req.params.id)
|
||||
const whisper = await getById(id)
|
||||
if (!whisper) {
|
||||
res.sendStatus(404)
|
||||
} else {
|
||||
res.json(whisper)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/v1/whisper', async (req, res) => {
|
||||
const { message } = req.body
|
||||
if (!message) {
|
||||
res.sendStatus(400)
|
||||
} else {
|
||||
const whisper = await create(message)
|
||||
res.status(201).json(whisper)
|
||||
}
|
||||
})
|
||||
|
||||
app.put('/api/v1/whisper/:id', async (req, res) => {
|
||||
const { message } = req.body
|
||||
const id = parseInt(req.params.id)
|
||||
|
||||
if (!message) {
|
||||
res.sendStatus(400)
|
||||
} else {
|
||||
const whisper = await getById(id)
|
||||
if (!whisper) {
|
||||
res.sendStatus(404)
|
||||
} else {
|
||||
await updateById(id, message)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.delete('/api/v1/whisper/:id', async (req, res) => {
|
||||
const id = parseInt(req.params.id)
|
||||
const whisper = await getById(id)
|
||||
|
||||
if (!whisper) {
|
||||
res.sendStatus(404)
|
||||
return
|
||||
}
|
||||
|
||||
await deleteById(id)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
export { app }
|
||||
@@ -0,0 +1,46 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const filename = path.join(process.cwd(), 'db.json')
|
||||
|
||||
const saveChanges = data => fs.writeFile(filename, JSON.stringify(data))
|
||||
|
||||
const readData = async () => {
|
||||
const data = await fs.readFile(filename, 'utf-8')
|
||||
return JSON.parse(data)
|
||||
}
|
||||
|
||||
const getAll = readData
|
||||
|
||||
const getById = async (id) => {
|
||||
const data = await readData()
|
||||
return data.find(item => item.id === id)
|
||||
}
|
||||
|
||||
const create = async (message) => {
|
||||
const data = await readData()
|
||||
const newItem = { message, id: data.length + 1 }
|
||||
await saveChanges(data.concat([newItem]))
|
||||
return newItem
|
||||
}
|
||||
|
||||
const updateById = async (id, message) => {
|
||||
const data = await readData()
|
||||
const newData = data.map(current => {
|
||||
if (current.id === id) {
|
||||
return { ...current, message }
|
||||
}
|
||||
return current
|
||||
})
|
||||
|
||||
await saveChanges(newData)
|
||||
}
|
||||
|
||||
const deleteById = async id => {
|
||||
const data = await readData()
|
||||
await saveChanges(data
|
||||
.filter(current => current.id !== id)
|
||||
)
|
||||
}
|
||||
|
||||
export { getAll, getById, create, updateById, deleteById }
|
||||
@@ -0,0 +1,9 @@
|
||||
const whispers = [{ id: 1, message: 'test' }, { id: 2, message: 'hello world' }]
|
||||
const inventedId = 12345
|
||||
const existingId = whispers[0].id
|
||||
|
||||
export {
|
||||
whispers,
|
||||
inventedId,
|
||||
existingId
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import supertest from 'supertest'
|
||||
import { app } from '../server'
|
||||
import { restoreDb, populateDb } from './utils.js'
|
||||
import { whispers, inventedId, existingId } from './fixtures.js'
|
||||
import { getById } from '../store'
|
||||
|
||||
describe('Server', () => {
|
||||
beforeEach(() => populateDb(whispers))
|
||||
afterAll(restoreDb)
|
||||
describe('GET /api/v1/whisper', () => {
|
||||
it("Should return an empty array when there's no data", async () => {
|
||||
await restoreDb() // empty the db
|
||||
const response = await supertest(app).get('/api/v1/whisper')
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toEqual([])
|
||||
})
|
||||
it('Should return all the whispers', async () => {
|
||||
const response = await supertest(app).get('/api/v1/whisper')
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toEqual(whispers)
|
||||
})
|
||||
})
|
||||
describe('GET /api/v1/whisper/:id', () => {
|
||||
it("Should return a 404 when the whisper doesn't exist", async () => {
|
||||
const response = await supertest(app).get(`/api/v1/whisper/${inventedId}`)
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
it('Should return a whisper details', async () => {
|
||||
const response = await supertest(app).get(`/api/v1/whisper/${existingId}`)
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toEqual(whispers.find(w => w.id === existingId))
|
||||
})
|
||||
})
|
||||
describe('POST /api/v1/whisper', () => {
|
||||
it('Should return a 400 when the body is empty', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/v1/whisper')
|
||||
.send({})
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
it('Should return a 400 when the body is invalid', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/v1/whisper')
|
||||
.send({ invented: 'This is a new whisper' })
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
it('Should return a 201 when the whisper is created', async () => {
|
||||
const newWhisper = { id: whispers.length + 1, message: 'This is a new whisper' }
|
||||
const response = await supertest(app)
|
||||
.post('/api/v1/whisper')
|
||||
.send({ message: newWhisper.message })
|
||||
|
||||
// HTTP Response
|
||||
expect(response.status).toBe(201)
|
||||
expect(response.body).toEqual(newWhisper)
|
||||
|
||||
// Database changes
|
||||
const storedWhisper = await getById(newWhisper.id)
|
||||
expect(storedWhisper).toStrictEqual(newWhisper)
|
||||
})
|
||||
})
|
||||
describe('PUT /api/v1/whisper/:id', () => {
|
||||
it('Should return a 400 when the body is empty', async () => {
|
||||
const response = await supertest(app)
|
||||
.put(`/api/v1/whisper/${existingId}`)
|
||||
.send({})
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
it('Should return a 400 when the body is invalid', async () => {
|
||||
const response = await supertest(app)
|
||||
.put(`/api/v1/whisper/${existingId}`)
|
||||
.send({ invented: 'This a new field' })
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
it("Should return a 404 when the whisper doesn't exist", async () => {
|
||||
const response = await supertest(app)
|
||||
.put(`/api/v1/whisper/${inventedId}`)
|
||||
.send({ message: 'Whisper updated' })
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
it('Should return a 200 when the whisper is updated', async () => {
|
||||
const response = await supertest(app)
|
||||
.put(`/api/v1/whisper/${existingId}`)
|
||||
.send({ message: 'Whisper updated' })
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
// Database changes
|
||||
const storedWhisper = await getById(existingId)
|
||||
expect(storedWhisper).toStrictEqual({ id: existingId, message: 'Whisper updated' })
|
||||
})
|
||||
})
|
||||
describe('DELETE /api/v1/whisper/:id', () => {
|
||||
it("Should return a 404 when the whisper doesn't exist", async () => {
|
||||
const response = await supertest(app).delete(`/api/v1/whisper/${inventedId}`)
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
it('Should return a 200 when the whisper is deleted', async () => {
|
||||
const response = await supertest(app).delete(`/api/v1/whisper/${existingId}`)
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
// Database changes
|
||||
const storedWhisper = await getById(existingId)
|
||||
expect(storedWhisper).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { getAll, getById, create, updateById, deleteById } from '../store.js'
|
||||
import { restoreDb, populateDb } from './utils.js'
|
||||
import { whispers, inventedId, existingId } from './fixtures.js'
|
||||
|
||||
describe('store', () => {
|
||||
beforeEach(() => populateDb(whispers))
|
||||
afterAll(restoreDb)
|
||||
describe('getAll', () => {
|
||||
it("Should return an empty array when there's no data", async () => {
|
||||
restoreDb()
|
||||
const data = await getAll()
|
||||
expect(data).toEqual([])
|
||||
})
|
||||
it('Should return an array with one item when there is one item', async () => {
|
||||
const data = await getAll()
|
||||
expect(data).toEqual(whispers)
|
||||
})
|
||||
})
|
||||
describe('getById', () => {
|
||||
it('Should return undefined when there is no item with the given id', async () => {
|
||||
const item = await getById(inventedId)
|
||||
expect(item).toBeUndefined()
|
||||
})
|
||||
it('Should return the item with the given id', async () => {
|
||||
const item = await getById(whispers[0].id)
|
||||
expect(item).toEqual(whispers[0])
|
||||
})
|
||||
})
|
||||
describe('create', () => {
|
||||
it('Should return the created item', async () => {
|
||||
const newItem = { id: whispers.length + 1, message: 'test 3' }
|
||||
const item = await create(newItem.message)
|
||||
expect(item).toEqual(newItem)
|
||||
})
|
||||
it('Should add the item to the db', async () => {
|
||||
const newItem = { id: whispers.length + 1, message: 'test 3' }
|
||||
const { id } = await create(newItem.message)
|
||||
const item = await getById(id)
|
||||
expect(item).toEqual(newItem)
|
||||
})
|
||||
})
|
||||
describe('updateById', () => {
|
||||
it('Should return undefined when there is no item with the given id', async () => {
|
||||
const item = await updateById(inventedId)
|
||||
expect(item).toBeUndefined()
|
||||
})
|
||||
it('Should not return the updated item', async () => {
|
||||
const updatedItem = { id: existingId, message: 'updated' }
|
||||
const item = await updateById(updatedItem.id, updatedItem.message)
|
||||
expect(item).toBeUndefined()
|
||||
})
|
||||
it('Should update the item in the db', async () => {
|
||||
const updatedItem = { id: existingId, message: 'updated' }
|
||||
await updateById(updatedItem.id, updatedItem.message)
|
||||
const item = await getById(existingId)
|
||||
expect(item).toEqual(updatedItem)
|
||||
})
|
||||
})
|
||||
describe('deleteById', () => {
|
||||
it('Should return undefined when there is no item with the given id', async () => {
|
||||
const item = await deleteById(inventedId)
|
||||
expect(item).toBeUndefined()
|
||||
})
|
||||
it('Should not return the deleted item', async () => {
|
||||
const item = await deleteById(existingId)
|
||||
expect(item).toBeUndefined()
|
||||
})
|
||||
it('Should delete the item from the db', async () => {
|
||||
await deleteById(existingId)
|
||||
const items = await getAll()
|
||||
expect(items).toEqual(whispers.filter(item => item.id !== existingId))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const dbPath = join(process.cwd(), 'db.json')
|
||||
const restoreDb = () => writeFileSync(dbPath, JSON.stringify([]))
|
||||
const populateDb = (data) => writeFileSync(dbPath, JSON.stringify(data))
|
||||
|
||||
export { restoreDb, populateDb }
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Whispering | Home</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="menu">
|
||||
<ul>
|
||||
<li><a href="/">Whispering</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<h1>Welcome to Whispering!</h1>
|
||||
<figure>
|
||||
<img src="people.jpg" alt="three people sitting at the table laughing together" />
|
||||
<figcaption>Photo by <a href="https://unsplash.com/photos/g1Kr4Ozfoac">Brooke Cagle</a> from <a
|
||||
href="https://unsplash.com/">Unsplash</a></figcaption>
|
||||
</figure>
|
||||
|
||||
<h2>What is Whispering?</h2>
|
||||
<p>Whispering is a microblogging platform that allows you to share your thoughts with the world and learn
|
||||
Node.js on the way.</p>
|
||||
|
||||
<h2>Community live ⚡️</h2>
|
||||
<p>Currently there are <%= whispers.length %> whispers available</p>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user