Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions app/api/generate-portfolio/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { NextRequest, NextResponse } from "next/server";
import { generateJobId, generationStatus } from "./storage";
import { createAdminClient, DATABASE_ID, JOBS_COLLECTION_ID } from "@/lib/appwrite";
import { PORTFOLIO_TEMPLATES } from "@/lib/templates";

// Fallback function using OpenAI-compatible API (Groq is free and fast)
async function generateWithGroq(userInfo: string) {
async function generateWithGroq(userInfo: string, templateId?: string) {
const apiKey = process.env.GROQ_API_KEY;
if (!apiKey) {
throw new Error("GROQ_API_KEY not configured");
}

const template = templateId && PORTFOLIO_TEMPLATES[templateId as keyof typeof PORTFOLIO_TEMPLATES]
? PORTFOLIO_TEMPLATES[templateId as keyof typeof PORTFOLIO_TEMPLATES]
: null;

const templatePrompt = template ? `\n\nSpecific Style Requirements (${template.name}):\n${template.systemPrompt}` : "";

const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: {
Expand Down Expand Up @@ -41,7 +48,7 @@ Requirements:
9. Ensure the design is visually appealing and professional
10. Extract and organize all relevant information from the user's data

Return ONLY the complete HTML code, no explanations or markdown code blocks. The HTML should be ready to render directly.`
Return ONLY the complete HTML code, no explanations or markdown code blocks. The HTML should be ready to render directly.${templatePrompt}`
}
],
temperature: 0.7,
Expand All @@ -65,6 +72,7 @@ export async function POST(request: NextRequest) {
const details = formData.get("details") as string;
const cvFile = formData.get("cv") as File | null;
const selectedModel = formData.get("model") as string || "gemini-2.5-flash";
const template = formData.get("template") as string || "";

let userInfo = details || "";

Expand Down Expand Up @@ -117,7 +125,7 @@ export async function POST(request: NextRequest) {
}

// Start async generation (don't await)
generatePortfolioAsync(jobId, userInfo, selectedModel, useDatabase);
generatePortfolioAsync(jobId, userInfo, selectedModel, useDatabase, template);

// Return job ID immediately
return NextResponse.json({
Expand All @@ -135,8 +143,9 @@ export async function POST(request: NextRequest) {
}

// Async generation function
async function generatePortfolioAsync(jobId: string, userInfo: string, selectedModel: string, useDatabase: boolean = true) {
async function generatePortfolioAsync(jobId: string, userInfo: string, selectedModel: string, useDatabase: boolean = true, templateId?: string) {
console.log(`[${jobId}] Starting portfolio generation with Groq using ${useDatabase ? 'database' : 'in-memory'} storage...`);
if (templateId) console.log(`[${jobId}] Requested template: ${templateId}`);

try {
let portfolio = "";
Expand All @@ -145,8 +154,8 @@ async function generatePortfolioAsync(jobId: string, userInfo: string, selectedM
// Use Groq for generation
try {
console.log(`[${jobId}] Using Groq Llama 3.3 70B...`);
portfolio = await generateWithGroq(userInfo);
usedProvider = "Groq Llama 3.3 70B";
portfolio = await generateWithGroq(userInfo, templateId);
usedProvider = `Groq Llama 3.3 70B (${templateId || 'Default'})`;
console.log(`[${jobId}] Portfolio generated successfully with Groq, length:`, portfolio.length);
} catch (groqError) {
console.error(`[${jobId}] Groq failed:`, (groqError as Error).message);
Expand Down
5 changes: 4 additions & 1 deletion app/api/generate-portfolio/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export async function GET(request: NextRequest) {
}

console.log(`[Status Check] Job ${jobId} not found in database or memory`);
console.log(`[Status Check] Available jobs in memory:`, Array.from(generationStatus.keys()));
console.log(`[Status Check] Current memory job count: ${generationStatus.size}`);
if (generationStatus.size > 0) {
console.log(`[Status Check] Available jobs in memory:`, Array.from(generationStatus.keys()).slice(0, 5));
}
return NextResponse.json({ error: "Job not found" }, { status: 404 });
}
} catch (error) {
Expand Down
22 changes: 15 additions & 7 deletions app/api/generate-portfolio/storage.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
// Shared in-memory storage for generation status
// In production, replace with Redis or a database
export const generationStatus = new Map<string, {
status: 'processing' | 'completed' | 'failed';
portfolio?: string;
error?: string;
provider?: string;
}>();
// Use globalThis to persist during HMR in development
const globalForGeneration = globalThis as unknown as {
generationStatus: Map<string, {
status: 'processing' | 'completed' | 'failed';
portfolio?: string;
error?: string;
provider?: string;
}>;
};

export const generationStatus = globalForGeneration.generationStatus || new Map();

if (process.env.NODE_ENV !== 'production') {
globalForGeneration.generationStatus = generationStatus;
}
Comment on lines +12 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Next.js 15 App Router production runtime, can different route handlers that import the same source file still execute with separate module instances/bundles, making module-scoped in-memory state non-shared unless stored on globalThis?

💡 Result:

Yes, in Next.js 15 App Router production runtime, different route handlers that import the same source file execute with separate module instances, making module-scoped in-memory state non-shared unless stored on globalThis.

Citations:


🏁 Script executed:

# Read the storage.ts file to confirm the current implementation
cat -n app/api/generate-portfolio/storage.ts

Repository: knurdz/Portfolium

Length of output: 863


🏁 Script executed:

# Check the route handlers that import generationStatus
cat -n app/api/generate-portfolio/route.ts | head -20
cat -n app/api/generate-portfolio/status/route.ts | head -20

Repository: knurdz/Portfolium

Length of output: 1959


Persist generationStatus on globalThis in production as well.

Line 14 limits global assignment to non-production environments. In Next.js 15 App Router production runtime, separate route handlers can execute with isolated module instances. Both app/api/generate-portfolio/route.ts and app/api/generate-portfolio/status/route.ts import generationStatus, but without persisting to globalThis in production, they may reference different Map instances, causing status queries to miss updates from the writer route.

🔧 Suggested fix
-const globalForGeneration = globalThis as unknown as {
-  generationStatus: Map<string, {
-    status: 'processing' | 'completed' | 'failed';
-    portfolio?: string;
-    error?: string;
-    provider?: string;
-  }>;
-};
-
-export const generationStatus = globalForGeneration.generationStatus || new Map();
-
-if (process.env.NODE_ENV !== 'production') {
-  globalForGeneration.generationStatus = generationStatus;
-}
+type GenerationStatusEntry = {
+  status: 'processing' | 'completed' | 'failed';
+  portfolio?: string;
+  error?: string;
+  provider?: string;
+};
+
+const globalForGeneration = globalThis as typeof globalThis & {
+  generationStatus?: Map<string, GenerationStatusEntry>;
+};
+
+export const generationStatus =
+  (globalForGeneration.generationStatus ??= new Map<string, GenerationStatusEntry>());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/generate-portfolio/storage.ts` around lines 12 - 16, The code only
sets globalForGeneration.generationStatus in non-production, causing separate
module instances in production to have different Map objects; always persist the
Map onto the shared global (globalForGeneration/globalThis) so both writer and
reader routes share the same generationStatus. Update the initialization for the
generationStatus constant and the assignment to ensure
globalForGeneration.generationStatus = generationStatus runs in production too
(i.e., remove the NODE_ENV check) and ensure you reference the existing
globalForGeneration/globalThis container used elsewhere so the Map is stored on
the shared global object.


// Generate unique job ID
export function generateJobId(): string {
Expand Down
40 changes: 26 additions & 14 deletions app/auth/check-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,60 @@ import { useSearchParams } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Mail, AlertCircle, Sparkles } from "lucide-react";
import { Suspense } from "react";
import { ThemeToggle } from "@/components/theme-toggle";

function CheckEmailContent() {
const searchParams = useSearchParams();
const error = searchParams.get("error");

return (
<div className="flex min-h-screen flex-col items-center justify-center p-6 bg-linear-to-br from-[#F9FAFB] via-[#EEF2FF] to-[#E0E7FF]">
<Card className="w-full max-w-md border-[#E5E7EB] shadow-lg">
<div className="flex min-h-screen flex-col items-center justify-center p-6 bg-background relative overflow-hidden">
{/* Theme Toggle */}
<div className="absolute top-6 right-6 z-50">
<ThemeToggle />
</div>

{/* Subtle Background Decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl"></div>
</div>

<Card className="w-full max-w-md border-border shadow-lg bg-card/95 backdrop-blur-sm relative z-10">
<CardHeader className="text-center space-y-3 pb-4">
<div className="flex justify-center mb-1">
<div className="w-16 h-16 bg-linear-to-br from-[#4F46E5] to-[#6366F1] rounded-xl flex items-center justify-center shadow-md">
<Mail className="w-8 h-8 text-white" />
<div className="w-16 h-16 bg-linear-to-br from-primary to-primary/80 rounded-xl flex items-center justify-center shadow-md">
<Mail className="w-8 h-8 text-primary-foreground" />
</div>
</div>
<CardTitle className="text-2xl font-bold text-[#111827] tracking-tight">
<CardTitle className="text-2xl font-bold text-foreground tracking-tight">
Check your email
</CardTitle>
<CardDescription className="text-sm text-[#6B7280]">
<CardDescription className="text-sm text-muted-foreground">
We&apos;ve sent you a verification link
</CardDescription>
</CardHeader>

<CardContent className="space-y-4 px-6 pb-6">
{error && (
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 mt-0.5 shrink-0" />
<p className="text-xs text-red-600 leading-relaxed">{error}</p>
<div className="flex items-start gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<AlertCircle className="h-4 w-4 text-destructive mt-0.5 shrink-0" />
<p className="text-xs text-destructive leading-relaxed">{error}</p>
</div>
)}

<div className="text-center space-y-3">
<p className="text-sm text-[#6B7280]">
<p className="text-sm text-muted-foreground">
We have sent a verification link to your email address.
</p>
<p className="text-sm text-[#6B7280]">
<p className="text-sm text-muted-foreground">
Please check your inbox and click the link to verify your account before accessing the dashboard.
</p>
</div>

<div className="flex items-start gap-2 p-3 bg-blue-50 border border-blue-200 rounded-lg mt-4">
<Sparkles className="h-4 w-4 text-blue-600 mt-0.5 shrink-0" />
<p className="text-xs text-blue-600 leading-relaxed">
<div className="flex items-start gap-2 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg mt-4">
<Sparkles className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<p className="text-xs text-blue-500 leading-relaxed">
Don&apos;t see the email? Check your spam folder or request a new verification link.
</p>
</div>
Expand Down
50 changes: 28 additions & 22 deletions app/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,62 +15,68 @@ import {
import { Mail, ArrowLeft, Sparkles, AlertCircle } from "lucide-react";

import { forgotPassword } from "@/lib/actions/auth";
import { ThemeToggle } from "@/components/theme-toggle";

export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [state, formAction, isPending] = useActionState(forgotPassword, null);

return (
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-[#F9FAFB] via-[#EEF2FF] to-[#E0E7FF] px-4 py-8">
<div className="min-h-screen flex items-center justify-center bg-background px-4 py-8 relative overflow-hidden">
{/* Theme Toggle */}
<div className="absolute top-6 right-6 z-50">
<ThemeToggle />
</div>

{/* Subtle Background Decoration */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-[#4F46E5]/5 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-[#0EA5E9]/5 rounded-full blur-3xl"></div>
<div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl"></div>
</div>

<Card className="w-full max-w-[440px] shadow-xl border-[#D1D5DB] bg-white/95 backdrop-blur-sm relative z-10">
<Card className="w-full max-w-[440px] shadow-xl border-border bg-card/95 backdrop-blur-sm relative z-10">
<CardHeader className="space-y-2 text-center pb-6">
{/* Logo/Brand */}
<div className="flex justify-center mb-1">
<Link href="/" className="w-12 h-12 bg-linear-to-br from-[#4F46E5] to-[#6366F1] rounded-xl flex items-center justify-center shadow-md hover:shadow-lg transition-shadow cursor-pointer">
<Sparkles className="w-6 h-6 text-white" />
<Link href="/" className="w-12 h-12 bg-linear-to-br from-primary to-primary/80 rounded-xl flex items-center justify-center shadow-md hover:shadow-lg transition-shadow cursor-pointer">
<Sparkles className="w-6 h-6 text-primary-foreground" />
</Link>
</div>
<CardTitle className="text-2xl font-bold text-[#111827] tracking-tight">
<CardTitle className="text-2xl font-bold text-foreground tracking-tight">
Forgot your password?
</CardTitle>
<CardDescription className="text-sm text-[#6B7280]">
<CardDescription className="text-sm text-muted-foreground">
Enter the email associated with your account
</CardDescription>
</CardHeader>

<CardContent className="space-y-5 px-6 pb-6">
{/* Error Message */}
{state?.error && (
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="h-4 w-4 text-red-600 mt-0.5 shrink-0" />
<p className="text-xs text-red-600 leading-relaxed">
<div className="flex items-start gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<AlertCircle className="h-4 w-4 text-destructive mt-0.5 shrink-0" />
<p className="text-xs text-destructive leading-relaxed">
{state.error}
</p>
</div>
)}

{/* Info Message */}
<div className="flex items-start gap-2 p-3 bg-[#EEF2FF] border border-[#C7D2FE] rounded-lg">
<Mail className="h-4 w-4 text-[#4F46E5] mt-0.5 shrink-0" />
<p className="text-xs text-[#4F46E5] leading-relaxed">
<div className="flex items-start gap-2 p-3 bg-primary/10 border border-primary/20 rounded-lg">
<Mail className="h-4 w-4 text-primary mt-0.5 shrink-0" />
<p className="text-xs text-primary leading-relaxed">
We&apos;ll send you a link to reset your password
</p>
</div>

{/* Email Input Form */}
<form action={formAction} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="email" className="text-sm font-medium text-[#111827]">
<Label htmlFor="email" className="text-sm font-medium text-foreground">
Email address
</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#9CA3AF]" />
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="email"
name="email"
Expand All @@ -79,7 +85,7 @@ export default function ForgotPasswordPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="h-11 pl-10 border-[#D1D5DB] focus-visible:ring-[#6366F1] focus-visible:ring-2 focus-visible:border-[#6366F1] transition-all placeholder:text-gray-400"
className="h-11 pl-10 border-input focus-visible:ring-primary focus-visible:ring-2 focus-visible:border-primary transition-all placeholder:text-muted-foreground/50"
/>
</div>
</div>
Expand All @@ -88,7 +94,7 @@ export default function ForgotPasswordPage() {
<Button
type="submit"
disabled={isPending}
className="w-full h-11 bg-linear-to-r from-[#4F46E5] to-[#6366F1] hover:from-[#3730A3] hover:to-[#4F46E5] text-white font-semibold transition-all shadow-md hover:shadow-lg disabled:opacity-50"
className="w-full h-11 bg-primary hover:bg-primary/90 text-primary-foreground font-semibold transition-all shadow-md hover:shadow-lg disabled:opacity-50"
>
{isPending ? "Sending..." : "Send Reset Link"}
</Button>
Expand All @@ -97,15 +103,15 @@ export default function ForgotPasswordPage() {
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-[#E5E7EB]" />
<span className="w-full border-t border-border" />
</div>
</div>

{/* Back to Sign In Link */}
<div className="text-center mt-8">
<Link
href="/auth/signin"
className="inline-flex items-center gap-2 text-sm font-medium text-[#6B7280] hover:text-[#4F46E5] transition-colors group"
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-primary transition-colors group"
>
<ArrowLeft className="h-4 w-4 group-hover:-translate-x-0.5 transition-transform" />
<span>Back to Sign In</span>
Expand All @@ -114,10 +120,10 @@ export default function ForgotPasswordPage() {

{/* Alternative: Create Account */}
<div className="text-center text-sm pt-2">
<span className="text-[#6B7280]">Don&apos;t have an account? </span>
<span className="text-muted-foreground">Don&apos;t have an account? </span>
<Link
href="/auth/signup"
className="font-semibold text-[#4F46E5] hover:text-[#3730A3] transition-colors hover:underline"
className="font-semibold text-primary hover:text-primary/80 transition-colors hover:underline"
>
Sign up
</Link>
Expand Down
Loading