Compare commits
4 Commits
522bf5ab56
...
0de96f7be0
Author | SHA1 | Date | |
---|---|---|---|
0de96f7be0 | |||
c9f46030ec | |||
cea7376e48 | |||
418b3e4a3f |
59
package-lock.json
generated
59
package-lock.json
generated
@ -14,15 +14,18 @@
|
|||||||
"@tanstack/react-query": "^5.63.0",
|
"@tanstack/react-query": "^5.63.0",
|
||||||
"@tanstack/react-query-devtools": "^5.63.0",
|
"@tanstack/react-query-devtools": "^5.63.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"rss": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/rss": "^0.0.32",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
@ -1049,6 +1052,13 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/rss": {
|
||||||
|
"version": "0.0.32",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/rss/-/rss-0.0.32.tgz",
|
||||||
|
"integrity": "sha512-2oKNqKyUY4RSdvl5eZR1n2Q9yvw3XTe3mQHsFPn9alaNBxfPnbXBtGP8R0SV8pK1PrVnLul0zx7izbm5/gF5Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.19.1",
|
"version": "8.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz",
|
||||||
@ -1976,6 +1986,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
@ -4845,6 +4865,37 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rss": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "2.1.13",
|
||||||
|
"xml": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rss/node_modules/mime-db": {
|
||||||
|
"version": "1.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
|
||||||
|
"integrity": "sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rss/node_modules/mime-types": {
|
||||||
|
"version": "2.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
|
||||||
|
"integrity": "sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "~1.25.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@ -5970,6 +6021,12 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
||||||
|
@ -15,15 +15,18 @@
|
|||||||
"@tanstack/react-query": "^5.63.0",
|
"@tanstack/react-query": "^5.63.0",
|
||||||
"@tanstack/react-query-devtools": "^5.63.0",
|
"@tanstack/react-query-devtools": "^5.63.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"rss": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/rss": "^0.0.32",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
40
src/app/api/feed/route.ts
Normal file
40
src/app/api/feed/route.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import RSS from "rss";
|
||||||
|
import { subDays } from "date-fns";
|
||||||
|
import { fetchChaptersRSS } from "@/lib/api";
|
||||||
|
import { Chapter } from "@/lib/types";
|
||||||
|
|
||||||
|
|
||||||
|
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
|
||||||
|
|
||||||
|
export async function GET(){
|
||||||
|
const now = new Date();
|
||||||
|
const yesterday = subDays(now, 1);
|
||||||
|
|
||||||
|
const data = await fetchChaptersRSS();
|
||||||
|
|
||||||
|
const feed = new RSS({
|
||||||
|
title: "Null Translation Group",
|
||||||
|
description: "Null Translation Group main hub",
|
||||||
|
site_url: BASE_URL,
|
||||||
|
feed_url: `${BASE_URL}/api/feed`,
|
||||||
|
language: "en",
|
||||||
|
});
|
||||||
|
|
||||||
|
data.forEach((chapter: Chapter) => {
|
||||||
|
feed.item({
|
||||||
|
title: chapter.book?.title + " - Chapter " + chapter.number + " " + chapter.title,
|
||||||
|
description: "Daily chapter release for " + chapter.book?.title,
|
||||||
|
url: `${BASE_URL}/books/${chapter.book?.documentId}/chapters/${chapter.documentId}`,
|
||||||
|
date: chapter.release_datetime,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const xml = feed.xml({ indent: true });
|
||||||
|
|
||||||
|
return new Response(xml, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import NavigationButtons from "@/components/NavigationButtons";
|
import NavigationButtons from "@/components/NavigationButtons";
|
||||||
import { Chapter } from "@/lib/types";
|
import { Chapter } from "@/lib/types";
|
||||||
import { fetchBookById } from "@/lib/api";
|
import { fetchBookById } from "@/lib/api";
|
||||||
|
import Head from "next/head";
|
||||||
export type paramsType = Promise<{ bookId: string; chapterId: string }>;
|
export type paramsType = Promise<{ bookId: string; chapterId: string }>;
|
||||||
|
|
||||||
// Dynamic page component
|
// Dynamic page component
|
||||||
@ -20,21 +20,33 @@ export default async function ChapterPage(props: { params: paramsType}) {
|
|||||||
|
|
||||||
if (current_chapter === undefined) {
|
if (current_chapter === undefined) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Chapter not found</title>
|
||||||
|
<meta name="description" content="Chapter not found" />
|
||||||
|
</Head>
|
||||||
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
<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>
|
<div dangerouslySetInnerHTML={{ __html: '<center><h1> Chapter not found !</h1></center>' }}></div>
|
||||||
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter} />
|
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter} />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{book.title} - Chapter {current_chapter.number}</title>
|
||||||
|
<meta name="description" content={book.description} />
|
||||||
|
</Head>
|
||||||
<div className="prose dark:prose-invert mx-auto p-6 bg-white dark:bg-gray-800 shadow-md rounded-lg mt-4">
|
<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} />
|
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter} />
|
||||||
<div className="pt-4" dangerouslySetInnerHTML={{ __html: current_chapter.content }}></div>
|
<div className="pt-4" dangerouslySetInnerHTML={{ __html: current_chapter.content }}></div>
|
||||||
|
|
||||||
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/>
|
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
@ -2,6 +2,8 @@ import { fetchBookById } from "@/lib/api";
|
|||||||
import { Book } from "@/lib/types";
|
import { Book } from "@/lib/types";
|
||||||
import { formatDateToMonthDayYear } from "@/lib/utils";
|
import { formatDateToMonthDayYear } from "@/lib/utils";
|
||||||
import ChapterDropdown from "@/components/ChapterDropdown";
|
import ChapterDropdown from "@/components/ChapterDropdown";
|
||||||
|
import { Ad } from "@/lib/types";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
export type paramsType = Promise<{ bookId: string}>;
|
export type paramsType = Promise<{ bookId: string}>;
|
||||||
|
|
||||||
@ -25,6 +27,11 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
const cover_media = cover?.at(0);
|
const cover_media = cover?.at(0);
|
||||||
const recentChapters = chapters.length > 6 ? chapters.slice(chapters.length - 6, chapters.length) : chapters;
|
const recentChapters = chapters.length > 6 ? chapters.slice(chapters.length - 6, chapters.length) : chapters;
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
</Head>
|
||||||
<div className="max-w-6xl mx-auto py-10 px-4">
|
<div className="max-w-6xl mx-auto py-10 px-4">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<img src={`${process.env.NEXT_PUBLIC_API_URL}${cover_media?.url}`}
|
<img src={`${process.env.NEXT_PUBLIC_API_URL}${cover_media?.url}`}
|
||||||
@ -33,7 +40,7 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
/>
|
/>
|
||||||
<h1 className="text-5xl pb-2 font-bold">{title}</h1>
|
<h1 className="text-5xl pb-2 font-bold">{title}</h1>
|
||||||
<a
|
<a
|
||||||
href="https://www.patreon.com/c/nulltranslationgroup/membership?view_as=patron" // Replace with your Patreon URL
|
href={Ad.patreon}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
className="ml-4 bg-yellow-500 text-white font-semibold py-2 px-4 rounded hover:bg-yellow-600 transition duration-200"
|
||||||
@ -86,5 +93,8 @@ export default async function BookPage(props: { params: paramsType }) {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>);
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
5
src/app/feed/page.tsx
Normal file
5
src/app/feed/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function FeedRedirect() {
|
||||||
|
redirect('/api/feed');
|
||||||
|
}
|
@ -34,6 +34,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
return (
|
return (
|
||||||
<html lang="en" className={isDarkMode ? "dark" : ""} suppressHydrationWarning>
|
<html lang="en" className={isDarkMode ? "dark" : ""} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Book } from "@/lib/types";
|
import { Book } from "@/lib/types";
|
||||||
import { fetchBooks } from "@/lib/api";
|
import { fetchBooks } from "@/lib/api";
|
||||||
|
import { Ad } from "@/lib/types";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
let books: Book[] = [];
|
let books: Book[] = [];
|
||||||
|
|
||||||
@ -15,10 +18,15 @@ export default async function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Null Translation Group</title>
|
||||||
|
<meta name="description" content="Null Translation Group main hub for all the books we are currently translating" />
|
||||||
|
</Head>
|
||||||
<div className="mx-auto p-6 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
|
<div className="mx-auto p-6 bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
|
||||||
<div className="hidden md:block bg-yellow-500 text-black py-2 px-4 rounded-lg hover:bg-yellow-600 transition duration-200 mb-6">
|
<div className="hidden md:block bg-yellow-500 text-black py-2 px-4 rounded-lg hover:bg-yellow-600 transition duration-200 mb-6">
|
||||||
<a
|
<a
|
||||||
href="https://patreon.com/NullTranslationGroup"
|
href={Ad.patreon}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="font-semibold text-center block"
|
className="font-semibold text-center block"
|
||||||
@ -61,5 +69,7 @@ export default async function HomePage() {
|
|||||||
)})}
|
)})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { subDays } from "date-fns";
|
||||||
import { Book, Chapter, Editor } from "./types";
|
import { Book, Chapter, Editor } from "./types";
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL as string;
|
const API_URL = process.env.NEXT_PUBLIC_API_URL as string;
|
||||||
@ -48,6 +49,14 @@ export async function fetchBooks(): Promise<Book[]> {
|
|||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchChaptersRSS(): Promise<Chapter[]> {
|
||||||
|
const currentDateTime = new Date()
|
||||||
|
const yesterday = subDays(currentDateTime, 1);
|
||||||
|
const data = await fetchFromAPI<{ data: Chapter[] }>
|
||||||
|
(`/api/chapters?populate=book&filters[release_datetime][$lte]=${currentDateTime.toISOString()}&filters[release_datetime][$gte]=${yesterday.toISOString()}`);
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchBookChapterLinks(bookId: string): Promise<Book> {
|
export async function fetchBookChapterLinks(bookId: string): Promise<Book> {
|
||||||
const currentDateTime = new Date().toISOString();
|
const currentDateTime = new Date().toISOString();
|
||||||
const data = await fetchFromAPI<{ data: Book }>(`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}`);
|
const data = await fetchFromAPI<{ data: Book }>(`/api/books/${bookId}?populate[chapters][filters][release_datetime][$lte]=${currentDateTime}`);
|
||||||
|
@ -64,6 +64,6 @@ export interface Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Ad = {
|
export const Ad = {
|
||||||
patreon: "https://patreon.com/nulltranslationgroup",
|
patreon: "https://patreon.com/nulltranslationgroup/membership",
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user