Added ad-walled support, with counters and changed a lot of the current pages look to see funnel people toward that path
This commit is contained in:
parent
9b186a462e
commit
906ad8a7a1
@ -1,10 +1,12 @@
|
||||
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";
|
||||
import Link from "next/link";
|
||||
|
||||
export type paramsType = Promise<{ bookId: string}>;
|
||||
export type paramsType = Promise<{ bookId: string }>;
|
||||
|
||||
export const metadata = {
|
||||
title: 'Null Translation Group',
|
||||
@ -25,63 +27,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 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">
|
||||
<img src={`${process.env.NEXT_PUBLIC_API_URL}${cover_media?.url}`}
|
||||
alt={cover_media?.alternativeText || `Cover of ${book.title}`}
|
||||
className="rounded-lg object-cover w-64 h-96"
|
||||
/>
|
||||
<h1 className="text-5xl pb-2 font-bold">{title}</h1>
|
||||
<a
|
||||
href={Ad.patreon}
|
||||
target="_blank"
|
||||
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>
|
||||
</div>
|
||||
<div className="max-w-6xl mx-auto py-10 px-4">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<img src={`${process.env.NEXT_PUBLIC_API_URL}${cover_media?.url}`}
|
||||
alt={cover_media?.alternativeText || `Cover of ${book.title}`}
|
||||
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"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-4">
|
||||
<strong>Author:</strong> {author}
|
||||
<br></br>
|
||||
<strong>Translator:</strong> Null Translation Group
|
||||
</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) => (
|
||||
<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">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-4">
|
||||
<strong>Author:</strong> {author}
|
||||
<br></br>
|
||||
<strong>Translator:</strong> Null Translation Group
|
||||
</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) => (
|
||||
<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"
|
||||
@ -95,19 +79,140 @@ 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">
|
||||
</ul>
|
||||
<div className="flex items-center justify-between mb-4 pt-4">
|
||||
<h2 className="text-3xl font-semibold">Next Chapters</h2>
|
||||
</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 ? (
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
)
|
||||
: (
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
)}
|
||||
</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 ? (
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
)
|
||||
: (
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
)}
|
||||
</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]) => (
|
||||
<li key={term} className="mb-2">
|
||||
<strong>{term}:</strong> {definition}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
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>
|
||||
);
|
||||
}
|
111
src/app/early/page.tsx
Normal file
111
src/app/early/page.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { fetchEarlyRelease } from "@/lib/api";
|
||||
import { Chapter } from "@/lib/types";
|
||||
import { encodeId } 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!] = [];
|
||||
}
|
||||
else{
|
||||
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 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-gray-700 dark:text-gray-300">
|
||||
Want to read even further? Join our Patreon to unlock 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={`/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={`/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>
|
||||
);
|
||||
|
||||
}
|
@ -2,6 +2,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 = {
|
||||
@ -43,14 +44,14 @@ export default async function ReleasePage() {
|
||||
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="hidden md:block bg-yellow-500 text-black py-2 px-4 rounded-lg hover:bg-yellow-600 transition duration-200 mb-6">
|
||||
<a
|
||||
href={Ad.patreon}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-center block"
|
||||
>
|
||||
WANT TO READ AHEAD OF SCHEDULE ? JOIN OUR PATREON !
|
||||
</a>
|
||||
<a
|
||||
href={Ad.patreon}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold text-center block"
|
||||
>
|
||||
WANT TO READ AHEAD OF SCHEDULE ? JOIN OUR PATREON !
|
||||
</a>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Current Releases Section */}
|
||||
@ -120,12 +121,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',
|
||||
});
|
||||
|
@ -34,11 +34,12 @@ export default function Navbar() {
|
||||
<Link href={"/releases"} className="hover:text-gray-400">
|
||||
Releases
|
||||
</Link>
|
||||
<Link href={Ad.patreon} className="hover:text-gray-400">
|
||||
<Link href={"/early"} className="hover:text-gray-400">
|
||||
Early Access
|
||||
</Link>
|
||||
<Link href={Ad.patreon} className="hover:text-yellow-400 text-yellow-200 font-semibold">
|
||||
Patreon
|
||||
</Link>
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -28,4 +28,14 @@ export async function markdownToHtml(markdown: string): Promise<string> {
|
||||
return markdown
|
||||
}
|
||||
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, '')
|
||||
}
|
Loading…
Reference in New Issue
Block a user