-
Notifications
You must be signed in to change notification settings - Fork 218
Solution #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Solution #181
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> |
| 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'); | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure the GET handler's file exists: you read and serve |
||
| }); | ||
|
Comment on lines
+13
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The GET handler serves |
||
|
|
||
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validation issue: this check treats falsy |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: you already create There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: you use |
||
|
|
||
| if (isJsonRequest) { | ||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify(expense)); | ||
|
Comment on lines
+54
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking: when |
||
| } 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 = { | ||
|
|
||
There was a problem hiding this comment.
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.htmlso the app can show the HTML form. Ensurepublic/index.htmlexists and contains an HTML form withdate,title, andamountfields that POSTs to/add-expense(this is required by the task).