Compare commits
6 Commits
54f4014302
...
9bfb0f7a98
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bfb0f7a98 | |||
| 0c9249a4c8 | |||
| c235bf73a8 | |||
| d2d8118ccd | |||
| 876b268cb6 | |||
| 8b200a8019 |
@ -1,3 +1,4 @@
|
||||
import { server } from "typescript";
|
||||
import { AuthenticationPage, AuthenticationRules } from "../pages/auth/auth";
|
||||
import { NewsData, NewsFeed } from "../pages/feed/news";
|
||||
import { NewsItem } from "../pages/feed/news-item";
|
||||
@ -17,7 +18,13 @@ interface APIResponse<T> {
|
||||
let newsFeed : NewsFeed | null = null;
|
||||
let authPage : AuthenticationPage | null = null;
|
||||
|
||||
class ErrorAPI extends Error {}
|
||||
class ErrorAPI extends Error {
|
||||
readonly code: string;
|
||||
constructor (error: ErrorDetails) {
|
||||
super(error.message);
|
||||
this.code = error.code;
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerAPI {
|
||||
protected endpoint: EndpointURL;
|
||||
@ -39,8 +46,8 @@ export class ControllerAPI {
|
||||
|
||||
const response = await fetch(url, options);
|
||||
const result: APIResponse<T> = await response.json();
|
||||
if (result.error) throw new ErrorAPI(result.error.message);
|
||||
if (result.response === undefined) throw new ErrorAPI("Missing response from API");
|
||||
if (result.error) throw new ErrorAPI(result.error);
|
||||
if (result.response === undefined) throw new Error("Missing response from API");
|
||||
return result.response;
|
||||
}
|
||||
|
||||
@ -63,7 +70,7 @@ export class ControllerAPI {
|
||||
|
||||
const result = await this.request<boolean>('/isAuthenticated', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // Include cookies for authentication
|
||||
credentials: 'include'
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@ -73,29 +80,37 @@ export class ControllerAPI {
|
||||
|
||||
const result = await this.request<NewsData[]>('/news', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // Include cookies for authentication
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!returnPage)
|
||||
return result;
|
||||
if (!newsFeed)
|
||||
newsFeed = new NewsFeed(result.map(x => new NewsItem(
|
||||
x.id,
|
||||
x.title,
|
||||
new Date(x.publishedAt),
|
||||
x.content,
|
||||
x.source
|
||||
)));
|
||||
newsFeed = new NewsFeed(result.map(feed => new NewsItem(feed)));
|
||||
return newsFeed;
|
||||
}
|
||||
|
||||
async getAuthRules(returnPage: boolean = false) : Promise<AuthenticationRules | AuthenticationPage> {
|
||||
const result = await this.request<AuthenticationRules>('/authenticationRules', {
|
||||
method: 'GET',
|
||||
credentials: 'include' // Include cookies for authentication
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!returnPage) return result;
|
||||
if (!authPage)
|
||||
authPage = new AuthenticationPage(result);
|
||||
return authPage;
|
||||
}
|
||||
|
||||
async setLike(post: string | NewsData) : Promise<{ status: boolean, likes: number }> {
|
||||
if (typeof post === "object")
|
||||
post = post.id;
|
||||
|
||||
return await this.request<{ status: boolean, likes: number }>('/setLike', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ post }),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,11 @@ import { NewsFeed } from "./pages/feed/news";
|
||||
import { AuthenticationPage } from "./pages/auth/auth";
|
||||
import { DefaultElement } from "./pages/default-element";
|
||||
|
||||
/*declare global {
|
||||
declare global {
|
||||
interface Window {
|
||||
selectedStyle: SupportedStyles;
|
||||
api: ControllerAPI;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
const styleLinker : {
|
||||
selectedStyle: SupportedStyles
|
||||
} = {
|
||||
@ -28,16 +28,16 @@ const currentStyle: () => SupportedStyles = () => styleLinker.selectedStyle;
|
||||
}
|
||||
}
|
||||
|
||||
const api : ControllerAPI = new ControllerAPI();
|
||||
window.api = new ControllerAPI();
|
||||
|
||||
async function authInit() {
|
||||
console.log("User is authenticated");
|
||||
return await api.getNews(true) as NewsFeed;
|
||||
return await window.api.getNews(true) as NewsFeed;
|
||||
}
|
||||
|
||||
async function logInInit () {
|
||||
console.log("User isn't authenticated");
|
||||
return await api.getAuthRules(true) as AuthenticationPage;
|
||||
return await window.api.getAuthRules(true) as AuthenticationPage;
|
||||
}
|
||||
|
||||
async function changeStyleLogic (activeElement: DefaultElement, navbar: DefaultElement) {
|
||||
@ -67,7 +67,7 @@ async function main() {
|
||||
navbar.style = currentStyle;
|
||||
|
||||
let activeElement: DefaultElement;
|
||||
if (await api.isAuthenticated()) {
|
||||
if (await window.api.isAuthenticated()) {
|
||||
activeElement = await authInit();
|
||||
} else {
|
||||
activeElement = await logInInit();
|
||||
|
||||
@ -11,11 +11,50 @@ export abstract class GefestElement {
|
||||
readonly gefestId: string;
|
||||
protected content: (GefestElement | string)[] | string;
|
||||
protected attributes: Record<string, string> = {};
|
||||
protected ignoreStyleClasses : Set<string> = new Set<string>;
|
||||
protected classList: Set<string> = new Set();
|
||||
protected eventHandlers: ((eventType: GefestElementEvents, eventData: unknown) => void)[] = [];
|
||||
|
||||
protected clickHandler: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the element from an HTML string.
|
||||
* @param html The HTML string to parse.
|
||||
* @returns The created GefestElement.
|
||||
*/
|
||||
static fromHTML(html: string): GefestElement {
|
||||
console.warn("Legacy and experiemental function!");
|
||||
|
||||
const htmlElement = new DOMParser().parseFromString(html, 'text/html').body.firstChild as HTMLElement;
|
||||
const element = new (class extends GefestElement {
|
||||
protected wrapHTML(content: string): string {
|
||||
return content;
|
||||
}
|
||||
})(htmlElement.innerHTML);
|
||||
|
||||
for (const attr of htmlElement.attributes) {
|
||||
try {
|
||||
element.setAttribute(attr.name, attr.value);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
for (const className of htmlElement.classList) {
|
||||
element.addClass(className);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the element.
|
||||
* @param content The content for the element. Can be a string, an array of strings, an array of GefestElements or a combination of both.
|
||||
*/
|
||||
constructor (content : (GefestElement | string)[] | string = []) {
|
||||
this.gefestId = GefestEngine.register(this);
|
||||
this.attributes["data-gefest-id"] = this.gefestId;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
emit(event: GefestElementEvents, data: unknown = undefined) {
|
||||
for (let handler of this.eventHandlers)
|
||||
new Promise((rs) => rs(handler(event, data)));
|
||||
@ -59,44 +98,6 @@ export abstract class GefestElement {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the element from an HTML string.
|
||||
* @param html The HTML string to parse.
|
||||
* @returns The created GefestElement.
|
||||
*/
|
||||
static fromHTML(html: string): GefestElement {
|
||||
console.warn("Legacy and experiemental function!");
|
||||
|
||||
const htmlElement = new DOMParser().parseFromString(html, 'text/html').body.firstChild as HTMLElement;
|
||||
const element = new (class extends GefestElement {
|
||||
protected wrapHTML(content: string): string {
|
||||
return content;
|
||||
}
|
||||
})(htmlElement.innerHTML);
|
||||
|
||||
for (const attr of htmlElement.attributes) {
|
||||
try {
|
||||
element.setAttribute(attr.name, attr.value);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
for (const className of htmlElement.classList) {
|
||||
element.addClass(className);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the element.
|
||||
* @param content The content for the element. Can be a string, an array of strings, an array of GefestElements or a combination of both.
|
||||
*/
|
||||
constructor (content : (GefestElement | string)[] | string = []) {
|
||||
this.gefestId = GefestEngine.register(this);
|
||||
this.attributes["data-gefest-id"] = this.gefestId;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to GefestEngine for rerender element
|
||||
*/
|
||||
@ -221,6 +222,9 @@ export abstract class GefestElement {
|
||||
* @returns The attributes string.
|
||||
*/
|
||||
protected attributesToString (): string {
|
||||
const ignoreMap = Object.fromEntries(
|
||||
Array.from(this.ignoreStyleClasses).map(x => [x, true])
|
||||
);
|
||||
let style : GefestStyle | null = null;
|
||||
if (typeof this.style === 'function') {
|
||||
style = this.style();
|
||||
@ -232,7 +236,9 @@ export abstract class GefestElement {
|
||||
if (style instanceof GefestStyle)
|
||||
attributes.class = [
|
||||
attributes.class,
|
||||
style.getClasses(this).join(" ")
|
||||
style.getClasses(this).filter(styleClass => !ignoreMap[
|
||||
styleClass
|
||||
]).join(" ")
|
||||
].join(" ");
|
||||
|
||||
return Object.entries(
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { GefestElement } from "../modules/Gefest/element";
|
||||
import { GefestA } from "../modules/Gefest/primitives/a";
|
||||
import { GefestI } from "../modules/Gefest/primitives/i";
|
||||
import { SupportedStyles } from "../styles";
|
||||
|
||||
type FontAwesomeGroup = "fas" | "far" | "fab"; // e.g. "fas fa-home"
|
||||
export type FontAwesomeIconType = `${FontAwesomeGroup} fa-${string}`;
|
||||
|
||||
export abstract class DefaultElement extends GefestElement {
|
||||
style: SupportedStyles | (() => SupportedStyles) | null = null;
|
||||
constructor(content: (GefestElement | string)[] | string) {
|
||||
@ -22,3 +27,20 @@ export class DefaultRoundedButton extends DefaultElement {
|
||||
this.addClass("border");
|
||||
}
|
||||
}
|
||||
|
||||
export class FontAwesomeIcon extends DefaultElement {
|
||||
constructor(icon: FontAwesomeIconType) {
|
||||
super("");
|
||||
this.ignoreStyleClasses.add("bg-light");
|
||||
this.ignoreStyleClasses.add("bg-dark");
|
||||
this.addClass(icon);
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultButton extends DefaultElement {
|
||||
constructor(content: (GefestElement | string)[] | string) {
|
||||
super(content);
|
||||
this.addClass("btn");
|
||||
this.addClass("btn-outline-secondary");
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,13 @@
|
||||
import { DefaultElement } from "../default-element";
|
||||
import { DefaultButton, DefaultElement, FontAwesomeIcon, FontAwesomeIconType } from "../default-element";
|
||||
import { GefestHeader } from "../../modules/Gefest/primitives/header";
|
||||
import { GefestSmall } from "../../modules/Gefest/primitives/small";
|
||||
import { GefestElement } from "../../modules/Gefest/element";
|
||||
import { GefestP } from "../../modules/Gefest/primitives/p";
|
||||
import { NewsSource } from "./news";
|
||||
import { NewsData, NewsSource } from "./news";
|
||||
import { GefestA } from "../../modules/Gefest/primitives/a";
|
||||
import { GefestI } from "../../modules/Gefest/primitives/i";
|
||||
import { GefestImg } from "../../modules/Gefest/primitives/img";
|
||||
import { GefestEngine } from "../../modules/Gefest/engine";
|
||||
|
||||
function dateItemFormater (value: number | string, isMonth = false) {
|
||||
const MONTHS = [
|
||||
@ -36,7 +37,7 @@ function formatDate(date: Date) {
|
||||
}:${dateItemFormater(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
function getSourceLnk (source: NewsSource, id: number) : string {
|
||||
function getSourceLnk (source: NewsSource, id: string) : string {
|
||||
const iconOfSource : GefestElement = !source.icon ?
|
||||
new GefestI("") : new GefestImg("", source.icon);
|
||||
if (iconOfSource instanceof GefestI) {
|
||||
@ -62,23 +63,141 @@ class CardBodyBS5 extends DefaultElement {
|
||||
}
|
||||
|
||||
export class NewsItem extends DefaultElement {
|
||||
constructor(id: number, title: string, publishDate: Date, content: string, source: NewsSource) {
|
||||
const header = new GefestHeader(title, 2);
|
||||
constructor(
|
||||
newsData: NewsData
|
||||
) {
|
||||
const header = new GefestHeader(newsData.title, 2);
|
||||
header.addClass("card-title");
|
||||
const published = new GefestSmall(formatDate(publishDate) + " " + getSourceLnk(source, id));
|
||||
const published = new GefestSmall(formatDate(new Date(newsData.publishedAt)) + " " + getSourceLnk(newsData.source, newsData.id));
|
||||
published.addClass("published-text");
|
||||
const contentParagraph = new GefestP(content);
|
||||
const contentParagraph = new GefestP(newsData.content);
|
||||
contentParagraph.addClass("card-text");
|
||||
|
||||
const splitter = new (class extends DefaultElement {
|
||||
constructor() {
|
||||
super([]);
|
||||
this.ignoreStyleClasses.add("bg-light");
|
||||
this.ignoreStyleClasses.add("bg-dark");
|
||||
this.ignoreStyleClasses.add("text-light");
|
||||
this.ignoreStyleClasses.add("text-dark");
|
||||
}
|
||||
protected htmlTag: string = "hr";
|
||||
});
|
||||
|
||||
const actions = new (class extends DefaultElement {
|
||||
constructor() {
|
||||
const commentsCounter = new (class extends GefestElement {
|
||||
comments: number = 0;
|
||||
constructor() {
|
||||
super([]);
|
||||
}
|
||||
|
||||
protected wrapHTML(): string {
|
||||
return ` ${this.comments}`;
|
||||
}
|
||||
});
|
||||
commentsCounter.comments = newsData.comments;
|
||||
|
||||
const comments = new DefaultButton([
|
||||
new FontAwesomeIcon("fas fa-comment"),
|
||||
commentsCounter
|
||||
]);
|
||||
|
||||
const likeBtn = new (class extends DefaultButton {
|
||||
private status: boolean;
|
||||
private likes: number;
|
||||
|
||||
constructor() {
|
||||
const likedIco = new FontAwesomeIcon("fas fa-heart");
|
||||
const unlikedIco = new FontAwesomeIcon("far fa-heart");
|
||||
|
||||
const likesCounter = new (class extends GefestElement {
|
||||
likes: number = 0;
|
||||
constructor() {
|
||||
super([]);
|
||||
}
|
||||
|
||||
protected wrapHTML(): string {
|
||||
return ` ${this.likes}`;
|
||||
}
|
||||
});
|
||||
super([
|
||||
likedIco, unlikedIco,
|
||||
likesCounter
|
||||
]);
|
||||
this.likes = newsData.likes;
|
||||
this.status = newsData.isLiked;
|
||||
likesCounter.likes = newsData.likes;
|
||||
|
||||
const setStatus = () => {
|
||||
likedIco.isHidden = !this.status;
|
||||
unlikedIco.isHidden = this.status;
|
||||
this.update();
|
||||
};
|
||||
setStatus();
|
||||
|
||||
this.onClick = async () => {
|
||||
likesCounter.likes = this.status ? --this.likes : ++this.likes;
|
||||
|
||||
// TODO: Dirty code, fix it
|
||||
this.status = !this.status;
|
||||
setStatus();
|
||||
const result = await window.api.setLike(newsData);
|
||||
this.status = result.status;
|
||||
likesCounter.likes = result.likes;
|
||||
this.likes = result.likes;
|
||||
setStatus();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
super([
|
||||
comments, likeBtn
|
||||
]);
|
||||
this.addClass("btn-group");
|
||||
}
|
||||
});
|
||||
const views = new (class extends DefaultElement {
|
||||
constructor() {
|
||||
const lookedShow = new (class extends GefestElement {
|
||||
views: number = 0;
|
||||
constructor() {
|
||||
super([]);
|
||||
}
|
||||
protected wrapHTML(content: string): string {
|
||||
return `${this.views} `;
|
||||
}
|
||||
});
|
||||
lookedShow.views = newsData.looked;
|
||||
|
||||
super([
|
||||
lookedShow,
|
||||
new FontAwesomeIcon("fas fa-eye"),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
super([
|
||||
new CardBodyBS5([
|
||||
header,
|
||||
published,
|
||||
contentParagraph
|
||||
contentParagraph,
|
||||
splitter,
|
||||
new (class extends DefaultElement {
|
||||
constructor() {
|
||||
super([actions, views]);
|
||||
[
|
||||
"d-flex",
|
||||
"justify-content-between",
|
||||
"align-items-center"
|
||||
].forEach(c => this.addClass(c));
|
||||
}
|
||||
}),
|
||||
])
|
||||
]);
|
||||
|
||||
this.addClass("card");
|
||||
this.addClass("feed-item");
|
||||
this.addClass("border");
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,63 @@
|
||||
import { NewsItem } from "./news-item";
|
||||
import { DefaultElement } from "../default-element";
|
||||
import { DefaultElement, FontAwesomeIcon, FontAwesomeIconType } from "../default-element";
|
||||
import { GefestHeader } from "../../modules/Gefest/primitives/header";
|
||||
import { UserType } from "../../api/user-info";
|
||||
import { GefestElement } from "../../modules/Gefest/element";
|
||||
import { GefestI } from "../../modules/Gefest/primitives/i";
|
||||
import { GefestButton } from "../../modules/Gefest/primitives/button";
|
||||
|
||||
export function isNewsUI (element : GefestElement): boolean {
|
||||
return element instanceof NewsUserInterfaceBody ||
|
||||
element instanceof NewsUserInterfaceCard;
|
||||
}
|
||||
class NewsUserInterfaceBtn extends DefaultElement {
|
||||
constructor(icon: FontAwesomeIconType) {
|
||||
const fontawesomeIcon = new FontAwesomeIcon(icon);
|
||||
|
||||
super([fontawesomeIcon]);
|
||||
this.ignoreStyleClasses.add("bg-light");
|
||||
this.ignoreStyleClasses.add("bg-dark");
|
||||
|
||||
this.setAttribute("type", "button");
|
||||
this.addClass("btn");
|
||||
this.addClass("btn-outline-secondary");
|
||||
}
|
||||
|
||||
protected htmlTag: string = "button";
|
||||
}
|
||||
class NewsUserInterfaceBody extends DefaultElement {
|
||||
constructor(userType : UserType) {
|
||||
const addFilter = new NewsUserInterfaceBtn("fas fa-filter");
|
||||
const search = new NewsUserInterfaceBtn("fas fa-search");
|
||||
//search.addClass("input-group-text");
|
||||
const searchArea = new (class extends DefaultElement {
|
||||
constructor () {
|
||||
super([]);
|
||||
this.setAttribute("type", "text");
|
||||
this.addClass("form-control");
|
||||
this.setAttribute("placeholder", "Type a request");
|
||||
}
|
||||
protected htmlTag: string = "input";
|
||||
});
|
||||
const panelElements: GefestElement[] = [ addFilter, search, searchArea ];
|
||||
switch (userType) {
|
||||
case "admin":
|
||||
const addSource = new NewsUserInterfaceBtn("fas fa-plus-circle");
|
||||
panelElements.push(addSource);
|
||||
break;
|
||||
case "user":
|
||||
break;
|
||||
default:
|
||||
const impossible: never = userType;
|
||||
break;
|
||||
}
|
||||
super([new (class extends DefaultElement {
|
||||
constructor () {
|
||||
super(panelElements);
|
||||
this.addClass("btn-group");
|
||||
this.setAttribute("role", "group");
|
||||
}
|
||||
})]);
|
||||
this.addClass("card-body");
|
||||
this.addClass("central-navbar");
|
||||
}
|
||||
@ -69,23 +115,28 @@ export class NewsUserInterface extends DefaultElement {
|
||||
export class NewsFeed extends DefaultElement {
|
||||
constructor(items: NewsItem[]) {
|
||||
super([
|
||||
new NewsUserInterface("user"),
|
||||
new NewsUserInterface("admin"),
|
||||
...items,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export interface NewsData {
|
||||
id: number;
|
||||
title : string;
|
||||
content : string;
|
||||
source : NewsSource;
|
||||
publishedAt : string;
|
||||
}
|
||||
|
||||
export interface NewsSource {
|
||||
name: string;
|
||||
icon?: string;
|
||||
sourceURL?: string;
|
||||
itemURL?: string;
|
||||
}
|
||||
|
||||
export interface NewsData {
|
||||
id: string;
|
||||
title : string;
|
||||
content : string;
|
||||
source : NewsSource;
|
||||
publishedAt : string;
|
||||
comments: number;
|
||||
likes: number;
|
||||
looked: number;
|
||||
isAdult: boolean;
|
||||
isLiked: boolean;
|
||||
}
|
||||
@ -1,14 +1,11 @@
|
||||
import { GefestA } from "../modules/Gefest/primitives/a";
|
||||
import { GefestI } from "../modules/Gefest/primitives/i";
|
||||
import { DefaultElement } from "./default-element";
|
||||
import { DefaultElement, FontAwesomeIcon, FontAwesomeIconType } from "./default-element";
|
||||
|
||||
type FontAwesomeGroup = "fas" | "far" | "fab"; // e.g. "fas fa-home"
|
||||
type FontAwesomeIcon = `${FontAwesomeGroup} fa-${string}`;
|
||||
|
||||
class NavbarButton extends DefaultElement {
|
||||
constructor(icon: FontAwesomeIcon) {
|
||||
const iconElement = new GefestI("");
|
||||
iconElement.addClass(icon);
|
||||
constructor(icon: FontAwesomeIconType) {
|
||||
const iconElement = new FontAwesomeIcon(icon);
|
||||
const elementA = new GefestA([
|
||||
iconElement
|
||||
]);
|
||||
@ -36,9 +33,13 @@ class NavbarItemContainerUL extends DefaultElement {
|
||||
const chatButton = new NavbarButton("fas fa-comments");
|
||||
chatButton.onClick = () => alert("Chat clicked!") as void;
|
||||
|
||||
const musicButton = new NavbarButton("fas fa-music");
|
||||
musicButton.onClick = () => alert("Music clicked!") as void;
|
||||
|
||||
super([
|
||||
newsButton,
|
||||
chatButton,
|
||||
musicButton,
|
||||
|
||||
themeToggle
|
||||
]);
|
||||
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.6.0",
|
||||
"body-parser": "^2.2.2",
|
||||
"express": "^5.2.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^6.0.3"
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.6.0",
|
||||
"body-parser": "^2.2.2",
|
||||
"express": "^5.2.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^6.0.3"
|
||||
@ -9,7 +10,7 @@
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "npx tsc && bash build-static.sh",
|
||||
"build": "rm -rf dist/ && npx tsc && bash build-static.sh",
|
||||
"start": "node dist/server.js"
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,4 +22,16 @@ router.get('/news', authRequired, (req, res) => {
|
||||
return res.json(response.getAnswer());
|
||||
});
|
||||
|
||||
router.post('/setLike', authRequired, (req, res) => {
|
||||
if (req.body?.post || typeof req.body.post !== "string") {
|
||||
// TODO: Some actions with API
|
||||
const testResponse = new ApiResponse({ status: true, likes: 1 });
|
||||
return res.json(testResponse.getAnswer());
|
||||
}
|
||||
return res.status(400).json((new ApiResponse({
|
||||
code: "MISSING_REQUIRED",
|
||||
message: "Missed required param: post(string) at JSON body",
|
||||
})).getAnswer());
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -27,9 +27,15 @@ interface OtherNewsSource {
|
||||
export type NewsSource = SystemNewsSource | OtherNewsSource;
|
||||
|
||||
export interface NewsItem {
|
||||
id : number;
|
||||
id : string;
|
||||
title : string;
|
||||
content : string;
|
||||
source: NewsSource;
|
||||
publishedAt : Date;
|
||||
looked: number;
|
||||
comments: number;
|
||||
likes: number;
|
||||
|
||||
isAdult: boolean;
|
||||
isLiked: boolean;
|
||||
}
|
||||
@ -2,6 +2,8 @@ import express, { Request, Response } from 'express';
|
||||
import api from './api';
|
||||
import path from 'path';
|
||||
import { AuthenticationRulesTypes } from './api/models/AuthenticationRules';
|
||||
import env from './utils/environments';
|
||||
import bodyparser from "body-parser";
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
@ -13,6 +15,9 @@ declare module 'express-serve-static-core' {
|
||||
}
|
||||
}
|
||||
|
||||
// Bodyparser
|
||||
app.use(bodyparser.json());
|
||||
|
||||
// Checking auth
|
||||
app.use((req, res, next) => {
|
||||
req.authenticationRules = { type: "password" };
|
||||
@ -39,6 +44,10 @@ app.get('/home', frontend);
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'static')));
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running at http://localhost:${port}`);
|
||||
app.listen(env.SERVER_PORT, env.SERVER_ADDRESS, (err) => {
|
||||
if (err)
|
||||
throw err;
|
||||
console.log(`Server is running at http://${
|
||||
env.SERVER_ADDRESS
|
||||
}:${env.SERVER_PORT}`);
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
class Database {}
|
||||
|
||||
export = new Database;
|
||||
27
src/utils/environments.ts
Normal file
27
src/utils/environments.ts
Normal file
@ -0,0 +1,27 @@
|
||||
function validationError(name: string): string {
|
||||
throw new Error(`There isn't environment ${name}`);
|
||||
}
|
||||
|
||||
function isEnabledSSL (ssl: string): boolean {
|
||||
return ssl === "1" ||
|
||||
ssl.toLowerCase() === "true" ||
|
||||
ssl.toLowerCase() === "on" ||
|
||||
ssl.toLowerCase() === "enable" ||
|
||||
ssl.toLowerCase() === "enabled";
|
||||
}
|
||||
|
||||
const API_ADDRESS: string = process.env.REMOTE_SERVER ?? "127.0.0.1";
|
||||
let API_PORT: number = Number(process.env.API_PORT);
|
||||
if (Number.isNaN(API_PORT))
|
||||
API_PORT = 8080;
|
||||
const API_SSL: boolean = isEnabledSSL(process.env.API_SSL ?? "");
|
||||
|
||||
const SERVER_ADDRESS: string = process.env.REMOTE_SERVER ?? "0.0.0.0";
|
||||
let SERVER_PORT: number = Number(process.env.SERVER_PORT);
|
||||
if (Number.isNaN(SERVER_PORT))
|
||||
SERVER_PORT = 3000;
|
||||
|
||||
export default {
|
||||
API_ADDRESS, API_PORT, API_SSL,
|
||||
SERVER_ADDRESS, SERVER_PORT,
|
||||
};
|
||||
@ -5,134 +5,199 @@ export function getTestNews (): NewsItem[] {
|
||||
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
id: "0",
|
||||
title: "Lorem Ispum",
|
||||
content: "Lorem Ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 1,
|
||||
isAdult: false,
|
||||
isLiked: true,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
id: "1",
|
||||
title: "News Item 1",
|
||||
content: "This is the content for news item 1. It contains some sample text to demonstrate the news functionality.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: '2',
|
||||
title: "News Item 2",
|
||||
content: "This is the content for news item 2. Another piece of sample news content for testing purposes.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: '3',
|
||||
title: "News Item 3",
|
||||
content: "News item 3 brings you the latest updates. Stay informed with our regular news feed.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
id: '4',
|
||||
title: "News Item 4",
|
||||
content: "Content for the fourth news item. This is part of the test data for the application.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
id: '5',
|
||||
title: "News Item 5",
|
||||
content: "Fifth news item in the list. More sample content to populate the news array.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
id: '6',
|
||||
title: "News Item 6",
|
||||
content: "This is news item number six. Continuing to add test data for the news feature.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
id: '7',
|
||||
title: "News Item 7",
|
||||
content: "Seventh item in the news feed. Sample text to ensure the array has sufficient data.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: '8',
|
||||
title: "News Item 8",
|
||||
content: "News item eight provides more content. This helps in testing the display of multiple items.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
id: '9',
|
||||
title: "News Item 9",
|
||||
content: "The ninth news item. Almost reaching the total of 13 items including the original.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
id: '10',
|
||||
title: "News Item 10",
|
||||
content: "Tenth item in the array. This completes the set of additional news items requested.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
id: '11',
|
||||
title: "News Item 11",
|
||||
content: "Eleventh news item. Continuing to add variety to the test data.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
id: '12',
|
||||
title: "News Item 12",
|
||||
content: "The twelfth and final news item. This brings the total to 13 items in the array.",
|
||||
publishedAt: new Date(),
|
||||
source: {
|
||||
name: "Test Destination",
|
||||
type: "system",
|
||||
}
|
||||
},
|
||||
looked: 1,
|
||||
comments: 0,
|
||||
likes: 0,
|
||||
isAdult: false,
|
||||
isLiked: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import { NewsSource, NewsItem } from "../../api/models/news";
|
||||
import requests from "../requests";
|
||||
|
||||
class NewsSourceTarget {
|
||||
protected source : NewsSource;
|
||||
protected parent : NewsParserService;
|
||||
constructor(source : NewsSource, parent : NewsParserService) {
|
||||
this.source = source;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
protected async readerRSS () : Promise<NewsItem[]> {
|
||||
if (!this.source.sourceURL) {
|
||||
throw new TypeError("RSS must have sourceURL");
|
||||
}
|
||||
await requests.get(this.source.sourceURL, "string");
|
||||
return [];
|
||||
}
|
||||
|
||||
async readAll() : Promise<NewsItem[]> {
|
||||
switch (this.source.type) {
|
||||
case "RSS":
|
||||
return this.readerRSS();
|
||||
case "system":
|
||||
return [];
|
||||
default:
|
||||
const impossible: never = this.source;
|
||||
return impossible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NewsParserService {
|
||||
protected sources : Set<NewsSourceTarget> = new Set<NewsSourceTarget>;
|
||||
protected sourceMap : WeakMap<NewsSource, NewsSourceTarget> = new WeakMap<NewsSource, NewsSourceTarget>;
|
||||
|
||||
protected newsItems : Set<NewsItem> = new Set<NewsItem>;
|
||||
|
||||
async setupSources(sources : NewsSource[]) {
|
||||
this.sources.clear();
|
||||
sources.forEach(source => this.addSource(source));
|
||||
}
|
||||
|
||||
async addSource(source : NewsSource) {
|
||||
const sourceTarget = new NewsSourceTarget(source, this);
|
||||
this.sources.add(sourceTarget);
|
||||
this.sourceMap.set(source, sourceTarget);
|
||||
|
||||
const news = await sourceTarget.readAll();
|
||||
news.forEach(newsItem => new Promise<void>((resolve) => {
|
||||
this.newsItems.add(newsItem);
|
||||
resolve();
|
||||
}));
|
||||
}
|
||||
|
||||
async removeSource(source : NewsSource) {
|
||||
const sourceTarget = this.sourceMap.get(source);
|
||||
if (sourceTarget) {
|
||||
this.sources.delete(sourceTarget);
|
||||
this.sourceMap.delete(source);
|
||||
|
||||
const forDeleting: Set<NewsItem> = new Set<NewsItem>;
|
||||
for (let newsItem of this.newsItems) {
|
||||
if (newsItem.source === source)
|
||||
forDeleting.add(newsItem);
|
||||
}
|
||||
for (let deleteItem of forDeleting) {
|
||||
this.newsItems.delete(deleteItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const newsParserService = new NewsParserService;
|
||||
Loading…
Reference in New Issue
Block a user