Compare commits
4 Commits
838e9730a4
...
406963f6bf
Author | SHA1 | Date | |
---|---|---|---|
406963f6bf | |||
f192e51f3c | |||
ad66be5039 | |||
9c78ed022d |
39
src/app/api/reports/route.ts
Normal file
39
src/app/api/reports/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
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";
|
||||
@ -50,6 +51,7 @@ export default async function ChapterPage(props: { params: paramsType}) {
|
||||
<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>
|
||||
);
|
||||
}
|
107
src/components/ReportButton.tsx
Normal file
107
src/components/ReportButton.tsx
Normal 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;
|
@ -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_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>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
@ -13,8 +40,9 @@ export async function fetchFromAPI<T>(
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
|
||||
const config: RequestInit = {
|
||||
method: "GET", // Default method is GET
|
||||
method: "GET",
|
||||
headers,
|
||||
...options,
|
||||
};
|
||||
@ -133,4 +161,36 @@ export async function fetchAnnouncementById(announcementId: string): Promise<Ann
|
||||
export async function fetchGlossaryByBookId(bookId: string): Promise<Glossary> {
|
||||
const data = await fetchFromAPI<Glossary>(`/api/glossaries?filters[book][documentId]=${bookId}`);
|
||||
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
31
src/middleware.ts
Normal 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*',
|
||||
};
|
Loading…
Reference in New Issue
Block a user