diff --git a/frontend/src/api/RESTful.ts b/frontend/src/api/RESTful.ts index d190069..7bd3b2f 100644 --- a/frontend/src/api/RESTful.ts +++ b/frontend/src/api/RESTful.ts @@ -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"; @@ -69,7 +70,7 @@ export class ControllerAPI { const result = await this.request('/isAuthenticated', { method: 'GET', - credentials: 'include' // Include cookies for authentication + credentials: 'include' }); return result; } @@ -79,33 +80,37 @@ export class ControllerAPI { const result = await this.request('/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, - x.comments, - x.likes, - x.isAdult, - x.isLiked - ))); + newsFeed = new NewsFeed(result.map(feed => new NewsItem(feed))); return newsFeed; } async getAuthRules(returnPage: boolean = false) : Promise { const result = await this.request('/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" + } + }); + } } \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 6fd34b6..ec0d391 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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(); diff --git a/frontend/src/pages/default-element.ts b/frontend/src/pages/default-element.ts index 930cfa0..15cb8f6 100644 --- a/frontend/src/pages/default-element.ts +++ b/frontend/src/pages/default-element.ts @@ -4,7 +4,7 @@ import { GefestI } from "../modules/Gefest/primitives/i"; import { SupportedStyles } from "../styles"; type FontAwesomeGroup = "fas" | "far" | "fab"; // e.g. "fas fa-home" -export type FontAwesomeIcon = `${FontAwesomeGroup} fa-${string}`; +export type FontAwesomeIconType = `${FontAwesomeGroup} fa-${string}`; export abstract class DefaultElement extends GefestElement { style: SupportedStyles | (() => SupportedStyles) | null = null; @@ -26,4 +26,21 @@ export class DefaultRoundedButton extends DefaultElement { this.addClass("rounded-circle"); 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"); + } } \ No newline at end of file diff --git a/frontend/src/pages/feed/news-item.ts b/frontend/src/pages/feed/news-item.ts index d51df50..04382ee 100644 --- a/frontend/src/pages/feed/news-item.ts +++ b/frontend/src/pages/feed/news-item.ts @@ -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 = [ @@ -63,21 +64,13 @@ class CardBodyBS5 extends DefaultElement { export class NewsItem extends DefaultElement { constructor( - id: string, - title: string, - publishDate: Date, - content: string, - source: NewsSource, - commentsCount: number, - likesCount: Number, - isAdult: boolean, - isLiked: boolean, + newsData: NewsData ) { - const header = new GefestHeader(title, 2); + 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 { @@ -91,12 +84,115 @@ export class NewsItem extends DefaultElement { 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, splitter, + new (class extends DefaultElement { + constructor() { + super([actions, views]); + [ + "d-flex", + "justify-content-between", + "align-items-center" + ].forEach(c => this.addClass(c)); + } + }), ]) ]); diff --git a/frontend/src/pages/feed/news.ts b/frontend/src/pages/feed/news.ts index db5e6a9..19133bd 100644 --- a/frontend/src/pages/feed/news.ts +++ b/frontend/src/pages/feed/news.ts @@ -1,5 +1,5 @@ import { NewsItem } from "./news-item"; -import { DefaultElement, FontAwesomeIcon } 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"; @@ -11,15 +11,8 @@ export function isNewsUI (element : GefestElement): boolean { element instanceof NewsUserInterfaceCard; } class NewsUserInterfaceBtn extends DefaultElement { - constructor(icon: FontAwesomeIcon) { - const fontawesomeIcon = new (class extends GefestI { - constructor () { - super(""); - this.ignoreStyleClasses.add("bg-light"); - this.ignoreStyleClasses.add("bg-dark"); - } - }); - fontawesomeIcon.addClass(icon); + constructor(icon: FontAwesomeIconType) { + const fontawesomeIcon = new FontAwesomeIcon(icon); super([fontawesomeIcon]); this.ignoreStyleClasses.add("bg-light"); @@ -143,6 +136,7 @@ export interface NewsData { publishedAt : string; comments: number; likes: number; + looked: number; isAdult: boolean; isLiked: boolean; } \ No newline at end of file diff --git a/frontend/src/pages/navbar.ts b/frontend/src/pages/navbar.ts index 795ff19..9622941 100644 --- a/frontend/src/pages/navbar.ts +++ b/frontend/src/pages/navbar.ts @@ -1,12 +1,11 @@ import { GefestA } from "../modules/Gefest/primitives/a"; import { GefestI } from "../modules/Gefest/primitives/i"; -import { DefaultElement, FontAwesomeIcon } from "./default-element"; +import { DefaultElement, FontAwesomeIcon, FontAwesomeIconType } from "./default-element"; 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 ]); diff --git a/src/api/index.ts b/src/api/index.ts index c718df1..2ba1353 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -18,8 +18,20 @@ router.get('/isAuthenticated', (req, res) => { router.get('/news', authRequired, (req, res) => { const newsList : NewsItem[] = getTestNews(); - const response = new ApiResponse(newsList); + const response = new ApiResponse(newsList); 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; \ No newline at end of file diff --git a/src/api/models/news.ts b/src/api/models/news.ts index b21a158..1ff6b02 100644 --- a/src/api/models/news.ts +++ b/src/api/models/news.ts @@ -32,6 +32,7 @@ export interface NewsItem { content : string; source: NewsSource; publishedAt : Date; + looked: number; comments: number; likes: number; diff --git a/src/utils/news/index.ts b/src/utils/news/index.ts index 2812637..85fd061 100644 --- a/src/utils/news/index.ts +++ b/src/utils/news/index.ts @@ -13,6 +13,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 1, isAdult: false, @@ -27,6 +28,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -41,6 +43,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -55,6 +58,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -69,6 +73,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -83,6 +88,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -97,6 +103,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -111,6 +118,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -125,6 +133,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -139,6 +148,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -153,6 +163,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -167,6 +178,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false, @@ -181,6 +193,7 @@ export function getTestNews (): NewsItem[] { name: "Test Destination", type: "system", }, + looked: 1, comments: 0, likes: 0, isAdult: false,