Compare commits
2 Commits
2316fe68e1
...
97e318ce6f
Author | SHA1 | Date | |
---|---|---|---|
97e318ce6f | |||
554a6dff45 |
1267
package-lock.json
generated
1267
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,10 +16,14 @@
|
|||||||
"@tanstack/react-query-devtools": "^5.63.0",
|
"@tanstack/react-query-devtools": "^5.63.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"html-react-parser": "^5.2.2",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"rss": "^1.2.2"
|
"remark": "^15.0.1",
|
||||||
|
"remark-html": "^16.0.1",
|
||||||
|
"rss": "^1.2.2",
|
||||||
|
"sanitize-html": "^2.14.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -27,6 +31,7 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/rss": "^0.0.32",
|
"@types/rss": "^0.0.32",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { fetchAnnouncementById } from "@/lib/api";
|
import { fetchAnnouncementById } from "@/lib/api";
|
||||||
import { Announcement } from "@/lib/types";
|
import { Announcement } from "@/lib/types";
|
||||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
import { formatDateToMonthDayYear, markdownToHtml } from "@/lib/utils";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Announcement Detail Page',
|
title: 'Announcement Detail Page',
|
||||||
@ -14,6 +14,7 @@ export default async function AnnouncementDetailPage(props: {params: paramsType}
|
|||||||
let announcement: Announcement;
|
let announcement: Announcement;
|
||||||
try{
|
try{
|
||||||
announcement = await fetchAnnouncementById(announcementId);
|
announcement = await fetchAnnouncementById(announcementId);
|
||||||
|
announcement.content = await markdownToHtml(announcement.content);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -1,41 +1,68 @@
|
|||||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
import { formatDateToMonthDayYear } from "@/lib/utils";
|
||||||
import { Announcement } from "@/lib/types";
|
import { Announcement } from "@/lib/types";
|
||||||
import { fetchAnnouncements } from "@/lib/api";
|
import { fetchAnnouncements } from "@/lib/api";
|
||||||
|
import { markdownToHtml } from "@/lib/utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Annoucement page',
|
title: "Announcement Page",
|
||||||
description: 'NullTranslationGroup Announcement page',
|
description: "NullTranslationGroup Announcement Page",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function AnnouncementPage() {
|
export default async function AnnouncementPage() {
|
||||||
let announcements = [];
|
let announcements: Announcement[] = [];
|
||||||
try {
|
try {
|
||||||
announcements = await fetchAnnouncements();
|
announcements = await fetchAnnouncements();
|
||||||
|
for (const announcement of announcements) {
|
||||||
|
announcement.content = await markdownToHtml(announcement.content);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return (
|
return (
|
||||||
<div className="text-center mt-10 text-red-500">
|
<div className="text-center mt-10 text-red-500">
|
||||||
<p>Failed to load announcements.</p>
|
<p>Failed to load announcements. Please try again later.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted_announcements:Announcement[] = announcements.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
|
// Sort announcements by date (newest first)
|
||||||
|
const sorted_announcements: Announcement[] = announcements.sort(
|
||||||
|
(a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime()
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto py-10 px-4">
|
<div className="max-w-6xl mx-auto py-10 px-4">
|
||||||
{sorted_announcements.map((announcement) => (
|
<h1 className="text-3xl font-bold text-center mb-8">Announcements</h1>
|
||||||
<Link href = {`/announcements/${announcement.documentId}`} key={announcement.documentId}>
|
{sorted_announcements.length === 0 ? (
|
||||||
<li key={announcement.id} className="mb-2 list-none">
|
<div className="text-center text-gray-500">
|
||||||
<div className="text-lg font-semibold">{announcement.title}</div>
|
<p>No announcements available.</p>
|
||||||
<div className="text-sm text-gray-500">{formatDateToMonthDayYear(new Date(announcement.datetime))}</div>
|
</div>
|
||||||
</li>
|
) : (
|
||||||
</Link>
|
<div className="space-y-6">
|
||||||
))}
|
{sorted_announcements.map((announcement) => {
|
||||||
</div>
|
const contents = announcement.content.split("\n");
|
||||||
);
|
announcement.content = contents.length > 3 ? contents.slice(0, 3).join("\n") : announcement.content;
|
||||||
|
return (
|
||||||
}
|
<Link
|
||||||
|
href={`/announcements/${announcement.documentId}`}
|
||||||
|
key={announcement.documentId}
|
||||||
|
>
|
||||||
|
<div className="prose dark:prose-invert max-w-none p-6 bg-white shadow-md rounded-lg cursor-pointer transition-colors duration-300 hover:shadow-lg dark:bg-gray-800 dark:shadow-gray-900">
|
||||||
|
<div className="text-sm text-gray-500 mb-4 dark:text-gray-400">
|
||||||
|
{formatDateToMonthDayYear(new Date(announcement.datetime))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-gray-700 mb-4 dark:text-gray-300 prose dark:prose-invert max-w-none"
|
||||||
|
dangerouslySetInnerHTML={{ __html: announcement.content }}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center text-blue-600 hover:text-blue-800 transition-colors duration-300 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
<span className="mr-2">Read More</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import NavigationButtons from "@/components/NavigationButtons";
|
import NavigationButtons from "@/components/NavigationButtons";
|
||||||
|
import ChapterRenderer from "@/components/ChapterContentRenderer";
|
||||||
import { Chapter } from "@/lib/types";
|
import { Chapter } from "@/lib/types";
|
||||||
import {fetchChapterByBookId } from "@/lib/api";
|
import { fetchChapterByBookId, fetchGlossaryByBookId } from "@/lib/api";
|
||||||
|
import { markdownToHtml } from "@/lib/utils";
|
||||||
export type paramsType = Promise<{ bookId: string; chapterId: string }>;
|
export type paramsType = Promise<{ bookId: string; chapterId: string }>;
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@ -26,16 +28,17 @@ export default async function ChapterPage(props: { params: paramsType}) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const glossary_data = await fetchGlossaryByBookId(bookId);
|
||||||
|
const english_glossary = glossary_data.english_english;
|
||||||
const sorted_chapters:Chapter[] = chapters.sort((a, b) => a.number - b.number);
|
const sorted_chapters:Chapter[] = chapters.sort((a, b) => a.number - b.number);
|
||||||
const current_chapter = sorted_chapters.find((chapter) => chapter.documentId === chapterId) || null;
|
const current_chapter = sorted_chapters.find((chapter) => chapter.documentId === chapterId) || null;
|
||||||
const next_chapter = current_chapter ? sorted_chapters.find((chapter) => chapter.number === current_chapter.number + 1 && new Date(chapter.release_datetime).getTime() <= new Date().getTime())?.documentId || "" : "";
|
const next_chapter = current_chapter ? sorted_chapters.find((chapter) => chapter.number === current_chapter.number + 1 && new Date(chapter.release_datetime).getTime() <= new Date().getTime())?.documentId || "" : "";
|
||||||
const prev_chapter = current_chapter ? sorted_chapters.find((chapter) => chapter.number === current_chapter.number - 1 && new Date(chapter.release_datetime).getTime() <= new Date().getTime())?.documentId || "" : "";
|
const prev_chapter = current_chapter ? sorted_chapters.find((chapter) => chapter.number === current_chapter.number - 1 && new Date(chapter.release_datetime).getTime() <= new Date().getTime())?.documentId || "" : "";
|
||||||
|
const chapter_content_html = current_chapter ? await markdownToHtml(current_chapter.content) : "";
|
||||||
if(current_chapter === null){
|
if(current_chapter === null){
|
||||||
return (
|
return (
|
||||||
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
||||||
<div dangerouslySetInnerHTML={{ __html: '<center><h1> Chapter not found !</h1></center>' }}></div>
|
<div dangerouslySetInnerHTML={{ __html: '<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter} /><center><NavigationButtons /><h1> Chapter not found !</h1></center>' }}></div>
|
||||||
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={""} nextChapter={""} />
|
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={""} nextChapter={""} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -44,8 +47,8 @@ export default async function ChapterPage(props: { params: paramsType}) {
|
|||||||
return (
|
return (
|
||||||
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
||||||
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter} />
|
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter} />
|
||||||
<div className="pt-4" dangerouslySetInnerHTML={{ __html: current_chapter.content }}></div>
|
<div className="pt-4"></div>
|
||||||
|
<ChapterRenderer content={chapter_content_html} glossary={english_glossary} />
|
||||||
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/>
|
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { fetchBookById } from "@/lib/api";
|
import { fetchBookById } from "@/lib/api";
|
||||||
import { Book, Chapter } from "@/lib/types";
|
import { Book, Chapter } from "@/lib/types";
|
||||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
import { formatDateToMonthDayYear, markdownToHtml } from "@/lib/utils";
|
||||||
import ChapterDropdown from "@/components/ChapterDropdown";
|
import ChapterDropdown from "@/components/ChapterDropdown";
|
||||||
import { Ad } from "@/lib/types";
|
import { Ad } from "@/lib/types";
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ export type paramsType = Promise<{ bookId: string}>;
|
|||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Null Translation Group',
|
title: 'Null Translation Group',
|
||||||
description: 'Null Translatin Group book',
|
description: 'Null Translation Group book description page',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function BookPage(props: { params: paramsType }) {
|
export default async function BookPage(props: { params: paramsType }) {
|
||||||
@ -25,7 +25,9 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, author, description, chapters, cover } = book;
|
const {title, author, description, chapters, cover, translator_note, glossary} = book;
|
||||||
|
const english_glossary = glossary?.english_english;
|
||||||
|
const translator_note_html = await markdownToHtml(translator_note);
|
||||||
const sorted_chapters:Chapter[] = chapters.sort((a, b) => a.number - b.number);
|
const sorted_chapters:Chapter[] = chapters.sort((a, b) => a.number - b.number);
|
||||||
|
|
||||||
const cover_media = cover?.at(0);
|
const cover_media = cover?.at(0);
|
||||||
@ -45,7 +47,7 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="ml-4 bg-yellow-500 text-white font-semibold py-2 px-4 rounded hover:bg-yellow-600 transition duration-200"
|
className="ml-4 bg-yellow-500 text-white font-semibold py-2 px-4 rounded hover:bg-yellow-600 transition duration-200"
|
||||||
>
|
>
|
||||||
Join Our Patreon for Unreleased Chapters
|
Join Our Patreon to read ahead!
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -54,8 +56,8 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
<br></br>
|
<br></br>
|
||||||
<strong>Translator:</strong> Null Translation Group
|
<strong>Translator:</strong> Null Translation Group
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-6">{description}</p>
|
<p className="mb-6">Description: {description}</p>
|
||||||
|
<div className="mb-6" dangerouslySetInnerHTML={{__html: translator_note_html}} />
|
||||||
<h2 className="text-3xl font-semibold mb-4">Recent Chapters</h2>
|
<h2 className="text-3xl font-semibold mb-4">Recent Chapters</h2>
|
||||||
<ul className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
<ul className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{recentChapters.map((chapter) => (
|
{recentChapters.map((chapter) => (
|
||||||
@ -93,6 +95,18 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
{glossary && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-3xl font-semibold">Glossary</h2>
|
||||||
|
<ul className="list-disc list-inside mt-4">
|
||||||
|
{Object.entries(english_glossary).map(([term, definition]) => (
|
||||||
|
<li key={term} className="mb-2">
|
||||||
|
<strong>{term}:</strong> {definition}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
50
src/components/ChapterContentRenderer.tsx
Normal file
50
src/components/ChapterContentRenderer.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import GlossaryPopup from "./GlossaryPopup";
|
||||||
|
import parse, { DOMNode, Element } from "html-react-parser";
|
||||||
|
|
||||||
|
interface ChapterRendererProps {
|
||||||
|
content: string;
|
||||||
|
glossary: JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChapterRenderer: React.FC<ChapterRendererProps> = ({ content, glossary }) => {
|
||||||
|
// Split content into lines
|
||||||
|
const content_lines = content.split("\n");
|
||||||
|
|
||||||
|
// Process each line for glossary replacement
|
||||||
|
const processed_lines = content_lines.map((line) => {
|
||||||
|
// Replace glossary terms in the line
|
||||||
|
Object.entries(glossary).forEach(([term, definition]) => {
|
||||||
|
const termRegex = new RegExp(`\\b${term}\\b`, "gi"); // Match whole word (case-insensitive)
|
||||||
|
line = line.replace(termRegex, `<glossarypopup term="${term}" definition="${definition}" />`);
|
||||||
|
});
|
||||||
|
|
||||||
|
line = line.replace(/<p>/g, "<div>").replace(/<\/p>/g, "</div>");
|
||||||
|
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{processed_lines.map((line, index) => (
|
||||||
|
<div key={index} className="mb-4">
|
||||||
|
{parse(line, {
|
||||||
|
replace: (domNode: DOMNode) => {
|
||||||
|
if (domNode instanceof Element && domNode.tagName === "glossarypopup") {
|
||||||
|
return (
|
||||||
|
<GlossaryPopup
|
||||||
|
term={domNode.attribs.term}
|
||||||
|
definition={domNode.attribs.definition}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return domNode;
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChapterRenderer;
|
46
src/components/GlossaryPopup.tsx
Normal file
46
src/components/GlossaryPopup.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client"
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
interface GlossaryPopupProps {
|
||||||
|
term: string;
|
||||||
|
definition: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GlossaryPopup: React.FC<GlossaryPopupProps> = ({ term, definition }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const popupRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
|
||||||
|
return() => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<span className="relative">
|
||||||
|
{term}
|
||||||
|
<sup
|
||||||
|
className="text-blue-600 cursor-pointer"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
[-]
|
||||||
|
</sup>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className="absolute left-0 top-full mt-1 w-64 bg-white border border-gray-300 rounded shadow-lg p-1 z-50"
|
||||||
|
>
|
||||||
|
<p className="text-gray-900 font-bold px-2 m-2">{term}</p>
|
||||||
|
<p className="text-gray-700 text-sm px-2 m-2">{definition}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlossaryPopup;
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import { addDays, subDays } from "date-fns";
|
import { addDays, subDays } from "date-fns";
|
||||||
import { Book, Chapter, Editor, Announcement } from "./types";
|
import { Book, Chapter, Editor, Announcement, Glossary } from "./types";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL as string;
|
const API_URL = process.env.NEXT_PUBLIC_API_URL as string;
|
||||||
const API_TOKEN = process.env.STRAPI_API_TOKEN as string;
|
const API_TOKEN = process.env.STRAPI_API_TOKEN as string;
|
||||||
@ -19,26 +19,26 @@ export async function fetchFromAPI<T>(
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let results: T[] = [];
|
let results: T[] = [];
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
try {
|
try {
|
||||||
do{
|
do {
|
||||||
const url = `${API_URL}${endpoint}&pagination[page]=${currentPage}&pagination[pageSize]=25`;
|
const url = `${API_URL}${endpoint}&pagination[page]=${currentPage}&pagination[pageSize]=25`;
|
||||||
const response = await fetch(url, {...config, next: {revalidate:30}});
|
const response = await fetch(url, { ...config, next: { revalidate: 30 } });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
console.error(`Error fetching ${url}:`, errorData);
|
console.error(`Error fetching ${url}:`, errorData);
|
||||||
throw new Error(errorData.message || `API fetch error (status: ${response.status})`);
|
throw new Error(errorData.message || `API fetch error (status: ${response.status})`);
|
||||||
}
|
}
|
||||||
const responseJson = await response.json();
|
const responseJson = await response.json();
|
||||||
results = results.concat(responseJson.data);
|
results = results.concat(responseJson.data);
|
||||||
totalPages = responseJson.meta?.pagination?.pageCount;
|
totalPages = responseJson.meta?.pagination?.pageCount;
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
}while(currentPage <= totalPages)
|
} while (currentPage <= totalPages)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fetch error:", error);
|
console.error("Fetch error:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -81,9 +81,13 @@ export async function fetchBookChapterLinks(bookId: string): Promise<Book> {
|
|||||||
*/
|
*/
|
||||||
export async function fetchBookById(bookId: string): Promise<Book> {
|
export async function fetchBookById(bookId: string): Promise<Book> {
|
||||||
const currentDateTime = new Date().toISOString();
|
const currentDateTime = new Date().toISOString();
|
||||||
const data = await fetchFromAPI<Book>(
|
//[chapters][filters][release_datetime][$lte]=${currentDateTime}
|
||||||
`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}&populate=cover`
|
const data = await fetchFromAPI<Book>(
|
||||||
);
|
`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}&populate=cover`
|
||||||
|
);
|
||||||
|
//I do not know why the hell it refuse to populate glossary only 1 field is allow to be populated after ????????
|
||||||
|
const glossary = await fetchGlossaryByBookId(bookId);
|
||||||
|
data[0].glossary = glossary;
|
||||||
data[0].chapters = data[0].chapters.sort((a, b) => a.number - b.number);
|
data[0].chapters = data[0].chapters.sort((a, b) => a.number - b.number);
|
||||||
return data[0];
|
return data[0];
|
||||||
}
|
}
|
||||||
@ -93,11 +97,9 @@ export async function fetchBookById(bookId: string): Promise<Book> {
|
|||||||
*/
|
*/
|
||||||
export async function fetchChapterByBookId(bookId: string, chapterId: string): Promise<Chapter[]> {
|
export async function fetchChapterByBookId(bookId: string, chapterId: string): Promise<Chapter[]> {
|
||||||
const currentChapter = await fetchFromAPI<Chapter>(`/api/chapters/${chapterId}?populate[book][fields][0]=title&filters[book][documentId]=${bookId}`);
|
const currentChapter = await fetchFromAPI<Chapter>(`/api/chapters/${chapterId}?populate[book][fields][0]=title&filters[book][documentId]=${bookId}`);
|
||||||
const bookWithAllChapters = await fetchFromAPI<Book>( `/api/books/${bookId}?populate[chapters][filters][number][$gte]=${
|
const bookWithAllChapters = await fetchFromAPI<Book>(`/api/books/${bookId}?populate[chapters][filters][number][$gte]=${currentChapter[0].number - 1
|
||||||
currentChapter[0].number - 1
|
}&populate[chapters][filters][number][$lte]=${currentChapter[0].number + 1
|
||||||
}&populate[chapters][filters][number][$lte]=${
|
}`);
|
||||||
currentChapter[0].number + 1
|
|
||||||
}`);
|
|
||||||
//const nextChapter = await fetchFromAPI<Chapter>(`/api/chapters?populate[book]&filters[book][id]=${bookId}&sort[number]=asc`);
|
//const nextChapter = await fetchFromAPI<Chapter>(`/api/chapters?populate[book]&filters[book][id]=${bookId}&sort[number]=asc`);
|
||||||
return bookWithAllChapters[0].chapters;
|
return bookWithAllChapters[0].chapters;
|
||||||
}
|
}
|
||||||
@ -110,20 +112,25 @@ export async function fetchEditors(): Promise<Editor[]> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChapterRelease = {current_chapters:Chapter[],future_chapters:Chapter[]}
|
export type ChapterRelease = { current_chapters: Chapter[], future_chapters: Chapter[] }
|
||||||
export async function fetchReleases(): Promise<{current_chapters:Chapter[],future_chapters:Chapter[]}> {
|
export async function fetchReleases(): Promise<{ current_chapters: Chapter[], future_chapters: Chapter[] }> {
|
||||||
const current_datetime = new Date()
|
const current_datetime = new Date()
|
||||||
const previous_week = subDays(current_datetime, 3);
|
const previous_week = subDays(current_datetime, 3);
|
||||||
const next_week = addDays(current_datetime, 3);
|
const next_week = addDays(current_datetime, 3);
|
||||||
|
|
||||||
const data = await fetchFromAPI<Chapter>(`/api/chapters/?populate[book][fields][0]=title&fields[0]=number&fields[1]=title&fields[2]=release_datetime&filters[release_datetime][$gte]=${previous_week.toISOString()}&filters[release_datetime][$lte]=${next_week.toISOString()}`);
|
const data = await fetchFromAPI<Chapter>(`/api/chapters/?populate[book][fields][0]=title&fields[0]=number&fields[1]=title&fields[2]=release_datetime&filters[release_datetime][$gte]=${previous_week.toISOString()}&filters[release_datetime][$lte]=${next_week.toISOString()}`);
|
||||||
const chapters: Chapter[] = data;
|
const chapters: Chapter[] = data;
|
||||||
const future_chapters = chapters.filter(chapter => new Date(chapter.release_datetime) > new Date());
|
const future_chapters = chapters.filter(chapter => new Date(chapter.release_datetime) > new Date());
|
||||||
const current_chapters = chapters.filter(chapter => new Date(chapter.release_datetime) <= new Date());
|
const current_chapters = chapters.filter(chapter => new Date(chapter.release_datetime) <= new Date());
|
||||||
return {current_chapters,future_chapters}
|
return { current_chapters, future_chapters }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAnnouncementById(announcementId: string): Promise<Announcement> {
|
export async function fetchAnnouncementById(announcementId: string): Promise<Announcement> {
|
||||||
const data = await fetchFromAPI<Announcement>(`/api/announcements/${announcementId}?`);
|
const data = await fetchFromAPI<Announcement>(`/api/announcements/${announcementId}?`);
|
||||||
return data[0];
|
return data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGlossaryByBookId(bookId: string): Promise<Glossary> {
|
||||||
|
const data = await fetchFromAPI<Glossary>(`/api/glossaries?filters[book][documentId]=${bookId}`);
|
||||||
|
return data[0];
|
||||||
}
|
}
|
@ -61,6 +61,10 @@ export interface Book {
|
|||||||
release_datetime: string;
|
release_datetime: string;
|
||||||
chapters: Chapter[];
|
chapters: Chapter[];
|
||||||
glossary: Glossary;
|
glossary: Glossary;
|
||||||
|
translator_note: string;
|
||||||
|
rating: number;
|
||||||
|
views: number;
|
||||||
|
readers: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Announcement {
|
export interface Announcement {
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { remark } from 'remark';
|
||||||
|
import html from 'remark-html';
|
||||||
|
import sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
export function formatDateToMonthDayYear(date: Date): string {
|
export function formatDateToMonthDayYear(date: Date): string {
|
||||||
return date.toLocaleDateString("en-US", {
|
return date.toLocaleDateString("en-US", {
|
||||||
month: "long",
|
month: "long",
|
||||||
@ -7,4 +11,21 @@ export function formatDateToMonthDayYear(date: Date): string {
|
|||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
timeZoneName: "short",
|
timeZoneName: "short",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markdownToHtml(markdown: string): Promise<string> {
|
||||||
|
|
||||||
|
const result = await remark().use(html).process(markdown)
|
||||||
|
const sanitizedHtml = sanitizeHtml(result.toString(), {
|
||||||
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre']),
|
||||||
|
allowedAttributes: {
|
||||||
|
...sanitizeHtml.defaults.allowedAttributes,
|
||||||
|
img: ['src', 'alt'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if(sanitizedHtml == ""){
|
||||||
|
//Already html
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
return sanitizedHtml
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user