Manage account details and API configuration.
+
-
-
-
- Account
-
-
- Signed in with Convex Auth.
- Profile management can be added here in a later phase.
-
-
-
-
-
- API Keys
-
-
-
-
OpenAI
-
Set `OPENAI_API_KEY` in your `.env` file.
-
-
-
Serper (optional)
-
Set `SERPER_API_KEY` in your `.env` file for more reliable search.
-
- Reload server after changes
-
-
+
+
+ Account Center
+ Active
+
+
Account & Billing
+
+ Manage your subscription, billing, and account details in one place.
+
-
-
- Billing
-
-
- Billing is not configured yet. Add Stripe or another provider when ready.
-
-
+
{
+ setTab(value)
+ router.replace(`/settings?tab=${value}`)
+ }}
+ className="relative"
+ >
+
+
+ Account
+ Billing
+
+
+ Current plan
+ Starter
+
+
+
+
+
+
+
+ Profile
+
+
+
+
+
{user?.name || user?.email || "Not provided"}
+
{user?.email || "Not provided"}
+
+ {/* TODO: Wire profile editing flow. */}
+
Coming soon.
+
+
+
+
+
Name
+
{user?.name || "Not provided"}
+
+
+
Email
+
{user?.email || "Not provided"}
+
+
+
Phone
+
{user?.phone || "Not provided"}
+
+
+
Sign-in Methods
+
+ {accounts.length > 0
+ ? Array.from(new Set(accounts.map((account) => {
+ if (account.provider === "password") return "Password";
+ if (account.provider === "google") return "Google";
+ return account.provider;
+ }))).join(", ")
+ : "Not provided"}
+
+
+
+
Email Verified
+
+ {user?.emailVerificationTime
+ ? new Date(user.emailVerificationTime).toLocaleDateString()
+ : "Not verified"}
+
+
+
+
User ID
+
{user?._id || "Not provided"}
+
+
+ {/* TODO: Wire security management flow. */}
+ Coming soon.
+
+
+
+
+
+ Integrations
+
+
+
+
+ {/* TODO: Replace with real provider status. */}
+
Coming soon.
+
Coming soon.
+
+
Linked
+
+
+
+ {/* TODO: Replace with real provider status. */}
+
Coming soon.
+
Coming soon.
+
+ {/* TODO: Wire provider disconnect. */}
+
Coming soon.
+
+
+
+ {/* TODO: Replace with real provider status. */}
+
Coming soon.
+
Coming soon.
+
+ {/* TODO: Wire provider connect. */}
+
Coming soon.
+
+
+
+
+
+
+
+
+
+
+ Plan
+
+
+
+
+
Starter
+
Upgrade to unlock full opportunity search and automation.
+
+
+ Subscribe to Pro
+
+
+
+
+
Pro includes
+
+
Unlimited projects and data sources
+
Advanced opportunity search
+
Priority analysis queue
+
+
+
+
+
+
+
+ Billing History
+
+
+
+
+
No invoices yet
+
Invoices will appear after your first payment.
+
+
Pro
+
+
+ Need a receipt? Complete checkout to generate your first invoice.
+
+
+ Subscribe to Pro
+
+
+
+
+
+
+
)
}
diff --git a/app/api/analysis/reprompt/route.ts b/app/api/analysis/reprompt/route.ts
new file mode 100644
index 0000000..f80164e
--- /dev/null
+++ b/app/api/analysis/reprompt/route.ts
@@ -0,0 +1,87 @@
+import { NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+import { convexAuthNextjsToken, isAuthenticatedNextjs } from "@convex-dev/auth/nextjs/server";
+import { fetchMutation, fetchQuery } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+import { scrapeWebsite, analyzeFromText } from "@/lib/scraper";
+import { repromptSection } from "@/lib/analysis-pipeline";
+
+const bodySchema = z.object({
+ analysisId: z.string().min(1),
+ sectionKey: z.enum([
+ "profile",
+ "features",
+ "competitors",
+ "keywords",
+ "problems",
+ "personas",
+ "useCases",
+ "dorkQueries",
+ ]),
+ prompt: z.string().optional(),
+});
+
+export async function POST(request: NextRequest) {
+ if (!(await isAuthenticatedNextjs())) {
+ const redirectUrl = new URL("/auth", request.url);
+ const referer = request.headers.get("referer");
+ const nextPath = referer ? new URL(referer).pathname + new URL(referer).search : "/";
+ redirectUrl.searchParams.set("next", nextPath);
+ return NextResponse.redirect(redirectUrl);
+ }
+
+ const body = await request.json();
+ const parsed = bodySchema.parse(body);
+ const token = await convexAuthNextjsToken();
+
+ const analysis = await fetchQuery(
+ api.analyses.getById,
+ { analysisId: parsed.analysisId as any },
+ { token }
+ );
+
+ if (!analysis) {
+ return NextResponse.json({ error: "Analysis not found." }, { status: 404 });
+ }
+
+ const dataSource = await fetchQuery(
+ api.dataSources.getById,
+ { dataSourceId: analysis.dataSourceId as any },
+ { token }
+ );
+
+ if (!dataSource) {
+ return NextResponse.json({ error: "Data source not found." }, { status: 404 });
+ }
+
+ const isManual = dataSource.url.startsWith("manual:") || dataSource.url === "manual-input";
+ const featureText = (analysis.features || []).map((f: any) => f.name).join("\n");
+ const content = isManual
+ ? await analyzeFromText(
+ analysis.productName,
+ analysis.description || "",
+ featureText
+ )
+ : await scrapeWebsite(dataSource.url);
+
+ const items = await repromptSection(
+ parsed.sectionKey,
+ content,
+ analysis as any,
+ parsed.prompt
+ );
+
+ await fetchMutation(
+ api.analysisSections.replaceSection,
+ {
+ analysisId: parsed.analysisId as any,
+ sectionKey: parsed.sectionKey,
+ items,
+ lastPrompt: parsed.prompt,
+ source: "ai",
+ },
+ { token }
+ );
+
+ return NextResponse.json({ success: true, items });
+}
diff --git a/app/api/checkout/route.ts b/app/api/checkout/route.ts
new file mode 100644
index 0000000..16ef5b9
--- /dev/null
+++ b/app/api/checkout/route.ts
@@ -0,0 +1,21 @@
+import { Checkout } from "@polar-sh/nextjs";
+import { NextResponse } from "next/server";
+
+export const GET = async () => {
+ if (!process.env.POLAR_ACCESS_TOKEN || !process.env.POLAR_SUCCESS_URL) {
+ return NextResponse.json(
+ {
+ error:
+ "Missing POLAR_ACCESS_TOKEN or POLAR_SUCCESS_URL environment variables.",
+ },
+ { status: 400 }
+ );
+ }
+
+ const handler = Checkout({
+ accessToken: process.env.POLAR_ACCESS_TOKEN,
+ successUrl: process.env.POLAR_SUCCESS_URL,
+ });
+
+ return handler();
+};
diff --git a/app/api/opportunities/route.ts b/app/api/opportunities/route.ts
index 174aa4d..aa7ef07 100644
--- a/app/api/opportunities/route.ts
+++ b/app/api/opportunities/route.ts
@@ -17,7 +17,9 @@ const searchSchema = z.object({
icon: z.string().optional(),
enabled: z.boolean(),
searchTemplate: z.string().optional(),
- rateLimit: z.number()
+ rateLimit: z.number(),
+ site: z.string().optional(),
+ custom: z.boolean().optional()
})),
strategies: z.array(z.string()),
maxResults: z.number().default(50)
@@ -115,9 +117,21 @@ export async function POST(request: NextRequest) {
);
}
+ const resultUrls = Array.from(
+ new Set(searchResults.map((result) => result.url).filter(Boolean))
+ )
+ const existingUrls = await fetchQuery(
+ api.seenUrls.listExisting,
+ { projectId: projectId as any, urls: resultUrls },
+ { token }
+ )
+ const existingSet = new Set(existingUrls)
+ const newUrls = resultUrls.filter((url) => !existingSet.has(url))
+ const filteredResults = searchResults.filter((result) => !existingSet.has(result.url))
+
// Score and rank
console.log(' Scoring opportunities...')
- const opportunities = scoreOpportunities(searchResults, analysis as EnhancedProductAnalysis)
+ const opportunities = scoreOpportunities(filteredResults, analysis as EnhancedProductAnalysis)
console.log(` โ Scored ${opportunities.length} opportunities`)
if (jobId) {
await fetchMutation(
@@ -135,18 +149,22 @@ export async function POST(request: NextRequest) {
);
}
+ if (newUrls.length > 0) {
+ await fetchMutation(
+ api.seenUrls.markSeenBatch,
+ { projectId: projectId as any, urls: newUrls, source: "search" },
+ { token }
+ );
+ }
+
return NextResponse.json({
success: true,
data: {
opportunities: opportunities.slice(0, 50),
stats: {
queriesGenerated: queries.length,
- rawResults: searchResults.length,
- opportunitiesFound: opportunities.length,
- highRelevance: opportunities.filter(o => o.relevanceScore >= 0.7).length,
- averageScore: opportunities.length > 0
- ? opportunities.reduce((a, o) => a + o.relevanceScore, 0) / opportunities.length
- : 0
+ rawResults: filteredResults.length,
+ opportunitiesFound: opportunities.length
},
queries: queries.map(q => ({
query: q.query,
diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx
index fa89e09..d0d5c6b 100644
--- a/app/onboarding/page.tsx
+++ b/app/onboarding/page.tsx
@@ -356,7 +356,7 @@ export default function OnboardingPage() {
- Describe Your Product
+ Product Details
Enter your product details and we'll extract the key information.
@@ -373,7 +373,7 @@ export default function OnboardingPage() {