Compare commits

...

10 Commits

17 changed files with 565 additions and 140 deletions

10
global.d.ts vendored
View File

@@ -1,3 +1,5 @@
import { link } from "fs";
declare namespace NodeJS { declare namespace NodeJS {
interface ProcessEnv { interface ProcessEnv {
NEXT_PUBLIC_API_URL: string; NEXT_PUBLIC_API_URL: string;
@@ -8,6 +10,14 @@ declare namespace NodeJS {
interface Window { interface Window {
kofiWidgetOverlay: any; kofiWidgetOverlay: any;
dataLayer: any[]; dataLayer: any[];
googletag: {
cmd: any[];
pubads: () => {
refresh: () => void;
enableSingleRequest: () => void;
};
enableServices: () => void;
};
} }
} }
export {}; export {};

1
public/ads.txt Normal file
View File

@@ -0,0 +1 @@
google.com, pub-1843060382170565, DIRECT, f08c47fec0942fa0

View File

@@ -16,8 +16,8 @@ export async function GET(){
feed_url: `${BASE_URL}/api/feed`, feed_url: `${BASE_URL}/api/feed`,
language: "en", language: "en",
}); });
const data_sorted = data.sort((a: Chapter, b: Chapter) => { return new Date(a.release_datetime).getTime() - new Date(b.release_datetime).getTime(); });
data.forEach((chapter: Chapter) => { data_sorted.forEach((chapter: Chapter) => {
feed.item({ feed.item({
title: chapter.book?.title + ": c" + chapter.number, title: chapter.book?.title + ": c" + chapter.number,
description: "Daily chapter release for " + chapter.book?.title, description: "Daily chapter release for " + chapter.book?.title,

View File

@@ -1,10 +1,11 @@
import { fetchBookById } from "@/lib/api"; import { fetchBookById } from "@/lib/api";
import { Book, Chapter } from "@/lib/types"; 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 ChapterDropdown from "@/components/ChapterDropdown";
import { Ad } from "@/lib/types"; 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 = { export const metadata = {
title: 'Null Translation Group', title: 'Null Translation Group',
@@ -25,63 +26,45 @@ 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 english_glossary = glossary?.english_english;
const translator_note_html = await markdownToHtml(translator_note); 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 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 ( return (
<div className="max-w-6xl mx-auto py-10 px-4"> <div className="max-w-6xl mx-auto py-10 px-4">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<img src={`${process.env.NEXT_PUBLIC_API_URL}${cover_media?.url}`} <img src={`${process.env.NEXT_PUBLIC_API_URL}${cover_media?.url}`}
alt={cover_media?.alternativeText || `Cover of ${book.title}`} alt={cover_media?.alternativeText || `Cover of ${book.title}`}
className="rounded-lg object-cover w-64 h-96" className="rounded-lg object-cover w-64 h-96"
/> />
<h1 className="text-5xl pb-2 font-bold">{title}</h1> <h1 className="text-5xl pb-2 font-bold">{title}</h1>
<a <h3 className="text-2xl text-green-600 dark:text-green-400 font-bold pb-2"> {book.release_rate} chapters/day</h3>
href={Ad.patreon} <a
target="_blank" href={Ad.patreon}
rel="noopener noreferrer" target="_blank"
className="ml-4 bg-yellow-500 text-white font-semibold py-2 px-4 rounded hover:bg-yellow-600 transition duration-200" 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 to read ahead! >
</a> Join Our Patreon to read ahead!
</div> </a>
</div>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-4"> <p className="text-lg text-gray-600 dark:text-gray-400 mb-4">
<strong>Author:</strong> {author} <strong>Author:</strong> {author}
<br></br> <br></br>
<strong>Translator:</strong> Null Translation Group <strong>Translator:</strong> Null Translation Group
</p> </p>
<p className="mb-6">Description: {description}</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> <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) => (
<li key={chapter.id}> <li key={chapter.id}>
<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"
>
<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>
</li>
))}
</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} />
</div>
{sorted_chapters.map((chapter) => (
<li key={chapter.id} className="mb-2 list-none">
<a <a
href={`/books/${bookId}/chapters/${chapter.documentId}`} 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" className="block bg-white dark:bg-gray-800 rounded-lg shadow p-4 hover:shadow-lg transition duration-200"
@@ -95,19 +78,140 @@ export default async function BookPage(props: { params: paramsType }) {
</a> </a>
</li> </li>
))} ))}
{glossary && ( </ul>
<div className="mt-8"> <div className="flex items-center justify-between mb-4 pt-4">
<h2 className="text-3xl font-semibold">Glossary</h2> <h2 className="text-3xl font-semibold">Next Chapters</h2>
<ul className="list-disc list-inside mt-4"> </div>
{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}`}
className="block bg-white dark:bg-gray-800 rounded-lg shadow p-4 hover:shadow-lg transition duration-200"
>
<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>
</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>
<ul className="list-disc list-inside mt-4">
{Object.entries(english_glossary).map(([term, definition]) => ( {Object.entries(english_glossary).map(([term, definition]) => (
<li key={term} className="mb-2"> <li key={term} className="mb-2">
<strong>{term}:</strong> {definition} <strong>{term}:</strong> {definition}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
)} )}
</div> </div>
); );
} }

View 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
View 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>
);
}

View File

@@ -35,18 +35,17 @@ export default function RootLayout({ children }: { children: React.ReactNode })
const Default_Fallback = () => ( const Default_Fallback = () => (
<div className="flex items-center justify-center min-h-screen bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300"> <div className="flex items-center justify-center min-h-screen bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-300">
<img <img
src="/logo.png" // Replace this with the actual path to your logo src="/logo.png" // Replace this with the actual path to your logo
alt="Loading..." alt="Loading..."
className="h-16 w-16 animate-bounce" // Add animation if desired className="h-16 w-16 animate-bounce" // Add animation if desired
/> />
<span className="ml-3 text-lg">Loading content...</span> <span className="ml-3 text-lg">Loading content...</span>
</div> </div>
) )
return ( return (
<html lang="en" className={isDarkMode ? "dark" : ""} suppressHydrationWarning> <html lang="en" className={isDarkMode ? "dark" : ""} suppressHydrationWarning>
<head> <head>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
@@ -63,21 +62,32 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}} }}
/> />
<Script <Script
async async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1843060382170565" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-1843060382170565"
crossOrigin="anonymous"></Script> crossOrigin="anonymous"></Script>
<Script <Script
src="https://www.googletagmanager.com/gtag/js?id=G-6SXB46RSDE" src="https://www.googletagmanager.com/gtag/js?id=G-6SXB46RSDE"
strategy="afterInteractive" strategy="afterInteractive"
/> />
<Script id="google-analytics" strategy="afterInteractive"> <Script id="google-analytics" strategy="afterInteractive">
{` {`
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'G-6SXB46RSDE'); gtag('config', 'G-6SXB46RSDE');
`} `}
</Script> </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> </head>
<body className="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen"> <body className="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
<KofiWidget /> <KofiWidget />

View File

@@ -1,7 +1,7 @@
import { formatDateToMonthDayYear } from "@/lib/utils"; import { formatDateToMonthDayYear } from "@/lib/utils";
import { Chapter, Ad } from "@/lib/types"; import { Chapter, Ad } from "@/lib/types";
import { fetchReleases } from "@/lib/api"; import { fetchReleases } from "@/lib/api";
import Link from "next/link"; import { Countdown } from "@/components/Countdown";
export const metadata = { export const metadata = {
@@ -43,14 +43,14 @@ export default async function ReleasePage() {
return ( return (
<div className="mx-auto p-6 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen"> <div className="mx-auto p-6 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
<div className="hidden md:block bg-yellow-500 text-black py-2 px-4 rounded-lg hover:bg-yellow-600 transition duration-200 mb-6"> <div className="hidden md:block bg-yellow-500 text-black py-2 px-4 rounded-lg hover:bg-yellow-600 transition duration-200 mb-6">
<a <a
href={Ad.patreon} href={Ad.patreon}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-semibold text-center block" className="font-semibold text-center block"
> >
WANT TO READ AHEAD OF SCHEDULE ? JOIN OUR PATREON ! WANT TO READ AHEAD OF SCHEDULE ? JOIN OUR PATREON !
</a> </a>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Current Releases Section */} {/* Current Releases Section */}
@@ -63,17 +63,17 @@ export default async function ReleasePage() {
.sort(([titleA], [titleB]) => titleA.localeCompare(titleB)) .sort(([titleA], [titleB]) => titleA.localeCompare(titleB))
.map(([bookTitle, chapters]) => ( .map(([bookTitle, chapters]) => (
<div key={bookTitle} className="mb-6"> <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"> className="text-lg font-semibold text-gray-700 dark:text-yellow-300 mb-4 hover:underline">
{bookTitle} {bookTitle}
</Link> </a>
<ul className="space-y-4"> <ul className="space-y-4">
{chapters.map((chapter) => ( {chapters.map((chapter) => (
<li <li
key={chapter.id} 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" 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"> <h4 className="text-md font-medium">
Chapter {chapter.number}: {chapter.title} Chapter {chapter.number}: {chapter.title}
@@ -84,7 +84,7 @@ export default async function ReleasePage() {
new Date(chapter.release_datetime) new Date(chapter.release_datetime)
)} )}
</p> </p>
</Link> </a>
</li> </li>
))} ))}
</ul> </ul>
@@ -107,10 +107,10 @@ export default async function ReleasePage() {
.sort(([titleA], [titleB]) => titleA.localeCompare(titleB)) .sort(([titleA], [titleB]) => titleA.localeCompare(titleB))
.map(([bookTitle, chapters]) => ( .map(([bookTitle, chapters]) => (
<div key={bookTitle} className="mb-6"> <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"> className="text-lg font-semibold text-blue-700 dark:text-blue-300 mb-4 hover:underline">
{bookTitle} {bookTitle}
</Link> </a>
<ul className="space-y-4"> <ul className="space-y-4">
{chapters.map((chapter) => ( {chapters.map((chapter) => (
<li <li
@@ -120,12 +120,9 @@ export default async function ReleasePage() {
<h4 className="text-md font-medium"> <h4 className="text-md font-medium">
Chapter {chapter.number}: {chapter.title} Chapter {chapter.number}: {chapter.title}
</h4> </h4>
<p className="text-sm"> <h3 className="text-md font-bold text-red-600 dark:text-red-400">
Release date:{" "} <Countdown release_datetime={chapter.release_datetime} />
{formatDateToMonthDayYear( </h3>
new Date(chapter.release_datetime)
)}
</p>
</li> </li>
))} ))}
</ul> </ul>

View 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>
);
}

View File

@@ -11,7 +11,7 @@ const KofiWidget = () => {
if (typeof window !== 'undefined' && window.kofiWidgetOverlay) { if (typeof window !== 'undefined' && window.kofiWidgetOverlay) {
window.kofiWidgetOverlay.draw('nulltranslationgroup', { window.kofiWidgetOverlay.draw('nulltranslationgroup', {
'type': 'floating-chat', 'type': 'floating-chat',
'floating-chat.donateButton.text': 'Support me', 'floating-chat.donateButton.text': '',
'floating-chat.donateButton.background-color': '#00b9fe', 'floating-chat.donateButton.background-color': '#00b9fe',
'floating-chat.donateButton.text-color': '#fff', 'floating-chat.donateButton.text-color': '#fff',
}); });

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { Ad } from "@/lib/types"; import { Ad } from "@/lib/types";
export default function Navbar() { export default function Navbar() {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -17,28 +16,29 @@ export default function Navbar() {
alt="Logo" alt="Logo"
className="h-8 w-8" 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 Null Translation Group
</Link> </a>
</div> </div>
{/* Navigation Links */} {/* Navigation Links */}
<nav className="hidden md:flex space-x-6"> <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 Announcements
</Link> </a>
<Link href="/" className="hover:text-gray-400"> <a href="https://nulltranslation.com" className="hover:text-gray-400">
Books Books
</Link> </a>
<Link href={"/releases"} className="hover:text-gray-400"> <a href={"/releases"} className="hover:text-gray-400">
Releases Releases
</Link> </a>
<Link href={Ad.patreon} className="hover:text-gray-400"> <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 Patreon
</Link> </a>
</nav> </nav>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
@@ -66,18 +66,21 @@ export default function Navbar() {
{/* Mobile Menu */} {/* Mobile Menu */}
{isMenuOpen && ( {isMenuOpen && (
<div className="md:hidden bg-gray-700 px-6 pb-4"> <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 Announcements
</Link> </a>
<Link href={"/releases"} className="block py-2 hover:text-gray-400"> <a href={"/releases"} className="block py-2 hover:text-gray-400">
Release Release
</Link> </a>
<Link href="/" className="block py-2 hover:text-gray-400"> <a href="https://nulltranslation.com" className="block py-2 hover:text-gray-400">
Book List Book List
</Link> </a>
<Link href={Ad.patreon} className="block py-2 hover:text-gray-400"> <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 Patreon
</Link> </a>
</div> </div>
)} )}
</div> </div>

View File

@@ -2,7 +2,8 @@
import React from "react"; import React from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Ad } from "@/lib/types"; import Link from "next/link";
import { encodeId } from "@/lib/utils";
interface NavigationButtonsProps { interface NavigationButtonsProps {
bookId: string; bookId: string;
@@ -21,7 +22,6 @@ const NavigationButtons: React.FC<NavigationButtonsProps> = ({ bookId, prevChapt
router.push(`/books/${bookId}`); router.push(`/books/${bookId}`);
}; };
return ( return (
<div className="mt-2 flex justify-between"> <div className="mt-2 flex justify-between">
<button <button
@@ -53,18 +53,16 @@ const NavigationButtons: React.FC<NavigationButtonsProps> = ({ bookId, prevChapt
Next Chapter Next Chapter
</button> </button>
) : ( ) : (
<a <Link
href={Ad.patreon} href={'/early?bookId=' + encodeId(bookId)}
target="_blank"
rel="noopener noreferrer"
className={` className={`
bg-yellow-500 text-white py-2 px-4 rounded bg-yellow-500 text-white py-2 px-4 rounded
hover:bg-yellow-600 hover:bg-yellow-600
transition duration-200 transition duration-200
`} `}
> >
Unreleased Chapters Unlock Early Chapter
</a> </Link>
) )
} }
</div> </div>

View File

@@ -13,10 +13,11 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [errorType, setErrorType] = useState('') const [errorType, setErrorType] = useState('')
const [details, setDetails] = useState('') const [details, setDetails] = useState('')
const [email, setEmail] = useState('')
const handleSubmitReport = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmitReport = async (event: React.FormEvent<HTMLFormElement>) => {
// Implement report submission here // Implement report submission here
event.preventDefault() event.preventDefault()
const response = await createReport(errorType,details,bookId,chapterId) const response = await createReport(errorType,details,bookId,chapterId,email)
//Linting be linting //Linting be linting
if (response.status === 201){ if (response.status === 201){
alert('Report submitted successfully') alert('Report submitted successfully')
@@ -26,11 +27,13 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => {
} }
setErrorType('') setErrorType('')
setDetails('') setDetails('')
setEmail('')
setIsOpen(false) setIsOpen(false)
} }
const handleExit = (event: React.MouseEvent) => { const handleExit = (event: React.MouseEvent) => {
setErrorType('') setErrorType('')
setDetails('') setDetails('')
setEmail('')
if (modalRef.current && !modalRef.current.contains(event.target as Node)) { if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setIsOpen(false) setIsOpen(false)
} }
@@ -51,6 +54,19 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => {
<div className="mt-4"> <div className="mt-4">
<form className="p-6 rounded-lg max-w-md mx-auto space-y-4" <form className="p-6 rounded-lg max-w-md mx-auto space-y-4"
onSubmit={handleSubmitReport}> 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> <div>
<label className="block text-md font-semibold mb-1 dark:text-gray-300" htmlFor="error-type"> <label className="block text-md font-semibold mb-1 dark:text-gray-300" htmlFor="error-type">
Select Error Type : Select Error Type :
@@ -80,6 +96,8 @@ const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => {
onChange={(e) => setDetails(e.target.value)} 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"/> 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>
<div className="flex justify-between"> <div className="flex justify-between">
<button <button
type="submit" type="submit"

View 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;

View File

@@ -80,12 +80,12 @@ export async function fetchFromAPI<T>(
* Populates optional fields like Chapters or Editors based on requirements. * Populates optional fields like Chapters or Editors based on requirements.
*/ */
export async function fetchBooks(): Promise<Book[]> { 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; return data;
} }
export async function fetchAnnouncements(): Promise<Announcement[]> { 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; return data;
} }
@@ -109,10 +109,21 @@ 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 nextday = addDays(new Date(), 1).toISOString();
//[chapters][filters][release_datetime][$lte]=${currentDateTime} //[chapters][filters][release_datetime][$lte]=${currentDateTime}
const data = await fetchFromAPI<Book>( 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 ???????? //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); const glossary = await fetchGlossaryByBookId(bookId);
data[0].glossary = glossary; data[0].glossary = glossary;
@@ -140,11 +151,17 @@ export async function fetchEditors(): Promise<Editor[]> {
return data; 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 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, 1);
const next_week = addDays(current_datetime, 3); 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 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;
@@ -168,13 +185,15 @@ export async function createReport(
details: string, details: string,
book_id: string, book_id: string,
chapter_id: string, chapter_id: string,
email: string
) { ) {
const payload = { const payload = {
data: { data: {
error_type: error_type, error_type: error_type,
details: details, details: details,
book: book_id, book: book_id,
chapter: chapter_id chapter: chapter_id,
report_email: email
} }
} }

View File

@@ -65,6 +65,7 @@ export interface Book {
rating: number; rating: number;
views: number; views: number;
readers: number; readers: number;
release_rate: number;
} }
export interface Announcement { export interface Announcement {

View File

@@ -29,3 +29,30 @@ export async function markdownToHtml(markdown: string): Promise<string> {
} }
return sanitizedHtml 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;
}