Compare commits
	
		
			10 Commits
		
	
	
		
			406963f6bf
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9f967bf9d7 | |||
| b1c3d9ef84 | |||
| 5082135d62 | |||
| 01dc9c9749 | |||
| b7f7fd1989 | |||
| 906ad8a7a1 | |||
| 9b186a462e | |||
| 4cbd28b396 | |||
| 96ea6c446c | |||
| ffe356dd2a | 
							
								
								
									
										10
									
								
								global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								global.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,5 @@ | ||||
| import { link } from "fs"; | ||||
|  | ||||
| declare namespace NodeJS { | ||||
|     interface ProcessEnv { | ||||
|       NEXT_PUBLIC_API_URL: string; | ||||
| @@ -8,6 +10,14 @@ declare namespace NodeJS { | ||||
|     interface Window { | ||||
|       kofiWidgetOverlay: any; | ||||
|       dataLayer: any[]; | ||||
|       googletag: { | ||||
|         cmd: any[]; | ||||
|         pubads: () => { | ||||
|           refresh: () => void; | ||||
|           enableSingleRequest: () => void; | ||||
|         }; | ||||
|         enableServices: () => void; | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| export {}; | ||||
							
								
								
									
										1
									
								
								public/ads.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/ads.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| google.com, pub-1843060382170565, DIRECT, f08c47fec0942fa0 | ||||
| @@ -16,8 +16,8 @@ export async function GET(){ | ||||
|         feed_url: `${BASE_URL}/api/feed`, | ||||
|         language: "en", | ||||
|     }); | ||||
|      | ||||
|     data.forEach((chapter: Chapter) => { | ||||
|     const data_sorted = data.sort((a: Chapter, b: Chapter) => { return new Date(a.release_datetime).getTime() - new Date(b.release_datetime).getTime(); }); | ||||
|     data_sorted.forEach((chapter: Chapter) => { | ||||
|         feed.item({ | ||||
|             title: chapter.book?.title + ": c" + chapter.number, | ||||
|             description: "Daily chapter release for " + chapter.book?.title, | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { fetchBookById } from "@/lib/api"; | ||||
| import { Book, Chapter } from "@/lib/types"; | ||||
| import { formatDateToMonthDayYear, markdownToHtml } from "@/lib/utils"; | ||||
| import { encodeId, formatDateToMonthDayYear, markdownToHtml } from "@/lib/utils"; | ||||
| import ChapterDropdown from "@/components/ChapterDropdown"; | ||||
| import { Ad } from "@/lib/types"; | ||||
| import { Countdown } from "@/components/Countdown"; | ||||
|  | ||||
| export type paramsType = Promise<{ bookId: string}>; | ||||
| export type paramsType = Promise<{ bookId: string }>; | ||||
|  | ||||
| export const metadata = { | ||||
|     title: 'Null Translation Group', | ||||
| @@ -25,14 +26,15 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     const {title, author, description, chapters, cover, translator_note, glossary} = 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 current_chapters = sorted_chapters.filter(chapter => new Date(chapter.release_datetime) < new Date()); | ||||
|     const next_chapter = sorted_chapters.find((chapter) => new Date(chapter.release_datetime) > new Date()); | ||||
|     const next_chapters = sorted_chapters.filter(chapter => new Date(chapter.release_datetime) > new Date()) | ||||
|     const cover_media = cover?.at(0); | ||||
|     const recentChapters = sorted_chapters.length > 6 ? sorted_chapters.slice(sorted_chapters.length - 6, sorted_chapters.length) : sorted_chapters; | ||||
|  | ||||
|     const recentChapters = sorted_chapters.length > 8 ? sorted_chapters.slice(sorted_chapters.length - 8, sorted_chapters.length - 2) : current_chapters; | ||||
|     return ( | ||||
|         <div className="max-w-6xl mx-auto py-10 px-4"> | ||||
|             <div className="flex flex-col items-center justify-center"> | ||||
| @@ -41,6 +43,7 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                     className="rounded-lg object-cover w-64 h-96" | ||||
|                 /> | ||||
|                 <h1 className="text-5xl pb-2 font-bold">{title}</h1> | ||||
|                 <h3 className="text-2xl text-green-600 dark:text-green-400 font-bold pb-2"> {book.release_rate} chapters/day</h3> | ||||
|                 <a | ||||
|                     href={Ad.patreon} | ||||
|                     target="_blank" | ||||
| @@ -57,7 +60,7 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                 <strong>Translator:</strong> Null Translation Group | ||||
|             </p> | ||||
|             <p className="mb-6">Description: {description}</p> | ||||
|                 <div className="mb-6" dangerouslySetInnerHTML={{__html: translator_note_html}} /> | ||||
|             <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) => ( | ||||
| @@ -77,10 +80,64 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                 ))} | ||||
|             </ul> | ||||
|             <div className="flex items-center justify-between mb-4 pt-4"> | ||||
|                     <h2 className="text-3xl font-semibold">All Chapters</h2> | ||||
|                     <ChapterDropdown chapters={sorted_chapters} bookId={bookId} /> | ||||
|                 <h2 className="text-3xl font-semibold">Next Chapters</h2> | ||||
|             </div> | ||||
|                 {sorted_chapters.map((chapter) => ( | ||||
|             {next_chapters.map((chapter) => | ||||
|                 ( | ||||
|                     <li key={chapter.id+"next_chapters"} className="mb-2 list-none relative"> | ||||
|                     <a | ||||
|                         href={`/books/${bookId}/chapters/${chapter.documentId}`} | ||||
|                         className="block bg-white dark:bg-gray-800 rounded-lg shadow p-4 hover:shadow-lg transition duration-200 relative opacity-50 pointer-events-none" | ||||
|                     > | ||||
|                         <h3 className="text-xl font-medium"> | ||||
|                             Chapter {chapter.number}: {chapter.title} | ||||
|                         </h3> | ||||
|                         <p className="text-sm text-gray-500 dark:text-gray-400 mt-2"> | ||||
|                             <strong>Release Date:</strong>{" "} | ||||
|                             {formatDateToMonthDayYear(new Date(chapter.release_datetime))} | ||||
|                         </p> | ||||
|                     </a> | ||||
|  | ||||
|                     {/* Overlay */} | ||||
|                     <div className="absolute inset-0 bg-gray-400 opacity-50 rounded-lg"></div> | ||||
|                     <div className="absolute inset-0 flex items-center justify-center"> | ||||
|                         <p className="text-xl font-bold text-red-500 bg-gray-800 bg-opacity-80 px-4 py-2 rounded-lg shadow-lg"> | ||||
|                            <Countdown release_datetime={chapter.release_datetime} /> | ||||
|                         </p> | ||||
|                         {chapter.number === next_chapter?.number ? ( | ||||
|                             <a | ||||
|                                 href={'/early?bookId=' + encodeId(bookId)} | ||||
|                                 className={` | ||||
|                                 bg-red-500 text-white py-2 px-4 mx-2 rounded  | ||||
|                                 hover:bg-red-600  | ||||
|                                 transition duration-200 | ||||
|                                 `} | ||||
|                             > | ||||
|                                 Unlock Early AdWalled Chapter | ||||
|                             </a> | ||||
|                             ) | ||||
|                          : ( | ||||
|                             <a | ||||
|                                 href={Ad.patreon} | ||||
|                                 className={` | ||||
|                                 bg-yellow-500 text-white py-2 px-4 mx-2 rounded  | ||||
|                                 hover:bg-yellow-600  | ||||
|                                 transition duration-200 | ||||
|                                 `} | ||||
|                             > | ||||
|                                 Join Patreon for More Early Chapters | ||||
|                             </a> | ||||
|                         )} | ||||
|                     </div> | ||||
|                 </li> | ||||
|                 ) | ||||
|             )} | ||||
|             <div className="flex items-center justify-between mb-4 pt-4"> | ||||
|                 <h2 className="text-3xl font-semibold">All Chapters</h2> | ||||
|                 <ChapterDropdown chapters={current_chapters} bookId={bookId} /> | ||||
|             </div> | ||||
|             {sorted_chapters.map((chapter) => | ||||
|                 new Date(chapter.release_datetime) < new Date() ? ( | ||||
|                     <li key={chapter.id} className="mb-2 list-none"> | ||||
|                         <a | ||||
|                             href={`/books/${bookId}/chapters/${chapter.documentId}`} | ||||
| @@ -90,11 +147,58 @@ export default async function BookPage(props: { params: paramsType }) { | ||||
|                                 Chapter {chapter.number}: {chapter.title} | ||||
|                             </h3> | ||||
|                             <p className="text-sm text-gray-500 dark:text-gray-400 mt-2"> | ||||
|                                 <strong>Release Date:</strong> {formatDateToMonthDayYear(new Date(chapter.release_datetime))} | ||||
|                                 <strong>Release Date:</strong>{" "} | ||||
|                                 {formatDateToMonthDayYear(new Date(chapter.release_datetime))} | ||||
|                             </p> | ||||
|                         </a> | ||||
|                     </li> | ||||
|                 ))} | ||||
|                 ) : <li key={chapter.id} className="mb-2 list-none relative"> | ||||
|                     <a | ||||
|                         href={`/books/${bookId}/chapters/${chapter.documentId}`} | ||||
|                         className="block bg-white dark:bg-gray-800 rounded-lg shadow p-4 hover:shadow-lg transition duration-200 relative opacity-50 pointer-events-none" | ||||
|                     > | ||||
|                         <h3 className="text-xl font-medium"> | ||||
|                             Chapter {chapter.number}: {chapter.title} | ||||
|                         </h3> | ||||
|                         <p className="text-sm text-gray-500 dark:text-gray-400 mt-2"> | ||||
|                             <strong>Release Date:</strong>{" "} | ||||
|                             {formatDateToMonthDayYear(new Date(chapter.release_datetime))} | ||||
|                         </p> | ||||
|                     </a> | ||||
|  | ||||
|                     {/* Overlay */} | ||||
|                     <div className="absolute inset-0 bg-gray-400 opacity-50 rounded-lg"></div> | ||||
|                     <div className="absolute inset-0 flex items-center justify-center"> | ||||
|                         <p className="text-xl font-bold text-red-500 bg-gray-800 bg-opacity-80 px-4 py-2 rounded-lg shadow-lg"> | ||||
|                            <Countdown release_datetime={chapter.release_datetime} /> | ||||
|                         </p> | ||||
|                         {chapter.number === next_chapter?.number ? ( | ||||
|                             <a | ||||
|                                 href={'/early?bookId=' + encodeId(bookId)} | ||||
|                                 className={` | ||||
|                                 bg-red-500 text-white py-2 px-4 mx-2 rounded  | ||||
|                                 hover:bg-red-600  | ||||
|                                 transition duration-200 | ||||
|                                 `} | ||||
|                             > | ||||
|                                 Unlock Early Ad-Walled Chapter | ||||
|                             </a> | ||||
|                             ) | ||||
|                          : ( | ||||
|                             <a | ||||
|                                 href={Ad.patreon} | ||||
|                                 className={` | ||||
|                                 bg-yellow-500 text-white py-2 px-4 mx-2 rounded  | ||||
|                                 hover:bg-yellow-600  | ||||
|                                 transition duration-200 | ||||
|                                 `} | ||||
|                             > | ||||
|                                 Join Patreon for More Early Chapters | ||||
|                             </a> | ||||
|                         )} | ||||
|                     </div> | ||||
|                 </li> | ||||
|             )} | ||||
|             {glossary && ( | ||||
|                 <div className="mt-8"> | ||||
|                     <h2 className="text-3xl font-semibold">Glossary</h2> | ||||
|   | ||||
							
								
								
									
										58
									
								
								src/app/early/[bookId]/[chapterId]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/app/early/[bookId]/[chapterId]/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import React from "react"; | ||||
| import NavigationButtons from "@/components/NavigationButtons"; | ||||
| import ReportButton from "@/components/ReportButton"; | ||||
| import ChapterRenderer from "@/components/ChapterContentRenderer"; | ||||
| import { Chapter } from "@/lib/types"; | ||||
| import { fetchChapterByBookId, fetchGlossaryByBookId } from "@/lib/api"; | ||||
| import { decodeId, markdownToHtml } from "@/lib/utils"; | ||||
| export type paramsType = Promise<{ bookId: string; chapterId: string }>; | ||||
|  | ||||
| export const metadata = { | ||||
|     title: 'Null Translation Group', | ||||
|     description: 'This is the chapter page default description', | ||||
| }; | ||||
|  | ||||
| // Dynamic page component | ||||
| export default async function ChapterPage(props: { params: paramsType}) { | ||||
|     let { bookId, chapterId } = await props.params; | ||||
|     bookId = decodeId(bookId); | ||||
|     chapterId = decodeId(chapterId); | ||||
|     let chapters: Chapter[]; | ||||
|     try{ | ||||
|         chapters = await fetchChapterByBookId(bookId, chapterId); | ||||
|     } | ||||
|     catch (error) { | ||||
|         console.error(error); | ||||
|         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> | ||||
|                 <NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={""} nextChapter={""} /> | ||||
|                 </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: '<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> | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     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"></div> | ||||
|                 <ChapterRenderer content={chapter_content_html} glossary={english_glossary} /> | ||||
|                 <NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/> | ||||
|                 <ReportButton bookId={bookId} chapterId={chapterId} /> | ||||
|             </div> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										112
									
								
								src/app/early/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/app/early/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| import { fetchEarlyRelease } from "@/lib/api"; | ||||
| import { Chapter } from "@/lib/types"; | ||||
| import { encodeId, linkvertise } from "@/lib/utils"; | ||||
| import { Ad } from "@/lib/types"; | ||||
| export type searchParams = Promise<{ bookId: string }>; | ||||
|  | ||||
| export const metadata = { | ||||
|     title: 'Null Translation Group', | ||||
|     description: 'Null Translation Group Early Adcess Page', | ||||
| }; | ||||
|  | ||||
| export default async function BookPage({ searchParams }: { searchParams: searchParams }) { | ||||
|     const bookId = (await searchParams).bookId; | ||||
|     let early_chapters: Chapter[] = [] | ||||
|     try { | ||||
|         early_chapters = await fetchEarlyRelease(); | ||||
|     } | ||||
|     catch { | ||||
|         console.error("Error fetching early chapters") | ||||
|         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> Error fetching early chapters</h1></center>' }}></div> | ||||
|             </div> | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     const by_book_title = early_chapters.reduce<{ [key: string]: Chapter[] }>((acc, chapter) => { | ||||
|         if (chapter.book === undefined) { | ||||
|             return acc; | ||||
|         } | ||||
|         else if (!acc[chapter.book.title!]) { | ||||
|             acc[chapter.book.title!] = []; | ||||
|         } | ||||
|         acc[chapter.book.title!].push(chapter); | ||||
|         return acc; | ||||
|     } | ||||
|         , {}); | ||||
|     for (const book of Object.keys(by_book_title)) { | ||||
|         by_book_title[book].sort((a, b) => a.number - b.number); | ||||
|         by_book_title[book] = by_book_title[book].slice(0, 1); | ||||
|     } | ||||
|     early_chapters = Array.from(Object.values(by_book_title)).flat() | ||||
|     early_chapters.sort((a, b) => a.book?.title.localeCompare(b.book?.title || "") || a.number - b.number); | ||||
|     const early_chapter_from_params = early_chapters.find((chapter) => encodeId(chapter.book?.documentId || "") === bookId) || null; | ||||
|     return ( | ||||
|         <div className="max-w-6xl mx-auto py-12 px-6"> | ||||
|             <div className="flex flex-col items-center text-center space-y-6"> | ||||
|                 <h1 className="text-4xl font-bold">Early Adcess Page</h1> | ||||
|                 <p className="text-lg text-gray-700 dark:text-gray-300"> | ||||
|                     Get early access to chapters by completing Linkvertise and Google Offerwall tasks. Read ahead by <span className="font-bold">1 chapter !</span> | ||||
|                 </p> | ||||
|                 <p className="text-lg text-gray-700 dark:text-gray-300"> | ||||
|                     Tasks vary from watching an ad video to completing surveys and more. | ||||
|                 </p> | ||||
|                 <p className="text-lg text-red-700 dark:text-red-300"> | ||||
|                     This is AdHell, and its meant to be that way. If you hate it support us on Patreon instead. | ||||
|                 </p> | ||||
|                 <p className="text-lg text-gray-700 dark:text-gray-300"> | ||||
|                     Want to read even further? Join our Patreon to unlock even more chapters! | ||||
|                 </p> | ||||
|  | ||||
|                 <a | ||||
|                     href={Ad.patreon} | ||||
|                     target="_blank" | ||||
|                     rel="noopener noreferrer" | ||||
|                     className="bg-yellow-500 text-white font-semibold py-3 px-6 rounded-lg shadow-md hover:bg-yellow-600 transition duration-300" | ||||
|                 > | ||||
|                     Join Our Patreon – Read More Ahead! | ||||
|                 </a> | ||||
|             </div> | ||||
|             {early_chapter_from_params && <div className="mt-10 mb-6 text-center"> | ||||
|                 <a | ||||
|                     href={linkvertise(1318261,`https://nulltranslation.com/early/${encodeId(early_chapter_from_params.book?.documentId || "")}/${encodeId(early_chapter_from_params.documentId)}`)} | ||||
|                     className="block bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition duration-300" | ||||
|                 > | ||||
|                     <h3 className="text-xl font-semibold text-gray-900 dark:text-white truncate"> | ||||
|                         {early_chapter_from_params.book?.title} | ||||
|                     </h3> | ||||
|                     <h4 className="text-lg font-medium text-gray-700 dark:text-gray-300 truncate"> | ||||
|                         Chapter {early_chapter_from_params.number}: {early_chapter_from_params.title} | ||||
|                     </h4> | ||||
|                     <p className="text-md text-red-600 dark:text-red-400 mt-3 font-semibold"> | ||||
|                         Click to Unlock Chapter | ||||
|                     </p> | ||||
|                 </a> | ||||
|             </div> | ||||
|             } | ||||
|             {/* Chapters Grid */} | ||||
|             <ul className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-10"> | ||||
|                 {early_chapters.map((chapter) => ( | ||||
|                     <li key={chapter.id}> | ||||
|                         <a | ||||
|                             href={linkvertise(1318261,`https://nulltranslation.com/early/${encodeId(chapter.book?.documentId || "")}/${encodeId(chapter.documentId)}`)} | ||||
|                             className="block bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition duration-300" | ||||
|                         > | ||||
|                             <h3 className="text-xl font-semibold text-gray-900 dark:text-white truncate"> | ||||
|                                 {chapter.book?.title} | ||||
|                             </h3> | ||||
|                             <h4 className="text-lg font-medium text-gray-700 dark:text-gray-300 truncate"> | ||||
|                                 Chapter {chapter.number}: {chapter.title} | ||||
|                             </h4> | ||||
|                             <p className="text-md text-red-600 dark:text-red-400 mt-3 font-semibold"> | ||||
|                                 Click to Unlock Chapter | ||||
|                             </p> | ||||
|                         </a> | ||||
|                     </li> | ||||
|                 ))} | ||||
|             </ul> | ||||
|         </div> | ||||
|     ); | ||||
|  | ||||
| } | ||||
| @@ -43,7 +43,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) | ||||
|       <span className="ml-3 text-lg">Loading content...</span> | ||||
|     </div> | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <html lang="en" className={isDarkMode ? "dark" : ""} suppressHydrationWarning> | ||||
|       <head> | ||||
| @@ -78,6 +77,17 @@ export default function RootLayout({ children }: { children: React.ReactNode }) | ||||
|           gtag('config', 'G-6SXB46RSDE'); | ||||
|         `} | ||||
|         </Script> | ||||
|         <Script | ||||
|           strategy="afterInteractive" | ||||
|           src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" | ||||
|           onLoad={() => { | ||||
|             window.googletag = window.googletag || { cmd: [] }; | ||||
|             window.googletag.cmd.push(() => { | ||||
|               window.googletag.pubads().enableSingleRequest(); | ||||
|               window.googletag.enableServices(); | ||||
|             }); | ||||
|           }} | ||||
|         /> | ||||
|       </head> | ||||
|       <body className="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen"> | ||||
|         <KofiWidget /> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { formatDateToMonthDayYear } from "@/lib/utils"; | ||||
| import { Chapter, Ad } from "@/lib/types"; | ||||
| import { fetchReleases } from "@/lib/api"; | ||||
| import Link from "next/link"; | ||||
| import { Countdown } from "@/components/Countdown"; | ||||
|  | ||||
|  | ||||
| export const metadata = { | ||||
| @@ -63,17 +63,17 @@ export default async function ReleasePage() { | ||||
|                             .sort(([titleA], [titleB]) => titleA.localeCompare(titleB)) | ||||
|                             .map(([bookTitle, chapters]) => ( | ||||
|                                 <div key={bookTitle} className="mb-6"> | ||||
|                                     <Link href={`/books/${chapters[0].book?.documentId}`} | ||||
|                                     <a href={`/books/${chapters[0].book?.documentId}`} | ||||
|                                         className="text-lg font-semibold text-gray-700 dark:text-yellow-300 mb-4 hover:underline"> | ||||
|                                         {bookTitle} | ||||
|                                     </Link> | ||||
|                                     </a> | ||||
|                                     <ul className="space-y-4"> | ||||
|                                         {chapters.map((chapter) => ( | ||||
|                                             <li | ||||
|                                                 key={chapter.id} | ||||
|                                                 className="p-4 bg-yellow-100 dark:bg-yellow-800 text-gray-800 dark:text-gray-100 rounded-lg shadow-sm border border-yellow-300 dark:border-yellow-700 transition-transform transform hover:scale-105" | ||||
|                                             > | ||||
|                                                 <Link href={`books/${chapter.book?.documentId}/chapters/${chapter.documentId}`} className="block"> | ||||
|                                                 <a href={`books/${chapter.book?.documentId}/chapters/${chapter.documentId}`} className="block"> | ||||
|  | ||||
|                                                     <h4 className="text-md font-medium"> | ||||
|                                                         Chapter {chapter.number}: {chapter.title} | ||||
| @@ -84,7 +84,7 @@ export default async function ReleasePage() { | ||||
|                                                             new Date(chapter.release_datetime) | ||||
|                                                         )} | ||||
|                                                     </p> | ||||
|                                                 </Link> | ||||
|                                                 </a> | ||||
|                                             </li> | ||||
|                                         ))} | ||||
|                                     </ul> | ||||
| @@ -107,10 +107,10 @@ export default async function ReleasePage() { | ||||
|                             .sort(([titleA], [titleB]) => titleA.localeCompare(titleB)) | ||||
|                             .map(([bookTitle, chapters]) => ( | ||||
|                                 <div key={bookTitle} className="mb-6"> | ||||
|                                     <Link href={`/books/${chapters[0].book?.documentId}`} | ||||
|                                     <a href={`/books/${chapters[0].book?.documentId}`} | ||||
|                                         className="text-lg font-semibold text-blue-700 dark:text-blue-300 mb-4 hover:underline"> | ||||
|                                         {bookTitle} | ||||
|                                     </Link> | ||||
|                                     </a> | ||||
|                                     <ul className="space-y-4"> | ||||
|                                         {chapters.map((chapter) => ( | ||||
|                                             <li | ||||
| @@ -120,12 +120,9 @@ export default async function ReleasePage() { | ||||
|                                                 <h4 className="text-md font-medium"> | ||||
|                                                     Chapter {chapter.number}: {chapter.title} | ||||
|                                                 </h4> | ||||
|                                                 <p className="text-sm"> | ||||
|                                                     Release date:{" "} | ||||
|                                                     {formatDateToMonthDayYear( | ||||
|                                                             new Date(chapter.release_datetime) | ||||
|                                                     )} | ||||
|                                                 </p> | ||||
|                                                 <h3 className="text-md font-bold text-red-600 dark:text-red-400"> | ||||
|                                                     <Countdown release_datetime={chapter.release_datetime} /> | ||||
|                                                 </h3> | ||||
|                                             </li> | ||||
|                                         ))} | ||||
|                                     </ul> | ||||
|   | ||||
							
								
								
									
										35
									
								
								src/components/Countdown.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/Countdown.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| "use client"; | ||||
|  | ||||
| import {useState, useEffect} from 'react'; | ||||
|  | ||||
| export function Countdown({release_datetime}: {release_datetime: string}) { | ||||
|     const calculateTimeLeft = () => { | ||||
|         const now = new Date().getTime(); | ||||
|         const releaseTime = new Date(release_datetime).getTime(); | ||||
|         const difference = releaseTime - now; | ||||
|      | ||||
|         if (difference <= 0) { | ||||
|           return "Released!"; | ||||
|         } | ||||
|  | ||||
|         const hours = Math.floor((difference / (1000 * 60 * 60)) % 24); | ||||
|         const minutes = Math.floor((difference / (1000 * 60)) % 60); | ||||
|         const seconds = Math.floor((difference / 1000) % 60); | ||||
|      | ||||
|         return `${hours}h ${minutes}m ${seconds}s`; | ||||
|     } | ||||
|      | ||||
|     const [timeLeft, setTimeLeft] = useState<string>(calculateTimeLeft()); | ||||
|  | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const timer = setTimeout(() => { | ||||
|             setTimeLeft(calculateTimeLeft()); | ||||
|         }, 1000); | ||||
|         return () => clearTimeout(timer); | ||||
|     }); | ||||
|  | ||||
|     return ( | ||||
|         <span>{timeLeft}</span> | ||||
|     ); | ||||
| } | ||||
| @@ -11,7 +11,7 @@ const KofiWidget = () => { | ||||
|       if (typeof window !== 'undefined' && window.kofiWidgetOverlay) { | ||||
|         window.kofiWidgetOverlay.draw('nulltranslationgroup', { | ||||
|           'type': 'floating-chat', | ||||
|           'floating-chat.donateButton.text': 'Support me', | ||||
|           'floating-chat.donateButton.text': '', | ||||
|           'floating-chat.donateButton.background-color': '#00b9fe', | ||||
|           'floating-chat.donateButton.text-color': '#fff', | ||||
|         }); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| "use client"; | ||||
| import React, { useState } from "react"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
| import { Ad } from "@/lib/types"; | ||||
| export default function Navbar() { | ||||
|   const [isMenuOpen, setIsMenuOpen] = useState(false); | ||||
| @@ -17,28 +16,29 @@ export default function Navbar() { | ||||
|             alt="Logo" | ||||
|             className="h-8 w-8" | ||||
|           /> | ||||
|           <Link href="/" className="text-2xl font-bold"> | ||||
|           <a href="https://nulltranslation.com" className="text-2xl font-bold"> | ||||
|             Null Translation Group | ||||
|           </Link> | ||||
|           </a> | ||||
|         </div> | ||||
|  | ||||
|         {/* Navigation Links */} | ||||
|  | ||||
|         <nav className="hidden md:flex space-x-6"> | ||||
|           <Link href={"/announcements"} className="hover:text-gray-400"> | ||||
|           <a href={"/announcements"} className="hover:text-gray-400"> | ||||
|             Announcements | ||||
|           </Link> | ||||
|           <Link href="/" className="hover:text-gray-400"> | ||||
|           </a> | ||||
|           <a href="https://nulltranslation.com" className="hover:text-gray-400"> | ||||
|             Books | ||||
|           </Link> | ||||
|           <Link href={"/releases"} className="hover:text-gray-400"> | ||||
|           </a> | ||||
|           <a href={"/releases"} className="hover:text-gray-400"> | ||||
|             Releases | ||||
|           </Link> | ||||
|           <Link href={Ad.patreon} className="hover:text-gray-400"> | ||||
|           </a> | ||||
|           <a href={"/early"} className="hover:text-gray-400"> | ||||
|             Early Access | ||||
|           </a> | ||||
|           <a href={Ad.patreon} className="hover:text-yellow-400 text-yellow-200 font-semibold"> | ||||
|             Patreon | ||||
|           </Link> | ||||
|  | ||||
|  | ||||
|           </a> | ||||
|         </nav> | ||||
|  | ||||
|         {/* Mobile Menu Button */} | ||||
| @@ -66,18 +66,21 @@ export default function Navbar() { | ||||
|       {/* Mobile Menu */} | ||||
|       {isMenuOpen && ( | ||||
|         <div className="md:hidden bg-gray-700 px-6 pb-4"> | ||||
|           <Link href={"/announcements"} className="block py-2 hover:text-gray-400"> | ||||
|           <a href={"/announcements"} className="block py-2 hover:text-gray-400"> | ||||
|             Announcements | ||||
|           </Link> | ||||
|           <Link href={"/releases"} className="block py-2 hover:text-gray-400"> | ||||
|           </a> | ||||
|           <a href={"/releases"} className="block py-2 hover:text-gray-400"> | ||||
|             Release | ||||
|           </Link> | ||||
|           <Link href="/" className="block py-2 hover:text-gray-400"> | ||||
|           </a> | ||||
|           <a href="https://nulltranslation.com" className="block py-2 hover:text-gray-400"> | ||||
|             Book List | ||||
|           </Link> | ||||
|           <Link href={Ad.patreon} className="block py-2 hover:text-gray-400"> | ||||
|           </a> | ||||
|           <a href={"/early"} className="hover:text-gray-400"> | ||||
|             Early Access | ||||
|           </a> | ||||
|           <a href={Ad.patreon} className="hover:text-yellow-400 text-yellow-200 font-semibold"> | ||||
|             Patreon | ||||
|           </Link> | ||||
|           </a> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   | ||||
| @@ -2,7 +2,8 @@ | ||||
|  | ||||
| import React from "react"; | ||||
| import { useRouter } from "next/navigation"; | ||||
| import { Ad } from "@/lib/types"; | ||||
| import Link from "next/link"; | ||||
| import { encodeId } from "@/lib/utils"; | ||||
|  | ||||
| interface NavigationButtonsProps { | ||||
|   bookId: string; | ||||
| @@ -21,7 +22,6 @@ const NavigationButtons: React.FC<NavigationButtonsProps> = ({ bookId, prevChapt | ||||
|     router.push(`/books/${bookId}`); | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   return ( | ||||
|     <div className="mt-2 flex justify-between"> | ||||
|       <button | ||||
| @@ -53,18 +53,16 @@ const NavigationButtons: React.FC<NavigationButtonsProps> = ({ bookId, prevChapt | ||||
|           Next Chapter | ||||
|         </button> | ||||
|       ) : ( | ||||
|         <a | ||||
|           href={Ad.patreon} | ||||
|           target="_blank" | ||||
|           rel="noopener noreferrer" | ||||
|         <Link | ||||
|           href={'/early?bookId=' + encodeId(bookId)} | ||||
|           className={` | ||||
|             bg-yellow-500 text-white py-2 px-4 rounded  | ||||
|             hover:bg-yellow-600  | ||||
|             transition duration-200 | ||||
|           `} | ||||
|         > | ||||
|           Unreleased Chapters | ||||
|         </a> | ||||
|           Unlock Early Chapter | ||||
|         </Link> | ||||
|       ) | ||||
|       } | ||||
|     </div> | ||||
|   | ||||
| @@ -13,10 +13,11 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => { | ||||
|     const [isOpen, setIsOpen] = useState(false) | ||||
|     const [errorType, setErrorType] = useState('') | ||||
|     const [details, setDetails] = useState('') | ||||
|     const [email, setEmail] = useState('') | ||||
|     const handleSubmitReport = async (event: React.FormEvent<HTMLFormElement>) => { | ||||
|         // Implement report submission here | ||||
|         event.preventDefault() | ||||
|         const response = await createReport(errorType,details,bookId,chapterId) | ||||
|         const response = await createReport(errorType,details,bookId,chapterId,email) | ||||
|         //Linting be linting | ||||
|         if (response.status === 201){ | ||||
|             alert('Report submitted successfully') | ||||
| @@ -26,11 +27,13 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => { | ||||
|         } | ||||
|         setErrorType('') | ||||
|         setDetails('')  | ||||
|         setEmail('') | ||||
|         setIsOpen(false) | ||||
|     } | ||||
|     const handleExit = (event: React.MouseEvent) => {         | ||||
|         setErrorType('') | ||||
|         setDetails('') | ||||
|         setEmail('') | ||||
|         if (modalRef.current && !modalRef.current.contains(event.target as Node)) { | ||||
|             setIsOpen(false) | ||||
|         } | ||||
| @@ -51,6 +54,19 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => { | ||||
|                     <div className="mt-4"> | ||||
|                         <form className="p-6 rounded-lg max-w-md mx-auto space-y-4" | ||||
|                             onSubmit={handleSubmitReport}> | ||||
|                             <div> | ||||
|                                 <label className="block text-md font-semibold mb-1" htmlFor="report_email"> | ||||
|                                     Email : | ||||
|                                 </label> | ||||
|                                 <input  | ||||
|                                 id="report_email"  | ||||
|                                 className="block w-full border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"  | ||||
|                                 type="email"  | ||||
|                                 placeholder="If you want credit/update" | ||||
|                                 value={email} | ||||
|                                 onChange={(e) => setEmail(e.target.value)}> | ||||
|                             </input> | ||||
|                             </div> | ||||
|                             <div> | ||||
|                                 <label className="block text-md font-semibold mb-1 dark:text-gray-300" htmlFor="error-type"> | ||||
|                                     Select Error Type : | ||||
| @@ -80,6 +96,8 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => { | ||||
|                                     onChange={(e) => setDetails(e.target.value)} | ||||
|                                     className="block w-full h-24 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none overflow-y-auto"/> | ||||
|                             </div> | ||||
|  | ||||
|                              | ||||
|                             <div className="flex justify-between"> | ||||
|                                 <button | ||||
|                                     type="submit" | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/hooks/useRefreshAds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/hooks/useRefreshAds.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| "use client" | ||||
|  | ||||
| import { useEffect } from 'react'; | ||||
| import { useRouter } from 'next/router'; | ||||
|  | ||||
| const useRefreshAds = () => { | ||||
|   const router = useRouter(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (typeof window !== 'undefined') { | ||||
|       console.log("Initializing Ad Refresh..."); | ||||
|       window.googletag = window.googletag || { cmd: [] }; | ||||
|  | ||||
|       router.events.on('routeChangeComplete', () => { | ||||
|         console.log("Refreshing Ads..."); | ||||
|         window.googletag.cmd.push(() => { | ||||
|           if (window.googletag?.pubads) { | ||||
|             window.googletag.pubads().refresh(); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       return () => { | ||||
|         router.events.off('routeChangeComplete', () => {}); | ||||
|       }; | ||||
|     } | ||||
|   }, [router]); | ||||
|  | ||||
|   return null; | ||||
| }; | ||||
|  | ||||
| export default useRefreshAds; | ||||
| @@ -80,12 +80,12 @@ export async function fetchFromAPI<T>( | ||||
|  * Populates optional fields like Chapters or Editors based on requirements. | ||||
|  */ | ||||
| export async function fetchBooks(): Promise<Book[]> { | ||||
|   const data = await fetchFromAPI<Book>("/api/books?populate=cover&sort[title]=asc"); | ||||
|   const data = await fetchFromAPI<Book>(`/api/books?populate=cover&sort[title]=asc&filters[release_datetime][$lte]=${new Date().toISOString()}`); | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| export async function fetchAnnouncements(): Promise<Announcement[]> { | ||||
|   const data = await fetchFromAPI<Announcement>("/api/announcements?"); | ||||
|   const data = await fetchFromAPI<Announcement>(`/api/announcements?filters[datetime][$lte]=${new Date().toISOString()}&sort[datetime]=desc`); | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| @@ -109,10 +109,21 @@ export async function fetchBookChapterLinks(bookId: string): Promise<Book> { | ||||
|  */ | ||||
| export async function fetchBookById(bookId: string): Promise<Book> { | ||||
|   const currentDateTime = new Date().toISOString(); | ||||
|   const nextday = addDays(new Date(), 1).toISOString(); | ||||
|   //[chapters][filters][release_datetime][$lte]=${currentDateTime} | ||||
|   const data = await fetchFromAPI<Book>( | ||||
|     `/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}&populate=cover` | ||||
|     `/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${nextday}&populate=cover` | ||||
|   ); | ||||
|   const stripped_data = [] | ||||
|   for (const chapter of data[0].chapters) { | ||||
|     const stripped_chapter = chapter; | ||||
|     if (new Date(chapter.release_datetime).toISOString() > currentDateTime) { | ||||
|       stripped_chapter.content = ""; | ||||
|       stripped_chapter.documentId = ""; | ||||
|     } | ||||
|     stripped_data.push(stripped_chapter) | ||||
|   } | ||||
|   data[0].chapters = stripped_data; | ||||
|   //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; | ||||
| @@ -140,11 +151,17 @@ export async function fetchEditors(): Promise<Editor[]> { | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| export async function fetchEarlyRelease(): Promise<Chapter[]> { | ||||
|   const current_datetime = new Date() | ||||
|   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]=${current_datetime.toISOString()}&filters[release_datetime][$lte]=${addDays(current_datetime, 1).toISOString()}`); | ||||
|   return data; | ||||
| } | ||||
|  | ||||
| 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 previous_week = subDays(current_datetime, 1); | ||||
|   const next_week = addDays(current_datetime, 1); | ||||
|  | ||||
|   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; | ||||
| @@ -168,13 +185,15 @@ export async function createReport( | ||||
|   details: string, | ||||
|   book_id: string, | ||||
|   chapter_id: string, | ||||
|   email: string | ||||
| ) { | ||||
|   const payload = { | ||||
|     data: { | ||||
|       error_type: error_type, | ||||
|       details: details, | ||||
|       book: book_id, | ||||
|       chapter: chapter_id | ||||
|       chapter: chapter_id, | ||||
|       report_email: email | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -65,6 +65,7 @@ export interface Book { | ||||
|     rating: number; | ||||
|     views: number; | ||||
|     readers: number; | ||||
|     release_rate: number; | ||||
|   } | ||||
|  | ||||
| export interface Announcement { | ||||
|   | ||||
| @@ -29,3 +29,30 @@ export async function markdownToHtml(markdown: string): Promise<string> { | ||||
|   } | ||||
|   return sanitizedHtml | ||||
| } | ||||
|  | ||||
| export function encodeId(documentId: string): string { | ||||
|   const salt = "salty_aff" | ||||
|   return Buffer.from(documentId + salt).toString('base64') | ||||
| } | ||||
|  | ||||
| export function decodeId(encodedId: string): string { | ||||
|   const salt = "salty_aff" | ||||
|   return Buffer.from(encodedId, 'base64').toString().replace(salt, '') | ||||
| } | ||||
|  | ||||
| export function btoa(str: string): string { | ||||
|   let buffer: Buffer; | ||||
|  | ||||
|   if (Buffer.isBuffer(str)) { | ||||
|     buffer = str; | ||||
|   } else { | ||||
|     buffer = Buffer.from(str, "binary"); | ||||
|   } | ||||
|   return buffer.toString("base64"); | ||||
| } | ||||
|  | ||||
| export function linkvertise(userid: number, link: string): string { | ||||
|   const base_url = `https://link-to.net/${userid}/${Math.floor(Math.random() * 1000)}/dynamic`; | ||||
|   const href = `${base_url}?r=${btoa(encodeURI(link))}`; | ||||
|   return href; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user