Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}

.container {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 500px;
animation: slideIn 0.5s ease-out;
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px);
}

to {
opacity: 1;
transform: translateY(0);
}
}

h1 {
color: #333;
margin-bottom: 10px;
font-size: 2em;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 0.95em;
}

.form-group {
margin-bottom: 20px;
position: relative;
}

label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 600;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.5px;
}

input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1em;
transition: all 0.3s ease;
background: #fafafa;
}

input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

input[type="date"] {
font-family: inherit;
}

button {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin-top: 10px;
}

button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}

button:active {
transform: translateY(0);
}

.icon {
display: inline-block;
margin-right: 8px;
}
</style>
</head>

<body>
<div class="container">
<h1><span class="icon">💰</span>Expense Tracker</h1>
<p class="subtitle">Додайте нову витрату</p>

<form action="/add-expense" method="POST">
<div class="form-group">
<label for="date">Дата</label>
<input type="date" id="date" name="date" required>
</div>

<div class="form-group">
<label for="title">Назва</label>
<input type="text" id="title" name="title" placeholder="наприклад, Продукти" required>
</div>

<div class="form-group">
<label for="amount">Сума (₴)</label>
<input type="number" id="amount" name="amount" step="0.01" min="0" placeholder="0.00" required>
</div>

<button type="submit"><span class="icon">🆗</span>Додати витрату</button>
</form>
</div>
</body>

</html>
81 changes: 79 additions & 2 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,85 @@
'use strict';

const http = require('http');
const path = require('path');
const fs = require('fs');
const querystring = require('querystring');

const dataPath = path.resolve(__dirname, '..', 'db', 'expense.json');
const htmlPath = path.resolve(__dirname, '..', 'public', 'index.html');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server expects to serve public/index.html so the app can show the HTML form. Ensure public/index.html exists and contains an HTML form with date, title, and amount fields that POSTs to /add-expense (this is required by the task).


function createServer() {
/* Write your code here */
// Return instance of http.Server class
return http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/') {
fs.readFile(htmlPath, (err, data) => {
if (err) {
res.writeHead(500);

return res.end('Error loading index.html');
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(data);
Comment on lines +14 to +21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the GET handler's file exists: you read and serve public/index.html here — make sure that file is present in the project and contains a <form method="POST" action="/add-expense"> with inputs named date, title, and amount, otherwise browser submissions won't work.

});
Comment on lines +13 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GET handler serves public/index.html. I couldn't find public/index.html in the project snapshot; add this file. It must contain a form that POSTs to /add-expense and include inputs named date, title, and amount (for example: <form method="POST" action="/add-expense"> with name="date", name="title", name="amount"). Without it the GET / route and the form requirement will fail.


return;
}

if (req.method === 'POST' && req.url === '/add-expense') {
let body = '';

req.on('data', (chunk) => {
body += chunk.toString();
});

req.on('end', () => {
try {
const contentType = req.headers['content-type'];
const isJsonRequest =
contentType && contentType.startsWith('application/json');

const expense = isJsonRequest
? JSON.parse(body)
: querystring.parse(body);

if (!expense.date || !expense.title || !expense.amount) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation issue: this check treats falsy expense.amount as missing, which will reject 0 or "0. If 0is a valid amount, change the check to test for null/empty (for example:expense.amount == null || expense.amount === ''`) instead of a simple falsy check.

res.writeHead(400, { 'Content-Type': 'text/plain' });

return res.end('Missing fields');
}

const prettyJson = JSON.stringify(expense, null, 2);

fs.writeFileSync(dataPath, prettyJson);
Comment on lines +50 to +52

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: you already create prettyJson with JSON.stringify(expense, null, 2) and write it to db/expense.json, which makes the stored file human-readable as required. Keep this behavior.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: you use fs.writeFileSync to save the JSON which blocks the event loop. While functional, prefer fs.writeFile (async) to avoid blocking, e.g. fs.writeFile(dataPath, prettyJson, err => { ... }).


if (isJsonRequest) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(expense));
Comment on lines +54 to +56

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: when isJsonRequest is true you send Content-Type: application/json and raw JSON. The task requires returning an HTML page that shows the pretty-printed JSON inside a <pre> element (same as the form-encoded branch). Change this branch to send Content-Type: 'text/html; charset=utf-8' and return an HTML document embedding prettyJson.

} else {
const responseHtml = `
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<pre>${prettyJson}</pre>
</body>
</html>
`;

res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(responseHtml);
}
} catch (error) {
res.writeHead(400);
res.end('Invalid Data');
}
});

return;
}

res.writeHead(404);
res.end('Not Found');
});
}

module.exports = {
Expand Down