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", | ||||
|     "axios": "^1.7.9", | ||||
|     "date-fns": "^4.1.0", | ||||
|     "html-react-parser": "^5.2.2", | ||||
|     "next": "15.1.4", | ||||
|     "react": "^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": { | ||||
|     "@eslint/eslintrc": "^3", | ||||
| @@ -27,6 +31,7 @@ | ||||
|     "@types/react": "^19", | ||||
|     "@types/react-dom": "^19", | ||||
|     "@types/rss": "^0.0.32", | ||||
|     "@types/sanitize-html": "^2.13.0", | ||||
|     "eslint": "^9", | ||||
|     "eslint-config-next": "15.1.4", | ||||
|     "postcss": "^8", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { fetchAnnouncementById } from "@/lib/api"; | ||||
| import { Announcement } from "@/lib/types"; | ||||
| import { formatDateToMonthDayYear } from "@/lib/utils"; | ||||
| import { formatDateToMonthDayYear, markdownToHtml } from "@/lib/utils"; | ||||
|  | ||||
| export const metadata = { | ||||
|     title: 'Announcement Detail Page', | ||||
| @@ -14,6 +14,7 @@ export default async function AnnouncementDetailPage(props: {params: paramsType} | ||||
|     let announcement: Announcement; | ||||
|     try{ | ||||
|         announcement = await fetchAnnouncementById(announcementId); | ||||
|         announcement.content = await markdownToHtml(announcement.content); | ||||
|     } | ||||
|     catch (error) { | ||||
|         console.error(error); | ||||
|   | ||||
| @@ -1,41 +1,68 @@ | ||||
| import { formatDateToMonthDayYear } from "@/lib/utils"; | ||||
| import { Announcement } from "@/lib/types"; | ||||
| import { fetchAnnouncements } from "@/lib/api"; | ||||
| import { markdownToHtml } from "@/lib/utils"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
|  | ||||
| export const metadata = { | ||||
|     title: 'Annoucement page', | ||||
|     description: 'NullTranslationGroup Announcement page', | ||||
|     title: "Announcement Page", | ||||
|     description: "NullTranslationGroup Announcement Page", | ||||
| }; | ||||
|  | ||||
| export default async function AnnouncementPage() { | ||||
|     let announcements = []; | ||||
|     let announcements: Announcement[] = []; | ||||
|     try { | ||||
|         announcements = await fetchAnnouncements(); | ||||
|         for (const announcement of announcements) { | ||||
|             announcement.content = await markdownToHtml(announcement.content); | ||||
|           } | ||||
|     } catch (error) { | ||||
|         console.error(error); | ||||
|         return ( | ||||
|             <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> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     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 ( | ||||
|             <div className="max-w-6xl mx-auto py-10 px-4">                     | ||||
|                 {sorted_announcements.map((announcement) => ( | ||||
|                     <Link href = {`/announcements/${announcement.documentId}`} key={announcement.documentId}> | ||||
|                         <li key={announcement.id} className="mb-2 list-none"> | ||||
|                             <div className="text-lg font-semibold">{announcement.title}</div> | ||||
|                             <div className="text-sm text-gray-500">{formatDateToMonthDayYear(new Date(announcement.datetime))}</div> | ||||
|                         </li> | ||||
|                     </Link> | ||||
|                 ))} | ||||
|             </div> | ||||
|         ); | ||||
|              | ||||
| } | ||||
|         <div className="max-w-6xl mx-auto py-10 px-4"> | ||||
|             <h1 className="text-3xl font-bold text-center mb-8">Announcements</h1> | ||||
|             {sorted_announcements.length === 0 ? ( | ||||
|                 <div className="text-center text-gray-500"> | ||||
|                     <p>No announcements available.</p> | ||||
|                 </div> | ||||
|             ) : ( | ||||
|                 <div className="space-y-6"> | ||||
|                     {sorted_announcements.map((announcement) => { | ||||
|                         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 NavigationButtons from "@/components/NavigationButtons"; | ||||
| import ChapterRenderer from "@/components/ChapterContentRenderer"; | ||||
| 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 const metadata = { | ||||
| @@ -26,16 +28,17 @@ export default async function ChapterPage(props: { params: paramsType}) { | ||||
|                 </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 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 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){ | ||||
|         return (     | ||||
|                 <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={""} /> | ||||
|                 </div> | ||||
|         ) | ||||
| @@ -44,8 +47,8 @@ export default async function ChapterPage(props: { params: paramsType}) { | ||||
|     return ( | ||||
|             <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} /> | ||||
|                 <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}/> | ||||
|             </div> | ||||
|     ); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { fetchBookById } from "@/lib/api"; | ||||
| import { Book, Chapter } from "@/lib/types"; | ||||
| import { formatDateToMonthDayYear } from "@/lib/utils"; | ||||
| import { formatDateToMonthDayYear, markdownToHtml } from "@/lib/utils"; | ||||
| import ChapterDropdown from "@/components/ChapterDropdown"; | ||||
| import { Ad } from "@/lib/types"; | ||||
|  | ||||
| @@ -8,7 +8,7 @@ export type paramsType = Promise<{ bookId: string}>; | ||||
|  | ||||
| export const metadata = { | ||||
|     title: 'Null Translation Group', | ||||
|     description: 'Null Translatin Group book', | ||||
|     description: 'Null Translation Group book description page', | ||||
| }; | ||||
|  | ||||
| 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 cover_media = cover?.at(0); | ||||
| @@ -45,7 +47,7 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                             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" | ||||
|                         > | ||||
|                             Join Our Patreon for Unreleased Chapters | ||||
|                             Join Our Patreon to read ahead! | ||||
|                         </a> | ||||
|                 </div> | ||||
|  | ||||
| @@ -54,8 +56,8 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                     <br></br> | ||||
|                     <strong>Translator:</strong> Null Translation Group | ||||
|                 </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> | ||||
|                 <ul className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> | ||||
|                     {recentChapters.map((chapter) => ( | ||||
| @@ -93,6 +95,18 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                         </a> | ||||
|                     </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> | ||||
|         ); | ||||
|              | ||||
|   | ||||
							
								
								
									
										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 { 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_TOKEN = process.env.STRAPI_API_TOKEN as string; | ||||
| @@ -19,26 +19,26 @@ export async function fetchFromAPI<T>( | ||||
|     ...options, | ||||
|   }; | ||||
|  | ||||
|    | ||||
|  | ||||
|  | ||||
|   let results: T[] = []; | ||||
|   let currentPage = 1; | ||||
|   let totalPages = 1; | ||||
|   try { | ||||
|     do{ | ||||
|     do { | ||||
|       const url = `${API_URL}${endpoint}&pagination[page]=${currentPage}&pagination[pageSize]=25`; | ||||
|       const response = await fetch(url, {...config, next: {revalidate:30}}); | ||||
|     if (!response.ok) { | ||||
|       const errorData = await response.json(); | ||||
|       console.error(`Error fetching ${url}:`, errorData); | ||||
|       throw new Error(errorData.message || `API fetch error (status: ${response.status})`); | ||||
|     } | ||||
|     const responseJson = await response.json(); | ||||
|     results = results.concat(responseJson.data); | ||||
|     totalPages = responseJson.meta?.pagination?.pageCount; | ||||
|     currentPage += 1; | ||||
|     }while(currentPage <= totalPages) | ||||
|      | ||||
|       const response = await fetch(url, { ...config, next: { revalidate: 30 } }); | ||||
|       if (!response.ok) { | ||||
|         const errorData = await response.json(); | ||||
|         console.error(`Error fetching ${url}:`, errorData); | ||||
|         throw new Error(errorData.message || `API fetch error (status: ${response.status})`); | ||||
|       } | ||||
|       const responseJson = await response.json(); | ||||
|       results = results.concat(responseJson.data); | ||||
|       totalPages = responseJson.meta?.pagination?.pageCount; | ||||
|       currentPage += 1; | ||||
|     } while (currentPage <= totalPages) | ||||
|  | ||||
|   } catch (error) { | ||||
|     console.error("Fetch error:", error); | ||||
|     throw error; | ||||
| @@ -81,9 +81,13 @@ export async function fetchBookChapterLinks(bookId: string): Promise<Book> { | ||||
|  */ | ||||
| export async function fetchBookById(bookId: string): Promise<Book> { | ||||
|   const currentDateTime = new Date().toISOString(); | ||||
|     const data = await fetchFromAPI<Book>( | ||||
|       `/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}&populate=cover` | ||||
|     ); | ||||
|   //[chapters][filters][release_datetime][$lte]=${currentDateTime} | ||||
|   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); | ||||
|   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[]> { | ||||
|   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]=${ | ||||
|     currentChapter[0].number - 1 | ||||
|   }&populate[chapters][filters][number][$lte]=${ | ||||
|     currentChapter[0].number + 1 | ||||
|   }`); | ||||
|   const bookWithAllChapters = await fetchFromAPI<Book>(`/api/books/${bookId}?populate[chapters][filters][number][$gte]=${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`); | ||||
|   return bookWithAllChapters[0].chapters; | ||||
| } | ||||
| @@ -110,20 +112,25 @@ export async function fetchEditors(): Promise<Editor[]> { | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| export type ChapterRelease = {current_chapters:Chapter[],future_chapters:Chapter[]} | ||||
| export async function fetchReleases(): Promise<{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[] }> { | ||||
|   const current_datetime = new Date() | ||||
|   const previous_week = subDays(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 chapters: Chapter[] = data; | ||||
|   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()); | ||||
|   return {current_chapters,future_chapters} | ||||
|   return { current_chapters, future_chapters } | ||||
| } | ||||
|  | ||||
| export async function fetchAnnouncementById(announcementId: string): Promise<Announcement> { | ||||
|   const data = await fetchFromAPI<Announcement>(`/api/announcements/${announcementId}?`); | ||||
|   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; | ||||
|     chapters: Chapter[]; | ||||
|     glossary: Glossary; | ||||
|     translator_note: string; | ||||
|     rating: number; | ||||
|     views: number; | ||||
|     readers: number; | ||||
|   } | ||||
|  | ||||
| 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 { | ||||
|   return date.toLocaleDateString("en-US", { | ||||
|     month: "long", | ||||
| @@ -7,4 +11,21 @@ export function formatDateToMonthDayYear(date: Date): string { | ||||
|     minute: "numeric", | ||||
|     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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user