feat(dashboard): "My Renders" page for users
- /dashboard/renders: user's own render jobs (live status + progress bar + cancel) and finished exports (thumbnail + size/duration + download); bilingual fa/en - server lib my-renders.ts (user-scoped /v1/renders + /v1/exports via session JWT) - user action routes: POST /api/renders/[id]/cancel, GET /api/exports/[id]/download (presigned URL) - dashboard sidebar: "رندرهای من / My Renders" nav item Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { MyRenders } from "@/components/dashboard/MyRenders";
|
||||
import { ACTIVE_STEPS, listMyExports, listMyRenders } from "@/lib/api/my-renders";
|
||||
import { createPageMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = createPageMetadata({
|
||||
title: "My Renders",
|
||||
description: "Your render jobs and finished videos.",
|
||||
path: "/dashboard/renders",
|
||||
});
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function MyRendersPage() {
|
||||
const [allJobs, exports] = await Promise.all([listMyRenders(), listMyExports()]);
|
||||
// Show active/failed jobs in the "processing" section; Done ones live as exports.
|
||||
const jobs = allJobs.filter((j) => ACTIVE_STEPS.has(j.step) || j.step === "Failed");
|
||||
return <MyRenders jobs={jobs} exports={exports} />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { gatewayUrl } from "@/lib/api/gateway";
|
||||
import { getAccessToken } from "@/lib/auth/session";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
interface Ctx { params: { exportId: string } }
|
||||
|
||||
/** Return a short-lived presigned download URL for the caller's own export. */
|
||||
export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
const token = await getAccessToken();
|
||||
if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
const res = await fetch(gatewayUrl(`/v1/exports/${params.exportId}/download-url`), {
|
||||
cache: "no-store",
|
||||
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) return NextResponse.json({ error: data?.message ?? "Download failed" }, { status: res.status });
|
||||
return NextResponse.json(data ?? {});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { gatewayUrl } from "@/lib/api/gateway";
|
||||
import { getAccessToken } from "@/lib/auth/session";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
interface Ctx { params: { jobId: string } }
|
||||
|
||||
/** Cancel the caller's own render job (user-scoped). */
|
||||
export async function POST(_req: NextRequest, { params }: Ctx) {
|
||||
const token = await getAccessToken();
|
||||
if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
const res = await fetch(gatewayUrl(`/v1/renders/${params.jobId}/cancel`), {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) return NextResponse.json({ error: data?.message ?? "Cancel failed" }, { status: res.status });
|
||||
return NextResponse.json(data ?? { ok: true });
|
||||
}
|
||||
Reference in New Issue
Block a user