feat: client and server logic to manage announcements (#3)
All checks were successful
Node.js CI / Lint and Test (push) Successful in 1m9s

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [x] I have pinned the dependencies to a specific patch version.

### Style

- [x] I have run the linter and resolved any errors.
- [x] My pull request uses an appropriate title, matching the conventional commit standards.
- [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #3
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
2025-07-05 19:27:20 -07:00
committed by Naomi Carrigan
parent a12f2b0315
commit 37081cab76
27 changed files with 2012 additions and 107 deletions

View File

@ -28,6 +28,7 @@
"@angular/forms": "20.0.6",
"@angular/platform-browser": "20.0.6",
"@angular/router": "20.0.6",
"ngx-markdown": "20.0.0",
"rxjs": "7.8.2",
"tslib": "2.8.1",
"zone.js": "0.15.1"

View File

@ -0,0 +1,38 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class AnnouncementsService {
public constructor() {}
// eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Getter for static URL.
private get url(): string {
return "http://localhost:20000/announcements";
}
public async getAnnouncements(): Promise<
Array<{
title: string;
content: string;
createdAt: string;
type: "products" | "community";
}>
> {
const response = await fetch(this.url);
if (!response.ok) {
return [];
}
return (await response.json()) as Array<{
title: string;
content: string;
createdAt: string;
type: "products" | "community";
}>;
}
}

View File

@ -0,0 +1,37 @@
hr {
width: 100%;
border: none;
border-top: 1px solid var(--foreground);
margin: 0;
}
:host ::ng-deep ul{
list-style-type: disc;
list-style-position: inside;
}
.announcement {
margin: auto;
margin-bottom: 1em;
width: 90%;
}
.tag {
display: inline-block;
padding: 0 0.5em;
border-radius: 50px;
font-size: 0.8em;
}
.products {
background-color: #e0f7fa;
color: #006064;
}
.community {
background-color: #e8f5e9;
color: #1b5e20;
}
.date {
font-style: italic;
}

View File

@ -0,0 +1,20 @@
<h1>Announcements</h1>
<p>Here are the most recent updates for our products and communities.</p>
<p>
If you want to see the full history, check out our
<a href="https://chat.nhcarrigan.com" target="_blank">chat server</a> or our
<a href="https://forum.nhcarrigan.com" target="_blank">forum</a>.
</p>
<div class="announcement" *ngFor="let announcement of announcements">
<hr />
<h2>{{ announcement.title }}</h2>
<p>
<span [class]="'tag ' + announcement.type">{{announcement.type}}</span>
<span class="date"> {{ announcement.createdAt | date: "mediumDate" }}</span>
</p>
<markdown [data]="announcement.content"></markdown>
<p class="type">Type: {{ announcement.type }}</p>
</div>
<div class="no-announcements" *ngIf="!announcements.length">
<p>There are no announcements at this time.</p>
</div>

View File

@ -0,0 +1,39 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule, DatePipe } from "@angular/common";
import { Component, SecurityContext } from "@angular/core";
import { MarkdownComponent, provideMarkdown } from "ngx-markdown";
import { AnnouncementsService } from "../announcements.js";
@Component({
imports: [ CommonModule, DatePipe, MarkdownComponent ],
providers: [ provideMarkdown({ sanitize: SecurityContext.HTML }) ],
selector: "app-announcements",
styleUrl: "./announcements.css",
templateUrl: "./announcements.html",
})
export class Announcements {
public announcements: Array<{
title: string;
content: string;
createdAt: string;
type: "products" | "community";
}> = [];
public constructor(
private readonly announcementsService: AnnouncementsService,
) {
void this.loadAnnouncements();
}
private async loadAnnouncements(): Promise<void> {
const announcements = await this.announcementsService.getAnnouncements();
this.announcements = announcements.sort((a, b) => {
return b.createdAt > a.createdAt
? 1
: -1;
});
}
}

View File

@ -5,6 +5,7 @@
*/
import { Routes } from "@angular/router";
import { Announcements } from "./announcements/announcements.js";
import { Home } from "./home/home.js";
import { Products } from "./products/products.js";
import { Soon } from "./soon/soon.js";
@ -12,5 +13,6 @@ import { Soon } from "./soon/soon.js";
export const routes: Routes = [
{ component: Home, path: "", pathMatch: "full" },
{ component: Products, path: "products" },
{ component: Announcements, path: "announcements" },
{ component: Soon, path: "**" },
];

View File

@ -4,6 +4,11 @@ ul {
margin: 0;
}
::ng-deep main{
overflow: hidden !important;
max-width: 100%;
}
#one {
transform: translateY(-200vh);
animation: slide-down 2s forwards;
@ -35,9 +40,14 @@ ul {
animation: slide-right 2s forwards 10s;
}
#seven {
transform: translateX(-200vw);
animation: slide-left 2s forwards 12s;
}
#fade {
opacity: 0;
animation: fade-in 2s forwards 12s;
animation: fade-in 2s forwards 14s;
display: flex;
flex-direction: row;
justify-content: space-evenly;

View File

@ -7,12 +7,14 @@
<p id="one">How may I help you today?</p>
<p id="two">I can assist you with:</p>
<ul>
<li id="three">Finding a product to suit your needs</li>
<li id="four">Manage your account, subscriptions, and licenses</li>
<li id="five">Modifying settings for individual products</li>
<li id="six">Answering your specific questions with a chat assistant</li>
<li id="three">Checking the latest updates.</li>
<li id="four">Finding a product to suit your needs</li>
<li id="five">Manage your account, subscriptions, and licenses</li>
<li id="six">Modifying settings for individual products</li>
<li id="seven">Answering your specific questions with a chat assistant</li>
</ul>
<div id="fade">
<a routerLink="/announcements" class="btn">View Announcements</a>
<a routerLink="/products" class="btn">Browse Products</a>
<a routerLink="/account" class="btn">Manage Account</a>
<a routerLink="/settings" class="btn">Modify Settings</a>

View File

@ -5,6 +5,8 @@
></a
>
<div [class]="dropdownClass">
<a routerLink="/announcements" class="nav-link">Announcements</a>
<hr />
<a routerLink="/products" class="nav-link">Products</a>
<hr />
<a routerLink="/account" class="nav-link">Account</a>