- - -
- - - Rocket Ship - - - - - - - - - - {{ title }} app is running! - - - Rocket Ship Smoke - - - -
- - -

Resources

-

Here are some links to help you get started:

- - - - -

Next Steps

-

What do you want to do next with your app?

- - - -
- - - - - - - - - - - -
- - -
-@switch (selection.value) { - @default { -
ng generate component xyz
-} - @case ('material') { -
ng add @angular/material
-} - @case ('pwa') { -
ng add @angular/pwa
-} - @case ('dependency') { -
ng add _____
-} - @case ('test') { -
ng test
-} - @case ('build') { -
ng build
-} - } -
- - - - - - - - - Gray Clouds Background - - - +
+

Blog Posts

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
- - - - - - - - - + \ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index e69de29..2b0f7c8 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -0,0 +1,107 @@ +body { + min-height: 100vh; + margin: 0; + font-family: 'Segoe UI', Arial, sans-serif; + /* Soft geometric pattern background */ + background: linear-gradient(135deg, #f4f6fa 60%, #e3f0ff 100%); + background-attachment: fixed; + /* Optional subtle pattern overlay */ + position: relative; +} +body::before { + content: ""; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + pointer-events: none; + opacity: 0.12; + background-image: + radial-gradient(circle at 20% 30%, #b3c6e0 0px, transparent 400px), + radial-gradient(circle at 80% 70%, #c8e0f4 0px, transparent 400px); + z-index: 0; +} +.header-tabs { + display: flex; + justify-content: center; + background: #fff; + border-radius: 10px 10px 0 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + margin: 32px auto 0 auto; + max-width: 600px; + overflow: hidden; + position: relative; + z-index: 1; +} +.tab { + flex: 1; + padding: 16px 0; + text-align: center; + cursor: pointer; + font-size: 1.1rem; + color: #4a5568; + background: #f4f6fa; + border: none; + outline: none; + transition: background 0.2s, color 0.2s; +} +.tab.active { + background: #fff; + color: #2d3a4b; + font-weight: 600; + border-bottom: 2px solid #2d3a4b; +} +.blog-list { + max-width: 600px; + margin: 40px auto; + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 24px rgba(0,0,0,0.08); + padding: 32px 24px; + position: relative; + z-index: 1; +} +.blog-list h2 { + text-align: center; + color: #2d3a4b; + margin-bottom: 28px; + font-size: 2rem; + letter-spacing: 1px; +} +.blog-list ul { + list-style: none; + padding: 0; + margin: 0; +} +.blog-list li { + margin-bottom: 16px; +} +.blog-list button { + width: 100%; + text-align: left; + padding: 18px 20px; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, #f9f9f9 80%, #e3f0ff 100%); + cursor: pointer; + transition: box-shadow 0.2s, background 0.2s; + box-shadow: 0 2px 8px rgba(44, 62, 80, 0.06); + outline: none; +} +.blog-list button:hover, .blog-list button:focus { + background: linear-gradient(90deg, #e3f0ff 80%, #f9f9f9 100%); + box-shadow: 0 4px 16px rgba(44, 62, 80, 0.12); +} +.blog-list h3 { + margin: 0 0 6px 0; + color: #1a2236; + font-size: 1.2rem; + font-weight: 600; +} +.blog-list p { + margin: 0 0 8px 0; + color: #4a5568; + font-size: 1rem; +} +.blog-list small { + color: #7b8794; + font-size: 0.92rem; +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c53dbc9..6a616bd 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,10 +1,12 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { GuestBookComponent } from './guest-book/guest-book.component'; +import { FormsModule } from '@angular/forms'; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, GuestBookComponent, FormsModule], templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) diff --git a/src/app/guest-book/guest-book.component.html b/src/app/guest-book/guest-book.component.html new file mode 100644 index 0000000..1a5d0f9 --- /dev/null +++ b/src/app/guest-book/guest-book.component.html @@ -0,0 +1,28 @@ +
+

Guest Book

+ + +
    +
  • + {{ entry.name }} ({{ entry.email }})
    + {{ entry.message }} +
  • +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/src/app/guest-book/guest-book.component.scss b/src/app/guest-book/guest-book.component.scss new file mode 100644 index 0000000..a3f7e08 --- /dev/null +++ b/src/app/guest-book/guest-book.component.scss @@ -0,0 +1,101 @@ +.guest-book { + max-width: 500px; + margin: 40px auto; + background: #232946; + border-radius: 12px; + box-shadow: 0 4px 24px rgba(0,0,0,0.18); + padding: 32px 24px; + color: #fff; + position: relative; + z-index: 1; +} + +.guest-book h2 { + text-align: center; + color: #eebbc3; + margin-bottom: 28px; + font-size: 2rem; + letter-spacing: 1px; +} + +.entries { + list-style: none; + padding: 0; + margin: 0 0 32px 0; +} + +.entries li { + background: #393e5c; + border-radius: 8px; + padding: 16px 18px; + margin-bottom: 14px; + color: #fff; + box-shadow: 0 2px 8px rgba(44, 62, 80, 0.10); +} + +.entries strong { + color: #eebbc3; +} + +.entries span { + display: block; + margin-top: 6px; + color: #b8c1ec; +} + +form { + display: flex; + flex-direction: column; + gap: 16px; +} + +form label { + font-weight: 500; + color: #eebbc3; + margin-bottom: 4px; + display: block; +} + +form input, +form textarea { + width: 100%; + padding: 10px 12px; + border-radius: 6px; + border: 1px solid #393e5c; + background: #181d2b; + color: #fff; + font-size: 1rem; + margin-top: 2px; + transition: border 0.2s; +} + +form input:focus, +form textarea:focus { + border: 1.5px solid #eebbc3; + outline: none; +} + +form textarea { + min-height: 60px; + resize: vertical; +} + +form button[type="submit"] { + background: linear-gradient(90deg, #eebbc3 60%, #b8c1ec 100%); + color: #232946; + font-weight: 600; + border: none; + border-radius: 6px; + padding: 12px 0; + font-size: 1.1rem; + cursor: pointer; + margin-top: 8px; + transition: background 0.2s, color 0.2s; +} + +form button[type="submit"]:disabled { + background: #393e5c; + color: #b8c1ec; + cursor: not-allowed; + opacity: 0.7; +} \ No newline at end of file diff --git a/src/app/guest-book/guest-book.component.spec.ts b/src/app/guest-book/guest-book.component.spec.ts new file mode 100644 index 0000000..68db222 --- /dev/null +++ b/src/app/guest-book/guest-book.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GuestBookComponent } from './guest-book.component'; + +describe('GuestBookComponent', () => { + let component: GuestBookComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GuestBookComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GuestBookComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/guest-book/guest-book.component.ts b/src/app/guest-book/guest-book.component.ts new file mode 100644 index 0000000..4753853 --- /dev/null +++ b/src/app/guest-book/guest-book.component.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { IMessage } from './message.model'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-guest-book', + imports: [FormsModule], + templateUrl: './guest-book.component.html', + styleUrl: './guest-book.component.scss' +}) +export class GuestBookComponent { + newEntry: IMessage; + messages: IMessage[] = []; + + constructor() { + this.newEntry = { + id: 0, + name: '', + message: '', + email: '', + date: new Date() + }; + } + + addEntry() { + if (this.newEntry.name && this.newEntry.message && this.newEntry.email) { + this.newEntry.id = this.messages.length + 1; + this.newEntry.date = new Date(); + this.messages.push({ ...this.newEntry }); + console.log(this.messages); + this.newEntry = { + id: 0, + name: '', + message: '', + email: '', + date: new Date() + }; + } + } + +} diff --git a/src/app/guest-book/message.model.ts b/src/app/guest-book/message.model.ts new file mode 100644 index 0000000..cfc0bef --- /dev/null +++ b/src/app/guest-book/message.model.ts @@ -0,0 +1,7 @@ +export interface IMessage { + id: number; + name: string; + message: string; + email: string; + date: Date; + } \ No newline at end of file From 0b1be25ce9d7630e4f96bb089b108444ed5eb764 Mon Sep 17 00:00:00 2001 From: Ievgien Date: Mon, 26 May 2025 17:51:33 +0400 Subject: [PATCH 02/34] add server and routing --- server/index.js | 148 ++++ server/package-lock.json | 772 ++++++++++++++++++ server/package.json | 17 + src/app/app-routes.ts | 8 +- src/app/app.component.html | 35 +- src/app/app.component.scss | 107 --- src/app/app.component.ts | 4 +- src/app/blog-list/blog-list.component.html | 26 + src/app/blog-list/blog-list.component.scss | 107 +++ src/app/blog-list/blog-list.component.spec.ts | 23 + src/app/blog-list/blog-list.component.ts | 12 + .../comment-panel.component.html | 1 + .../comment-panel.component.scss | 0 .../comment-panel.component.spec.ts | 23 + .../comment-panel/comment-panel.component.ts | 11 + .../comment/comment.component.html | 1 + .../comment/comment.component.scss | 0 .../comment/comment.component.spec.ts | 23 + .../comment/comment.component.ts | 11 + .../comment-panel/comment/product.model.ts | 7 + src/app/guest-book/guest-book.component.html | 2 +- src/app/guest-book/guest-book.component.ts | 12 +- .../site-header/site-header.component.html | 6 + .../site-header/site-header.component.scss | 29 + .../site-header/site-header.component.spec.ts | 23 + src/app/site-header/site-header.component.ts | 12 + 26 files changed, 1272 insertions(+), 148 deletions(-) create mode 100644 server/index.js create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 src/app/blog-list/blog-list.component.html create mode 100644 src/app/blog-list/blog-list.component.scss create mode 100644 src/app/blog-list/blog-list.component.spec.ts create mode 100644 src/app/blog-list/blog-list.component.ts create mode 100644 src/app/guest-book/comment-panel/comment-panel.component.html create mode 100644 src/app/guest-book/comment-panel/comment-panel.component.scss create mode 100644 src/app/guest-book/comment-panel/comment-panel.component.spec.ts create mode 100644 src/app/guest-book/comment-panel/comment-panel.component.ts create mode 100644 src/app/guest-book/comment-panel/comment/comment.component.html create mode 100644 src/app/guest-book/comment-panel/comment/comment.component.scss create mode 100644 src/app/guest-book/comment-panel/comment/comment.component.spec.ts create mode 100644 src/app/guest-book/comment-panel/comment/comment.component.ts create mode 100644 src/app/guest-book/comment-panel/comment/product.model.ts create mode 100644 src/app/site-header/site-header.component.html create mode 100644 src/app/site-header/site-header.component.scss create mode 100644 src/app/site-header/site-header.component.spec.ts create mode 100644 src/app/site-header/site-header.component.ts diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..767ac53 --- /dev/null +++ b/server/index.js @@ -0,0 +1,148 @@ +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); + +const app = express(); +const PORT = 3000; + +app.use(cors()); +app.use(bodyParser.json()); + +// --- Blog Data Model --- +// { +// id: number, +// title: string, +// blogHtml: string, +// author: string, +// date: Date +// } + +// Generate random HTML content for blogs +function randomHtml(length) { + const base = `

Angular Article

${'Angular is awesome. '.repeat(50)}

  • Component
  • Service
  • Directive

${'Learn more about Angular. '.repeat(50)}

`; + let html = ''; + while (html.length < length) { + html += base; + } + return html.slice(0, length); +} + +// Initialize 15 blogs +let blogs = Array.from({ length: 15 }, (_, i) => ({ + id: i + 1, + title: `Angular Topic #${i + 1}`, + blogHtml: randomHtml(500 + Math.floor(Math.random() * 4500)), + author: `Author ${i + 1}`, + date: new Date(2025, 4, i + 1) +})); + +// --- Comments Data Model --- +// { +// id: number, +// name: string, +// message: string, +// email: string, +// date: Date +// } + +// Initialize comments for blogs +let comments = [ + { + id: 1, + blogId: 1, + name: "John Doe", + message: "Great article on Angular!", + email: "john@example.com", + date: new Date() + }, + { + id: 2, + blogId: 1, + name: "Jane Smith", + message: "Very helpful, thanks!", + email: "jane@example.com", + date: new Date() + }, + { + id: 3, + blogId: 2, + name: "Alex Brown", + message: "I love Angular topics.", + email: "alex@example.com", + date: new Date() + } +]; + +// 1. Get all blogs +app.get('/blogs', (req, res) => { + res.json(blogs); +}); + +// 2. Get blog by id +app.get('/blogs/:id', (req, res) => { + const id = parseInt(req.params.id, 10); + const blog = blogs.find(b => b.id === id); + if (!blog) { + return res.status(404).json({ error: 'Blog not found' }); + } + res.json(blog); +}); + +// 3. Add a new blog +app.post('/blogs', (req, res) => { + const { title, blogHtml, author, date } = req.body; + if (!title || !blogHtml || !author || !date) { + return res.status(400).json({ error: 'Missing fields' }); + } + const newBlog = { + id: blogs.length ? blogs[blogs.length - 1].id + 1 : 1, + title, + blogHtml, + author, + date: new Date(date) + }; + blogs.push(newBlog); + res.status(201).json(newBlog); +}); + +// 4. Get comments by blogId +app.get('/blogs/:blogId/comments', (req, res) => { + const blogId = parseInt(req.params.blogId, 10); + const blogComments = comments.filter(c => c.blogId === blogId); + res.json(blogComments); +}); + +// 5. Add comment by blogId +app.post('/blogs/:blogId/comments', (req, res) => { + const blogId = parseInt(req.params.blogId, 10); + const { name, message, email, date } = req.body; + if (!name || !message || !email || !date) { + return res.status(400).json({ error: 'Missing fields' }); + } + const newComment = { + id: comments.length ? comments[comments.length - 1].id + 1 : 1, + blogId, + name, + message, + email, + date: new Date(date) + }; + comments.push(newComment); + res.status(201).json(newComment); +}); + +// 6. Delete comment by blogId and comment id +app.delete('/blogs/:blogId/comments/:commentId', (req, res) => { + const blogId = parseInt(req.params.blogId, 10); + const commentId = parseInt(req.params.commentId, 10); + const index = comments.findIndex(c => c.blogId === blogId && c.id === commentId); + if (index === -1) { + return res.status(404).json({ error: 'Comment not found' }); + } + comments.splice(index, 1); + res.status(204).send(); +}); + +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..5f005e0 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,772 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "express": "^5.1.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..7d08c62 --- /dev/null +++ b/server/package.json @@ -0,0 +1,17 @@ +{ + "name": "server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "express": "^5.1.0" + } +} diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index a1cf04d..9de7f9f 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,3 +1,9 @@ import { Routes } from '@angular/router'; +import { BlogListComponent } from './blog-list/blog-list.component'; +import { GuestBookComponent } from './guest-book/guest-book.component'; -export const appRoutes: Routes = []; +export const appRoutes: Routes = [ + { path: 'blogs', component: BlogListComponent, title: "Home - List of blogs" }, + { path: 'guest-book', component: GuestBookComponent, title: "GuestBook - Welcome to guest book" }, + { path: '', redirectTo: '/blogs', pathMatch: 'full' }, +]; diff --git a/src/app/app.component.html b/src/app/app.component.html index 5d509e8..242b1c6 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,33 +1,2 @@ -
- - -
- -
-

Blog Posts

-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- - \ No newline at end of file + + \ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 2b0f7c8..e69de29 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,107 +0,0 @@ -body { - min-height: 100vh; - margin: 0; - font-family: 'Segoe UI', Arial, sans-serif; - /* Soft geometric pattern background */ - background: linear-gradient(135deg, #f4f6fa 60%, #e3f0ff 100%); - background-attachment: fixed; - /* Optional subtle pattern overlay */ - position: relative; -} -body::before { - content: ""; - position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - pointer-events: none; - opacity: 0.12; - background-image: - radial-gradient(circle at 20% 30%, #b3c6e0 0px, transparent 400px), - radial-gradient(circle at 80% 70%, #c8e0f4 0px, transparent 400px); - z-index: 0; -} -.header-tabs { - display: flex; - justify-content: center; - background: #fff; - border-radius: 10px 10px 0 0; - box-shadow: 0 2px 8px rgba(0,0,0,0.04); - margin: 32px auto 0 auto; - max-width: 600px; - overflow: hidden; - position: relative; - z-index: 1; -} -.tab { - flex: 1; - padding: 16px 0; - text-align: center; - cursor: pointer; - font-size: 1.1rem; - color: #4a5568; - background: #f4f6fa; - border: none; - outline: none; - transition: background 0.2s, color 0.2s; -} -.tab.active { - background: #fff; - color: #2d3a4b; - font-weight: 600; - border-bottom: 2px solid #2d3a4b; -} -.blog-list { - max-width: 600px; - margin: 40px auto; - background: #fff; - border-radius: 12px; - box-shadow: 0 4px 24px rgba(0,0,0,0.08); - padding: 32px 24px; - position: relative; - z-index: 1; -} -.blog-list h2 { - text-align: center; - color: #2d3a4b; - margin-bottom: 28px; - font-size: 2rem; - letter-spacing: 1px; -} -.blog-list ul { - list-style: none; - padding: 0; - margin: 0; -} -.blog-list li { - margin-bottom: 16px; -} -.blog-list button { - width: 100%; - text-align: left; - padding: 18px 20px; - border: none; - border-radius: 8px; - background: linear-gradient(90deg, #f9f9f9 80%, #e3f0ff 100%); - cursor: pointer; - transition: box-shadow 0.2s, background 0.2s; - box-shadow: 0 2px 8px rgba(44, 62, 80, 0.06); - outline: none; -} -.blog-list button:hover, .blog-list button:focus { - background: linear-gradient(90deg, #e3f0ff 80%, #f9f9f9 100%); - box-shadow: 0 4px 16px rgba(44, 62, 80, 0.12); -} -.blog-list h3 { - margin: 0 0 6px 0; - color: #1a2236; - font-size: 1.2rem; - font-weight: 600; -} -.blog-list p { - margin: 0 0 8px 0; - color: #4a5568; - font-size: 1rem; -} -.blog-list small { - color: #7b8794; - font-size: 0.92rem; -} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6a616bd..09857c1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,10 +3,12 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { GuestBookComponent } from './guest-book/guest-book.component'; import { FormsModule } from '@angular/forms'; +import { SiteHeaderComponent } from "./site-header/site-header.component"; @Component({ selector: 'app-root', - imports: [RouterOutlet, GuestBookComponent, FormsModule], + standalone: true, + imports: [RouterOutlet, GuestBookComponent, FormsModule, SiteHeaderComponent], templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) diff --git a/src/app/blog-list/blog-list.component.html b/src/app/blog-list/blog-list.component.html new file mode 100644 index 0000000..e9f920f --- /dev/null +++ b/src/app/blog-list/blog-list.component.html @@ -0,0 +1,26 @@ +
+

Blog Posts

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
diff --git a/src/app/blog-list/blog-list.component.scss b/src/app/blog-list/blog-list.component.scss new file mode 100644 index 0000000..2b0f7c8 --- /dev/null +++ b/src/app/blog-list/blog-list.component.scss @@ -0,0 +1,107 @@ +body { + min-height: 100vh; + margin: 0; + font-family: 'Segoe UI', Arial, sans-serif; + /* Soft geometric pattern background */ + background: linear-gradient(135deg, #f4f6fa 60%, #e3f0ff 100%); + background-attachment: fixed; + /* Optional subtle pattern overlay */ + position: relative; +} +body::before { + content: ""; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + pointer-events: none; + opacity: 0.12; + background-image: + radial-gradient(circle at 20% 30%, #b3c6e0 0px, transparent 400px), + radial-gradient(circle at 80% 70%, #c8e0f4 0px, transparent 400px); + z-index: 0; +} +.header-tabs { + display: flex; + justify-content: center; + background: #fff; + border-radius: 10px 10px 0 0; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + margin: 32px auto 0 auto; + max-width: 600px; + overflow: hidden; + position: relative; + z-index: 1; +} +.tab { + flex: 1; + padding: 16px 0; + text-align: center; + cursor: pointer; + font-size: 1.1rem; + color: #4a5568; + background: #f4f6fa; + border: none; + outline: none; + transition: background 0.2s, color 0.2s; +} +.tab.active { + background: #fff; + color: #2d3a4b; + font-weight: 600; + border-bottom: 2px solid #2d3a4b; +} +.blog-list { + max-width: 600px; + margin: 40px auto; + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 24px rgba(0,0,0,0.08); + padding: 32px 24px; + position: relative; + z-index: 1; +} +.blog-list h2 { + text-align: center; + color: #2d3a4b; + margin-bottom: 28px; + font-size: 2rem; + letter-spacing: 1px; +} +.blog-list ul { + list-style: none; + padding: 0; + margin: 0; +} +.blog-list li { + margin-bottom: 16px; +} +.blog-list button { + width: 100%; + text-align: left; + padding: 18px 20px; + border: none; + border-radius: 8px; + background: linear-gradient(90deg, #f9f9f9 80%, #e3f0ff 100%); + cursor: pointer; + transition: box-shadow 0.2s, background 0.2s; + box-shadow: 0 2px 8px rgba(44, 62, 80, 0.06); + outline: none; +} +.blog-list button:hover, .blog-list button:focus { + background: linear-gradient(90deg, #e3f0ff 80%, #f9f9f9 100%); + box-shadow: 0 4px 16px rgba(44, 62, 80, 0.12); +} +.blog-list h3 { + margin: 0 0 6px 0; + color: #1a2236; + font-size: 1.2rem; + font-weight: 600; +} +.blog-list p { + margin: 0 0 8px 0; + color: #4a5568; + font-size: 1rem; +} +.blog-list small { + color: #7b8794; + font-size: 0.92rem; +} \ No newline at end of file diff --git a/src/app/blog-list/blog-list.component.spec.ts b/src/app/blog-list/blog-list.component.spec.ts new file mode 100644 index 0000000..6f43546 --- /dev/null +++ b/src/app/blog-list/blog-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BlogListComponent } from './blog-list.component'; + +describe('BlogListComponent', () => { + let component: BlogListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BlogListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BlogListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/blog-list/blog-list.component.ts b/src/app/blog-list/blog-list.component.ts new file mode 100644 index 0000000..43c34b6 --- /dev/null +++ b/src/app/blog-list/blog-list.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-blog-list', + imports: [], + standalone: true, + templateUrl: './blog-list.component.html', + styleUrl: './blog-list.component.scss' +}) +export class BlogListComponent { + +} diff --git a/src/app/guest-book/comment-panel/comment-panel.component.html b/src/app/guest-book/comment-panel/comment-panel.component.html new file mode 100644 index 0000000..b4d3f15 --- /dev/null +++ b/src/app/guest-book/comment-panel/comment-panel.component.html @@ -0,0 +1 @@ +

comment-panel works!

diff --git a/src/app/guest-book/comment-panel/comment-panel.component.scss b/src/app/guest-book/comment-panel/comment-panel.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/guest-book/comment-panel/comment-panel.component.spec.ts b/src/app/guest-book/comment-panel/comment-panel.component.spec.ts new file mode 100644 index 0000000..d4007ae --- /dev/null +++ b/src/app/guest-book/comment-panel/comment-panel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CommentPanelComponent } from './comment-panel.component'; + +describe('CommentPanelComponent', () => { + let component: CommentPanelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CommentPanelComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CommentPanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/guest-book/comment-panel/comment-panel.component.ts b/src/app/guest-book/comment-panel/comment-panel.component.ts new file mode 100644 index 0000000..4cf110f --- /dev/null +++ b/src/app/guest-book/comment-panel/comment-panel.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-comment-panel', + imports: [], + templateUrl: './comment-panel.component.html', + styleUrl: './comment-panel.component.scss' +}) +export class CommentPanelComponent { + +} diff --git a/src/app/guest-book/comment-panel/comment/comment.component.html b/src/app/guest-book/comment-panel/comment/comment.component.html new file mode 100644 index 0000000..c551707 --- /dev/null +++ b/src/app/guest-book/comment-panel/comment/comment.component.html @@ -0,0 +1 @@ +

comment works!

diff --git a/src/app/guest-book/comment-panel/comment/comment.component.scss b/src/app/guest-book/comment-panel/comment/comment.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/guest-book/comment-panel/comment/comment.component.spec.ts b/src/app/guest-book/comment-panel/comment/comment.component.spec.ts new file mode 100644 index 0000000..4463e54 --- /dev/null +++ b/src/app/guest-book/comment-panel/comment/comment.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CommentComponent } from './comment.component'; + +describe('CommentComponent', () => { + let component: CommentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CommentComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CommentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/guest-book/comment-panel/comment/comment.component.ts b/src/app/guest-book/comment-panel/comment/comment.component.ts new file mode 100644 index 0000000..d77c951 --- /dev/null +++ b/src/app/guest-book/comment-panel/comment/comment.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-comment', + imports: [], + templateUrl: './comment.component.html', + styleUrl: './comment.component.scss' +}) +export class CommentComponent { + +} diff --git a/src/app/guest-book/comment-panel/comment/product.model.ts b/src/app/guest-book/comment-panel/comment/product.model.ts new file mode 100644 index 0000000..02993e3 --- /dev/null +++ b/src/app/guest-book/comment-panel/comment/product.model.ts @@ -0,0 +1,7 @@ +export interface IComment { + id: number; + usderId: string; + comment: string; + email: string; + date: number; +} diff --git a/src/app/guest-book/guest-book.component.html b/src/app/guest-book/guest-book.component.html index 1a5d0f9..a44fa8c 100644 --- a/src/app/guest-book/guest-book.component.html +++ b/src/app/guest-book/guest-book.component.html @@ -3,7 +3,7 @@

Guest Book

    -
  • +
  • {{ entry.name }} ({{ entry.email }})
    {{ entry.message }}
  • diff --git a/src/app/guest-book/guest-book.component.ts b/src/app/guest-book/guest-book.component.ts index 4753853..689f859 100644 --- a/src/app/guest-book/guest-book.component.ts +++ b/src/app/guest-book/guest-book.component.ts @@ -1,16 +1,18 @@ import { Component } from '@angular/core'; import { IMessage } from './message.model'; import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-guest-book', - imports: [FormsModule], + imports: [FormsModule, CommonModule], + standalone: true, templateUrl: './guest-book.component.html', styleUrl: './guest-book.component.scss' }) export class GuestBookComponent { newEntry: IMessage; - messages: IMessage[] = []; + guestMessages: IMessage[] = []; constructor() { this.newEntry = { @@ -24,10 +26,10 @@ export class GuestBookComponent { addEntry() { if (this.newEntry.name && this.newEntry.message && this.newEntry.email) { - this.newEntry.id = this.messages.length + 1; + this.newEntry.id = this.guestMessages.length + 1; this.newEntry.date = new Date(); - this.messages.push({ ...this.newEntry }); - console.log(this.messages); + this.guestMessages.push({ ...this.newEntry }); + console.log(this.guestMessages); this.newEntry = { id: 0, name: '', diff --git a/src/app/site-header/site-header.component.html b/src/app/site-header/site-header.component.html new file mode 100644 index 0000000..2a65572 --- /dev/null +++ b/src/app/site-header/site-header.component.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/src/app/site-header/site-header.component.scss b/src/app/site-header/site-header.component.scss new file mode 100644 index 0000000..3c45edf --- /dev/null +++ b/src/app/site-header/site-header.component.scss @@ -0,0 +1,29 @@ +.site-header { + background: linear-gradient(90deg, #4e54c8 0%, #8f94fb 100%); + padding: 0.5rem 0; + box-shadow: 0 2px 8px rgba(78, 84, 200, 0.1); +} + +.nav-tabs { + display: flex; + justify-content: center; + gap: 2rem; +} + +.tab { + color: #fff; + text-decoration: none; + font-size: 1.2rem; + padding: 0.5rem 1.5rem; + border-radius: 25px; + transition: background 0.2s, color 0.2s; + font-weight: 500; + letter-spacing: 1px; +} + +.tab:hover, +.tab.active { + background: #fff; + color: #4e54c8; + box-shadow: 0 2px 8px rgba(78, 84, 200, 0.15); +} diff --git a/src/app/site-header/site-header.component.spec.ts b/src/app/site-header/site-header.component.spec.ts new file mode 100644 index 0000000..162d5ea --- /dev/null +++ b/src/app/site-header/site-header.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SiteHeaderComponent } from './site-header.component'; + +describe('SiteHeaderComponent', () => { + let component: SiteHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SiteHeaderComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SiteHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/site-header/site-header.component.ts b/src/app/site-header/site-header.component.ts new file mode 100644 index 0000000..b6f3d5a --- /dev/null +++ b/src/app/site-header/site-header.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { RouterModule} from '@angular/router'; + +@Component({ + selector: 'app-site-header', + imports: [RouterModule], + templateUrl: './site-header.component.html', + styleUrl: './site-header.component.scss' +}) +export class SiteHeaderComponent { + +} From 65e5068df5eeb6354d7240472d42bfc4ef5c14d9 Mon Sep 17 00:00:00 2001 From: Ievgien Date: Mon, 26 May 2025 18:01:02 +0400 Subject: [PATCH 03/34] add meaningull blogs --- server/index.js | 231 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 214 insertions(+), 17 deletions(-) diff --git a/server/index.js b/server/index.js index 767ac53..e6f58cd 100644 --- a/server/index.js +++ b/server/index.js @@ -17,24 +17,221 @@ app.use(bodyParser.json()); // date: Date // } -// Generate random HTML content for blogs -function randomHtml(length) { - const base = `

    Angular Article

    ${'Angular is awesome. '.repeat(50)}

    • Component
    • Service
    • Directive

    ${'Learn more about Angular. '.repeat(50)}

    `; - let html = ''; - while (html.length < length) { - html += base; +// Initialize 15 blogs with real HTML content (500-4000 symbols each) +let blogs = [ + { + id: 1, + title: "Getting Started with Angular", + blogHtml: `

    Getting Started with Angular

    +

    Angular is a platform for building mobile and desktop web applications. It provides a way to structure your code and make it maintainable, scalable, and testable. To get started, install the Angular CLI using npm install -g @angular/cli. Then, create a new project with ng new my-app and serve it locally with ng serve. The CLI will scaffold your project structure, including modules, components, and services. Angular uses TypeScript, a superset of JavaScript, which brings strong typing and object-oriented features to your codebase. The framework encourages modular development and supports features like dependency injection, routing, and forms out of the box. As you build your first Angular app, you'll learn about components, templates, and data binding, which are the core building blocks of any Angular application.

    +
      +
    • Install Node.js and npm
    • +
    • Install Angular CLI
    • +
    • Create a new project
    • +
    • Serve your app locally
    • +
    +

    With these steps, you are ready to start exploring Angular's powerful features and build robust web applications.

    `, + author: "Alice Johnson", + date: new Date("2025-05-01") + }, + { + id: 2, + title: "Understanding Angular Components", + blogHtml: `

    Understanding Angular Components

    +

    Components are the fundamental building blocks of Angular applications. Each component consists of a TypeScript class, an HTML template, and optional CSS styles. The class contains the logic and data, while the template defines the view. Components are declared using the @Component decorator, which specifies the selector, template, and styles. You can create a new component using the Angular CLI with ng generate component my-component. Components can communicate with each other using input and output properties, allowing for modular and reusable code. Angular's change detection mechanism ensures that the view is always in sync with the underlying data. By organizing your application into components, you can manage complexity and improve maintainability.

    +
      +
    • Component class (logic and data)
    • +
    • Template (view)
    • +
    • Styles (CSS)
    • +
    • Input and Output properties
    • +
    +

    Mastering components is essential for any Angular developer.

    `, + author: "Bob Smith", + date: new Date("2025-05-02") + }, + { + id: 3, + title: "Angular Directives Explained", + blogHtml: `

    Angular Directives Explained

    +

    Directives in Angular are classes that add additional behavior to elements in your Angular applications. There are three types of directives: components, attribute directives, and structural directives. Attribute directives change the appearance or behavior of an element, component, or another directive. Structural directives change the DOM layout by adding and removing DOM elements. Common built-in directives include *ngIf for conditional rendering and *ngFor for iterating over lists. You can also create your own custom directives to encapsulate reusable logic. Directives are a powerful way to extend HTML and create dynamic, interactive applications.

    +
      +
    • Attribute directives (e.g., ngClass, ngStyle)
    • +
    • Structural directives (e.g., *ngIf, *ngFor)
    • +
    • Custom directives
    • +
    +

    Understanding directives is key to building flexible Angular applications.

    `, + author: "Carol Lee", + date: new Date("2025-05-03") + }, + { + id: 4, + title: "Angular Services and Dependency Injection", + blogHtml: `

    Angular Services and Dependency Injection

    +

    Services in Angular are used to organize and share code across your app. They are typically used for data access, business logic, or shared state. Angular's dependency injection system makes it easy to provide and inject services where needed. To create a service, use the CLI command ng generate service my-service. You can then inject the service into components or other services using the constructor. Services are provided in the root injector by default, making them singletons throughout your app. This pattern promotes code reuse and separation of concerns.

    +
      +
    • Create services for shared logic
    • +
    • Inject services using constructors
    • +
    • Use providedIn: 'root' for singleton services
    • +
    +

    Dependency injection is a core concept in Angular that enables modular and testable code.

    `, + author: "David Kim", + date: new Date("2025-05-04") + }, + { + id: 5, + title: "Routing in Angular Applications", + blogHtml: `

    Routing in Angular Applications

    +

    Routing allows you to navigate between different views or components in your Angular application. The Angular Router maps URL paths to components, enabling deep linking and navigation. To set up routing, import RouterModule and define your routes in the app-routing.module.ts. Use the <router-outlet> directive in your template to display routed components. Navigation can be triggered programmatically or using the routerLink directive in templates. Angular's router supports features like route guards, lazy loading, and parameterized routes, making it a powerful tool for building single-page applications.

    +
      +
    • Define routes in RouterModule
    • +
    • Use <router-outlet> for routed views
    • +
    • Navigate with routerLink
    • +
    +

    Routing is essential for building scalable Angular apps.

    `, + author: "Eva Brown", + date: new Date("2025-05-05") + }, + { + id: 6, + title: "Angular Forms: Template vs Reactive", + blogHtml: `

    Angular Forms: Template vs Reactive

    +

    Angular provides two approaches for handling forms: template-driven and reactive forms. Template-driven forms are easy to use and suitable for simple scenarios, while reactive forms offer more control and scalability for complex forms. Template-driven forms use directives in the template, while reactive forms are defined in the component class using FormGroup and FormControl. Both approaches support validation, but reactive forms make it easier to implement custom validators and dynamic form controls. Choose the approach that best fits your application's needs.

    +
      +
    • Template-driven forms (ngModel)
    • +
    • Reactive forms (FormGroup, FormControl)
    • +
    • Validation and custom validators
    • +
    +

    Understanding both approaches helps you build robust forms in Angular.

    `, + author: "Frank Green", + date: new Date("2025-05-06") + }, + { + id: 7, + title: "State Management in Angular", + blogHtml: `

    State Management in Angular

    +

    Managing state is crucial for complex Angular applications. Angular provides several options for state management, including services, RxJS, and third-party libraries like NgRx. Services can be used to share state between components, while RxJS observables enable reactive programming. NgRx is a popular library that implements the Redux pattern, providing a single source of truth for your application's state. Choosing the right state management solution depends on your app's complexity and requirements.

    +
      +
    • Use services for shared state
    • +
    • Leverage RxJS for reactive state
    • +
    • Consider NgRx for large-scale apps
    • +
    +

    Effective state management leads to maintainable and scalable Angular apps.

    `, + author: "Grace Miller", + date: new Date("2025-05-07") + }, + { + id: 8, + title: "Consuming REST APIs in Angular", + blogHtml: `

    Consuming REST APIs in Angular

    +

    Angular's HttpClient module makes it easy to communicate with RESTful APIs. Import HttpClientModule in your app module and inject HttpClient into your services. Use methods like get, post, put, and delete to interact with your backend. Handle responses using RxJS observables and operators like map and catchError. Always handle errors gracefully and provide feedback to users. Organize your API calls in services to keep your components clean and focused.

    +
      +
    • Import HttpClientModule
    • +
    • Inject HttpClient in services
    • +
    • Use RxJS for response handling
    • +
    +

    Consuming APIs is a core skill for Angular developers.

    `, + author: "Henry Wilson", + date: new Date("2025-05-08") + }, + { + id: 9, + title: "Angular Pipes: Transforming Data", + blogHtml: `

    Angular Pipes: Transforming Data

    +

    Pipes in Angular are used to transform data in templates. Built-in pipes include DatePipe, UpperCasePipe, LowerCasePipe, and CurrencyPipe. You can also create custom pipes to implement your own data transformations. Pipes are easy to use: simply add the pipe symbol (|) followed by the pipe name in your template. For example, {{ birthday | date:'longDate' }} formats a date. Pipes help keep your templates clean and readable.

    +
      +
    • Built-in pipes (date, uppercase, lowercase, currency)
    • +
    • Custom pipes
    • +
    • Use pipes in templates
    • +
    +

    Pipes are a powerful feature for formatting and transforming data in Angular.

    `, + author: "Ivy Martinez", + date: new Date("2025-05-09") + }, + { + id: 10, + title: "Optimizing Angular Performance", + blogHtml: `

    Optimizing Angular Performance

    +

    Performance is critical for modern web applications. Angular provides several tools and techniques to optimize performance, including Ahead-of-Time (AOT) compilation, lazy loading, and change detection strategies. Use AOT to compile your app at build time, reducing load times. Implement lazy loading to load modules only when needed. Use OnPush change detection to minimize unnecessary checks. Profile your app with browser developer tools and Angular DevTools to identify bottlenecks. Optimize your code and assets to ensure a smooth user experience.

    +
      +
    • Use AOT compilation
    • +
    • Implement lazy loading
    • +
    • Optimize change detection
    • +
    +

    Optimizing performance leads to faster and more responsive Angular apps.

    `, + author: "Jack Lee", + date: new Date("2025-05-10") + }, + { + id: 11, + title: "Unit Testing in Angular", + blogHtml: `

    Unit Testing in Angular

    +

    Testing is an essential part of software development. Angular supports unit testing with Jasmine and Karma. Write tests for your components, services, and pipes to ensure they work as expected. Use the Angular CLI to run tests with ng test. Mock dependencies and use spies to isolate units of code. Aim for high test coverage to catch bugs early and improve code quality. Testing also makes refactoring safer and more efficient.

    +
      +
    • Write tests for components and services
    • +
    • Use Jasmine and Karma
    • +
    • Run tests with Angular CLI
    • +
    +

    Unit testing is key to building reliable Angular applications.

    `, + author: "Karen Young", + date: new Date("2025-05-11") + }, + { + id: 12, + title: "Angular Animations", + blogHtml: `

    Angular Animations

    +

    Animations can greatly enhance the user experience in your Angular applications. Angular provides a powerful animation API based on CSS and JavaScript. Define animations in your component metadata using the animations property. Use triggers, states, and transitions to control animations. Angular animations are built on top of the Web Animations API, providing smooth and performant effects. Use animations to provide feedback, guide users, and make your app more engaging.

    +
      +
    • Define animations in component metadata
    • +
    • Use triggers, states, and transitions
    • +
    • Enhance user experience
    • +
    +

    Animations make your Angular apps more dynamic and interactive.

    `, + author: "Leo Turner", + date: new Date("2025-05-12") + }, + { + id: 13, + title: "Lazy Loading Modules in Angular", + blogHtml: `

    Lazy Loading Modules in Angular

    +

    Lazy loading is a technique that loads modules only when they are needed, reducing the initial load time of your Angular application. Configure lazy loading in your routing module using the loadChildren property. Organize your app into feature modules and load them on demand. Lazy loading improves performance, especially for large applications with many routes. Monitor your app's bundle size and optimize your code to take full advantage of lazy loading.

    +
      +
    • Configure lazy loading in routing
    • +
    • Organize app into feature modules
    • +
    • Improve performance for large apps
    • +
    +

    Lazy loading is essential for scalable Angular applications.

    `, + author: "Mona Scott", + date: new Date("2025-05-13") + }, + { + id: 14, + title: "Angular CLI Tips and Tricks", + blogHtml: `

    Angular CLI Tips and Tricks

    +

    The Angular CLI is a powerful tool that streamlines development. Use commands like ng generate to scaffold components, services, and modules. Use ng build --prod to build your app for production. The CLI also supports testing, linting, and deployment. Customize your project with the angular.json configuration file. Explore CLI options to boost your productivity and maintain a clean codebase.

    +
      +
    • Use ng generate for scaffolding
    • +
    • Build for production with ng build --prod
    • +
    • Customize with angular.json
    • +
    +

    The CLI is an indispensable tool for Angular developers.

    `, + author: "Nina Adams", + date: new Date("2025-05-14") + }, + { + id: 15, + title: "Deploying Angular Apps", + blogHtml: `

    Deploying Angular Apps

    +

    Deployment is the final step in the development process. Build your Angular app for production using ng build --prod. This command creates an optimized bundle in the dist/ folder. You can deploy your app to various platforms, including Firebase Hosting, Netlify, Vercel, or your own server. Configure environment variables for different deployment targets. Monitor your app after deployment to ensure it runs smoothly and fix any issues that arise. Proper deployment practices ensure your users have a seamless experience.

    +
      +
    • Build for production
    • +
    • Deploy to popular platforms
    • +
    • Monitor and maintain your app
    • +
    +

    Successful deployment is crucial for delivering value to your users.

    `, + author: "Oscar Perez", + date: new Date("2025-05-15") } - return html.slice(0, length); -} - -// Initialize 15 blogs -let blogs = Array.from({ length: 15 }, (_, i) => ({ - id: i + 1, - title: `Angular Topic #${i + 1}`, - blogHtml: randomHtml(500 + Math.floor(Math.random() * 4500)), - author: `Author ${i + 1}`, - date: new Date(2025, 4, i + 1) -})); +]; // --- Comments Data Model --- // { From e2504f2deff1e66376ac56bf95e97bc3d0ca7c2d Mon Sep 17 00:00:00 2001 From: Ievgien Date: Mon, 26 May 2025 18:54:35 +0400 Subject: [PATCH 04/34] remove hardcoded blogs --- angular.json | 3 +- server/index.js | 45 ++++++++-------------- src/app/blog-list/blog-list.component.html | 22 ++--------- src/app/blog-list/blog-list.component.ts | 11 +++++- src/app/blog-list/blog.service.spec.ts | 16 ++++++++ src/app/blog-list/blog.service.ts | 19 +++++++++ src/app/blog-list/message.model.ts | 7 ++++ src/main.ts | 6 ++- src/proxy.conf.json | 8 ++++ 9 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 src/app/blog-list/blog.service.spec.ts create mode 100644 src/app/blog-list/blog.service.ts create mode 100644 src/app/blog-list/message.model.ts create mode 100644 src/proxy.conf.json diff --git a/angular.json b/angular.json index b5db2fb..04bab6a 100644 --- a/angular.json +++ b/angular.json @@ -68,7 +68,8 @@ "buildTarget": "angular-template:build:production" }, "development": { - "buildTarget": "angular-template:build:development" + "buildTarget": "angular-template:build:development", + "proxyConfig": "src/proxy.conf.json" } }, "defaultConfiguration": "development" diff --git a/server/index.js b/server/index.js index e6f58cd..ea4f5bd 100644 --- a/server/index.js +++ b/server/index.js @@ -22,8 +22,7 @@ let blogs = [ { id: 1, title: "Getting Started with Angular", - blogHtml: `

    Getting Started with Angular

    -

    Angular is a platform for building mobile and desktop web applications. It provides a way to structure your code and make it maintainable, scalable, and testable. To get started, install the Angular CLI using npm install -g @angular/cli. Then, create a new project with ng new my-app and serve it locally with ng serve. The CLI will scaffold your project structure, including modules, components, and services. Angular uses TypeScript, a superset of JavaScript, which brings strong typing and object-oriented features to your codebase. The framework encourages modular development and supports features like dependency injection, routing, and forms out of the box. As you build your first Angular app, you'll learn about components, templates, and data binding, which are the core building blocks of any Angular application.

    + blogHtml: `

    Angular is a platform for building mobile and desktop web applications. It provides a way to structure your code and make it maintainable, scalable, and testable. To get started, install the Angular CLI using npm install -g @angular/cli. Then, create a new project with ng new my-app and serve it locally with ng serve. The CLI will scaffold your project structure, including modules, components, and services. Angular uses TypeScript, a superset of JavaScript, which brings strong typing and object-oriented features to your codebase. The framework encourages modular development and supports features like dependency injection, routing, and forms out of the box. As you build your first Angular app, you'll learn about components, templates, and data binding, which are the core building blocks of any Angular application.

    • Install Node.js and npm
    • Install Angular CLI
    • @@ -37,8 +36,7 @@ let blogs = [ { id: 2, title: "Understanding Angular Components", - blogHtml: `

      Understanding Angular Components

      -

      Components are the fundamental building blocks of Angular applications. Each component consists of a TypeScript class, an HTML template, and optional CSS styles. The class contains the logic and data, while the template defines the view. Components are declared using the @Component decorator, which specifies the selector, template, and styles. You can create a new component using the Angular CLI with ng generate component my-component. Components can communicate with each other using input and output properties, allowing for modular and reusable code. Angular's change detection mechanism ensures that the view is always in sync with the underlying data. By organizing your application into components, you can manage complexity and improve maintainability.

      + blogHtml: `

      Components are the fundamental building blocks of Angular applications. Each component consists of a TypeScript class, an HTML template, and optional CSS styles. The class contains the logic and data, while the template defines the view. Components are declared using the @Component decorator, which specifies the selector, template, and styles. You can create a new component using the Angular CLI with ng generate component my-component. Components can communicate with each other using input and output properties, allowing for modular and reusable code. Angular's change detection mechanism ensures that the view is always in sync with the underlying data. By organizing your application into components, you can manage complexity and improve maintainability.

      • Component class (logic and data)
      • Template (view)
      • @@ -52,8 +50,7 @@ let blogs = [ { id: 3, title: "Angular Directives Explained", - blogHtml: `

        Angular Directives Explained

        -

        Directives in Angular are classes that add additional behavior to elements in your Angular applications. There are three types of directives: components, attribute directives, and structural directives. Attribute directives change the appearance or behavior of an element, component, or another directive. Structural directives change the DOM layout by adding and removing DOM elements. Common built-in directives include *ngIf for conditional rendering and *ngFor for iterating over lists. You can also create your own custom directives to encapsulate reusable logic. Directives are a powerful way to extend HTML and create dynamic, interactive applications.

        + blogHtml: `

        Directives in Angular are classes that add additional behavior to elements in your Angular applications. There are three types of directives: components, attribute directives, and structural directives. Attribute directives change the appearance or behavior of an element, component, or another directive. Structural directives change the DOM layout by adding and removing DOM elements. Common built-in directives include *ngIf for conditional rendering and *ngFor for iterating over lists. You can also create your own custom directives to encapsulate reusable logic. Directives are a powerful way to extend HTML and create dynamic, interactive applications.

        • Attribute directives (e.g., ngClass, ngStyle)
        • Structural directives (e.g., *ngIf, *ngFor)
        • @@ -66,8 +63,7 @@ let blogs = [ { id: 4, title: "Angular Services and Dependency Injection", - blogHtml: `

          Angular Services and Dependency Injection

          -

          Services in Angular are used to organize and share code across your app. They are typically used for data access, business logic, or shared state. Angular's dependency injection system makes it easy to provide and inject services where needed. To create a service, use the CLI command ng generate service my-service. You can then inject the service into components or other services using the constructor. Services are provided in the root injector by default, making them singletons throughout your app. This pattern promotes code reuse and separation of concerns.

          + blogHtml: `

          Services in Angular are used to organize and share code across your app. They are typically used for data access, business logic, or shared state. Angular's dependency injection system makes it easy to provide and inject services where needed. To create a service, use the CLI command ng generate service my-service. You can then inject the service into components or other services using the constructor. Services are provided in the root injector by default, making them singletons throughout your app. This pattern promotes code reuse and separation of concerns.

          • Create services for shared logic
          • Inject services using constructors
          • @@ -80,8 +76,7 @@ let blogs = [ { id: 5, title: "Routing in Angular Applications", - blogHtml: `

            Routing in Angular Applications

            -

            Routing allows you to navigate between different views or components in your Angular application. The Angular Router maps URL paths to components, enabling deep linking and navigation. To set up routing, import RouterModule and define your routes in the app-routing.module.ts. Use the <router-outlet> directive in your template to display routed components. Navigation can be triggered programmatically or using the routerLink directive in templates. Angular's router supports features like route guards, lazy loading, and parameterized routes, making it a powerful tool for building single-page applications.

            + blogHtml: `

            Routing allows you to navigate between different views or components in your Angular application. The Angular Router maps URL paths to components, enabling deep linking and navigation. To set up routing, import RouterModule and define your routes in the app-routing.module.ts. Use the <router-outlet> directive in your template to display routed components. Navigation can be triggered programmatically or using the routerLink directive in templates. Angular's router supports features like route guards, lazy loading, and parameterized routes, making it a powerful tool for building single-page applications.

            • Define routes in RouterModule
            • Use <router-outlet> for routed views
            • @@ -94,8 +89,7 @@ let blogs = [ { id: 6, title: "Angular Forms: Template vs Reactive", - blogHtml: `

              Angular Forms: Template vs Reactive

              -

              Angular provides two approaches for handling forms: template-driven and reactive forms. Template-driven forms are easy to use and suitable for simple scenarios, while reactive forms offer more control and scalability for complex forms. Template-driven forms use directives in the template, while reactive forms are defined in the component class using FormGroup and FormControl. Both approaches support validation, but reactive forms make it easier to implement custom validators and dynamic form controls. Choose the approach that best fits your application's needs.

              + blogHtml: `

              Angular provides two approaches for handling forms: template-driven and reactive forms. Template-driven forms are easy to use and suitable for simple scenarios, while reactive forms offer more control and scalability for complex forms. Template-driven forms use directives in the template, while reactive forms are defined in the component class using FormGroup and FormControl. Both approaches support validation, but reactive forms make it easier to implement custom validators and dynamic form controls. Choose the approach that best fits your application's needs.

              • Template-driven forms (ngModel)
              • Reactive forms (FormGroup, FormControl)
              • @@ -108,8 +102,7 @@ let blogs = [ { id: 7, title: "State Management in Angular", - blogHtml: `

                State Management in Angular

                -

                Managing state is crucial for complex Angular applications. Angular provides several options for state management, including services, RxJS, and third-party libraries like NgRx. Services can be used to share state between components, while RxJS observables enable reactive programming. NgRx is a popular library that implements the Redux pattern, providing a single source of truth for your application's state. Choosing the right state management solution depends on your app's complexity and requirements.

                + blogHtml: `

                Managing state is crucial for complex Angular applications. Angular provides several options for state management, including services, RxJS, and third-party libraries like NgRx. Services can be used to share state between components, while RxJS observables enable reactive programming. NgRx is a popular library that implements the Redux pattern, providing a single source of truth for your application's state. Choosing the right state management solution depends on your app's complexity and requirements.

                • Use services for shared state
                • Leverage RxJS for reactive state
                • @@ -122,8 +115,7 @@ let blogs = [ { id: 8, title: "Consuming REST APIs in Angular", - blogHtml: `

                  Consuming REST APIs in Angular

                  -

                  Angular's HttpClient module makes it easy to communicate with RESTful APIs. Import HttpClientModule in your app module and inject HttpClient into your services. Use methods like get, post, put, and delete to interact with your backend. Handle responses using RxJS observables and operators like map and catchError. Always handle errors gracefully and provide feedback to users. Organize your API calls in services to keep your components clean and focused.

                  + blogHtml: `

                  Angular's HttpClient module makes it easy to communicate with RESTful APIs. Import HttpClientModule in your app module and inject HttpClient into your services. Use methods like get, post, put, and delete to interact with your backend. Handle responses using RxJS observables and operators like map and catchError. Always handle errors gracefully and provide feedback to users. Organize your API calls in services to keep your components clean and focused.

                  • Import HttpClientModule
                  • Inject HttpClient in services
                  • @@ -136,8 +128,7 @@ let blogs = [ { id: 9, title: "Angular Pipes: Transforming Data", - blogHtml: `

                    Angular Pipes: Transforming Data

                    -

                    Pipes in Angular are used to transform data in templates. Built-in pipes include DatePipe, UpperCasePipe, LowerCasePipe, and CurrencyPipe. You can also create custom pipes to implement your own data transformations. Pipes are easy to use: simply add the pipe symbol (|) followed by the pipe name in your template. For example, {{ birthday | date:'longDate' }} formats a date. Pipes help keep your templates clean and readable.

                    + blogHtml: `

                    Pipes in Angular are used to transform data in templates. Built-in pipes include DatePipe, UpperCasePipe, LowerCasePipe, and CurrencyPipe. You can also create custom pipes to implement your own data transformations. Pipes are easy to use: simply add the pipe symbol (|) followed by the pipe name in your template. For example, {{ birthday | date:'longDate' }} formats a date. Pipes help keep your templates clean and readable.

                    • Built-in pipes (date, uppercase, lowercase, currency)
                    • Custom pipes
                    • @@ -150,8 +141,7 @@ let blogs = [ { id: 10, title: "Optimizing Angular Performance", - blogHtml: `

                      Optimizing Angular Performance

                      -

                      Performance is critical for modern web applications. Angular provides several tools and techniques to optimize performance, including Ahead-of-Time (AOT) compilation, lazy loading, and change detection strategies. Use AOT to compile your app at build time, reducing load times. Implement lazy loading to load modules only when needed. Use OnPush change detection to minimize unnecessary checks. Profile your app with browser developer tools and Angular DevTools to identify bottlenecks. Optimize your code and assets to ensure a smooth user experience.

                      + blogHtml: `

                      Performance is critical for modern web applications. Angular provides several tools and techniques to optimize performance, including Ahead-of-Time (AOT) compilation, lazy loading, and change detection strategies. Use AOT to compile your app at build time, reducing load times. Implement lazy loading to load modules only when needed. Use OnPush change detection to minimize unnecessary checks. Profile your app with browser developer tools and Angular DevTools to identify bottlenecks. Optimize your code and assets to ensure a smooth user experience.

                      • Use AOT compilation
                      • Implement lazy loading
                      • @@ -164,8 +154,7 @@ let blogs = [ { id: 11, title: "Unit Testing in Angular", - blogHtml: `

                        Unit Testing in Angular

                        -

                        Testing is an essential part of software development. Angular supports unit testing with Jasmine and Karma. Write tests for your components, services, and pipes to ensure they work as expected. Use the Angular CLI to run tests with ng test. Mock dependencies and use spies to isolate units of code. Aim for high test coverage to catch bugs early and improve code quality. Testing also makes refactoring safer and more efficient.

                        + blogHtml: `

                        Testing is an essential part of software development. Angular supports unit testing with Jasmine and Karma. Write tests for your components, services, and pipes to ensure they work as expected. Use the Angular CLI to run tests with ng test. Mock dependencies and use spies to isolate units of code. Aim for high test coverage to catch bugs early and improve code quality. Testing also makes refactoring safer and more efficient.

                        • Write tests for components and services
                        • Use Jasmine and Karma
                        • @@ -178,8 +167,7 @@ let blogs = [ { id: 12, title: "Angular Animations", - blogHtml: `

                          Angular Animations

                          -

                          Animations can greatly enhance the user experience in your Angular applications. Angular provides a powerful animation API based on CSS and JavaScript. Define animations in your component metadata using the animations property. Use triggers, states, and transitions to control animations. Angular animations are built on top of the Web Animations API, providing smooth and performant effects. Use animations to provide feedback, guide users, and make your app more engaging.

                          + blogHtml: `

                          Animations can greatly enhance the user experience in your Angular applications. Angular provides a powerful animation API based on CSS and JavaScript. Define animations in your component metadata using the animations property. Use triggers, states, and transitions to control animations. Angular animations are built on top of the Web Animations API, providing smooth and performant effects. Use animations to provide feedback, guide users, and make your app more engaging.

                          • Define animations in component metadata
                          • Use triggers, states, and transitions
                          • @@ -192,8 +180,7 @@ let blogs = [ { id: 13, title: "Lazy Loading Modules in Angular", - blogHtml: `

                            Lazy Loading Modules in Angular

                            -

                            Lazy loading is a technique that loads modules only when they are needed, reducing the initial load time of your Angular application. Configure lazy loading in your routing module using the loadChildren property. Organize your app into feature modules and load them on demand. Lazy loading improves performance, especially for large applications with many routes. Monitor your app's bundle size and optimize your code to take full advantage of lazy loading.

                            + blogHtml: `

                            Lazy loading is a technique that loads modules only when they are needed, reducing the initial load time of your Angular application. Configure lazy loading in your routing module using the loadChildren property. Organize your app into feature modules and load them on demand. Lazy loading improves performance, especially for large applications with many routes. Monitor your app's bundle size and optimize your code to take full advantage of lazy loading.

                            • Configure lazy loading in routing
                            • Organize app into feature modules
                            • @@ -206,8 +193,7 @@ let blogs = [ { id: 14, title: "Angular CLI Tips and Tricks", - blogHtml: `

                              Angular CLI Tips and Tricks

                              -

                              The Angular CLI is a powerful tool that streamlines development. Use commands like ng generate to scaffold components, services, and modules. Use ng build --prod to build your app for production. The CLI also supports testing, linting, and deployment. Customize your project with the angular.json configuration file. Explore CLI options to boost your productivity and maintain a clean codebase.

                              + blogHtml: `

                              The Angular CLI is a powerful tool that streamlines development. Use commands like ng generate to scaffold components, services, and modules. Use ng build --prod to build your app for production. The CLI also supports testing, linting, and deployment. Customize your project with the angular.json configuration file. Explore CLI options to boost your productivity and maintain a clean codebase.

                              • Use ng generate for scaffolding
                              • Build for production with ng build --prod
                              • @@ -220,8 +206,7 @@ let blogs = [ { id: 15, title: "Deploying Angular Apps", - blogHtml: `

                                Deploying Angular Apps

                                -

                                Deployment is the final step in the development process. Build your Angular app for production using ng build --prod. This command creates an optimized bundle in the dist/ folder. You can deploy your app to various platforms, including Firebase Hosting, Netlify, Vercel, or your own server. Configure environment variables for different deployment targets. Monitor your app after deployment to ensure it runs smoothly and fix any issues that arise. Proper deployment practices ensure your users have a seamless experience.

                                + blogHtml: `

                                Deployment is the final step in the development process. Build your Angular app for production using ng build --prod. This command creates an optimized bundle in the dist/ folder. You can deploy your app to various platforms, including Firebase Hosting, Netlify, Vercel, or your own server. Configure environment variables for different deployment targets. Monitor your app after deployment to ensure it runs smoothly and fix any issues that arise. Proper deployment practices ensure your users have a seamless experience.

                                • Build for production
                                • Deploy to popular platforms
                                • diff --git a/src/app/blog-list/blog-list.component.html b/src/app/blog-list/blog-list.component.html index e9f920f..b591112 100644 --- a/src/app/blog-list/blog-list.component.html +++ b/src/app/blog-list/blog-list.component.html @@ -1,25 +1,11 @@

                                  Blog Posts

                                    -
                                  • +
                                  • -
                                  • -
                                  • - -
                                  • -
                                  • -
                                  diff --git a/src/app/blog-list/blog-list.component.ts b/src/app/blog-list/blog-list.component.ts index 43c34b6..743e57d 100644 --- a/src/app/blog-list/blog-list.component.ts +++ b/src/app/blog-list/blog-list.component.ts @@ -1,12 +1,21 @@ import { Component } from '@angular/core'; +import { BlogService } from './blog.service'; +import { IBlog } from './message.model'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-blog-list', - imports: [], + imports: [CommonModule], standalone: true, templateUrl: './blog-list.component.html', styleUrl: './blog-list.component.scss' }) export class BlogListComponent { + blogs: IBlog[] = []; + constructor(private blogService: BlogService) { + this.blogService.getBlogs().subscribe((data: any) => { + this.blogs = data; + }); + } } diff --git a/src/app/blog-list/blog.service.spec.ts b/src/app/blog-list/blog.service.spec.ts new file mode 100644 index 0000000..64866b7 --- /dev/null +++ b/src/app/blog-list/blog.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BlogService } from './blog.service'; + +describe('BlogService', () => { + let service: BlogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BlogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/blog-list/blog.service.ts b/src/app/blog-list/blog.service.ts new file mode 100644 index 0000000..57764a3 --- /dev/null +++ b/src/app/blog-list/blog.service.ts @@ -0,0 +1,19 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { IBlog } from './message.model'; + +@Injectable({ + providedIn: 'root' +}) +export class BlogService { + + constructor(private http: HttpClient) { } + /** + * Fetches the list of blogs from the server. + * @returns An Observable that emits an array of IBlog objects. + */ + getBlogs(): Observable { + return this.http.get('/blogs'); + } +} diff --git a/src/app/blog-list/message.model.ts b/src/app/blog-list/message.model.ts new file mode 100644 index 0000000..fab51ce --- /dev/null +++ b/src/app/blog-list/message.model.ts @@ -0,0 +1,7 @@ +export interface IBlog { + id: number, + title: string, + blogHtml: string, + author: string, + date: Date + } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 75a3f6a..e17ab74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,8 +2,12 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { provideRouter } from '@angular/router'; import { appRoutes } from './app/app-routes'; import { AppComponent } from './app/app.component'; +import { provideHttpClient } from '@angular/common/http'; bootstrapApplication(AppComponent, { - providers: [provideRouter(appRoutes)] + providers: [ + provideRouter(appRoutes), + provideHttpClient() + ] }) .catch(err => console.error(err)); diff --git a/src/proxy.conf.json b/src/proxy.conf.json new file mode 100644 index 0000000..4d9fff8 --- /dev/null +++ b/src/proxy.conf.json @@ -0,0 +1,8 @@ +{ + "/blogs": { + "target": "http://localhost:3000", + "secure": false, + "changeOrigin": true, + "logLevel": "debug" + } +} \ No newline at end of file From 2b1e5cfd1a5c7b4581a2f0b16855a00ca1bc0ce0 Mon Sep 17 00:00:00 2001 From: Ievgien Date: Mon, 26 May 2025 20:13:07 +0400 Subject: [PATCH 05/34] add page to leave comments to blogs --- src/app/app-routes.ts | 2 + .../blog-comment/blog-comment.component.html | 55 +++++++++++ .../blog-comment/blog-comment.component.scss | 91 +++++++++++++++++++ .../blog-comment.component.spec.ts | 23 +++++ .../blog-comment/blog-comment.component.ts | 46 ++++++++++ .../blog.model.ts} | 0 src/app/blog-comment/blog.service.spec.ts | 16 ++++ src/app/blog-comment/blog.service.ts | 15 +++ src/app/blog-comment/comment.model.ts | 7 ++ src/app/blog-list/blog-list.component.html | 2 +- src/app/blog-list/blog-list.component.ts | 10 +- src/app/blog-list/blog.model.ts | 7 ++ src/app/blog-list/blog.service.ts | 2 +- src/proxy.conf.json | 4 +- 14 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 src/app/blog-comment/blog-comment.component.html create mode 100644 src/app/blog-comment/blog-comment.component.scss create mode 100644 src/app/blog-comment/blog-comment.component.spec.ts create mode 100644 src/app/blog-comment/blog-comment.component.ts rename src/app/{blog-list/message.model.ts => blog-comment/blog.model.ts} (100%) create mode 100644 src/app/blog-comment/blog.service.spec.ts create mode 100644 src/app/blog-comment/blog.service.ts create mode 100644 src/app/blog-comment/comment.model.ts create mode 100644 src/app/blog-list/blog.model.ts diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 9de7f9f..b89c541 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,9 +1,11 @@ import { Routes } from '@angular/router'; import { BlogListComponent } from './blog-list/blog-list.component'; import { GuestBookComponent } from './guest-book/guest-book.component'; +import { BlogCommentComponent } from './blog-comment/blog-comment.component'; export const appRoutes: Routes = [ { path: 'blogs', component: BlogListComponent, title: "Home - List of blogs" }, + { path: 'blogs/:id', component: BlogCommentComponent, title: "Blog Details" }, { path: 'guest-book', component: GuestBookComponent, title: "GuestBook - Welcome to guest book" }, { path: '', redirectTo: '/blogs', pathMatch: 'full' }, ]; diff --git a/src/app/blog-comment/blog-comment.component.html b/src/app/blog-comment/blog-comment.component.html new file mode 100644 index 0000000..b39819f --- /dev/null +++ b/src/app/blog-comment/blog-comment.component.html @@ -0,0 +1,55 @@ +
                                  +
                                  +

                                  {{ blog.title }}

                                  +
                                  + By {{ blog.author }} + {{ blog.date | date:'mediumDate' }} +
                                  +
                                  +
                                  + +
                                  +

                                  Comments

                                  +
                                  No comments yet. Be the first to comment!
                                  +
                                    +
                                  • +
                                    + {{ comment.name }} + {{ comment.date | date:'short' }} +
                                    +
                                    {{ comment.message }}
                                    +
                                  • +
                                  +
                                  + +
                                  +

                                  Add a Comment

                                  +
                                  + + + + +
                                  +
                                  +
                                  diff --git a/src/app/blog-comment/blog-comment.component.scss b/src/app/blog-comment/blog-comment.component.scss new file mode 100644 index 0000000..c0c2102 --- /dev/null +++ b/src/app/blog-comment/blog-comment.component.scss @@ -0,0 +1,91 @@ +.blog-comment-container { + max-width: 700px; + margin: 2rem auto; + background: #fff; + border-radius: 14px; + box-shadow: 0 4px 24px rgba(78, 84, 200, 0.10); + padding: 2rem 2.5rem; + font-family: 'Segoe UI', Arial, sans-serif; +} +.blog-details h2 { + margin-bottom: 0.2rem; + color: #4e54c8; +} +.blog-meta { + color: #888; + font-size: 0.95rem; + margin-bottom: 1.2rem; + display: flex; + gap: 1.5rem; +} +.blog-content { + margin-bottom: 2rem; + line-height: 1.7; + color: #222; +} +.comments-section { + margin-bottom: 2rem; +} +.comments-section h3 { + margin-bottom: 1rem; + color: #4e54c8; +} +.no-comments { + color: #aaa; + font-style: italic; + margin-bottom: 1rem; +} +ul { + list-style: none; + padding: 0; +} +.comment { + background: #f7f8fa; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + box-shadow: 0 1px 4px rgba(78, 84, 200, 0.05); +} +.comment-header { + display: flex; + justify-content: space-between; + font-size: 0.95rem; + margin-bottom: 0.5rem; + color: #4e54c8; +} +.comment-message { + color: #333; +} +.add-comment-section h3 { + margin-bottom: 1rem; + color: #4e54c8; +} +.input, .textarea { + width: 100%; + padding: 0.7rem; + margin-bottom: 0.8rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 1rem; + background: #f9f9fb; + transition: border 0.2s; +} +.input:focus, .textarea:focus { + border-color: #4e54c8; + outline: none; +} +.submit-btn { + background: linear-gradient(90deg, #8f94fb 0%, #4e54c8 100%); + color: #fff; + border: none; + border-radius: 6px; + padding: 0.7rem 1.5rem; + font-size: 1rem; + cursor: pointer; + transition: background 0.2s, box-shadow 0.2s; + box-shadow: 0 2px 8px rgba(78, 84, 200, 0.08); +} +.submit-btn:disabled { + background: #bfc2e6; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/app/blog-comment/blog-comment.component.spec.ts b/src/app/blog-comment/blog-comment.component.spec.ts new file mode 100644 index 0000000..cfcc49e --- /dev/null +++ b/src/app/blog-comment/blog-comment.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BlogCommentComponent } from './blog-comment.component'; + +describe('BlogCommentComponent', () => { + let component: BlogCommentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BlogCommentComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BlogCommentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/blog-comment/blog-comment.component.ts b/src/app/blog-comment/blog-comment.component.ts new file mode 100644 index 0000000..2592735 --- /dev/null +++ b/src/app/blog-comment/blog-comment.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit } from '@angular/core'; +import { IBlog } from '../blog-list/blog.model'; +import { IComment } from './comment.model'; +import { ActivatedRoute } from '@angular/router'; +import { BlogService } from './blog.service'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-blog-comment', + imports: [CommonModule, FormsModule], + templateUrl: './blog-comment.component.html', + styleUrl: './blog-comment.component.scss' +}) +export class BlogCommentComponent implements OnInit{ + blogId!: number; + blog: IBlog | undefined; + comments: IComment[] = []; + newComment: IComment = { + id: 0, + name: '', + email: '', + message: '', + date: new Date() + }; + constructor(private route: ActivatedRoute, private blogService: BlogService) { + this.route.paramMap.subscribe(params => { + this.blogId = Number(params.get('id')); + }); + } + ngOnInit(): void { + this.blogService.getBlog(this.blogId).subscribe((data: IBlog) => { + this.blog = data;}); + } + + addComment() { + if (this.newComment.name && this.newComment.email && this.newComment.message) { + this.newComment.id = this.comments.length + 1; // Simple ID generation + this.newComment.date = new Date(); + this.comments.push({ ...this.newComment }); + this.newComment = { id: 0, name: '', email: '', message: '', date: new Date() }; // Reset form + } else { + alert('Please fill in all fields.'); + } + } +} diff --git a/src/app/blog-list/message.model.ts b/src/app/blog-comment/blog.model.ts similarity index 100% rename from src/app/blog-list/message.model.ts rename to src/app/blog-comment/blog.model.ts diff --git a/src/app/blog-comment/blog.service.spec.ts b/src/app/blog-comment/blog.service.spec.ts new file mode 100644 index 0000000..64866b7 --- /dev/null +++ b/src/app/blog-comment/blog.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BlogService } from './blog.service'; + +describe('BlogService', () => { + let service: BlogService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BlogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/blog-comment/blog.service.ts b/src/app/blog-comment/blog.service.ts new file mode 100644 index 0000000..c1a0b4d --- /dev/null +++ b/src/app/blog-comment/blog.service.ts @@ -0,0 +1,15 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { IBlog } from './blog.model'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class BlogService { + + constructor(private http: HttpClient) { } + getBlog(id: number): Observable { + return this.http.get(`/blogs/${id}`); + } +} diff --git a/src/app/blog-comment/comment.model.ts b/src/app/blog-comment/comment.model.ts new file mode 100644 index 0000000..952a4b4 --- /dev/null +++ b/src/app/blog-comment/comment.model.ts @@ -0,0 +1,7 @@ +export interface IComment { + id: number; + name: string; + message: string; + email: string; + date: Date; + } \ No newline at end of file diff --git a/src/app/blog-list/blog-list.component.html b/src/app/blog-list/blog-list.component.html index b591112..14cd527 100644 --- a/src/app/blog-list/blog-list.component.html +++ b/src/app/blog-list/blog-list.component.html @@ -2,7 +2,7 @@

                                  Blog Posts

                                  • -
                                  -
                                  +