Compare commits

..

2 Commits

15 changed files with 304 additions and 59 deletions

View File

@ -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<T> {
}
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<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 result: APIResponse<T> = await response.json();
@ -50,21 +61,21 @@ export class ControllerAPI {
async isAuthenticated(): Promise<boolean> {
if (!this.checkFirstAuth()) return false;
const result = await this.request<boolean>('/api/isAuthenticated', {
const result = await this.request<boolean>('/isAuthenticated', {
method: 'GET',
credentials: 'include' // Include cookies for authentication
});
return result;
}
async getNews(returnElement: boolean = false) : Promise<NewsFeed | NewsData[]> {
async getNews(returnPage: boolean = false) : Promise<NewsFeed | NewsData[]> {
this.throwIfUnauthed();
const result = await this.request<NewsData[]>('/api/news', {
const result = await this.request<NewsData[]>('/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<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 {
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,11 +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");
@ -19,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();
@ -27,13 +31,35 @@ async function authInit() {
async function logInInit () {
console.log("User is not authenticated");
// Load the auth page
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 = "<div id='app'></div>" + navbar.build();
if (await api.isAuthenticated()) {

View File

@ -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 `<div ${attrString}>${content}</div>`;
return `<${this.htmlTag} ${attrString}>${content}</${this.htmlTag}>`;
}
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(''));
}
}

View File

@ -2,7 +2,7 @@ import { GefestElement } from "./element";
// For protecting the set of elements from direct access
const elements: Set<GefestElement> = new Set();
const elementsMapping: Map<string, GefestElement> = new Map<string, GefestElement> ();
const elementsMapping: Record<string, GefestElement> = {};
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();
}
}

View File

@ -0,0 +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 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() {
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";
export abstract class DefaultElement extends GefestElement {
style: SupportedStyles | null = null;
style: SupportedStyles | (() => SupportedStyles) | null = null;
constructor(content: (GefestElement | string)[] | string) {
super(content);
}
@ -12,3 +12,13 @@ export abstract class DefaultElement extends GefestElement {
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
class NewsUserInterfaceButton extends DefaultElement {
class NewsUserInterfaceButton extends DefaultElement { // <-- TODO: Fix it to DefaultRoundedButton
constructor(userType : UserType, card : NewsUserInterfaceCard) {
const uiIcon = new GefestI("");
uiIcon.addClass({

View File

@ -25,6 +25,7 @@ class NavbarButton extends DefaultElement {
return `<li ${attrString}>${content}</li>`;
}
}
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");
}

View File

@ -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;
@ -65,5 +65,3 @@ export class LightStyle extends SkeletonStyle {
}
export type SupportedStyles = SkeletonStyle;
export const defaultStyle = new LightStyle();

View File

@ -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());

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 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();
});

View File

@ -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!");