feat: add admin API routes for tags, timeline events, uploads, and uses management
Some checks failed
Deploy to VPS / deploy (push) Has been cancelled
Some checks failed
Deploy to VPS / deploy (push) Has been cancelled
- Implemented GET and POST endpoints for managing tags in `src/app/api/admin/tags/route.ts`. - Created PUT and DELETE endpoints for timeline events in `src/app/api/admin/timeline/[id]/route.ts`. - Added GET and POST endpoints for timeline management in `src/app/api/admin/timeline/route.ts`. - Developed file upload functionality with validation in `src/app/api/admin/upload/route.ts`. - Introduced PUT and DELETE endpoints for managing uses items in `src/app/api/admin/uses/[id]/route.ts`. - Added GET and POST endpoints for uses management in `src/app/api/admin/uses/route.ts`. feat: enhance admin UI components for better user experience - Created `AdminSidebar` component for navigation in `src/components/admin/AdminSidebar.tsx`. - Developed `ImageUpload` component for handling image uploads in `src/components/admin/ImageUpload.tsx`. - Implemented `RichTextEditor` component for rich text editing in `src/components/admin/RichTextEditor.tsx`. - Added `TagsInput` component for managing tags in `src/components/admin/TagsInput.tsx`. - Created `TiptapRenderer` component for rendering HTML content in `src/components/writing/TiptapRenderer.tsx`. feat: establish database interaction layer with Prisma - Added database connection and session management in `src/lib/db.ts` and `src/lib/auth.ts`. - Implemented CRUD operations for posts, products, projects, settings, timeline events, and uses items in respective files under `src/lib/db/`. - Introduced utility functions for formatting dates and slug generation in `src/lib/types.ts`.
This commit is contained in:
40
src/app/api/admin/auth/route.ts
Normal file
40
src/app/api/admin/auth/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getSession } from "@/lib/auth";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { username, password } = await req.json();
|
||||
if (!username || !password) {
|
||||
return Response.json({ error: "نام کاربری و رمز عبور الزامی است" }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.adminUser.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
return Response.json({ error: "نام کاربری یا رمز عبور اشتباه است" }, { status: 401 });
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password);
|
||||
if (!valid) {
|
||||
return Response.json({ error: "نام کاربری یا رمز عبور اشتباه است" }, { status: 401 });
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
session.userId = user.id;
|
||||
session.username = user.username;
|
||||
session.isLoggedIn = true;
|
||||
await session.save();
|
||||
|
||||
return Response.json({ success: true, username: user.username });
|
||||
} catch (err) {
|
||||
console.error("[auth/login]", err);
|
||||
return Response.json({ error: "خطای سرور" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE() {
|
||||
const session = await getSession();
|
||||
session.destroy();
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
33
src/app/api/admin/messages/[id]/route.ts
Normal file
33
src/app/api/admin/messages/[id]/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
interface Params { params: Promise<{ id: string }> }
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const { isRead } = await req.json();
|
||||
const msg = await prisma.contactMessage.update({
|
||||
where: { id },
|
||||
data: { isRead },
|
||||
});
|
||||
return Response.json(msg);
|
||||
} catch (err) {
|
||||
console.error("[messages/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
await prisma.contactMessage.delete({ where: { id } });
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[messages/DELETE]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
14
src/app/api/admin/messages/route.ts
Normal file
14
src/app/api/admin/messages/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAuth();
|
||||
const messages = await prisma.contactMessage.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
return Response.json(messages);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
57
src/app/api/admin/posts/[id]/route.ts
Normal file
57
src/app/api/admin/posts/[id]/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
interface Params { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const post = await prisma.post.findUnique({
|
||||
where: { id },
|
||||
include: { tags: { include: { tag: true } } },
|
||||
});
|
||||
if (!post) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
return Response.json(post);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const { tagIds, ...data } = body;
|
||||
|
||||
await prisma.postTag.deleteMany({ where: { postId: id } });
|
||||
const post = await prisma.post.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
tags: tagIds?.length
|
||||
? { create: tagIds.map((tid: string) => ({ tagId: tid })) }
|
||||
: undefined,
|
||||
},
|
||||
include: { tags: { include: { tag: true } } },
|
||||
});
|
||||
return Response.json(post);
|
||||
} catch (err) {
|
||||
console.error("[posts/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
await prisma.post.delete({ where: { id } });
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[posts/DELETE]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
40
src/app/api/admin/posts/route.ts
Normal file
40
src/app/api/admin/posts/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const locale = req.nextUrl.searchParams.get("locale") ?? "fa";
|
||||
const posts = await prisma.post.findMany({
|
||||
where: { locale },
|
||||
include: { tags: { include: { tag: true } } },
|
||||
orderBy: { publishedAt: "desc" },
|
||||
});
|
||||
return Response.json(posts);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const body = await req.json();
|
||||
const { tagIds, ...data } = body;
|
||||
|
||||
const post = await prisma.post.create({
|
||||
data: {
|
||||
...data,
|
||||
tags: tagIds?.length
|
||||
? { create: tagIds.map((id: string) => ({ tagId: id })) }
|
||||
: undefined,
|
||||
},
|
||||
include: { tags: { include: { tag: true } } },
|
||||
});
|
||||
return Response.json(post, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[posts/POST]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
42
src/app/api/admin/products/[id]/route.ts
Normal file
42
src/app/api/admin/products/[id]/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
interface Params { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const product = await prisma.product.findUnique({ where: { id } });
|
||||
if (!product) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
return Response.json(product);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const product = await prisma.product.update({ where: { id }, data: body });
|
||||
return Response.json(product);
|
||||
} catch (err) {
|
||||
console.error("[products/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
await prisma.product.delete({ where: { id } });
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[products/DELETE]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
29
src/app/api/admin/products/route.ts
Normal file
29
src/app/api/admin/products/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const locale = req.nextUrl.searchParams.get("locale") ?? "fa";
|
||||
const products = await prisma.product.findMany({
|
||||
where: { locale },
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
|
||||
});
|
||||
return Response.json(products);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const body = await req.json();
|
||||
const product = await prisma.product.create({ data: body });
|
||||
return Response.json(product, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[products/POST]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
42
src/app/api/admin/projects/[id]/route.ts
Normal file
42
src/app/api/admin/projects/[id]/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
interface Params { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const project = await prisma.project.findUnique({ where: { id } });
|
||||
if (!project) return Response.json({ error: "Not found" }, { status: 404 });
|
||||
return Response.json(project);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const project = await prisma.project.update({ where: { id }, data: body });
|
||||
return Response.json(project);
|
||||
} catch (err) {
|
||||
console.error("[projects/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
await prisma.project.delete({ where: { id } });
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[projects/DELETE]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
29
src/app/api/admin/projects/route.ts
Normal file
29
src/app/api/admin/projects/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const locale = req.nextUrl.searchParams.get("locale") ?? "fa";
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { locale },
|
||||
orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }],
|
||||
});
|
||||
return Response.json(projects);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const body = await req.json();
|
||||
const project = await prisma.project.create({ data: body });
|
||||
return Response.json(project, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[projects/POST]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
30
src/app/api/admin/settings/route.ts
Normal file
30
src/app/api/admin/settings/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAuth();
|
||||
const settings = await prisma.siteSettings.findUnique({ where: { id: "main" } });
|
||||
return Response.json(settings ?? {});
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const body = await req.json();
|
||||
const { id, updatedAt, ...data } = body;
|
||||
const settings = await prisma.siteSettings.upsert({
|
||||
where: { id: "main" },
|
||||
create: { id: "main", siteTitle: "Ali Taghavi", ...data },
|
||||
update: data,
|
||||
});
|
||||
return Response.json(settings);
|
||||
} catch (err) {
|
||||
console.error("[settings/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
45
src/app/api/admin/setup/route.ts
Normal file
45
src/app/api/admin/setup/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
// One-time setup endpoint — creates the admin user and default settings.
|
||||
// Disable or remove this route after first use in production.
|
||||
export async function POST(req: Request) {
|
||||
const setupKey = req.headers.get("x-setup-key");
|
||||
if (setupKey !== process.env.SETUP_KEY) {
|
||||
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await prisma.adminUser.findFirst();
|
||||
if (existing) {
|
||||
return Response.json({ error: "Admin already exists" }, { status: 409 });
|
||||
}
|
||||
|
||||
const { username, password } = await req.json();
|
||||
if (!username || !password || password.length < 8) {
|
||||
return Response.json({ error: "Username and password (min 8 chars) required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 12);
|
||||
const user = await prisma.adminUser.create({ data: { username, password: hashed } });
|
||||
|
||||
await prisma.siteSettings.upsert({
|
||||
where: { id: "main" },
|
||||
create: {
|
||||
id: "main",
|
||||
siteTitle: "Ali Taghavi",
|
||||
description: "Founder, builder, and strategist based in Tehran.",
|
||||
currentStatus: "در حال ساخت HyperAccount — زیرساخت هویت برای محصولات ایرانی",
|
||||
socialGithub: "https://github.com/alitaghavi",
|
||||
socialLinkedin: "https://linkedin.com/in/alitaghavi",
|
||||
socialTelegram: "https://t.me/alitaghavi",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
return Response.json({ success: true, userId: user.id });
|
||||
} catch (err) {
|
||||
console.error("[setup]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
29
src/app/api/admin/tags/route.ts
Normal file
29
src/app/api/admin/tags/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAuth();
|
||||
const tags = await prisma.tag.findMany({ orderBy: { title: "asc" } });
|
||||
return Response.json(tags);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { title, slug } = await req.json();
|
||||
const tag = await prisma.tag.upsert({
|
||||
where: { slug },
|
||||
create: { title, slug },
|
||||
update: { title },
|
||||
});
|
||||
return Response.json(tag, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[tags/POST]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
30
src/app/api/admin/timeline/[id]/route.ts
Normal file
30
src/app/api/admin/timeline/[id]/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
interface Params { params: Promise<{ id: string }> }
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const event = await prisma.timelineEvent.update({ where: { id }, data: body });
|
||||
return Response.json(event);
|
||||
} catch (err) {
|
||||
console.error("[timeline/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
await prisma.timelineEvent.delete({ where: { id } });
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[timeline/DELETE]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
27
src/app/api/admin/timeline/route.ts
Normal file
27
src/app/api/admin/timeline/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAuth();
|
||||
const events = await prisma.timelineEvent.findMany({
|
||||
orderBy: [{ sortOrder: "asc" }, { date: "desc" }],
|
||||
});
|
||||
return Response.json(events);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const body = await req.json();
|
||||
const event = await prisma.timelineEvent.create({ data: body });
|
||||
return Response.json(event, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[timeline/POST]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
44
src/app/api/admin/upload/route.ts
Normal file
44
src/app/api/admin/upload/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import path from "path";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"];
|
||||
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) return Response.json({ error: "No file provided" }, { status: 400 });
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
return Response.json({ error: "File type not allowed" }, { status: 400 });
|
||||
}
|
||||
if (file.size > MAX_SIZE) {
|
||||
return Response.json({ error: "File too large (max 10MB)" }, { status: 400 });
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
|
||||
const ext = file.name.split(".").pop() ?? "jpg";
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).slice(2, 8);
|
||||
const filename = `${timestamp}-${random}.${ext}`;
|
||||
|
||||
const uploadDir = path.join(process.cwd(), "public", "uploads");
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
await writeFile(path.join(uploadDir, filename), buffer);
|
||||
|
||||
return Response.json({ url: `/uploads/${filename}` });
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message === "Unauthorized") {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
console.error("[upload]", err);
|
||||
return Response.json({ error: "Upload failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
30
src/app/api/admin/uses/[id]/route.ts
Normal file
30
src/app/api/admin/uses/[id]/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
interface Params { params: Promise<{ id: string }> }
|
||||
|
||||
export async function PUT(req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const item = await prisma.usesItem.update({ where: { id }, data: body });
|
||||
return Response.json(item);
|
||||
} catch (err) {
|
||||
console.error("[uses/PUT]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Params) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const { id } = await params;
|
||||
await prisma.usesItem.delete({ where: { id } });
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[uses/DELETE]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
27
src/app/api/admin/uses/route.ts
Normal file
27
src/app/api/admin/uses/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireAuth();
|
||||
const items = await prisma.usesItem.findMany({
|
||||
orderBy: [{ category: "asc" }, { sortOrder: "asc" }],
|
||||
});
|
||||
return Response.json(items);
|
||||
} catch {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await requireAuth();
|
||||
const body = await req.json();
|
||||
const item = await prisma.usesItem.create({ data: body });
|
||||
return Response.json(item, { status: 201 });
|
||||
} catch (err) {
|
||||
console.error("[uses/POST]", err);
|
||||
return Response.json({ error: "Server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Resend } from "resend";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
@@ -11,23 +12,29 @@ export async function POST(req: NextRequest) {
|
||||
return Response.json({ error: "Missing required fields" }, { status: 400 });
|
||||
}
|
||||
|
||||
await resend.emails.send({
|
||||
from: "biztaghavi.com <noreply@biztaghavi.com>",
|
||||
to: [process.env.CONTACT_EMAIL ?? "ali@biztaghavi.com"],
|
||||
replyTo: email,
|
||||
subject: subject ? `[biztaghavi.com] ${subject}` : `[biztaghavi.com] پیام از ${name}`,
|
||||
text: `نام: ${name}\nایمیل: ${email}\n\n${message}`,
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px;">
|
||||
<p><strong>نام:</strong> ${name}</p>
|
||||
<p><strong>ایمیل:</strong> ${email}</p>
|
||||
${subject ? `<p><strong>موضوع:</strong> ${subject}</p>` : ""}
|
||||
<hr />
|
||||
<p style="white-space: pre-wrap;">${message}</p>
|
||||
</div>
|
||||
`,
|
||||
await prisma.contactMessage.create({
|
||||
data: { name, email, subject: subject ?? null, message },
|
||||
});
|
||||
|
||||
if (process.env.RESEND_API_KEY) {
|
||||
await resend.emails.send({
|
||||
from: "biztaghavi.com <noreply@biztaghavi.com>",
|
||||
to: [process.env.CONTACT_EMAIL ?? "ali@biztaghavi.com"],
|
||||
replyTo: email,
|
||||
subject: subject ? `[biztaghavi.com] ${subject}` : `[biztaghavi.com] پیام از ${name}`,
|
||||
text: `نام: ${name}\nایمیل: ${email}\n\n${message}`,
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px;">
|
||||
<p><strong>نام:</strong> ${name}</p>
|
||||
<p><strong>ایمیل:</strong> ${email}</p>
|
||||
${subject ? `<p><strong>موضوع:</strong> ${subject}</p>` : ""}
|
||||
<hr />
|
||||
<p style="white-space: pre-wrap;">${message}</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error("[contact]", err);
|
||||
|
||||
@@ -8,6 +8,7 @@ export async function POST(req: NextRequest) {
|
||||
return Response.json({ message: "Invalid secret" }, { status: 401 });
|
||||
}
|
||||
|
||||
revalidateTag("sanity", "default");
|
||||
revalidateTag("posts");
|
||||
revalidateTag("projects");
|
||||
return Response.json({ revalidated: true, now: Date.now() });
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { sanityFetch } from "@/lib/sanity/client";
|
||||
import { rssPostsQuery } from "@/lib/sanity/queries";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const SITE_URL = "https://biztaghavi.com";
|
||||
|
||||
export async function GET() {
|
||||
const posts = await sanityFetch<
|
||||
Array<{ _id: string; title: string; slug: { current: string }; excerpt?: string; publishedAt: string; category: string }>
|
||||
>(rssPostsQuery);
|
||||
const posts = await prisma.post.findMany({
|
||||
where: { locale: "fa", publishedAt: { lte: new Date() } },
|
||||
orderBy: { publishedAt: "desc" },
|
||||
take: 20,
|
||||
select: { id: true, title: true, slug: true, excerpt: true, publishedAt: true, category: true },
|
||||
});
|
||||
|
||||
const items = (posts ?? [])
|
||||
const items = posts
|
||||
.map((post) => {
|
||||
const url = `${SITE_URL}/writing/${post.slug.current}`;
|
||||
const url = `${SITE_URL}/writing/${post.slug}`;
|
||||
const date = post.publishedAt ? new Date(post.publishedAt).toUTCString() : "";
|
||||
return `
|
||||
<item>
|
||||
|
||||
Reference in New Issue
Block a user