mgply09ox: Publish an image post through a server upload pipeline
This commit is contained in:
@@ -1,17 +1,43 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { SESSION_COOKIE } from "@/lib/cookie";
|
||||
import { createTextPost } from "@/lib/post-create";
|
||||
import { createImagePost } from "@/lib/post-media";
|
||||
|
||||
// POST /api/posts { content }
|
||||
// Creates a public text draft on the feed, then publishes it. Returns { id }.
|
||||
// The create call can report status "draft" for a post already on the home
|
||||
// feed, so the browser reloads the first page and confirms the id there.
|
||||
// POST /api/posts
|
||||
// - JSON { content } -> public text post
|
||||
// - multipart/form-data content+file -> public image post (one file)
|
||||
// Both create a public draft on the feed, then publish it, and return { id }.
|
||||
// For an image the server also uploads the bytes to a presigned URL and confirms
|
||||
// the upload; the browser never receives the feed token. The create call can
|
||||
// report status "draft" for a post already on the home feed, so the browser
|
||||
// reloads the first page and confirms the id there.
|
||||
// 401 { error: "sign_in" } without a session.
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const sessionId = req.cookies.get(SESSION_COOKIE)?.value;
|
||||
const contentType = req.headers.get("content-type") ?? "";
|
||||
|
||||
if (contentType.includes("multipart/form-data")) {
|
||||
let content = "";
|
||||
let file: File | null = null;
|
||||
try {
|
||||
const form = await req.formData();
|
||||
const c = form.get("content");
|
||||
content = typeof c === "string" ? c : "";
|
||||
const f = form.get("file");
|
||||
file = f instanceof File ? f : null;
|
||||
} catch {
|
||||
return NextResponse.json({ error: "bad_file" }, { status: 400 });
|
||||
}
|
||||
const result = await createImagePost(sessionId, content, file);
|
||||
if (!result.ok) {
|
||||
return NextResponse.json({ error: result.error }, { status: result.status });
|
||||
}
|
||||
return NextResponse.json({ id: result.id });
|
||||
}
|
||||
|
||||
let content: unknown = null;
|
||||
try {
|
||||
content = ((await req.json()) as { content?: unknown }).content;
|
||||
|
||||
@@ -47,6 +47,14 @@ button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.composer textarea { font: inherit; width: 100%; padding: 12px; border: 1px solid #999; border-radius: 0; resize: vertical; background: #fff; color: var(--ink); }
|
||||
.composer textarea:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
|
||||
.composer .composer-post { align-self: flex-end; margin: 0; padding: 10px 24px; border-radius: 0; }
|
||||
.composer-media { display: flex; flex-direction: column; gap: 8px; }
|
||||
.composer-image { display: inline-flex; align-items: center; gap: 8px; align-self: flex-start; cursor: pointer; }
|
||||
.composer-image span { padding: 6px 12px; border: 1px solid #999; background: #fafafa; }
|
||||
.composer-image input[type="file"] { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); border: 0; }
|
||||
.composer-preview { display: flex; align-items: center; gap: 10px; }
|
||||
.composer-preview img { width: 64px; height: 64px; object-fit: cover; border: 1px solid var(--line); }
|
||||
.composer-filename { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.composer-remove { margin: 0; padding: 6px 12px; border-radius: 0; }
|
||||
|
||||
.comments { display: flex; flex-direction: column; gap: 10px; padding-top: 10px; border-top: 1px solid #eee; }
|
||||
.comment-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user