diff --git a/app/api/approve-blog/route.ts b/app/api/approve-blog/route.ts new file mode 100644 index 000000000..87ae59784 --- /dev/null +++ b/app/api/approve-blog/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@/utils/supabase/server"; +import { withAuth } from "@/utils/withAuth"; +// Todo only admin can approve blog +async function updateBlogStatus(request: NextRequest) { + const supabase = createClient(); + + try { + const { id, status } = await request.json(); + + const { data, error } = await supabase + .from("blog") + .update({ status: status }) + .eq("id", id); + + if (error) { + console.error("Error updating blog status:", error); + return NextResponse.json( + { error: "Failed to update blog status" }, + { status: 500 }, + ); + } + + return NextResponse.json({ + message: "Blog status updated successfully", + data, + }); + } catch (error) { + console.error("Error parsing request body:", error); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); + } +} + +async function getBlog(request: NextRequest) { + const supabase = createClient(); + + try { + const { data, error } = await supabase + .from("blog") + .select("*") + .eq("status", "pending"); + + if (error) { + return NextResponse.json( + { error: "failed to fetch blogs" }, + { status: 500 }, + ); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error("Error parsing request body:", error); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 }, + ); + } +} + +export const POST = updateBlogStatus; +export const GET = getBlog; diff --git a/app/api/submit-blog/route.ts b/app/api/submit-blog/route.ts new file mode 100644 index 000000000..032f4424a --- /dev/null +++ b/app/api/submit-blog/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from "next/server"; +import { withAuth } from "@/utils/withAuth"; +import { createClient } from "@/utils/supabase/server"; + +// TODO add rate limiting to this endpoint to prevent spam submissions +async function submitBlog(request: NextRequest) { + const supabase = createClient(); + + try { + const { title, url, excerpt, date, author, tag, content, readingTime } = + await request.json(); + console.log("request", request); + + const { data: blog, error } = await supabase.from("blog").insert([ + { + title, + url, + excerpt, + date, + author, + readingtime: readingTime, + tag, + content, + status: "pending", + }, + ]); + console.log("blog", blog); + + if (error) { + console.error("error submitting blog", error); + return NextResponse.json( + { error: "Error submitting blog. Please contact PearAI team." }, + { status: 500 }, + ); + } + + return NextResponse.json({ blog }); + } catch (error) { + console.error("error submitting blog", error); + return NextResponse.json( + { error: "Error submitting blog. Please contact PearAI team." }, + { status: 500 }, + ); + } +} + +export const POST = withAuth(submitBlog); diff --git a/app/blog/secret-admin-page/page.tsx b/app/blog/secret-admin-page/page.tsx new file mode 100644 index 000000000..52b2af88b --- /dev/null +++ b/app/blog/secret-admin-page/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { MoreHorizontal, Loader2 } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +interface Blog { + id: number; + title: string; + url: string; + excerpt: string; + date: string; + author: string; + tag: string | null; + content: string; + readingtime: number; + status: string; +} + +export default function BlogApproval() { + const [blogs, setBlogs] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchBlogs(); + }, []); + + const fetchBlogs = async () => { + try { + const response = await fetch("/api/approve-blog"); + if (!response.ok) { + throw new Error("Failed to fetch blogs"); + } + const data = await response.json(); + setBlogs(data); + } catch (err) { + setError("Failed to load blogs. Please try again."); + } finally { + setIsLoading(false); + } + }; + + const handleApprove = async (blogId: number) => { + try { + const response = await fetch("/api/approve-blog", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: blogId, status: "approved" }), + }); + + if (!response.ok) { + throw new Error("Failed to approve blog"); + } + + // Update the local state to reflect the change + } catch (err) { + setError("Failed to approve blog."); + } + }; + + const handleReject = async (blogId: number) => { + try { + const response = await fetch("/api/approve-blog", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: blogId, status: "rejected" }), + }); + + if (!response.ok) { + throw new Error("Failed to reject blog"); + } + + // Update the local state to reflect the change + } catch (err) { + setError("Failed to reject blog."); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
{error}
+ ); + } + + return ( + + + Approve Blogs + + + + + + ID + Title + URL + Excerpt + Date + Author + Tag + Content + Reading Time + Status + Actions + + + + {blogs.map((blog) => ( + + {blog.id} + {blog.title} + {blog.url} + {blog.excerpt} + {blog.date} + {blog.author} + {blog.tag || "N/A"} + {blog.content.substring(0, 50)}... + {blog.readingtime} min + {blog.status} + + + + + + + handleApprove(blog.id)}> + Approve + + handleReject(blog.id)}> + Reject + + + + + + ))} + +
+
+
+ ); +} diff --git a/app/blog/submit-blog/page.tsx b/app/blog/submit-blog/page.tsx new file mode 100644 index 000000000..54dc8b2ea --- /dev/null +++ b/app/blog/submit-blog/page.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Loader2 } from "lucide-react"; + +export default function SubmitBlog() { + const [blogData, setBlogData] = useState({ + title: "", + url: "", + excerpt: "", + date: "", + author: "", + readingTime: "", + thumbnail: "", + tags: "", + content: "", + }); + const [isLoading, setIsLoading] = useState(false); + + const handleChange = ( + e: React.ChangeEvent, + ) => { + const { name, value } = e.target; + setBlogData({ ...blogData, [name]: value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + try { + // Make the API call to submit the blog + const response = await fetch("/api/submit-blog", { + method: "POST", + body: JSON.stringify(blogData), + headers: { + "Content-Type": "application/json", + }, + }); + + const data = await response.json(); + console.log("Response:", data); + + // Reset form or show success message + } catch (error) { + console.error("Error submitting blog:", error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + Submit Your Blog + + Fill in the details to submit your blog post. + + + +
+
+ + +
+
+ + +
+
+ +