Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Blog Submission - No-code Blog Feature #318

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
64 changes: 64 additions & 0 deletions app/api/approve-blog/route.ts
Original file line number Diff line number Diff line change
@@ -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;
47 changes: 47 additions & 0 deletions app/api/submit-blog/route.ts
Original file line number Diff line number Diff line change
@@ -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);
173 changes: 173 additions & 0 deletions app/blog/secret-admin-page/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Blog[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="flex h-screen items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
);
}

if (error) {
return (
<div className="flex h-screen items-center justify-center">{error}</div>
);
}

return (
<Card className="mt-20 w-full">
<CardHeader>
<CardTitle>Approve Blogs</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Title</TableHead>
<TableHead>URL</TableHead>
<TableHead>Excerpt</TableHead>
<TableHead>Date</TableHead>
<TableHead>Author</TableHead>
<TableHead>Tag</TableHead>
<TableHead>Content</TableHead>
<TableHead>Reading Time</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{blogs.map((blog) => (
<TableRow key={blog.id}>
<TableCell>{blog.id}</TableCell>
<TableCell>{blog.title}</TableCell>
<TableCell>{blog.url}</TableCell>
<TableCell>{blog.excerpt}</TableCell>
<TableCell>{blog.date}</TableCell>
<TableCell>{blog.author}</TableCell>
<TableCell>{blog.tag || "N/A"}</TableCell>
<TableCell>{blog.content.substring(0, 50)}...</TableCell>
<TableCell>{blog.readingtime} min</TableCell>
<TableCell>{blog.status}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleApprove(blog.id)}>
Approve
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleReject(blog.id)}>
Reject
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
}
Loading