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,8 +1,10 @@
|
|||||||
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";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export type paramsType = Promise<{ bookId: string }>;
|
export type paramsType = Promise<{ bookId: string }>;
|
||||||
|
|
||||||
@ -29,10 +31,11 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
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">
|
||||||
@ -41,6 +44,7 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
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>
|
||||||
|
<h3 className="text-2xl text-green-600 dark:text-green-400 font-bold pb-2"> {book.release_rate} chapters/day</h3>
|
||||||
<a
|
<a
|
||||||
href={Ad.patreon}
|
href={Ad.patreon}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -77,10 +81,64 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="flex items-center justify-between mb-4 pt-4">
|
<div className="flex items-center justify-between mb-4 pt-4">
|
||||||
<h2 className="text-3xl font-semibold">All Chapters</h2>
|
<h2 className="text-3xl font-semibold">Next Chapters</h2>
|
||||||
<ChapterDropdown chapters={sorted_chapters} bookId={bookId} />
|
|
||||||
</div>
|
</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 ? (
|
||||||
|
<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">
|
<li key={chapter.id} className="mb-2 list-none">
|
||||||
<a
|
<a
|
||||||
href={`/books/${bookId}/chapters/${chapter.documentId}`}
|
href={`/books/${bookId}/chapters/${chapter.documentId}`}
|
||||||
@ -90,11 +148,58 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
Chapter {chapter.number}: {chapter.title}
|
Chapter {chapter.number}: {chapter.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
<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>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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 && (
|
{glossary && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<h2 className="text-3xl font-semibold">Glossary</h2>
|
<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>
|
||||||
|
);
|
||||||
|
}
|
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 { Chapter, Ad } from "@/lib/types";
|
||||||
import { fetchReleases } from "@/lib/api";
|
import { fetchReleases } from "@/lib/api";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { Countdown } from "@/components/Countdown";
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@ -120,12 +121,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>
|
||||||
|
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) {
|
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',
|
||||||
});
|
});
|
||||||
|
@ -34,11 +34,12 @@ export default function Navbar() {
|
|||||||
<Link href={"/releases"} className="hover:text-gray-400">
|
<Link href={"/releases"} className="hover:text-gray-400">
|
||||||
Releases
|
Releases
|
||||||
</Link>
|
</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
|
Patreon
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -29,3 +29,13 @@ 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, '')
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user