Unverified Commit adf0d995 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

feat(panel): implement user-side modpack submissions with drag & drop context upload

parent 598f3d31
Loading
Loading
Loading
Loading
+59 −4
Changes for panel/dev/mockApi.ts: 59 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -853,10 +853,65 @@ async function handleImageRoute(ctx: SessionContext): Promise<boolean> {
}

async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> {
  if (ctx.parts[2] !== "submissions") return false;
  const isAdminSubmissions = ctx.parts[2] === "submissions";
  const isMeSubmissions = ctx.parts[2] === "me" && ctx.parts[3] === "submissions";
  if (!isAdminSubmissions && !isMeSubmissions) return false;

  // GET /api/v1/me/submissions
  if (isMeSubmissions && is("GET", ctx) && ctx.parts.length === 4) {
    const userSubs = ctx.state.submissions.filter((s) => s.submitted_by === ctx.account.email);
    sendJSON(ctx.res, 200, { submissions: userSubs });
    return true;
  }

  // POST /api/v1/me/submissions
  if (isMeSubmissions && is("POST", ctx) && ctx.parts.length === 4) {
    const body = await readJSON<{ display_name?: string }>(ctx.req);
    const displayName = body.display_name?.trim();
    if (!displayName) {
      sendError(ctx.res, 400, "bad_request", "display_name is required");
      return true;
    }
    const id = `sub-${Date.now()}`;
    const newSub: Submission = {
      id,
      submitted_by: ctx.account.email,
      display_name: displayName,
      context_ref: `minio/contexts/${id}/context.tar.gz`,
      status: "pending_review",
      created_at: new Date().toISOString(),
    };
    ctx.state.submissions.unshift(newSub);
    sendJSON(ctx.res, 201, newSub);
    return true;
  }

  // POST /api/v1/me/submissions/{id}/context
  if (isMeSubmissions && is("POST", ctx) && ctx.parts[5] === "context" && ctx.parts.length === 6) {
    const id = ctx.parts[4];
    const sub = ctx.state.submissions.find((s) => s.id === id);
    if (!sub) {
      sendError(ctx.res, 404, "not_found", "submission not found");
      return true;
    }
    if (sub.submitted_by !== ctx.account.email) {
      sendError(ctx.res, 404, "not_found", "submission not found");
      return true;
    }
    if (sub.status !== "pending_review") {
      sendError(ctx.res, 409, "already_reviewed", "submission has already been reviewed");
      return true;
    }

    // Read the body stream to end so the socket is clean
    for await (const _ of ctx.req) { /* discard */ }

    sendJSON(ctx.res, 200, sub);
    return true;
  }

  // GET /api/v1/submissions
  if (is("GET", ctx) && ctx.parts.length === 3) {
  if (isAdminSubmissions && is("GET", ctx) && ctx.parts.length === 3) {
    if (ctx.account.role !== "admin") {
      sendError(ctx.res, 403, "forbidden", "admin account required");
      return true;
@@ -866,7 +921,7 @@ async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> {
  }

  // POST /api/v1/submissions/{id}/approve
  if (is("POST", ctx) && ctx.parts[4] === "approve" && ctx.parts.length === 5) {
  if (isAdminSubmissions && is("POST", ctx) && ctx.parts[4] === "approve" && ctx.parts.length === 5) {
    if (ctx.account.role !== "admin") {
      sendError(ctx.res, 403, "forbidden", "admin account required");
      return true;
@@ -917,7 +972,7 @@ async function handleSubmissionRoute(ctx: SessionContext): Promise<boolean> {
  }

  // POST /api/v1/submissions/{id}/reject
  if (is("POST", ctx) && ctx.parts[4] === "reject" && ctx.parts.length === 5) {
  if (isAdminSubmissions && is("POST", ctx) && ctx.parts[4] === "reject" && ctx.parts.length === 5) {
    if (ctx.account.role !== "admin") {
      sendError(ctx.res, 403, "forbidden", "admin account required");
      return true;
+2 −0
Changes for panel/src/App.tsx: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ import { Account } from "@/pages/Account";
import { ImageAdmin } from "@/pages/admin/ImageAdmin";
import { ImageBuildPage } from "@/pages/admin/ImageBuildPage";
import { SubmissionsPage } from "@/pages/admin/SubmissionsPage";
import { MySubmissionsPage } from "@/pages/MySubmissionsPage";

// Three UX surfaces over two Zero-Trust tiers (DESIGN-WEB-3SIDES):
//   /        User-Side    — app-tier, every authenticated principal
@@ -45,6 +46,7 @@ export default function App() {
              <Route path="servers/:name" element={<ServerConsole />} />
              <Route path="servers/:name/players" element={<ServerPlayers />} />
              <Route path="servers/:name/backups" element={<ServerBackups />} />
              <Route path="submissions" element={<MySubmissionsPage />} />
              <Route path="account" element={<Account />} />

              {/* Admin-Side — admin-tier (server & content ops).
+4 −0
Changes for panel/src/i18n/index.ts: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ import enOps from "./resources/en-US/ops.json";
import enErrors from "./resources/en-US/errors.json";
import enNavigation from "./resources/en-US/navigation.json";
import enBackups from "./resources/en-US/backups.json";
import enSubmissions from "./resources/en-US/submissions.json";
import zhCommon from "./resources/zh-CN/common.json";
import zhAuth from "./resources/zh-CN/auth.json";
import zhDashboard from "./resources/zh-CN/dashboard.json";
@@ -21,6 +22,7 @@ import zhOps from "./resources/zh-CN/ops.json";
import zhErrors from "./resources/zh-CN/errors.json";
import zhNavigation from "./resources/zh-CN/navigation.json";
import zhBackups from "./resources/zh-CN/backups.json";
import zhSubmissions from "./resources/zh-CN/submissions.json";

i18next
  .use(LanguageDetector)
@@ -38,6 +40,7 @@ i18next
        errors: enErrors,
        navigation: enNavigation,
        backups: enBackups,
        submissions: enSubmissions,
      },
      "zh-CN": {
        common: zhCommon,
@@ -50,6 +53,7 @@ i18next
        errors: zhErrors,
        navigation: zhNavigation,
        backups: zhBackups,
        submissions: zhSubmissions,
      },
    },
    fallbackLng: "en-US",
+1 −0
Changes for panel/src/i18n/resources/en-US/navigation.json: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
{
  "dashboard": "Dashboard",
  "my_servers": "Servers",
  "my_submissions": "My Submissions",
  "account": "Account",
  "admin_section": "Admin",
  "admin_images": "Images",
+33 −0
Changes for panel/src/i18n/resources/en-US/submissions.json: 33 added lines, 0 removed lines.
Original line number Diff line number Diff line
{
  "title": "My Submissions",
  "subtitle": "Submit your custom modpacks for review and track their build status. Approved submissions will automatically trigger safe builds.",
  "submit_card_title": "Submit New Modpack",
  "submit_card_desc": "Provide a name and upload your build context gzip tarball (.tar.gz).",
  "display_name_label": "Display Name",
  "display_name_placeholder": "e.g., Pixelmon Adventure Pack",
  "file_label": "Build Context (.tar.gz)",
  "file_drag_hint": "Drag and drop a .tar.gz file here, or click to browse",
  "file_selected": "Selected file: {{name}} ({{size}})",
  "submit_btn": "Submit",
  "submitting_create": "Creating submission...",
  "submitting_upload": "Uploading build context...",
  "submit_success": "Modpack submitted successfully!",
  "list_card_title": "Submission History",
  "filter_all": "All Statuses",
  "status_pending_review": "Pending Review",
  "status_approved": "Approved",
  "status_rejected": "Rejected",
  "table_display_name": "Modpack Name",
  "table_status": "Status",
  "table_created_at": "Submitted At",
  "table_reviewed_by": "Reviewed By",
  "table_image_ref": "Image Reference",
  "table_reject_reason": "Reject Reason",
  "no_submissions_title": "No Submissions Found",
  "no_submissions_hint": "You haven't submitted any modpacks yet.",
  "search_placeholder": "Search submissions...",
  "error_file_type": "Please upload a valid .tar.gz file.",
  "error_file_size": "File exceeds the allowed size limit.",
  "error_name_required": "Display name is required.",
  "error_file_required": "Build context file is required."
}
Loading