Added releases changed the way apis work underneath to give more room for caching, i.e remove unneeded crap when it is unneeded
This commit is contained in:
33
src/app/announcements/[announcementId]/page.tsx
Normal file
33
src/app/announcements/[announcementId]/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { fetchAnnouncementById } from "@/lib/api";
|
||||
import { Announcement } from "@/lib/types";
|
||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
||||
|
||||
export const metadata = {
|
||||
title: 'Announcement Detail Page',
|
||||
description: 'NullTranslationGroup Announcement page',
|
||||
};
|
||||
|
||||
export type paramsType = Promise<{ announcementId: string }>;
|
||||
|
||||
export default async function AnnouncementDetailPage(props: {params: paramsType}) {
|
||||
const { announcementId } = await props.params;
|
||||
let announcement: Announcement;
|
||||
try{
|
||||
announcement = await fetchAnnouncementById(announcementId);
|
||||
}
|
||||
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> Announcement not found !</h1></center>' }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
||||
<h1>{announcement.title}</h1>
|
||||
<h2>Release Date: {formatDateToMonthDayYear(new Date(announcement.datetime))}</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: announcement.content }}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
src/app/announcements/page.tsx
Normal file
38
src/app/announcements/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
||||
import { Announcement } from "@/lib/types";
|
||||
import { fetchAnnouncements } from "@/lib/api";
|
||||
|
||||
|
||||
export const metadata = {
|
||||
title: 'Annoucement page',
|
||||
description: 'NullTranslationGroup Announcement page',
|
||||
};
|
||||
|
||||
export default async function AnnouncementPage() {
|
||||
let announcements = [];
|
||||
try {
|
||||
announcements = await fetchAnnouncements();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div className="text-center mt-10 text-red-500">
|
||||
<p>Failed to load announcements.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sorted_announcements:Announcement[] = announcements.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto py-10 px-4">
|
||||
{sorted_announcements.map((announcement) => (
|
||||
<li key={announcement.id} className="mb-2 list-none">
|
||||
<div className="text-lg font-semibold">{announcement.title}</div>
|
||||
<div className="text-sm text-gray-500">{formatDateToMonthDayYear(new Date(announcement.datetime))}</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import NavigationButtons from "@/components/NavigationButtons";
|
||||
import { Book, Chapter } from "@/lib/types";
|
||||
import { fetchBookById } from "@/lib/api";
|
||||
import { Chapter } from "@/lib/types";
|
||||
import {fetchChapterByBookId } from "@/lib/api";
|
||||
export type paramsType = Promise<{ bookId: string; chapterId: string }>;
|
||||
|
||||
export const metadata = {
|
||||
@@ -13,13 +13,12 @@ export const metadata = {
|
||||
export default async function ChapterPage(props: { params: paramsType}) {
|
||||
const { bookId, chapterId } = await props.params;
|
||||
|
||||
let book: Book;
|
||||
let chapters: Chapter[];
|
||||
try{
|
||||
book = await fetchBookById(bookId);
|
||||
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>
|
||||
@@ -28,11 +27,10 @@ export default async function ChapterPage(props: { params: paramsType}) {
|
||||
)
|
||||
}
|
||||
|
||||
const chapters :Chapter[] = book.chapters;
|
||||
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)?.documentId || "" : "";
|
||||
const prev_chapter = current_chapter ? sorted_chapters.find((chapter) => chapter.number === current_chapter.number - 1)?.documentId || "" : "";
|
||||
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 || "" : "";
|
||||
|
||||
if(current_chapter === null){
|
||||
return (
|
||||
|
||||
144
src/app/releases/page.tsx
Normal file
144
src/app/releases/page.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
||||
import { Chapter, Ad } from "@/lib/types";
|
||||
import { fetchReleases } from "@/lib/api";
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
export const metadata = {
|
||||
title: 'Release page',
|
||||
description: 'NullTranslationGroup Announcement page',
|
||||
};
|
||||
|
||||
export default async function ReleasePage() {
|
||||
let current_chapters: Chapter[] = [];
|
||||
let future_chapters: Chapter[] = [];
|
||||
try {
|
||||
const releases = await fetchReleases();
|
||||
current_chapters = releases.current_chapters
|
||||
future_chapters = releases.future_chapters
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div className="text-center mt-10 text-red-500">
|
||||
<p>Failed to load releases.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const sorted_current_chapters = current_chapters.sort((a, b) => new Date(a.release_datetime).getTime() - new Date(b.release_datetime).getTime());
|
||||
const sorted_future_chapters = future_chapters.sort((a, b) => new Date(a.release_datetime).getTime() - new Date(b.release_datetime).getTime());
|
||||
const groupChaptersByNovel = (chapters: Chapter[]) => {
|
||||
return chapters.reduce((acc, chapter) => {
|
||||
const bookTitle = chapter.book?.title || "Unknown Title";
|
||||
if (!acc[bookTitle]) {
|
||||
acc[bookTitle] = [];
|
||||
}
|
||||
acc[bookTitle].push(chapter);
|
||||
return acc;
|
||||
}, {} as Record<string, Chapter[]>);
|
||||
};
|
||||
|
||||
const groupedCurrentChapters = groupChaptersByNovel(sorted_current_chapters)
|
||||
const groupedFutureChapters = groupChaptersByNovel(sorted_future_chapters);
|
||||
|
||||
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>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Current Releases Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-yellow-500 mb-6 border-b-2 border-yellow-500 pb-2">
|
||||
Current Releases
|
||||
</h2>
|
||||
{Object.keys(groupedCurrentChapters).length > 0 ? (
|
||||
Object.entries(groupedCurrentChapters)
|
||||
.sort(([titleA], [titleB]) => titleA.localeCompare(titleB))
|
||||
.map(([bookTitle, chapters]) => (
|
||||
<div key={bookTitle} className="mb-6">
|
||||
<Link href={`/books/${chapters[0].book?.documentId}`}
|
||||
className="text-lg font-semibold text-gray-700 dark:text-yellow-300 mb-4 hover:underline">
|
||||
{bookTitle}
|
||||
</Link>
|
||||
<ul className="space-y-4">
|
||||
{chapters.map((chapter) => (
|
||||
<li
|
||||
key={chapter.id}
|
||||
className="p-4 bg-yellow-100 dark:bg-yellow-800 text-gray-800 dark:text-gray-100 rounded-lg shadow-sm border border-yellow-300 dark:border-yellow-700 transition-transform transform hover:scale-105"
|
||||
>
|
||||
<Link href={`books/${chapter.book?.documentId}/chapters/${chapter.documentId}`} className="block">
|
||||
|
||||
<h4 className="text-md font-medium">
|
||||
Chapter {chapter.number}: {chapter.title}
|
||||
</h4>
|
||||
<p className="text-sm">
|
||||
Released on:{" "}
|
||||
{formatDateToMonthDayYear(
|
||||
new Date(chapter.release_datetime)
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-600 dark:text-gray-400 italic">
|
||||
No current releases available.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Future Releases Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-blue-500 mb-6 border-b-2 border-blue-500 pb-2">
|
||||
Future Releases
|
||||
</h2>
|
||||
{Object.keys(groupedFutureChapters).length > 0 ? (
|
||||
Object.entries(groupedFutureChapters)
|
||||
.sort(([titleA], [titleB]) => titleA.localeCompare(titleB))
|
||||
.map(([bookTitle, chapters]) => (
|
||||
<div key={bookTitle} className="mb-6">
|
||||
<Link href={`/books/${chapters[0].book?.documentId}`}
|
||||
className="text-lg font-semibold text-blue-700 dark:text-blue-300 mb-4 hover:underline">
|
||||
{bookTitle}
|
||||
</Link>
|
||||
<ul className="space-y-4">
|
||||
{chapters.map((chapter) => (
|
||||
<li
|
||||
key={chapter.id}
|
||||
className="p-4 bg-blue-100 dark:bg-blue-800 text-gray-800 dark:text-gray-100 rounded-lg shadow-sm border border-blue-300 dark:border-blue-700 transition-transform transform hover:scale-105"
|
||||
>
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-600 dark:text-gray-400 italic">
|
||||
No future releases available.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -23,13 +23,22 @@ export default function Navbar() {
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
|
||||
<nav className="hidden md:flex space-x-6">
|
||||
<Link href={"/announcements"} className="hover:text-gray-400">
|
||||
Announcements
|
||||
</Link>
|
||||
<Link href="/" className="hover:text-gray-400">
|
||||
Book List
|
||||
Books
|
||||
</Link>
|
||||
<Link href={"/releases"} className="hover:text-gray-400">
|
||||
Releases
|
||||
</Link>
|
||||
<Link href={Ad.patreon} className="hover:text-gray-400">
|
||||
Patreon
|
||||
</Link>
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -57,6 +66,12 @@ export default function Navbar() {
|
||||
{/* Mobile Menu */}
|
||||
{isMenuOpen && (
|
||||
<div className="md:hidden bg-gray-700 px-6 pb-4">
|
||||
<Link href={"/announcements"} className="hover:text-gray-400">
|
||||
Announcements
|
||||
</Link>
|
||||
<Link href={"/releases"} className="hover:text-gray-400">
|
||||
Release
|
||||
</Link>
|
||||
<Link href="/" className="block py-2 hover:text-gray-400">
|
||||
Book List
|
||||
</Link>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { subDays } from "date-fns";
|
||||
import { Book, Chapter, Editor } from "./types";
|
||||
import { addDays, subDays } from "date-fns";
|
||||
import { Book, Chapter, Editor, Announcement } from "./types";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL as string;
|
||||
const API_TOKEN = process.env.STRAPI_API_TOKEN as string;
|
||||
|
||||
/**
|
||||
* Centralized API fetch function with TypeScript support.
|
||||
* Handles GET, POST, PUT, DELETE methods and includes headers by default.
|
||||
*/
|
||||
export async function fetchFromAPI<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${API_URL}${endpoint}`;
|
||||
|
||||
): Promise<T[]> {
|
||||
const headers: HeadersInit = {
|
||||
Authorization: `Bearer ${API_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
@@ -25,19 +19,32 @@ export async function fetchFromAPI<T>(
|
||||
...options,
|
||||
};
|
||||
|
||||
|
||||
|
||||
let results: T[] = [];
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
try {
|
||||
const response = await fetch(url, {...config, next: {revalidate:30}});
|
||||
do{
|
||||
const url = `${API_URL}${endpoint}&pagination[page]=${currentPage}&pagination[pageSize]=25`;
|
||||
const response = await fetch(url, {...config, next: {revalidate:30}});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error(`Error fetching ${url}:`, errorData);
|
||||
throw new Error(errorData.message || `API fetch error (status: ${response.status})`);
|
||||
}
|
||||
const responseJson = await response.json();
|
||||
return responseJson;
|
||||
results = results.concat(responseJson.data);
|
||||
totalPages = responseJson.meta?.pagination?.pageCount;
|
||||
currentPage += 1;
|
||||
}while(currentPage <= totalPages)
|
||||
|
||||
} catch (error) {
|
||||
console.error("Fetch error:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,22 +52,27 @@ export async function fetchFromAPI<T>(
|
||||
* Populates optional fields like Chapters or Editors based on requirements.
|
||||
*/
|
||||
export async function fetchBooks(): Promise<Book[]> {
|
||||
const data = await fetchFromAPI<{ data: Book[] }>("/api/books?populate=*&chapters.sort=number:desc");
|
||||
return data.data;
|
||||
const data = await fetchFromAPI<Book>("/api/books?populate=cover&sort[title]=asc");
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchAnnouncements(): Promise<Announcement[]> {
|
||||
const data = await fetchFromAPI<Announcement>("/api/announcements?");
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchChaptersRSS(): Promise<Chapter[]> {
|
||||
const currentDateTime = new Date()
|
||||
const yesterday = subDays(currentDateTime, 1);
|
||||
const data = await fetchFromAPI<{ data: Chapter[] }>
|
||||
const data = await fetchFromAPI<Chapter>
|
||||
(`/api/chapters?populate=book&filters[release_datetime][$lte]=${currentDateTime.toISOString()}&filters[release_datetime][$gte]=${yesterday.toISOString()}`);
|
||||
return data.data;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchBookChapterLinks(bookId: string): Promise<Book> {
|
||||
const currentDateTime = new Date().toISOString();
|
||||
const data = await fetchFromAPI<{ data: Book }>(`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}`);
|
||||
return data.data
|
||||
const data = await fetchFromAPI<Book>(`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}`);
|
||||
return data[0]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,25 +81,49 @@ export async function fetchBookChapterLinks(bookId: string): Promise<Book> {
|
||||
*/
|
||||
export async function fetchBookById(bookId: string): Promise<Book> {
|
||||
const currentDateTime = new Date().toISOString();
|
||||
const data = await fetchFromAPI<{ data: Book }>(
|
||||
const data = await fetchFromAPI<Book>(
|
||||
`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}&populate=cover`
|
||||
);
|
||||
data.data.chapters = data.data.chapters.sort((a, b) => a.number - b.number);
|
||||
return data.data;
|
||||
data[0].chapters = data[0].chapters.sort((a, b) => a.number - b.number);
|
||||
return data[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a specific chapter by ID.
|
||||
*/
|
||||
export async function fetchChapterById(chapterId: string): Promise<Chapter> {
|
||||
const data = await fetchFromAPI<{ data: Chapter }>(`/api/chapters/${chapterId}?populate[book][fields]=documentId`);
|
||||
return data.data;
|
||||
export async function fetchChapterByBookId(bookId: string, chapterId: string): Promise<Chapter[]> {
|
||||
const currentChapter = await fetchFromAPI<Chapter>(`/api/chapters/${chapterId}?populate[book][fields][0]=title&filters[book][documentId]=${bookId}`);
|
||||
const bookWithAllChapters = await fetchFromAPI<Book>( `/api/books/${bookId}?populate[chapters][filters][number][$gte]=${
|
||||
currentChapter[0].number - 1
|
||||
}&populate[chapters][filters][number][$lte]=${
|
||||
currentChapter[0].number + 1
|
||||
}`);
|
||||
//const nextChapter = await fetchFromAPI<Chapter>(`/api/chapters?populate[book]&filters[book][id]=${bookId}&sort[number]=asc`);
|
||||
return bookWithAllChapters[0].chapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all editors.
|
||||
*/
|
||||
export async function fetchEditors(): Promise<Editor[]> {
|
||||
const data = await fetchFromAPI<{ data: Editor[] }>("/api/editors");
|
||||
return data.data;
|
||||
const data = await fetchFromAPI<Editor>("/api/editors");
|
||||
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 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 future_chapters = chapters.filter(chapter => new Date(chapter.release_datetime) > new Date());
|
||||
const current_chapters = chapters.filter(chapter => new Date(chapter.release_datetime) <= new Date());
|
||||
return {current_chapters,future_chapters}
|
||||
}
|
||||
|
||||
export async function fetchAnnouncementById(announcementId: string): Promise<Announcement> {
|
||||
const data = await fetchFromAPI<Announcement>(`/api/announcements/${announcementId}`);
|
||||
return data[0];
|
||||
}
|
||||
@@ -63,6 +63,19 @@ export interface Book {
|
||||
glossary: Glossary;
|
||||
}
|
||||
|
||||
export interface Announcement {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
datetime: string;
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
book_title: string;
|
||||
chapter: string;
|
||||
datetime: string;
|
||||
url: string;
|
||||
}
|
||||
export const Ad = {
|
||||
patreon: "https://patreon.com/nulltranslationgroup/membership",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user