generated from nhcarrigan/template
feat: client and server logic to manage announcements #3
@ -28,6 +28,7 @@
|
|||||||
"@angular/forms": "20.0.6",
|
"@angular/forms": "20.0.6",
|
||||||
"@angular/platform-browser": "20.0.6",
|
"@angular/platform-browser": "20.0.6",
|
||||||
"@angular/router": "20.0.6",
|
"@angular/router": "20.0.6",
|
||||||
|
"ngx-markdown": "20.0.0",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"zone.js": "0.15.1"
|
"zone.js": "0.15.1"
|
||||||
|
38
client/src/app/announcements.ts
Normal file
38
client/src/app/announcements.ts
Normal 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";
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
}
|
37
client/src/app/announcements/announcements.css
Normal file
37
client/src/app/announcements/announcements.css
Normal 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;
|
||||||
|
}
|
20
client/src/app/announcements/announcements.html
Normal file
20
client/src/app/announcements/announcements.html
Normal 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>
|
39
client/src/app/announcements/announcements.ts
Normal file
39
client/src/app/announcements/announcements.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Routes } from "@angular/router";
|
import { Routes } from "@angular/router";
|
||||||
|
import { Announcements } from "./announcements/announcements.js";
|
||||||
import { Home } from "./home/home.js";
|
import { Home } from "./home/home.js";
|
||||||
import { Products } from "./products/products.js";
|
import { Products } from "./products/products.js";
|
||||||
import { Soon } from "./soon/soon.js";
|
import { Soon } from "./soon/soon.js";
|
||||||
@ -12,5 +13,6 @@ import { Soon } from "./soon/soon.js";
|
|||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ component: Home, path: "", pathMatch: "full" },
|
{ component: Home, path: "", pathMatch: "full" },
|
||||||
{ component: Products, path: "products" },
|
{ component: Products, path: "products" },
|
||||||
|
{ component: Announcements, path: "announcements" },
|
||||||
{ component: Soon, path: "**" },
|
{ component: Soon, path: "**" },
|
||||||
];
|
];
|
||||||
|
@ -4,6 +4,11 @@ ul {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep main{
|
||||||
|
overflow: hidden !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#one {
|
#one {
|
||||||
transform: translateY(-200vh);
|
transform: translateY(-200vh);
|
||||||
animation: slide-down 2s forwards;
|
animation: slide-down 2s forwards;
|
||||||
@ -35,9 +40,14 @@ ul {
|
|||||||
animation: slide-right 2s forwards 10s;
|
animation: slide-right 2s forwards 10s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#seven {
|
||||||
|
transform: translateX(-200vw);
|
||||||
|
animation: slide-left 2s forwards 12s;
|
||||||
|
}
|
||||||
|
|
||||||
#fade {
|
#fade {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: fade-in 2s forwards 12s;
|
animation: fade-in 2s forwards 14s;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
|
@ -7,12 +7,14 @@
|
|||||||
<p id="one">How may I help you today?</p>
|
<p id="one">How may I help you today?</p>
|
||||||
<p id="two">I can assist you with:</p>
|
<p id="two">I can assist you with:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li id="three">Finding a product to suit your needs</li>
|
<li id="three">Checking the latest updates.</li>
|
||||||
<li id="four">Manage your account, subscriptions, and licenses</li>
|
<li id="four">Finding a product to suit your needs</li>
|
||||||
<li id="five">Modifying settings for individual products</li>
|
<li id="five">Manage your account, subscriptions, and licenses</li>
|
||||||
<li id="six">Answering your specific questions with a chat assistant</li>
|
<li id="six">Modifying settings for individual products</li>
|
||||||
|
<li id="seven">Answering your specific questions with a chat assistant</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div id="fade">
|
<div id="fade">
|
||||||
|
<a routerLink="/announcements" class="btn">View Announcements</a>
|
||||||
<a routerLink="/products" class="btn">Browse Products</a>
|
<a routerLink="/products" class="btn">Browse Products</a>
|
||||||
<a routerLink="/account" class="btn">Manage Account</a>
|
<a routerLink="/account" class="btn">Manage Account</a>
|
||||||
<a routerLink="/settings" class="btn">Modify Settings</a>
|
<a routerLink="/settings" class="btn">Modify Settings</a>
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
></a
|
></a
|
||||||
>
|
>
|
||||||
<div [class]="dropdownClass">
|
<div [class]="dropdownClass">
|
||||||
|
<a routerLink="/announcements" class="nav-link">Announcements</a>
|
||||||
|
<hr />
|
||||||
<a routerLink="/products" class="nav-link">Products</a>
|
<a routerLink="/products" class="nav-link">Products</a>
|
||||||
<hr />
|
<hr />
|
||||||
<a routerLink="/account" class="nav-link">Account</a>
|
<a routerLink="/account" class="nav-link">Account</a>
|
||||||
|
1191
pnpm-lock.yaml
generated
1191
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.12.3",
|
"packageManager": "pnpm@10.12.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cors": "11.0.1",
|
||||||
"@nhcarrigan/logger": "1.0.0",
|
"@nhcarrigan/logger": "1.0.0",
|
||||||
"@prisma/client": "6.11.1",
|
"@prisma/client": "6.11.1",
|
||||||
"fastify": "5.4.0"
|
"fastify": "5.4.0"
|
||||||
|
@ -17,11 +17,13 @@ export const corsHook: onRequestHookHandler = async(request, response) => {
|
|||||||
if (!request.url.startsWith("/submit")) {
|
if (!request.url.startsWith("/submit")) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (request.headers.origin !== "https://forms.nhcarrigan.com") {
|
if (request.headers.origin !== "http://localhost:4200"
|
||||||
|
&& request.headers.origin !== "https://hikari.nhcarrigan.com") {
|
||||||
|
console.log(request);
|
||||||
return await response.
|
return await response.
|
||||||
status(403).
|
status(403).
|
||||||
send({
|
send({
|
||||||
error: "Forms can only be submitted through our website. Thanks.",
|
error: "This route is only accessible from our dashboard at https://hikari.nhcarrigan.com.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import cors from "@fastify/cors";
|
||||||
import fastify from "fastify";
|
import fastify from "fastify";
|
||||||
import { corsHook } from "./hooks/cors.js";
|
import { corsHook } from "./hooks/cors.js";
|
||||||
import { ipHook } from "./hooks/ips.js";
|
import { ipHook } from "./hooks/ips.js";
|
||||||
@ -15,6 +16,17 @@ const server = fastify({
|
|||||||
logger: false,
|
logger: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This needs to be first, to ensure all requests have CORS configured.
|
||||||
|
* Our CORS settings allow for any origin, because we have a custom hook
|
||||||
|
* that guards specific routes from CORS requests.
|
||||||
|
* This is to allow our uptime monitor to access the health check route, for example.
|
||||||
|
* @see routesWithoutCors.ts
|
||||||
|
*/
|
||||||
|
server.register(cors, {
|
||||||
|
origin: "*",
|
||||||
|
});
|
||||||
|
|
||||||
server.addHook("preHandler", corsHook);
|
server.addHook("preHandler", corsHook);
|
||||||
server.addHook("preHandler", ipHook);
|
server.addHook("preHandler", ipHook);
|
||||||
|
|
||||||
|
@ -28,7 +28,14 @@ export const announcementRoutes: FastifyPluginAsync = async(server) => {
|
|||||||
take: 10,
|
take: 10,
|
||||||
});
|
});
|
||||||
return await reply.status(200).type("application/json").
|
return await reply.status(200).type("application/json").
|
||||||
send(announcements);
|
send(announcements.map((announcement) => {
|
||||||
|
return {
|
||||||
|
content: announcement.content,
|
||||||
|
createdAt: announcement.createdAt,
|
||||||
|
title: announcement.title,
|
||||||
|
type: announcement.type,
|
||||||
|
};
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body.
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify requires Body instead of body.
|
||||||
|
Reference in New Issue
Block a user