Compare commits

...

4 Commits

5 changed files with 240 additions and 1 deletions

View File

@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const API_TOKEN = process.env.STRAPI_API_TOKEN;
const body = await request.json();
if (!API_URL || !API_TOKEN) {
return NextResponse.json({ message: 'Server configuration error' }, {status: 500});
}
try {
const response = await fetch(`${API_URL}/api/reports`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) {
return NextResponse.json({ error: data }, { status: response.status });
}
return NextResponse.json({data: data}, {status: response.status});
} catch (error) {
console.error('Error handling request:', error);
NextResponse.json({message: 'Going to server error'}, {status: 500});
}
} catch (error) {
console.error('Error handling request:', error);
return NextResponse.json({ message: 'Internal Server Error'}, { status: 500 });
}
}

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import NavigationButtons from "@/components/NavigationButtons"; import NavigationButtons from "@/components/NavigationButtons";
import ReportButton from "@/components/ReportButton";
import ChapterRenderer from "@/components/ChapterContentRenderer"; import ChapterRenderer from "@/components/ChapterContentRenderer";
import { Chapter } from "@/lib/types"; import { Chapter } from "@/lib/types";
import { fetchChapterByBookId, fetchGlossaryByBookId } from "@/lib/api"; import { fetchChapterByBookId, fetchGlossaryByBookId } from "@/lib/api";
@ -50,6 +51,7 @@ export default async function ChapterPage(props: { params: paramsType}) {
<div className="pt-4"></div> <div className="pt-4"></div>
<ChapterRenderer content={chapter_content_html} glossary={english_glossary} /> <ChapterRenderer content={chapter_content_html} glossary={english_glossary} />
<NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/> <NavigationButtons bookId={bookId} documentId={chapterId} prevChapter={prev_chapter} nextChapter={next_chapter}/>
<ReportButton bookId={bookId} chapterId={chapterId} />
</div> </div>
); );
} }

View File

@ -0,0 +1,107 @@
"use client"
import React, { useState, useRef } from "react";
import { createReport
} from "@/lib/api";
interface ReportButtonProps {
bookId: string;
chapterId: string;
}
const ReportButton: React.FC<ReportButtonProps> = ({ bookId, chapterId }) => {
const modalRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false)
const [errorType, setErrorType] = useState('')
const [details, setDetails] = useState('')
const handleSubmitReport = async (event: React.FormEvent<HTMLFormElement>) => {
// Implement report submission here
event.preventDefault()
const response = await createReport(errorType,details,bookId,chapterId)
//Linting be linting
if (response.status === 201){
alert('Report submitted successfully')
}
else{
alert('Failed to submit report')
}
setErrorType('')
setDetails('')
setIsOpen(false)
}
const handleExit = (event: React.MouseEvent) => {
setErrorType('')
setDetails('')
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
event.stopPropagation()
}
return (
<div className="flex justify-center pt-4"
onClick={handleExit}>
<button onClick={() => setIsOpen(true)} className="px-4 py-2 bg-red-600 text-white font-semibold rounded-lg shadow-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2"> Report </button>
{isOpen && (<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div
className="dark:bg-gray-800 bg-white w-11/12 md:w-1/2 lg:w-1/3 p-6 rounded-lg shadow-lg relative"
ref={modalRef}
onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-center border-b pb-3">
<h3 className="text-lg font-semibold ">Report Chapter</h3>
</div>
<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 dark:text-gray-300" htmlFor="error-type">
Select Error Type :
</label>
<select
id="error-type"
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 focus:border-blue-500"
value={errorType}
onChange={(e)=> setErrorType(e.target.value)}
>
<option value="">Select your Type</option>
<option value="Spelling Error">Spelling Error</option>
<option value="Pronoun Error">Pronoun Error</option>
<option value="Formatting Error">Formatting Error</option>
<option value="Missing Content">Missing Content</option>
</select>
</div>
<div>
<label className="block text-md font-semibold mb-1" htmlFor="details">
Additional Details :
</label>
<textarea
id="details"
placeholder="Provide additional details"
value={details}
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"
className="px-4 py-2 bg-green-500 text-white font-medium rounded-lg hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2"
>
Submit
</button>
<button
onClick={handleExit}
className="px-4 py-2 dark:bg-gray-500 bg-gray-200 rounded-lg hover:bg-gray-300"
>
Close
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
);
}
export default ReportButton;

View File

@ -4,6 +4,33 @@ import { Book, Chapter, Editor, Announcement, Glossary } from "./types";
const API_URL = process.env.NEXT_PUBLIC_API_URL as string; const API_URL = process.env.NEXT_PUBLIC_API_URL as string;
const API_TOKEN = process.env.STRAPI_API_TOKEN as string; const API_TOKEN = process.env.STRAPI_API_TOKEN as string;
export async function createFromAPI<T>(
endpoint: string,
payload: string,
options: RequestInit = {},
): Promise<T> {
const headers: HeadersInit = {
Authorization: `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
};
const config: RequestInit = {
method: "POST",
headers,
body: payload,
...options,
}
const response = await fetch(`${API_URL}${endpoint}`, config)
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`)
}
return response.json()
}
export async function fetchFromAPI<T>( export async function fetchFromAPI<T>(
endpoint: string, endpoint: string,
options: RequestInit = {} options: RequestInit = {}
@ -13,8 +40,9 @@ export async function fetchFromAPI<T>(
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
const config: RequestInit = { const config: RequestInit = {
method: "GET", // Default method is GET method: "GET",
headers, headers,
...options, ...options,
}; };
@ -134,3 +162,35 @@ export async function fetchGlossaryByBookId(bookId: string): Promise<Glossary> {
const data = await fetchFromAPI<Glossary>(`/api/glossaries?filters[book][documentId]=${bookId}`); const data = await fetchFromAPI<Glossary>(`/api/glossaries?filters[book][documentId]=${bookId}`);
return data[0]; return data[0];
} }
export async function createReport(
error_type: string,
details: string,
book_id: string,
chapter_id: string,
) {
const payload = {
data: {
error_type: error_type,
details: details,
book: book_id,
chapter: chapter_id
}
}
const response = await fetch(
'/api/reports',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
}
)
if (!response.ok) {
throw new Error(`API request failed with status ${response.status}`)
}
return response
}

31
src/middleware.ts Normal file
View File

@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
const rateLimitCache = new Map<string, { timestamp: number; count: number }>();
const RATE_LIMIT_WINDOW_MS = 60000;
const RATE_LIMIT_MAX_REQUESTS = 5;
export function middleware(request: Request) {
const ip = request.headers.get('x-forwarded-for') || '127.0.0.1';
const now = Date.now();
const userRateLimit = rateLimitCache.get(ip) || { count: 0, timestamp: now };
if (now - userRateLimit.timestamp > RATE_LIMIT_WINDOW_MS) {
rateLimitCache.set(ip, { count: 1, timestamp: now });
} else if (userRateLimit.count >= RATE_LIMIT_MAX_REQUESTS) {
return NextResponse.json(
{ message: 'Rate limit exceeded. Please wait a moment.' },
{ status: 429 }
);
} else {
userRateLimit.count += 1;
rateLimitCache.set(ip, userRateLimit);
}
return NextResponse.next();
}
// Apply middleware only to API routes
export const config = {
matcher: '/api/:path*',
};