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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
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_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,
|
||||||
};
|
};
|
||||||
@ -133,4 +161,36 @@ export async function fetchAnnouncementById(announcementId: string): Promise<Ann
|
|||||||
export async function fetchGlossaryByBookId(bookId: string): Promise<Glossary> {
|
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
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