diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template
new file mode 100644
index 0000000..bb13dfc
--- /dev/null
+++ b/.github/workflows/test.yml-template
@@ -0,0 +1,23 @@
+name: Test
+
+on:
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [20.x]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm install
+ - run: npm test
diff --git a/db/expense.json b/db/expense.json
index 1bc75a6..d5378c6 100644
--- a/db/expense.json
+++ b/db/expense.json
@@ -1,5 +1,5 @@
{
- "date": "2024-01-25",
- "title": "Test Expense",
- "amount": "100"
+ "date": "2026-03-20",
+ "title": "dsff",
+ "amount": "5"
}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 28a4d31..4fbbc4f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
- "@mate-academy/scripts": "^1.8.6",
+ "@mate-academy/scripts": "^2.1.3",
"axios": "^1.7.2",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
@@ -1468,10 +1468,11 @@
}
},
"node_modules/@mate-academy/scripts": {
- "version": "1.8.6",
- "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz",
- "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz",
+ "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@octokit/rest": "^17.11.2",
"@types/get-port": "^4.2.0",
diff --git a/package.json b/package.json
index 8a92721..cd53c9e 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
- "@mate-academy/scripts": "^1.8.6",
+ "@mate-academy/scripts": "^2.1.3",
"axios": "^1.7.2",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..180cb3c
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ Expense Form
+
+
+
+
+
+
+
+
+
+
diff --git a/public/styles.css b/public/styles.css
new file mode 100644
index 0000000..dfff418
--- /dev/null
+++ b/public/styles.css
@@ -0,0 +1,89 @@
+body {
+ font-family: Arial, sans-serif;
+ max-width: 600px;
+ margin: 50px auto;
+ padding: 20px;
+ background-color: #f5f5f5;
+}
+
+form {
+ background: white;
+ padding: 30px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ margin-bottom: 20px;
+}
+
+h1 {
+ color: #333;
+ margin-bottom: 30px;
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+ color: #555;
+ font-weight: bold;
+}
+
+input {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 20px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ box-sizing: border-box;
+ font-size: 14px;
+}
+
+button {
+ background-color: #4CAF50;
+ color: white;
+ padding: 12px 30px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ width: 100%;
+}
+
+button:hover {
+ background-color: #45a049;
+}
+
+#result {
+ background: white;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ display: none;
+}
+
+#result:not(:empty) {
+ display: block;
+}
+
+.success {
+ color: #4CAF50;
+}
+
+.success h2 {
+ margin-top: 0;
+}
+
+.error {
+ color: #f44336;
+}
+
+.error h2 {
+ margin-top: 0;
+}
+
+pre {
+ background-color: #f5f5f5;
+ padding: 15px;
+ border-radius: 4px;
+ overflow-x: auto;
+ color: #333;
+ font-size: 14px;
+}
diff --git a/src/createServer.js b/src/createServer.js
index 1cf1dda..b5bcadc 100644
--- a/src/createServer.js
+++ b/src/createServer.js
@@ -1,8 +1,131 @@
'use strict';
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+
+/**
+ * Helper function to send HTTP responses
+ * @param {http.ServerResponse} res - Response object
+ * @param {number} statusCode - HTTP status code (200, 404, 500, etc.)
+ * @param {string} contentType - Content-Type header value
+ * @param {string|object} data - Data to send (will be stringified if object)
+ */
+function sendResponse(res, statusCode, contentType, data) {
+ res.writeHead(statusCode, { 'Content-Type': contentType });
+
+ // If data is an object and content type is JSON, stringify it
+ if (typeof data === 'object' && contentType === 'application/json') {
+ res.end(JSON.stringify(data));
+ } else {
+ res.end(data);
+ }
+}
+
function createServer() {
- /* Write your code here */
- // Return instance of http.Server class
+ const server = http.createServer((req, res) => {
+ const { method, url } = req;
+
+ // Serve index.html
+ if (method === 'GET' && url === '/') {
+ const filePath = path.resolve(__dirname, '../public/index.html');
+
+ fs.readFile(filePath, 'utf8', (err, data) => {
+ if (err) {
+ sendResponse(res, 500, 'text/plain', 'Error loading page');
+
+ return;
+ }
+
+ sendResponse(res, 200, 'text/html', data);
+ });
+
+ return;
+ }
+
+ // Serve styles.css
+ if (method === 'GET' && url === '/styles.css') {
+ const filePath = path.resolve(__dirname, '../public/styles.css');
+
+ fs.readFile(filePath, 'utf8', (err, data) => {
+ if (err) {
+ sendResponse(res, 500, 'text/plain', 'Error loading stylesheet');
+
+ return;
+ }
+
+ sendResponse(res, 200, 'text/css', data);
+ });
+
+ return;
+ }
+
+ // Handle POST request - save expense data
+ if (method === 'POST' && url === '/add-expense') {
+ let body = '';
+
+ req.on('data', (chunk) => {
+ body += chunk.toString();
+ });
+
+ req.on('end', () => {
+ let expense;
+
+ try {
+ // Parse JSON body
+ expense = JSON.parse(body);
+ } catch (error) {
+ sendResponse(res, 400, 'application/json', { error: 'invalid JSON' });
+
+ return;
+ }
+
+ // Validate required fields
+ const requiredFields = ['date', 'title', 'amount'];
+ const missingFields = requiredFields.filter((field) => !expense[field]);
+
+ if (missingFields.length > 0) {
+ sendResponse(
+ res,
+ 400,
+ 'text/plain',
+ `Missing required fields: ${missingFields.join(', ')}`,
+ );
+
+ return;
+ }
+
+ // Save to file
+ const dataPath = path.resolve(__dirname, '../db/expense.json');
+ const dirPath = path.dirname(dataPath);
+
+ // Enshore directory exists
+ if (!fs.existsSync(dirPath)) {
+ fs.mkdirSync(dirPath, { recursive: true });
+ }
+
+ try {
+ fs.writeFileSync(dataPath, JSON.stringify(expense, null, 2));
+
+ // Return HTML page with well-formatted JSON
+ const html = `${JSON.stringify(expense, null, 2)}`;
+
+ sendResponse(res, 200, 'text/html', html);
+ } catch (error) {
+ sendResponse(res, 500, 'application/json', {
+ error: 'Failed to save expense',
+ });
+ }
+ });
+
+ return;
+ }
+
+ // Handle 404 for all other routes
+ sendResponse(res, 404, 'text/plain', 'Not found');
+ });
+
+ return server;
}
module.exports = {
diff --git a/tests/formDataServer.test.js b/tests/formDataServer.test.js
index 0ee1766..f9a94d4 100644
--- a/tests/formDataServer.test.js
+++ b/tests/formDataServer.test.js
@@ -83,8 +83,10 @@ describe('Form Data Server', () => {
};
const response = await axios.post(`${HOST}/add-expense`, expense);
- expect(response.headers['content-type']).toBe('application/json');
- expect(response.data).toStrictEqual(expense);
+ expect(response.headers['content-type']).toBe('text/html');
+ expect(response.data).toBe(
+ `${JSON.stringify(expense, null, 2)}`,
+ );
});
it('should return 404 for invalid url', async () => {