From a4c5b441f6dccc5cc23d7604f49992272400f25b Mon Sep 17 00:00:00 2001 From: FullGreaM Date: Tue, 28 Apr 2026 02:59:58 +0300 Subject: [PATCH] too many changes, i'm tired for describe all. But I try fix themes logic, okay? --- frontend/src/api/RESTful.ts | 36 +++++-- frontend/src/css/main.css | 7 ++ frontend/src/main.ts | 35 +++++-- frontend/src/modules/Gefest/element.ts | 44 ++++++--- frontend/src/modules/Gefest/engine.ts | 32 ++++-- frontend/src/pages/auth/auth.ts | 131 ++++++++++++++++++++++++- frontend/src/pages/default-element.ts | 12 ++- frontend/src/pages/feed/news.ts | 2 +- frontend/src/pages/navbar.ts | 5 +- frontend/src/styles.ts | 6 +- src/api/index.ts | 8 +- src/api/models/AuthenticationRules.ts | 3 + src/api/models/news.ts | 14 +++ src/server.ts | 7 +- src/utils/news.ts | 15 +-- 15 files changed, 295 insertions(+), 62 deletions(-) create mode 100644 src/api/models/AuthenticationRules.ts create mode 100644 src/api/models/news.ts diff --git a/frontend/src/api/RESTful.ts b/frontend/src/api/RESTful.ts index cdb3f64..b7f55b3 100644 --- a/frontend/src/api/RESTful.ts +++ b/frontend/src/api/RESTful.ts @@ -1,7 +1,8 @@ +import { AuthenticationPage, AuthenticationRules } from "../pages/auth/auth"; import { NewsData, NewsFeed } from "../pages/feed/news"; import { NewsItem } from "../pages/feed/news-item"; -type EndpointURL = "" | `http${'s' | ''}://${string}`; +type EndpointURL = "/api" | `http${'s' | ''}://${string}`; interface ErrorDetails { code: string, @@ -14,17 +15,27 @@ interface APIResponse { } let newsFeed : NewsFeed | null = null; +let authPage : AuthenticationPage | null = null; class ErrorAPI extends Error {} export class ControllerAPI { protected endpoint: EndpointURL; - constructor(endpoint: EndpointURL = "") { + constructor(endpoint: EndpointURL = "/api") { this.endpoint = endpoint; } protected async request(path: string, options: RequestInit = {}): Promise { - const url = new URL(path, this.endpoint || window.location.origin).href; + let url : string; + if (this.endpoint === "/api") { + url = new URL("/api", window.location.origin).href + path; + } + else if (/\/$/i.test(this.endpoint)) { + url = this.endpoint + path.slice(1); + } + else { + url = this.endpoint + path; + } const response = await fetch(url, options); const result: APIResponse = await response.json(); @@ -50,21 +61,21 @@ export class ControllerAPI { async isAuthenticated(): Promise { if (!this.checkFirstAuth()) return false; - const result = await this.request('/api/isAuthenticated', { + const result = await this.request('/isAuthenticated', { method: 'GET', credentials: 'include' // Include cookies for authentication }); return result; } - async getNews(returnElement: boolean = false) : Promise { + async getNews(returnPage: boolean = false) : Promise { this.throwIfUnauthed(); - const result = await this.request('/api/news', { + const result = await this.request('/news', { method: 'GET', credentials: 'include' // Include cookies for authentication }); - if (!returnElement) + if (!returnPage) return result; if (!newsFeed) newsFeed = new NewsFeed(result.map(x => new NewsItem( @@ -76,4 +87,15 @@ export class ControllerAPI { ))); return newsFeed; } + + async getAuthRules(returnPage: boolean = false) : Promise { + const result = await this.request('/authenticationRules', { + method: 'GET', + credentials: 'include' // Include cookies for authentication + }); + if (!returnPage) return result; + if (!authPage) + authPage = new AuthenticationPage(result); + return authPage; + } } \ No newline at end of file diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css index 3f4dc0e..68e18c0 100644 --- a/frontend/src/css/main.css +++ b/frontend/src/css/main.css @@ -58,4 +58,11 @@ .nav-item { cursor: pointer; padding: 12px 24px; font-size: 1.5em; +} + +.auth-card { + margin-left: 30vw; + margin-right: 30vw; + margin-top: 30vh; + margin-bottom: 30vh; } \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 04d68b2..9c09447 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,12 +1,15 @@ import { GefestEngine } from "./modules/Gefest/engine"; -import { Navbar } from "./pages/navbar"; -import { DarkStyle, LightStyle, defaultStyle } from "./styles"; +import { Navbar, themeToggle } from "./pages/navbar"; +import { DarkStyle, LightStyle, SupportedStyles } from "./styles"; import { ControllerAPI } from "./api/RESTful"; import { NewsFeed } from "./pages/feed/news"; import { AuthenticationPage } from "./pages/auth/auth"; // I'll add a dark mode toggle later, but for now let's just use the light style by default -let currentStyle = defaultStyle; +const defaultStyle = new LightStyle(); +const styleMemLink = { + currentStyle: () : SupportedStyles => defaultStyle +}; { // I hate trash in global scope, so I wrap it in a block const appElement = document.getElementById("app"); @@ -20,7 +23,7 @@ const api : ControllerAPI = new ControllerAPI(); async function authInit() { console.log("User is authenticated"); const newsFeed = await api.getNews(true) as NewsFeed; - newsFeed.style = currentStyle; + newsFeed.style = styleMemLink.currentStyle; const appElement = document.getElementById("app"); if (appElement) appElement.innerHTML = newsFeed.build(); @@ -28,17 +31,35 @@ async function authInit() { async function logInInit () { console.log("User is not authenticated"); - const authPage = new AuthenticationPage(); - authPage.style = currentStyle; + const authPage = await api.getAuthRules(true) as AuthenticationPage; + authPage.style = styleMemLink.currentStyle; const appElement = document.getElementById("app"); if (appElement) appElement.innerHTML = authPage.build(); } +async function changeStyleLogic () { + const themes : SupportedStyles[] = [ + defaultStyle, + new DarkStyle() + ]; + let choosed = 0; + + themeToggle.onClick = () => { + choosed++; + if (choosed >= themes.length) + choosed = 0; + + styleMemLink.currentStyle = () => themes[choosed] as SupportedStyles; + GefestEngine.render(); + }; +} + async function main() { + await changeStyleLogic(); // init navbar const navbar = new Navbar(); - navbar.style = currentStyle; + navbar.style = styleMemLink.currentStyle; document.body.innerHTML = "
" + navbar.build(); if (await api.isAuthenticated()) { diff --git a/frontend/src/modules/Gefest/element.ts b/frontend/src/modules/Gefest/element.ts index f9c4021..eb27699 100644 --- a/frontend/src/modules/Gefest/element.ts +++ b/frontend/src/modules/Gefest/element.ts @@ -4,8 +4,9 @@ import { GefestStyle } from './style'; // The general class of Gefest type GefestElementEvents = "onClick"; export abstract class GefestElement { - style: GefestStyle | null = null; + style: GefestStyle | (() => GefestStyle) | null = null; isHidden: boolean = false; + id : string | null = null; //onClick: (() => void) | null = null; readonly gefestId: string; protected content: (GefestElement | string)[] | string; @@ -64,6 +65,8 @@ export abstract class GefestElement { * @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 { @@ -98,7 +101,7 @@ export abstract class GefestElement { * Call to GefestEngine for rerender element */ update (): void { - GefestEngine.reRenderByGI(this.gefestId); + GefestEngine.render(this.gefestId); } /** @@ -112,6 +115,11 @@ export abstract class GefestElement { else this.removeAttribute('hidden'); + if (this.id !== null) + this.setAttribute('id', this.id); + else + this.removeAttribute('id'); + if (!isFromStyle) return this.applyStyle(this.render(), this); return this.render(); @@ -200,8 +208,11 @@ export abstract class GefestElement { * @returns The styled HTML. */ protected applyStyle(html: string, element?: GefestElement): string { - if (this.style) + if (this.style) { + if (typeof this.style === "function") + return this.style().applyStyle(html, element); return this.style.applyStyle(html, element); + } return html; } @@ -222,8 +233,9 @@ export abstract class GefestElement { */ protected wrapHTML (content: string): string { const attrString = this.attributesToString(); - return `
${content}
`; + return `<${this.htmlTag} ${attrString}>${content}`; } + protected htmlTag: string = "div"; /** * Renders the element and returns it as a string. @@ -234,17 +246,23 @@ export abstract class GefestElement { return this.wrapHTML(this.content); } const rendered : string[] = []; - for (const item of this.content) { - if (typeof item === 'string') { - rendered.push(item); - } - else if (item instanceof GefestElement) { - if (!item.style && this.style) { - item.style = this.style; + if (!this.isHidden) + for (const item of this.content) { + if (typeof item === 'string') { + rendered.push(item); + } + else if (item instanceof GefestElement) { + if (!item.style && this.style !== null) { + if (typeof this.style === "function") { + item.style = this.style; + } + else if (this.style instanceof GefestStyle) { + item.style = () => (this.style as GefestStyle); + } + } + rendered.push(item.build()); } - rendered.push(item.build()); } - } return this.wrapHTML(rendered.join('')); } } \ No newline at end of file diff --git a/frontend/src/modules/Gefest/engine.ts b/frontend/src/modules/Gefest/engine.ts index b561e95..8ef2000 100644 --- a/frontend/src/modules/Gefest/engine.ts +++ b/frontend/src/modules/Gefest/engine.ts @@ -2,7 +2,7 @@ import { GefestElement } from "./element"; // For protecting the set of elements from direct access const elements: Set = new Set(); -const elementsMapping: Map = new Map (); +const elementsMapping: Record = {}; export class GefestEngine { static register (element: GefestElement) : string { elements.add(element); @@ -10,8 +10,8 @@ export class GefestEngine { let gefestId : string; do { gefestId = `gefest-${Math.random().toString(36).substr(2, 9)}`; - } while(!!elementsMapping.get(gefestId)); - elementsMapping.set(gefestId, element); + } while(!!elementsMapping[gefestId]); + elementsMapping[gefestId] = element; return gefestId; } @@ -31,14 +31,28 @@ export class GefestEngine { await GefestEngine.activateOnClick(); } - static reRenderByGI (gefestId: string) { - const gefestElement = elementsMapping.get(gefestId); - if (!gefestElement) return; - const elementHTML = document.querySelectorAll(`[data-gefest-id="${gefestId}"]`); + static render (gefestId: string | string[] | null = null) { + let gefestElements : string[] | null = null; + if (typeof gefestId === "string") + gefestId = [gefestId]; + else if (gefestId === null) { + gefestId = Object.keys(elementsMapping); + gefestElements = gefestId; + } + //const gefestElement = elementsMapping.get(gefestId); + if (!gefestElements) + gefestElements = gefestId.filter(gid => !!elementsMapping[gid]); + if (gefestElements.length === 0) return; + const elementHTML = document.querySelectorAll('[data-gefest-id]'); if (elementHTML.length > 0) { - const htmlElement = elementHTML[0] as HTMLElement; - htmlElement.outerHTML = gefestElement.build(); + for (let htmlElement of elementHTML) { + const gid = htmlElement.getAttribute("data-gefest-id"); + if (!gid || !elementsMapping[gid]) + continue; + + htmlElement.outerHTML = elementsMapping[gid].build(); + } GefestEngine.activateOnClick(); } } diff --git a/frontend/src/pages/auth/auth.ts b/frontend/src/pages/auth/auth.ts index 10dd3f7..9b930a2 100644 --- a/frontend/src/pages/auth/auth.ts +++ b/frontend/src/pages/auth/auth.ts @@ -1,7 +1,134 @@ +import { GefestElement } from "../../modules/Gefest/element"; +import { GefestA } from "../../modules/Gefest/primitives/a"; +import { GefestCenter } from "../../modules/Gefest/primitives/center"; +import { GefestHeader } from "../../modules/Gefest/primitives/header"; import { DefaultElement } from "../default-element"; -export class AuthenticationPage extends DefaultElement { +export interface AuthenticationRules { + type: "password" +}; + +type formType = "text" | "email" | "password" | "textarea"; +function getFormItem (elementHtmlId: string, title: string, type: formType = "text", placeholder : string | null = null): DefaultElement { + const label: DefaultElement = new (class extends DefaultElement { + constructor() { + super([ + title + ]); + this.addClass("form-label"); + this.setAttribute("for", elementHtmlId); + } + protected htmlTag = "label"; + }); + const input: DefaultElement = new (class extends DefaultElement { + constructor() { + super([]); + this.id = elementHtmlId; + this.addClass("form-control"); + if (!{ + text: true, + email: true, + password: true, + textarea: false + }[type]) { + this.setAttribute("type", type); + } + if (placeholder !== null) { + this.setAttribute("placeholder", placeholder); + } + } + protected htmlTag = { + text: "input", + email: "input", + password: "input", + textarea: "textarea" + }[type]; + }); + + return new (class extends DefaultElement { constructor() { super([ + label, input + ]); this.addClass("mb-3"); } }); +} +class RegisterForm extends DefaultElement { + link: GefestA; + constructor(authRules : AuthenticationRules) { + const header = new GefestHeader("Registration", 2); + header.addClass("card-title"); + const link = new GefestA("Login to account"); + link.addClass("link-secondary"); + link.addClass("noselect"); + link.setPersonalStyle("cursor: pointer;"); + + const args = [ + getFormItem("username", "Username"), + getFormItem("password", "Password", "password"), + ]; + switch (authRules.type) { + case "password": + args.push(getFormItem( + "serverPassword", + "Server Password", + "password" + )); + break; + } + + super([ + new GefestCenter([header, link]), + ...args + ]); + this.link = link; + this.isHidden = true; + } +} +class LoginForm extends DefaultElement { + link: GefestA; constructor() { - super([]); + const header = new GefestHeader("Login", 2); + header.addClass("card-title"); + const link = new GefestA("Create a new account"); + link.addClass("link-secondary"); + link.addClass("noselect"); + link.setPersonalStyle("cursor: pointer;"); + + super([ + new GefestCenter([ + header, link + ]), + getFormItem("username", "Username"), + getFormItem("password", "Password", "password"), + ]); + this.link = link; + } +} +export class AuthenticationPage extends DefaultElement { + constructor(authRules : AuthenticationRules) { + const login = new LoginForm; + const register = new RegisterForm(authRules); + + const update = () => { + login.update(); + register.update(); + }; + login.link.onClick = () => { + login.isHidden = true; + register.isHidden = false; + update(); + }; + register.link.onClick = () => { + login.isHidden = false; + register.isHidden = true; + update(); + }; + + super([ + new (class extends DefaultElement { constructor() { super([ + login, + register + ]); this.addClass("card-body"); } }) + ]); + this.addClass("card"); + this.addClass("border"); + this.addClass("auth-card"); } } \ No newline at end of file diff --git a/frontend/src/pages/default-element.ts b/frontend/src/pages/default-element.ts index 3e8bc16..cc91525 100644 --- a/frontend/src/pages/default-element.ts +++ b/frontend/src/pages/default-element.ts @@ -2,7 +2,7 @@ import { GefestElement } from "../modules/Gefest/element"; import { SupportedStyles } from "../styles"; export abstract class DefaultElement extends GefestElement { - style: SupportedStyles | null = null; + style: SupportedStyles | (() => SupportedStyles) | null = null; constructor(content: (GefestElement | string)[] | string) { super(content); } @@ -11,4 +11,14 @@ export abstract class DefaultElement extends GefestElement { if (!this.style) throw new Error("Setup style is required"); return super.build(isFromStyle); } +} + +export class DefaultRoundedButton extends DefaultElement { + constructor(btnContent : GefestElement | string) { + super([ btnContent ]); + this.setAttribute("type", "button"); + this.addClass("btn"); + this.addClass("rounded-circle"); + this.addClass("border"); + } } \ No newline at end of file diff --git a/frontend/src/pages/feed/news.ts b/frontend/src/pages/feed/news.ts index 4f3b1e0..c2e6a3f 100644 --- a/frontend/src/pages/feed/news.ts +++ b/frontend/src/pages/feed/news.ts @@ -28,7 +28,7 @@ class NewsUserInterfaceCard extends DefaultElement { } } // ^^ Card -class NewsUserInterfaceButton extends DefaultElement { +class NewsUserInterfaceButton extends DefaultElement { // <-- TODO: Fix it to DefaultRoundedButton constructor(userType : UserType, card : NewsUserInterfaceCard) { const uiIcon = new GefestI(""); uiIcon.addClass({ diff --git a/frontend/src/pages/navbar.ts b/frontend/src/pages/navbar.ts index 89f730b..5ebf5a0 100644 --- a/frontend/src/pages/navbar.ts +++ b/frontend/src/pages/navbar.ts @@ -25,6 +25,7 @@ class NavbarButton extends DefaultElement { return `
  • ${content}
  • `; } } +export const themeToggle = new NavbarButton("fas fa-paint-roller"); class NavbarItemContainerUL extends DefaultElement { constructor() { @@ -37,7 +38,9 @@ class NavbarItemContainerUL extends DefaultElement { super([ newsButton, - chatButton + chatButton, + + themeToggle ]); this.addClass("navbar-nav"); } diff --git a/frontend/src/styles.ts b/frontend/src/styles.ts index 6b60483..66e54a8 100644 --- a/frontend/src/styles.ts +++ b/frontend/src/styles.ts @@ -3,7 +3,7 @@ import { GefestElement } from "./modules/Gefest/element"; import { Navbar } from "./pages/navbar"; import { isNewsUI, NewsUserInterface } from "./pages/feed/news"; -export abstract class SkeletonStyle extends GefestStyle { +abstract class SkeletonStyle extends GefestStyle { protected abstract navbar(element: GefestElement): void; protected abstract newsui(element: GefestElement): void; protected abstract styleHandle(element: GefestElement): void; @@ -64,6 +64,4 @@ export class LightStyle extends SkeletonStyle { } } -export type SupportedStyles = SkeletonStyle; - -export const defaultStyle = new LightStyle(); \ No newline at end of file +export type SupportedStyles = SkeletonStyle; \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index c2e3014..113cc2b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,10 +1,16 @@ import { Router } from "express"; import { ApiResponse } from '../utils/rest-api-answer'; -import { NewsItem, getTestNews } from "../utils/news"; +import { getTestNews } from "../utils/news"; import authRequired from "../utils/auth-required"; +import { NewsItem } from "./models/news"; const router = Router(); +router.get('/authenticationRules', (req, res) => { + const response = new ApiResponse(req.authenticationRules); + return res.json(response.getAnswer()); +}); + router.get('/isAuthenticated', (req, res) => { const response = new ApiResponse(req.isAuthenticated); return res.json(response.getAnswer()); diff --git a/src/api/models/AuthenticationRules.ts b/src/api/models/AuthenticationRules.ts new file mode 100644 index 0000000..41d3c98 --- /dev/null +++ b/src/api/models/AuthenticationRules.ts @@ -0,0 +1,3 @@ +export interface AuthenticationRulesTypes { + type: "password"; +} \ No newline at end of file diff --git a/src/api/models/news.ts b/src/api/models/news.ts new file mode 100644 index 0000000..11fa85b --- /dev/null +++ b/src/api/models/news.ts @@ -0,0 +1,14 @@ +export interface Source { + name: string, + icon?: string, + sourceURL?: string, + itemURL?: string, +} + +export interface NewsItem { + id : number; + title : string; + content : string; + source: Source; + publishedAt : Date; +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 3f2dd3b..417df50 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,19 +1,22 @@ import express, { Request, Response } from 'express'; import api from './api'; import path from 'path'; +import { AuthenticationRulesTypes } from './api/models/AuthenticationRules'; const app = express(); const port = 3000; declare module 'express-serve-static-core' { interface Request { - isAuthenticated?: boolean; + isAuthenticated: boolean; + authenticationRules: AuthenticationRulesTypes; } } // Checking auth app.use((req, res, next) => { - req.isAuthenticated = true; // Mock authentication, replace with real logic + req.authenticationRules = { type: "password" }; + req.isAuthenticated = false; // Mock authentication, replace with real logic // Add your authentication logic here next(); }); diff --git a/src/utils/news.ts b/src/utils/news.ts index 11649a4..abf49e2 100644 --- a/src/utils/news.ts +++ b/src/utils/news.ts @@ -1,17 +1,4 @@ -export interface Source { - name: string, - icon?: string, - sourceURL?: string, - itemURL?: string, -} - -export interface NewsItem { - id : number; - title : string; - content : string; - source: Source; - publishedAt : Date; -} +import { NewsItem } from "../api/models/news"; export function getTestNews (): NewsItem[] { console.warn("Remove getTestNews from production! Only for tests!");