added supabase

This commit is contained in:
Austin
2026-02-20 09:12:47 -06:00
parent ecbc399ec4
commit b54ad064ff
54 changed files with 3162 additions and 445 deletions
@@ -0,0 +1,116 @@
import { createClient } from "@/utils/supabase/server";
import { notFound } from "next/navigation";
import { updateAuthor } from "../../actions";
interface PageProps {
params: Promise<{ name: string }>;
}
export default async function EditAuthorPage({ params }: PageProps) {
const { name } = await params;
const decodedName = decodeURIComponent(name);
const supabase = await createClient();
const { data: author, error } = await supabase
.from("authors")
.select("name, bio, x_link, fb_link, insta_link, pfp_link")
.eq("name", decodedName)
.single();
if (error || !author) {
notFound();
}
return (
<div>
<h1 className="text-3xl font-bold mb-8">Edit Author</h1>
<div className="bg-white rounded-lg shadow-sm p-6 max-w-2xl">
<form action={updateAuthor} className="space-y-4">
<input type="hidden" name="originalName" value={author.name} />
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
type="text"
name="name"
defaultValue={author.name}
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bio
</label>
<textarea
name="bio"
rows={4}
defaultValue={author.bio}
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Profile Picture URL
</label>
<input
type="text"
name="pfp_link"
defaultValue={author.pfp_link || ""}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
X (Twitter) Link
</label>
<input
type="text"
name="x_link"
defaultValue={author.x_link || ""}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Facebook Link
</label>
<input
type="text"
name="fb_link"
defaultValue={author.fb_link || ""}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Instagram Link
</label>
<input
type="text"
name="insta_link"
defaultValue={author.insta_link || ""}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="submit"
className="bg-accent text-white px-6 py-2 rounded-md hover:bg-accent-dark"
>
Update Author
</button>
<a
href="/admin/authors"
className="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</a>
</div>
</form>
</div>
</div>
);
}
+71
View File
@@ -0,0 +1,71 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
export async function createAuthor(formData: FormData) {
const supabase = await createClient();
const name = formData.get("name") as string;
const bio = formData.get("bio") as string;
const x_link = (formData.get("x_link") as string) || null;
const fb_link = (formData.get("fb_link") as string) || null;
const insta_link = (formData.get("insta_link") as string) || null;
const pfp_link = (formData.get("pfp_link") as string) || null;
const { error } = await supabase.from("authors").insert({
name,
bio,
x_link,
fb_link,
insta_link,
pfp_link,
});
if (error) {
throw new Error(`Failed to create author: ${error.message}`);
}
revalidatePath("/authors");
revalidatePath("/admin/authors");
redirect("/admin/authors");
}
export async function updateAuthor(formData: FormData) {
const supabase = await createClient();
const originalName = formData.get("originalName") as string;
const name = formData.get("name") as string;
const bio = formData.get("bio") as string;
const x_link = (formData.get("x_link") as string) || null;
const fb_link = (formData.get("fb_link") as string) || null;
const insta_link = (formData.get("insta_link") as string) || null;
const pfp_link = (formData.get("pfp_link") as string) || null;
const { error } = await supabase
.from("authors")
.update({ name, bio, x_link, fb_link, insta_link, pfp_link })
.eq("name", originalName);
if (error) {
throw new Error(`Failed to update author: ${error.message}`);
}
revalidatePath("/authors");
revalidatePath("/admin/authors");
redirect("/admin/authors");
}
export async function deleteAuthor(name: string) {
const supabase = await createClient();
const { error } = await supabase.from("authors").delete().eq("name", name);
if (error) {
throw new Error(`Failed to delete author: ${error.message}`);
}
revalidatePath("/authors");
revalidatePath("/admin/authors");
}
@@ -0,0 +1,89 @@
import { createAuthor } from "../actions";
export default function NewAuthorPage() {
return (
<div>
<h1 className="text-3xl font-bold mb-8">New Author</h1>
<div className="bg-white rounded-lg shadow-sm p-6 max-w-2xl">
<form action={createAuthor} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
type="text"
name="name"
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bio
</label>
<textarea
name="bio"
rows={4}
required
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Profile Picture URL
</label>
<input
type="text"
name="pfp_link"
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
X (Twitter) Link
</label>
<input
type="text"
name="x_link"
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Facebook Link
</label>
<input
type="text"
name="fb_link"
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Instagram Link
</label>
<input
type="text"
name="insta_link"
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="submit"
className="bg-accent text-white px-6 py-2 rounded-md hover:bg-accent-dark"
>
Create Author
</button>
<a
href="/admin/authors"
className="px-6 py-2 border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</a>
</div>
</form>
</div>
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { deleteAuthor } from "./actions";
export default async function AdminAuthorsPage() {
const supabase = await createClient();
const { data: authors } = await supabase
.from("authors")
.select("name, bio, x_link, fb_link, insta_link, pfp_link")
.order("name");
return (
<div>
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Authors</h1>
<Link
href="/admin/authors/new"
className="bg-accent text-white px-4 py-2 rounded-md hover:bg-accent-dark"
>
New Author
</Link>
</div>
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Bio
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Links
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{authors?.map((author) => (
<tr key={author.name} className="hover:bg-gray-50">
<td className="px-6 py-4 font-medium">{author.name}</td>
<td className="px-6 py-4">
<p className="text-gray-600 line-clamp-2 max-w-md">
{author.bio}
</p>
</td>
<td className="px-6 py-4">
<div className="flex gap-2">
{author.x_link && (
<span className="text-xs bg-gray-100 px-2 py-1 rounded">
X
</span>
)}
{author.fb_link && (
<span className="text-xs bg-gray-100 px-2 py-1 rounded">
FB
</span>
)}
{author.insta_link && (
<span className="text-xs bg-gray-100 px-2 py-1 rounded">
IG
</span>
)}
</div>
</td>
<td className="px-6 py-4 text-right space-x-2">
<Link
href={`/admin/authors/${encodeURIComponent(author.name)}/edit`}
className="text-sm text-accent hover:text-accent-dark"
>
Edit
</Link>
<form
action={deleteAuthor.bind(null, author.name)}
className="inline"
>
<button
type="submit"
className="text-sm text-red-600 hover:text-red-800"
>
Delete
</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
{(!authors || authors.length === 0) && (
<p className="text-gray-500 text-center py-8">No authors found.</p>
)}
</div>
</div>
);
}