Skip to content

Commit 4933789

Browse files
committed
Add interactive project submission form with GitHub OAuth
- Multi-step form wizard with 5 steps (repo details, project info, location, optional details, review) - Real-time validation and GitHub repo auto-fetch - Auto-generated TOML preview before submission - Automatic PR creation with user's GitHub token - Optional logo upload included in PR - Form state auto-saves to localStorage - API routes for repo validation and project submission - Success page with next steps after submission - Updated main submit page with prominent CTA for quick form - Supports both manual Git workflow and new form-based submission
1 parent 8ed3600 commit 4933789

File tree

18 files changed

+1984
-7
lines changed

18 files changed

+1984
-7
lines changed

app/api/submit-project/route.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { auth } from "@/lib/auth";
3+
import { ProjectSubmissionSchema } from "@/lib/schema";
4+
import { generateTOML } from "@/lib/toml-generator";
5+
import { createProjectPR } from "@/lib/github";
6+
import { getProjectFiles } from "@/lib/projects";
7+
import { z } from "zod";
8+
9+
// Extended schema to include optional logo
10+
const SubmissionWithLogoSchema = ProjectSubmissionSchema.extend({
11+
logoFile: z
12+
.object({
13+
content: z.string(), // base64
14+
filename: z.string(),
15+
})
16+
.optional(),
17+
});
18+
19+
export async function POST(request: NextRequest) {
20+
try {
21+
// Check authentication
22+
const session = await auth();
23+
24+
if (!session || !session.accessToken || !session.user) {
25+
return NextResponse.json(
26+
{ error: "Unauthorized. Please sign in with GitHub." },
27+
{ status: 401 }
28+
);
29+
}
30+
31+
// Parse and validate request body
32+
const body = await request.json();
33+
const validationResult = SubmissionWithLogoSchema.safeParse(body);
34+
35+
if (!validationResult.success) {
36+
return NextResponse.json(
37+
{
38+
error: "Validation failed",
39+
details: validationResult.error.issues,
40+
},
41+
{ status: 400 }
42+
);
43+
}
44+
45+
const data = validationResult.data;
46+
47+
// Check for duplicate slug
48+
const existingProjects = getProjectFiles();
49+
const slugExists = existingProjects.some(
50+
(file) => file.replace(".toml", "") === data.slug
51+
);
52+
53+
if (slugExists) {
54+
return NextResponse.json(
55+
{
56+
error: `A project with slug "${data.slug}" already exists. Please choose a different slug.`,
57+
},
58+
{ status: 409 }
59+
);
60+
}
61+
62+
// Generate TOML content
63+
const tomlContent = generateTOML(data);
64+
65+
// Prepare logo data if provided
66+
let logoData;
67+
if (data.logoFile) {
68+
logoData = {
69+
content: data.logoFile.content,
70+
filename: data.logoFile.filename,
71+
};
72+
}
73+
74+
// Create PR using user's access token
75+
const pr = await createProjectPR({
76+
slug: data.slug,
77+
content: tomlContent,
78+
submitterName: session.user.name || undefined,
79+
userToken: session.accessToken,
80+
logo: logoData,
81+
});
82+
83+
return NextResponse.json({
84+
success: true,
85+
prUrl: pr.url,
86+
prNumber: pr.number,
87+
message: "Pull request created successfully!",
88+
});
89+
} catch (error: any) {
90+
console.error("Submit project error:", error);
91+
92+
// Handle specific errors
93+
if (error.message?.includes("branch")) {
94+
return NextResponse.json(
95+
{ error: "Failed to create branch. Please try again." },
96+
{ status: 500 }
97+
);
98+
}
99+
100+
if (error.status === 401 || error.status === 403) {
101+
return NextResponse.json(
102+
{
103+
error: "GitHub authentication failed. Please sign in again.",
104+
requiresAuth: true,
105+
},
106+
{ status: 401 }
107+
);
108+
}
109+
110+
return NextResponse.json(
111+
{ error: error.message || "Failed to create pull request" },
112+
{ status: 500 }
113+
);
114+
}
115+
}

app/api/validate-repo/route.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getRepoMetadata, hasFossradarTopic, isRepoAccessible } from "@/lib/github";
3+
4+
export async function GET(request: NextRequest) {
5+
try {
6+
const { searchParams } = new URL(request.url);
7+
const repoUrl = searchParams.get("repoUrl");
8+
9+
if (!repoUrl) {
10+
return NextResponse.json(
11+
{ error: "Repository URL is required" },
12+
{ status: 400 }
13+
);
14+
}
15+
16+
// Validate GitHub URL format
17+
const githubUrlRegex = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/;
18+
if (!githubUrlRegex.test(repoUrl)) {
19+
return NextResponse.json(
20+
{ error: "Invalid GitHub repository URL format" },
21+
{ status: 400 }
22+
);
23+
}
24+
25+
// Check if repository is accessible
26+
const accessible = await isRepoAccessible(repoUrl);
27+
if (!accessible) {
28+
return NextResponse.json(
29+
{ error: "Repository not found or is private" },
30+
{ status: 404 }
31+
);
32+
}
33+
34+
// Fetch repository metadata
35+
const metadata = await getRepoMetadata(repoUrl);
36+
if (!metadata) {
37+
return NextResponse.json(
38+
{ error: "Failed to fetch repository metadata" },
39+
{ status: 500 }
40+
);
41+
}
42+
43+
// Check for fossradar topic
44+
const hasTopic = await hasFossradarTopic(repoUrl);
45+
46+
// Return all data
47+
return NextResponse.json({
48+
accessible: true,
49+
hasFossradarTopic: hasTopic,
50+
metadata: {
51+
name: metadata.description || "",
52+
description: metadata.description || "",
53+
stars: metadata.stars,
54+
language: metadata.language || "",
55+
license: metadata.license || "Unknown",
56+
homepage: metadata.homepage || "",
57+
topics: metadata.topics,
58+
archived: metadata.archived,
59+
},
60+
});
61+
} catch (error: any) {
62+
console.error("Validate repo error:", error);
63+
return NextResponse.json(
64+
{ error: "Failed to validate repository" },
65+
{ status: 500 }
66+
);
67+
}
68+
}

app/submit/form/page.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ProjectSubmissionForm } from "@/components/ProjectSubmissionForm";
2+
import { TricolorRadar } from "@/components/TricolorRadar";
3+
import { Github, Map } from "lucide-react";
4+
import Link from "next/link";
5+
import type { Metadata } from "next";
6+
7+
export const metadata: Metadata = {
8+
title: "Quick Submit Form - FOSSRadar.in",
9+
description: "Submit your open source project to FOSSRadar.in using our quick and easy submission form",
10+
};
11+
12+
export default function SubmitFormPage() {
13+
return (
14+
<div className="min-h-screen bg-gray-50 dark:bg-black">
15+
{/* Header */}
16+
<header className="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
17+
<div className="container mx-auto px-4 py-4 sm:py-6">
18+
<div className="flex items-center justify-between gap-4">
19+
<Link href="/" className="flex items-start gap-2 sm:gap-3 flex-1 min-w-0">
20+
<TricolorRadar className="h-8 w-8 sm:h-10 sm:w-10 flex-shrink-0 mt-1" />
21+
<div className="min-w-0">
22+
<h1 className="text-3xl sm:text-4xl text-gray-900 dark:text-gray-100 tracking-wider truncate" style={{ fontFamily: 'var(--font-vt323)' }}>
23+
fossradar
24+
</h1>
25+
<p className="text-gray-600 dark:text-gray-400 mt-1 text-xs sm:text-sm truncate">
26+
Submit Your Project
27+
</p>
28+
</div>
29+
</Link>
30+
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
31+
<Link
32+
href="/radar"
33+
className="p-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 transition-colors"
34+
aria-label="Radar"
35+
>
36+
<Map className="h-4 w-4" />
37+
</Link>
38+
<Link
39+
href="https://github.com/wbfoss/fossradar"
40+
target="_blank"
41+
rel="noopener noreferrer"
42+
className="p-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 transition-colors"
43+
aria-label="GitHub Repository"
44+
>
45+
<Github className="h-5 w-5" />
46+
</Link>
47+
</div>
48+
</div>
49+
</div>
50+
</header>
51+
52+
{/* Main Content */}
53+
<main className="container mx-auto px-4 py-8 sm:py-12">
54+
<div className="mb-8 text-center">
55+
<h1 className="text-3xl sm:text-4xl font-heading font-normal text-gray-900 dark:text-gray-100 mb-3">
56+
Quick Project Submission
57+
</h1>
58+
<p className="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
59+
Complete this form to submit your project. We'll create a pull request automatically!
60+
</p>
61+
<p className="text-sm text-gray-500 dark:text-gray-500 mt-2">
62+
Prefer the manual method?{" "}
63+
<Link href="/submit" className="text-blue-600 dark:text-blue-400 hover:underline">
64+
View Git-based workflow
65+
</Link>
66+
</p>
67+
</div>
68+
69+
<ProjectSubmissionForm />
70+
</main>
71+
</div>
72+
);
73+
}

app/submit/page.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,52 @@ export default function SubmitPage() {
160160
</p>
161161
</div>
162162

163+
{/* Quick Form CTA */}
164+
<div className="mb-12 p-6 sm:p-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 text-white">
165+
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
166+
<div className="flex-1">
167+
<h2 className="text-2xl sm:text-3xl font-heading font-normal mb-3 tracking-wide">
168+
New: Quick Submission Form
169+
</h2>
170+
<p className="text-base sm:text-lg text-blue-50 leading-relaxed">
171+
Skip the Git workflow! Use our interactive form to submit your project in minutes.
172+
We'll create the pull request automatically for you.
173+
</p>
174+
<ul className="mt-4 space-y-2 text-sm text-blue-50">
175+
<li className="flex items-center gap-2">
176+
<CheckCircle2 className="h-4 w-4" />
177+
Auto-fetch project details from GitHub
178+
</li>
179+
<li className="flex items-center gap-2">
180+
<CheckCircle2 className="h-4 w-4" />
181+
Real-time validation and guidance
182+
</li>
183+
<li className="flex items-center gap-2">
184+
<CheckCircle2 className="h-4 w-4" />
185+
Automatic pull request creation
186+
</li>
187+
</ul>
188+
</div>
189+
<div className="flex-shrink-0">
190+
<Link
191+
href="/submit/form"
192+
className="inline-flex items-center gap-2 px-8 py-4 rounded-lg bg-white text-blue-600 hover:bg-blue-50 font-semibold transition-colors shadow-lg text-lg"
193+
>
194+
<Plus className="h-5 w-5" />
195+
Use Quick Form
196+
<ExternalLink className="h-4 w-4" />
197+
</Link>
198+
</div>
199+
</div>
200+
</div>
201+
202+
{/* Divider */}
203+
<div className="mb-12 flex items-center gap-4">
204+
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-800"></div>
205+
<span className="text-sm text-gray-500 dark:text-gray-400">or use the traditional Git workflow</span>
206+
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-800"></div>
207+
</div>
208+
163209
{/* Eligibility Section */}
164210
<div className="mb-12">
165211
<div className="flex items-center gap-3 mb-4">

0 commit comments

Comments
 (0)