too many changes, i'm tired for describe all. But I try fix themes logic, okay?

This commit is contained in:
FullGreaM 2026-04-28 02:59:58 +03:00
parent 3de0e95569
commit a4c5b441f6
15 changed files with 295 additions and 62 deletions

View File

@ -1,7 +1,8 @@
import { AuthenticationPage, AuthenticationRules } from "../pages/auth/auth";
import { NewsData, NewsFeed } from "../pages/feed/news"; import { NewsData, NewsFeed } from "../pages/feed/news";
import { NewsItem } from "../pages/feed/news-item"; import { NewsItem } from "../pages/feed/news-item";
type EndpointURL = "" | `http${'s' | ''}://${string}`; type EndpointURL = "/api" | `http${'s' | ''}://${string}`;
interface ErrorDetails { interface ErrorDetails {
code: string, code: string,
@ -14,17 +15,27 @@ interface APIResponse<T> {
} }
let newsFeed : NewsFeed | null = null; let newsFeed : NewsFeed | null = null;
let authPage : AuthenticationPage | null = null;
class ErrorAPI extends Error {} class ErrorAPI extends Error {}
export class ControllerAPI { export class ControllerAPI {
protected endpoint: EndpointURL; protected endpoint: EndpointURL;
constructor(endpoint: EndpointURL = "") { constructor(endpoint: EndpointURL = "/api") {
this.endpoint = endpoint; this.endpoint = endpoint;
} }
protected async request<T>(path: string, options: RequestInit = {}): Promise<T> { protected async request<T>(path: string, options: RequestInit = {}): Promise<T> {
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 response = await fetch(url, options);
const result: APIResponse<T> = await response.json(); const result: APIResponse<T> = await response.json();
@ -50,21 +61,21 @@ export class ControllerAPI {
async isAuthenticated(): Promise<boolean> { async isAuthenticated(): Promise<boolean> {
if (!this.checkFirstAuth()) return false; if (!this.checkFirstAuth()) return false;
const result = await this.request<boolean>('/api/isAuthenticated', { const result = await this.request<boolean>('/isAuthenticated', {
method: 'GET', method: 'GET',
credentials: 'include' // Include cookies for authentication credentials: 'include' // Include cookies for authentication
}); });
return result; return result;
} }
async getNews(returnElement: boolean = false) : Promise<NewsFeed | NewsData[]> { async getNews(returnPage: boolean = false) : Promise<NewsFeed | NewsData[]> {
this.throwIfUnauthed(); this.throwIfUnauthed();
const result = await this.request<NewsData[]>('/api/news', { const result = await this.request<NewsData[]>('/news', {
method: 'GET', method: 'GET',
credentials: 'include' // Include cookies for authentication credentials: 'include' // Include cookies for authentication
}); });
if (!returnElement) if (!returnPage)
return result; return result;
if (!newsFeed) if (!newsFeed)
newsFeed = new NewsFeed(result.map(x => new NewsItem( newsFeed = new NewsFeed(result.map(x => new NewsItem(
@ -76,4 +87,15 @@ export class ControllerAPI {
))); )));
return newsFeed; 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
});
if (!returnPage) return result;
if (!authPage)
authPage = new AuthenticationPage(result);
return authPage;
}
} }

View File

@ -59,3 +59,10 @@
.nav-item { .nav-item {
cursor: pointer; padding: 12px 24px; font-size: 1.5em; cursor: pointer; padding: 12px 24px; font-size: 1.5em;
} }
.auth-card {
margin-left: 30vw;
margin-right: 30vw;
margin-top: 30vh;
margin-bottom: 30vh;
}

View File

@ -1,12 +1,15 @@
import { GefestEngine } from "./modules/Gefest/engine"; import { GefestEngine } from "./modules/Gefest/engine";
import { Navbar } from "./pages/navbar"; import { Navbar, themeToggle } from "./pages/navbar";
import { DarkStyle, LightStyle, defaultStyle } from "./styles"; import { DarkStyle, LightStyle, SupportedStyles } from "./styles";
import { ControllerAPI } from "./api/RESTful"; import { ControllerAPI } from "./api/RESTful";
import { NewsFeed } from "./pages/feed/news"; import { NewsFeed } from "./pages/feed/news";
import { AuthenticationPage } from "./pages/auth/auth"; 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 // 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 { // I hate trash in global scope, so I wrap it in a block
const appElement = document.getElementById("app"); const appElement = document.getElementById("app");
@ -20,7 +23,7 @@ const api : ControllerAPI = new ControllerAPI();
async function authInit() { async function authInit() {
console.log("User is authenticated"); console.log("User is authenticated");
const newsFeed = await api.getNews(true) as NewsFeed; const newsFeed = await api.getNews(true) as NewsFeed;
newsFeed.style = currentStyle; newsFeed.style = styleMemLink.currentStyle;
const appElement = document.getElementById("app"); const appElement = document.getElementById("app");
if (appElement) if (appElement)
appElement.innerHTML = newsFeed.build(); appElement.innerHTML = newsFeed.build();
@ -28,17 +31,35 @@ async function authInit() {
async function logInInit () { async function logInInit () {
console.log("User is not authenticated"); console.log("User is not authenticated");
const authPage = new AuthenticationPage(); const authPage = await api.getAuthRules(true) as AuthenticationPage;
authPage.style = currentStyle; authPage.style = styleMemLink.currentStyle;
const appElement = document.getElementById("app"); const appElement = document.getElementById("app");
if (appElement) if (appElement)
appElement.innerHTML = authPage.build(); 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() { async function main() {
await changeStyleLogic();
// init navbar // init navbar
const navbar = new Navbar(); const navbar = new Navbar();
navbar.style = currentStyle; navbar.style = styleMemLink.currentStyle;
document.body.innerHTML = "<div id='app'></div>" + navbar.build(); document.body.innerHTML = "<div id='app'></div>" + navbar.build();
if (await api.isAuthenticated()) { if (await api.isAuthenticated()) {

View File

@ -4,8 +4,9 @@ import { GefestStyle } from './style';
// The general class of Gefest // The general class of Gefest
type GefestElementEvents = "onClick"; type GefestElementEvents = "onClick";
export abstract class GefestElement { export abstract class GefestElement {
style: GefestStyle | null = null; style: GefestStyle | (() => GefestStyle) | null = null;
isHidden: boolean = false; isHidden: boolean = false;
id : string | null = null;
//onClick: (() => void) | null = null; //onClick: (() => void) | null = null;
readonly gefestId: string; readonly gefestId: string;
protected content: (GefestElement | string)[] | string; protected content: (GefestElement | string)[] | string;
@ -64,6 +65,8 @@ export abstract class GefestElement {
* @returns The created GefestElement. * @returns The created GefestElement.
*/ */
static fromHTML(html: string): 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 htmlElement = new DOMParser().parseFromString(html, 'text/html').body.firstChild as HTMLElement;
const element = new (class extends GefestElement { const element = new (class extends GefestElement {
protected wrapHTML(content: string): string { protected wrapHTML(content: string): string {
@ -98,7 +101,7 @@ export abstract class GefestElement {
* Call to GefestEngine for rerender element * Call to GefestEngine for rerender element
*/ */
update (): void { update (): void {
GefestEngine.reRenderByGI(this.gefestId); GefestEngine.render(this.gefestId);
} }
/** /**
@ -112,6 +115,11 @@ export abstract class GefestElement {
else else
this.removeAttribute('hidden'); this.removeAttribute('hidden');
if (this.id !== null)
this.setAttribute('id', this.id);
else
this.removeAttribute('id');
if (!isFromStyle) if (!isFromStyle)
return this.applyStyle(this.render(), this); return this.applyStyle(this.render(), this);
return this.render(); return this.render();
@ -200,8 +208,11 @@ export abstract class GefestElement {
* @returns The styled HTML. * @returns The styled HTML.
*/ */
protected applyStyle(html: string, element?: GefestElement): string { 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 this.style.applyStyle(html, element);
}
return html; return html;
} }
@ -222,8 +233,9 @@ export abstract class GefestElement {
*/ */
protected wrapHTML (content: string): string { protected wrapHTML (content: string): string {
const attrString = this.attributesToString(); const attrString = this.attributesToString();
return `<div ${attrString}>${content}</div>`; return `<${this.htmlTag} ${attrString}>${content}</${this.htmlTag}>`;
} }
protected htmlTag: string = "div";
/** /**
* Renders the element and returns it as a string. * Renders the element and returns it as a string.
@ -234,17 +246,23 @@ export abstract class GefestElement {
return this.wrapHTML(this.content); return this.wrapHTML(this.content);
} }
const rendered : string[] = []; const rendered : string[] = [];
for (const item of this.content) { if (!this.isHidden)
if (typeof item === 'string') { for (const item of this.content) {
rendered.push(item); if (typeof item === 'string') {
} rendered.push(item);
else if (item instanceof GefestElement) { }
if (!item.style && this.style) { else if (item instanceof GefestElement) {
item.style = this.style; 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('')); return this.wrapHTML(rendered.join(''));
} }
} }

View File

@ -2,7 +2,7 @@ import { GefestElement } from "./element";
// For protecting the set of elements from direct access // For protecting the set of elements from direct access
const elements: Set<GefestElement> = new Set(); const elements: Set<GefestElement> = new Set();
const elementsMapping: Map<string, GefestElement> = new Map<string, GefestElement> (); const elementsMapping: Record<string, GefestElement> = {};
export class GefestEngine { export class GefestEngine {
static register (element: GefestElement) : string { static register (element: GefestElement) : string {
elements.add(element); elements.add(element);
@ -10,8 +10,8 @@ export class GefestEngine {
let gefestId : string; let gefestId : string;
do { do {
gefestId = `gefest-${Math.random().toString(36).substr(2, 9)}`; gefestId = `gefest-${Math.random().toString(36).substr(2, 9)}`;
} while(!!elementsMapping.get(gefestId)); } while(!!elementsMapping[gefestId]);
elementsMapping.set(gefestId, element); elementsMapping[gefestId] = element;
return gefestId; return gefestId;
} }
@ -31,14 +31,28 @@ export class GefestEngine {
await GefestEngine.activateOnClick(); await GefestEngine.activateOnClick();
} }
static reRenderByGI (gefestId: string) { static render (gefestId: string | string[] | null = null) {
const gefestElement = elementsMapping.get(gefestId); let gefestElements : string[] | null = null;
if (!gefestElement) return; if (typeof gefestId === "string")
const elementHTML = document.querySelectorAll(`[data-gefest-id="${gefestId}"]`); 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) { if (elementHTML.length > 0) {
const htmlElement = elementHTML[0] as HTMLElement; for (let htmlElement of elementHTML) {
htmlElement.outerHTML = gefestElement.build(); const gid = htmlElement.getAttribute("data-gefest-id");
if (!gid || !elementsMapping[gid])
continue;
htmlElement.outerHTML = elementsMapping[gid].build();
}
GefestEngine.activateOnClick(); GefestEngine.activateOnClick();
} }
} }

View File

@ -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"; import { DefaultElement } from "../default-element";
export class AuthenticationPage extends DefaultElement { export interface AuthenticationRules {
constructor() { type: "password"
super([]); };
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() {
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");
} }
} }

View File

@ -2,7 +2,7 @@ import { GefestElement } from "../modules/Gefest/element";
import { SupportedStyles } from "../styles"; import { SupportedStyles } from "../styles";
export abstract class DefaultElement extends GefestElement { export abstract class DefaultElement extends GefestElement {
style: SupportedStyles | null = null; style: SupportedStyles | (() => SupportedStyles) | null = null;
constructor(content: (GefestElement | string)[] | string) { constructor(content: (GefestElement | string)[] | string) {
super(content); super(content);
} }
@ -12,3 +12,13 @@ export abstract class DefaultElement extends GefestElement {
return super.build(isFromStyle); 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");
}
}

View File

@ -28,7 +28,7 @@ class NewsUserInterfaceCard extends DefaultElement {
} }
} }
// ^^ Card // ^^ Card
class NewsUserInterfaceButton extends DefaultElement { class NewsUserInterfaceButton extends DefaultElement { // <-- TODO: Fix it to DefaultRoundedButton
constructor(userType : UserType, card : NewsUserInterfaceCard) { constructor(userType : UserType, card : NewsUserInterfaceCard) {
const uiIcon = new GefestI(""); const uiIcon = new GefestI("");
uiIcon.addClass({ uiIcon.addClass({

View File

@ -25,6 +25,7 @@ class NavbarButton extends DefaultElement {
return `<li ${attrString}>${content}</li>`; return `<li ${attrString}>${content}</li>`;
} }
} }
export const themeToggle = new NavbarButton("fas fa-paint-roller");
class NavbarItemContainerUL extends DefaultElement { class NavbarItemContainerUL extends DefaultElement {
constructor() { constructor() {
@ -37,7 +38,9 @@ class NavbarItemContainerUL extends DefaultElement {
super([ super([
newsButton, newsButton,
chatButton chatButton,
themeToggle
]); ]);
this.addClass("navbar-nav"); this.addClass("navbar-nav");
} }

View File

@ -3,7 +3,7 @@ import { GefestElement } from "./modules/Gefest/element";
import { Navbar } from "./pages/navbar"; import { Navbar } from "./pages/navbar";
import { isNewsUI, NewsUserInterface } from "./pages/feed/news"; 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 navbar(element: GefestElement): void;
protected abstract newsui(element: GefestElement): void; protected abstract newsui(element: GefestElement): void;
protected abstract styleHandle(element: GefestElement): void; protected abstract styleHandle(element: GefestElement): void;
@ -65,5 +65,3 @@ export class LightStyle extends SkeletonStyle {
} }
export type SupportedStyles = SkeletonStyle; export type SupportedStyles = SkeletonStyle;
export const defaultStyle = new LightStyle();

View File

@ -1,10 +1,16 @@
import { Router } from "express"; import { Router } from "express";
import { ApiResponse } from '../utils/rest-api-answer'; 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 authRequired from "../utils/auth-required";
import { NewsItem } from "./models/news";
const router = Router(); const router = Router();
router.get('/authenticationRules', (req, res) => {
const response = new ApiResponse(req.authenticationRules);
return res.json(response.getAnswer());
});
router.get('/isAuthenticated', (req, res) => { router.get('/isAuthenticated', (req, res) => {
const response = new ApiResponse(req.isAuthenticated); const response = new ApiResponse(req.isAuthenticated);
return res.json(response.getAnswer()); return res.json(response.getAnswer());

View File

@ -0,0 +1,3 @@
export interface AuthenticationRulesTypes {
type: "password";
}

14
src/api/models/news.ts Normal file
View File

@ -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;
}

View File

@ -1,19 +1,22 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express';
import api from './api'; import api from './api';
import path from 'path'; import path from 'path';
import { AuthenticationRulesTypes } from './api/models/AuthenticationRules';
const app = express(); const app = express();
const port = 3000; const port = 3000;
declare module 'express-serve-static-core' { declare module 'express-serve-static-core' {
interface Request { interface Request {
isAuthenticated?: boolean; isAuthenticated: boolean;
authenticationRules: AuthenticationRulesTypes;
} }
} }
// Checking auth // Checking auth
app.use((req, res, next) => { 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 // Add your authentication logic here
next(); next();
}); });

View File

@ -1,17 +1,4 @@
export interface Source { import { NewsItem } from "../api/models/news";
name: string,
icon?: string,
sourceURL?: string,
itemURL?: string,
}
export interface NewsItem {
id : number;
title : string;
content : string;
source: Source;
publishedAt : Date;
}
export function getTestNews (): NewsItem[] { export function getTestNews (): NewsItem[] {
console.warn("Remove getTestNews from production! Only for tests!"); console.warn("Remove getTestNews from production! Only for tests!");