From 65a0df30f16437d93f741f4787f99541b1ea30cb Mon Sep 17 00:00:00 2001 From: toridoriv Date: Wed, 11 Oct 2023 00:08:49 -0300 Subject: [PATCH] :sparkles: Add catalog (summary and paginated) --- app/catalog/catalog-paginated.view.ts | 57 +++++++++++++++++++++ app/catalog/catalog-summary.view.ts | 48 +++++++++++++++++ app/catalog/catalog.helpers.ts | 54 +++++++++++++++++++ app/core/fanfiction.ts | 2 +- app/core/tag.ts | 44 +++++----------- app/home/home.endpoint.ts | 43 ---------------- app/views/catalog-paginated.hbs | 12 +++++ app/views/{home.hbs => catalog-summary.hbs} | 0 app/views/layouts/main.hbs | 5 +- app/views/public/styles/main.css | 6 ++- 10 files changed, 193 insertions(+), 78 deletions(-) create mode 100644 app/catalog/catalog-paginated.view.ts create mode 100644 app/catalog/catalog-summary.view.ts create mode 100644 app/catalog/catalog.helpers.ts delete mode 100644 app/home/home.endpoint.ts create mode 100644 app/views/catalog-paginated.hbs rename app/views/{home.hbs => catalog-summary.hbs} (100%) diff --git a/app/catalog/catalog-paginated.view.ts b/app/catalog/catalog-paginated.view.ts new file mode 100644 index 0000000..fe1b577 --- /dev/null +++ b/app/catalog/catalog-paginated.view.ts @@ -0,0 +1,57 @@ +import { EndpointView, LiteFanfictionOutput } from "@common"; +import { z } from "@deps"; + +const CatalogItemSchema = z.custom(); +const CatalogPageSchema = z.object({ + href: z.string(), + isActive: z.boolean().default(false), + index: z.number().int(), +}); + +const options = { + projection: { + _id: 0, + chapters: 0, + kind: 0, + }, + limit: 5, + sort: { created_at: -1 as const }, +}; + +export default EndpointView.init({ + method: "get", + path: "/fanfiction-catalog/pages/:page", + view: "catalog-paginated", + payload: { + params: z.object({ + page: z.coerce.number().int(), + }), + }, + context: z.object({ + fanfictions: z.array(CatalogItemSchema), + pages: z.array(CatalogPageSchema), + currentPage: z.number().int(), + }), +}).registerHandler(async function main(req, res) { + const total = await res.app.fanfics.countDocuments(); + const totalPages = Math.ceil(total / options.limit); + const currentPage = req.params.page; + const pages = Array.from( + { length: totalPages }, + (_, index) => + CatalogPageSchema.parse({ + href: this.path.replace(":page", String(index + 1)), + isActive: currentPage === index + 1, + index: index + 1, + }), + ); + const skip = currentPage === 1 + ? 0 + : currentPage * options.limit - options.limit; + const fanfictions = await res.app.fanfics + .find({}, options) + .skip(skip) + .toArray(); + + return this.renderOk(res, { fanfictions, pages, currentPage }); +}); diff --git a/app/catalog/catalog-summary.view.ts b/app/catalog/catalog-summary.view.ts new file mode 100644 index 0000000..bd55ba7 --- /dev/null +++ b/app/catalog/catalog-summary.view.ts @@ -0,0 +1,48 @@ +import { EndpointView, LiteFanfictionOutput } from "@common"; +import { z } from "@deps"; + +const CatalogSummarySchema = z.custom(); + +const limit = 3; +const projection = { + _id: 0, + chapters: 0, + kind: 0, +}; + +const recentlyAddedOptions = { + limit, + projection, + sort: { created_at: -1 as const }, +}; + +const recentlyUpdatedQuery = { + $expr: { $gt: ["$updated_at", "$created_at"] }, +}; + +const recentlyUpdatedOptions = { + limit, + projection, + sort: { + updated_at: -1 as const, + }, +}; + +export default EndpointView.init({ + method: "get", + path: "/", + view: "catalog-summary", + context: z.object({ + recentlyAdded: z.array(CatalogSummarySchema), + recentlyUpdated: z.array(CatalogSummarySchema), + }), +}).registerHandler(async function main(_req, res) { + const recentlyAdded = await res.app.fanfics + .find({}, recentlyAddedOptions) + .toArray(); + const recentlyUpdated = await res.app.fanfics + .find(recentlyUpdatedQuery, recentlyUpdatedOptions) + .toArray(); + + return this.renderOk(res, { recentlyAdded, recentlyUpdated }); +}); diff --git a/app/catalog/catalog.helpers.ts b/app/catalog/catalog.helpers.ts new file mode 100644 index 0000000..c441471 --- /dev/null +++ b/app/catalog/catalog.helpers.ts @@ -0,0 +1,54 @@ +import { + getAuthorTag, + getFandomTag, + getLanguageTag, + getOriginTag, + getRelationshipTag, + type LiteFanfictionOutput, + type Tag, + type TextOutput, + type TextWithTranslationsOutput, +} from "@common"; + +export function getTags(fanfiction: LiteFanfictionOutput) { + const tags: Tag[] = [getAuthorTag(fanfiction.author.name)]; + + if (fanfiction.fandom) { + tags.push(getFandomTag(fanfiction.fandom)); + } + + if (fanfiction.relationship) { + tags.push(getRelationshipTag(fanfiction.relationship)); + } + + tags.push(getLanguageTag(fanfiction.language_code)); + tags.push(getOriginTag(fanfiction.source)); + + return tags; +} + +export function getTextToDisplay(text: TextOutput | TextOutput[]) { + if (typeof text === "undefined") { + return; + } + + if (Array.isArray(text)) { + if (text.length === 0) { + return; + } + + return text[0].rich || text[0].raw; + } + + return text.rich || text.raw; +} + +export function getMainTranslation(localized: TextWithTranslationsOutput) { + return localized.translations[0]; +} + +export function isCurrentPage(page: number) { + const currentPage = window.location.pathname.split("/").reverse()[0]; + + return Number(currentPage) === page; +} diff --git a/app/core/fanfiction.ts b/app/core/fanfiction.ts index 8791d1d..e5ecee7 100644 --- a/app/core/fanfiction.ts +++ b/app/core/fanfiction.ts @@ -68,7 +68,6 @@ export const LiteFanfictionSchema = z.object({ created_at: z.coerce.date().default(createTimestamp), updated_at: z.coerce.date().default(createTimestamp), author: AuthorSchema, - kind: z.literal("fanfiction").default("fanfiction").catch("fanfiction"), origin_id: z.coerce.string().min(1), origin_url: z.string().url(), source: z.string().default(""), @@ -84,6 +83,7 @@ export const LiteFanfictionSchema = z.object({ }); export const RawFanfictionSchema = LiteFanfictionSchema.extend({ + kind: z.literal("fanfiction").default("fanfiction").catch("fanfiction"), chapters: z.array(ChapterSchema).default([]), }); diff --git a/app/core/tag.ts b/app/core/tag.ts index baefd1e..68d140d 100644 --- a/app/core/tag.ts +++ b/app/core/tag.ts @@ -1,4 +1,4 @@ -import { FanfictionOutput } from "./fanfiction.ts"; +import { z } from "@deps"; import { getLanguageName, LanguageCode } from "./localization.ts"; /* -------------------------------------------------------------------------- */ @@ -36,13 +36,19 @@ export const ICON_BY_TAG_NAME = Object.freeze({ [TagName.Author]: "user", }); -export type Tag = { - href: string; - icon: typeof ICON_BY_TAG_NAME[TagName]; - name: TagName; - value: string; - total?: number; -}; +export type Tag = z.output; + +export const TagSchema = z.object({ + href: z.string().default(""), + icon: z.nativeEnum(ICON_BY_TAG_NAME), + name: z.nativeEnum(TagName), + value: z.string(), + total: z.number().optional(), +}).transform((tag) => { + tag.href = getTagHref(tag.name, tag.value); + + return tag; +}); export function getTagHref(name: TagName, ...values: string[]) { return `/fanfictions?${QUERY_BY_TAG_NAME[name]}${ @@ -109,28 +115,6 @@ export function getAuthorTag( }; } -export function getAllAvailableTagsForFanfiction( - fanfiction: FanfictionOutput, -): Tag[] { - const tags: Tag[] = [getAuthorTag(fanfiction.author.name)]; - - if (fanfiction.fandom) { - tags.push(getFandomTag(fanfiction.fandom)); - } - - if (fanfiction.relationship) { - tags.push(getRelationshipTag(fanfiction.relationship)); - } - - tags.push(getLanguageTag(fanfiction.language_code)); - - const url = new URL(fanfiction.origin_url); - - tags.push(getOriginTag(url.hostname)); - - return tags; -} - export function sortTagsDescending(tags: Tag[]) { return (tags as Required[]).sort(isMoreFrequent); } diff --git a/app/home/home.endpoint.ts b/app/home/home.endpoint.ts deleted file mode 100644 index dd5622b..0000000 --- a/app/home/home.endpoint.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { EndpointView, LiteFanfictionOutput } from "@common"; -import { z } from "@deps"; - -const projection = { - _id: 0, - chapters: 0, - kind: 0, -}; - -export default EndpointView.init({ - method: "get", - path: "/", - view: "home", - context: z.object({ - recentlyAdded: z.custom(), - recentlyUpdated: z.custom(), - }), -}).registerHandler(async function main(_req, res) { - const recentlyAdded = await res.app.fanfics - .find( - {}, - { - projection, - limit: 3, - }, - ) - .sort({ created_at: -1 }) - .toArray(); - const recentlyUpdated = await res.app.fanfics - .find( - { - $expr: { $gt: ["$updated_at", "$created_at"] }, - }, - { - projection, - limit: 3, - }, - ) - .sort({ updated_at: -1 }) - .toArray(); - - return this.renderOk(res, { recentlyAdded, recentlyUpdated }); -}); diff --git a/app/views/catalog-paginated.hbs b/app/views/catalog-paginated.hbs new file mode 100644 index 0000000..fe471bd --- /dev/null +++ b/app/views/catalog-paginated.hbs @@ -0,0 +1,12 @@ +{{#each fanfictions}} + {{> fanfiction-card . }} +{{/each}} + \ No newline at end of file diff --git a/app/views/home.hbs b/app/views/catalog-summary.hbs similarity index 100% rename from app/views/home.hbs rename to app/views/catalog-summary.hbs diff --git a/app/views/layouts/main.hbs b/app/views/layouts/main.hbs index caacf5a..ea736a1 100644 --- a/app/views/layouts/main.hbs +++ b/app/views/layouts/main.hbs @@ -15,7 +15,7 @@ - 🏠 Home