diff --git a/src/app/books/[bookId]/page.tsx b/src/app/books/[bookId]/page.tsx index c1be3d1..d9e7e89 100644 --- a/src/app/books/[bookId]/page.tsx +++ b/src/app/books/[bookId]/page.tsx @@ -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 ( -
-
- {cover_media?.alternativeText -

{title}

- - Join Our Patreon to read ahead! - -
+
+
+ {cover_media?.alternativeText +

{title}

+

{book.release_rate} chapters/day

+ + Join Our Patreon to read ahead! + +
-

- Author: {author} -

- Translator: Null Translation Group -

-

Description: {description}

-
-

Recent Chapters

- -
-

All Chapters

- -
- {sorted_chapters.map((chapter) => ( -
  • +

    + Author: {author} +

    + Translator: Null Translation Group +

    +

    Description: {description}

    +
    +

    Recent Chapters

    + +
    + )} +
  • + ); + } diff --git a/src/app/early/[bookId]/[chapterId]/page.tsx b/src/app/early/[bookId]/[chapterId]/page.tsx new file mode 100644 index 0000000..b2a0654 --- /dev/null +++ b/src/app/early/[bookId]/[chapterId]/page.tsx @@ -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 ( +
    +

    Chapter not found !

    ' }}>
    + +
    + ) + } + 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 ( +
    +

    Chapter not found !

    ' }}>
    + +
    + ) + } + + return ( +
    + +
    + + + +
    + ); +} \ No newline at end of file diff --git a/src/app/early/page.tsx b/src/app/early/page.tsx new file mode 100644 index 0000000..b1d2d05 --- /dev/null +++ b/src/app/early/page.tsx @@ -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 ( +
    +

    Error fetching early chapters

    ' }}>
    +
    + ) + } + + 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 ( +
    +
    +

    Early Adcess Page

    +

    + Get early access to chapters by completing Google Offerwall tasks. Read ahead by 1 chapter ! +

    +

    + Tasks vary from watching an ad video to completing surveys and more. +

    +

    + Want to read even further? Join our Patreon to unlock more chapters! +

    + + + Join Our Patreon – Read More Ahead! + +
    + {early_chapter_from_params &&
    + +

    + {early_chapter_from_params.book?.title} +

    +

    + Chapter {early_chapter_from_params.number}: {early_chapter_from_params.title} +

    +

    + Click to Unlock Chapter +

    +
    +
    + } + {/* Chapters Grid */} + +
    + ); + +} diff --git a/src/app/releases/page.tsx b/src/app/releases/page.tsx index ca45ab0..1ee02b4 100644 --- a/src/app/releases/page.tsx +++ b/src/app/releases/page.tsx @@ -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 (
    - - WANT TO READ AHEAD OF SCHEDULE ? JOIN OUR PATREON ! - + + WANT TO READ AHEAD OF SCHEDULE ? JOIN OUR PATREON ! +
    {/* Current Releases Section */} @@ -120,12 +121,9 @@ export default async function ReleasePage() {

    Chapter {chapter.number}: {chapter.title}

    -

    - Release date:{" "} - {formatDateToMonthDayYear( - new Date(chapter.release_datetime) - )} -

    +

    + +

    ))} diff --git a/src/components/Countdown.tsx b/src/components/Countdown.tsx new file mode 100644 index 0000000..260dc73 --- /dev/null +++ b/src/components/Countdown.tsx @@ -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(calculateTimeLeft()); + + + useEffect(() => { + const timer = setTimeout(() => { + setTimeLeft(calculateTimeLeft()); + }, 1000); + return () => clearTimeout(timer); + }); + + return ( + {timeLeft} + ); +} diff --git a/src/components/KofiWidget.tsx b/src/components/KofiWidget.tsx index c4db5cd..da4cc81 100644 --- a/src/components/KofiWidget.tsx +++ b/src/components/KofiWidget.tsx @@ -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', }); diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx index f220d44..0711256 100644 --- a/src/components/NavigationBar.tsx +++ b/src/components/NavigationBar.tsx @@ -34,11 +34,12 @@ export default function Navbar() { Releases - + + Early Access + + Patreon - - {/* Mobile Menu Button */} diff --git a/src/components/NavigationButtons.tsx b/src/components/NavigationButtons.tsx index 962511c..dbed932 100644 --- a/src/components/NavigationButtons.tsx +++ b/src/components/NavigationButtons.tsx @@ -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 = ({ bookId, prevChapt router.push(`/books/${bookId}`); }; - return (
    ) : ( - - Unreleased Chapters - + Unlock Early Chapter + ) }
    diff --git a/src/components/ReportButton.tsx b/src/components/ReportButton.tsx index be7c6c5..111fcdd 100644 --- a/src/components/ReportButton.tsx +++ b/src/components/ReportButton.tsx @@ -13,10 +13,11 @@ const ReportButton: React.FC = ({ 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) => { // 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 = ({ 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 = ({ bookId, chapterId }) => {
    +
    + + setEmail(e.target.value)}> + +
    + +