From d4cd02d99e71d69d2a20fe8f57313053991dc546 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 13:43:45 +0530 Subject: [PATCH 1/8] fix vulnerabilities --- middleware.ts | 67 +++++++++- next.config.ts | 20 ++- src/app/dashboard/code-police/[id]/page.tsx | 16 +-- .../dashboard/code-police/connect/page.tsx | 25 ++-- src/app/dashboard/database/connect/page.tsx | 73 +++++------ src/app/dashboard/equity/new/page.tsx | 1 + src/app/dashboard/equity/portfolio/page.tsx | 1 - src/app/dashboard/page.tsx | 1 - src/app/dashboard/pitch-deck/[id]/page.tsx | 22 ++-- .../dashboard/pitch-deck/studio/[id]/page.tsx | 90 ++++++------- src/app/dashboard/settings/page.tsx | 13 +- src/app/page.tsx | 92 +++++++++++--- src/components/ui/features-section-demo-3.tsx | 35 ++--- src/lib/security/rate-limit.ts | 120 ++++++++++++++++++ 14 files changed, 410 insertions(+), 166 deletions(-) create mode 100644 src/lib/security/rate-limit.ts diff --git a/middleware.ts b/middleware.ts index 34cae11..dc103a6 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,10 +1,13 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; /** * ============================================================================ - * CLERK MIDDLEWARE - CENTRALIZED AUTHENTICATION + * CLERK MIDDLEWARE - CENTRALIZED AUTHENTICATION & SECURITY * ============================================================================ * This middleware protects routes and handles authentication across the app. + * Also adds security headers to all responses. * * Route Protection: * - Public routes: Landing page, sign-in, sign-up, public API endpoints @@ -14,6 +17,7 @@ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; * 1. Public routes are accessible to everyone * 2. Protected routes redirect to sign-in if not authenticated * 3. API routes return 401 if not authenticated (handled in route handlers) + * 4. Security headers are added to all responses */ // Define public routes that don't require authentication @@ -22,13 +26,71 @@ const isPublicRoute = createRouteMatcher([ "/sign-in(.*)", // Sign in page and sub-routes "/sign-up(.*)", // Sign up page and sub-routes "/api/webhooks/(.*)", // Webhook endpoints (verified by webhook secret) + "/api/health", // Health check endpoint ]); -export default clerkMiddleware(async (auth, request) => { +/** + * Security headers to protect against common attacks + */ +const securityHeaders = { + // Prevent clickjacking + "X-Frame-Options": "DENY", + // Prevent MIME type sniffing + "X-Content-Type-Options": "nosniff", + // Control referrer information + "Referrer-Policy": "strict-origin-when-cross-origin", + // Restrict browser features + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + // XSS protection (legacy, but still useful) + "X-XSS-Protection": "1; mode=block", +}; + +/** + * Content Security Policy - adjust as needed for your specific requirements + */ +const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://clerk.com https://*.clerk.accounts.dev https://challenges.cloudflare.com https://prod.spline.design; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + img-src 'self' blob: data: https: http:; + font-src 'self' https://fonts.gstatic.com data:; + connect-src 'self' https://*.clerk.accounts.dev https://clerk.com https://api.clerk.com https://*.github.com https://api.github.com wss://*.clerk.accounts.dev https://prod.spline.design https://*.firebase.com https://*.firebaseio.com https://*.googleapis.com; + frame-src 'self' https://clerk.com https://*.clerk.accounts.dev https://challenges.cloudflare.com https://prod.spline.design; + worker-src 'self' blob:; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; +`.replace(/\s{2,}/g, ' ').trim(); + +export default clerkMiddleware(async (auth, request: NextRequest) => { // Protect all routes except public ones if (!isPublicRoute(request)) { await auth.protect(); } + + // Get response (will be created by next middleware/handler) + const response = NextResponse.next(); + + // Add security headers + for (const [key, value] of Object.entries(securityHeaders)) { + response.headers.set(key, value); + } + + // Add CSP header (only in production to avoid dev issues) + if (process.env.NODE_ENV === "production") { + response.headers.set("Content-Security-Policy", cspHeader); + } + + // Add HSTS header (only in production over HTTPS) + if (process.env.NODE_ENV === "production") { + response.headers.set( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains; preload" + ); + } + + return response; }); export const config = { @@ -39,3 +101,4 @@ export const config = { "/(api|trpc)(.*)", ], }; + diff --git a/next.config.ts b/next.config.ts index 2636543..091532c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -15,12 +15,28 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_SEPOLIA_RPC_URL: process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL, NEXT_PUBLIC_EQUITY_TOKEN_ADDRESS: process.env.NEXT_PUBLIC_EQUITY_TOKEN_ADDRESS, }, - // Optimize image handling + // Optimize image handling with specific domains images: { remotePatterns: [ { protocol: 'https', - hostname: '**', + hostname: 'images.unsplash.com', + }, + { + protocol: 'https', + hostname: 'assets.aceternity.com', + }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + { + protocol: 'https', + hostname: 'img.clerk.com', + }, + { + protocol: 'https', + hostname: 'firebasestorage.googleapis.com', }, ], }, diff --git a/src/app/dashboard/code-police/[id]/page.tsx b/src/app/dashboard/code-police/[id]/page.tsx index de8e653..972e4c4 100644 --- a/src/app/dashboard/code-police/[id]/page.tsx +++ b/src/app/dashboard/code-police/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, use } from "react"; +import { useState, useEffect, use, useCallback } from "react"; import Link from "next/link"; import { Shield, @@ -23,7 +23,6 @@ import { GitPullRequest, Wrench, ExternalLink, - Code, } from "lucide-react"; import { ProjectSettings } from "@/components/code-police/ProjectSettings"; @@ -106,7 +105,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st const [isCreatingPR, setIsCreatingPR] = useState(null); // Fetch project and analysis runs - const fetchData = async (showRefresh = false) => { + const fetchData = useCallback(async (showRefresh = false) => { if (showRefresh) setIsRefreshing(true); try { // Fetch project @@ -126,8 +125,8 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st if (runsRes.ok && runsData.runs) { setRuns(runsData.runs); // Expand first run by default - if (runsData.runs.length > 0 && !expandedRun) { - setExpandedRun(runsData.runs[0].id); + if (runsData.runs.length > 0) { + setExpandedRun((prev) => prev || runsData.runs[0].id); } } } catch (err) { @@ -136,11 +135,11 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st setIsLoading(false); setIsRefreshing(false); } - }; + }, [projectId]); useEffect(() => { fetchData(); - }, [projectId]); + }, [fetchData]); // Update project settings const handleUpdateProject = async (updates: Partial) => { @@ -346,7 +345,8 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st } }; - const formatDate = (dateStr: string) => { + // Helper function to format relative time + const _formatDate = (dateStr: string) => { const date = new Date(dateStr); const now = new Date(); const diff = now.getTime() - date.getTime(); diff --git a/src/app/dashboard/code-police/connect/page.tsx b/src/app/dashboard/code-police/connect/page.tsx index f590b55..be8d64a 100644 --- a/src/app/dashboard/code-police/connect/page.tsx +++ b/src/app/dashboard/code-police/connect/page.tsx @@ -8,10 +8,8 @@ import { ArrowLeft, Github, Loader2, - CheckCircle2, AlertCircle, Search, - GitBranch, Star, Lock, Unlock, @@ -41,7 +39,7 @@ export default function ConnectRepositoryPage() { const [filteredRepos, setFilteredRepos] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [isLoading, setIsLoading] = useState(true); - const [selectedRepo, setSelectedRepo] = useState(null); + const [selectedRepo] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(""); const [githubConnected, setGithubConnected] = useState(false); @@ -53,15 +51,15 @@ export default function ConnectRepositoryPage() { try { const response = await fetch("/api/github/repos"); const data = await response.json(); - - console.log("[ConnectRepo] API Response:", { - status: response.status, - connected: data.connected, + + console.log("[ConnectRepo] API Response:", { + status: response.status, + connected: data.connected, repoCount: data.repos?.length || 0, hasError: !!data.error, - message: data.message + message: data.message }); - + if (!response.ok) { throw new Error(data.error || data.message || "Failed to fetch repositories"); } @@ -69,7 +67,7 @@ export default function ConnectRepositoryPage() { setRepos(data.repos || []); setFilteredRepos(data.repos || []); setGithubConnected(data.connected); - + if (!data.connected) { setError(data.message || "GitHub not connected. Please connect your GitHub account in Settings."); } else if (data.repos && data.repos.length === 0) { @@ -195,7 +193,7 @@ export default function ConnectRepositoryPage() {

No Repositories Found

- {githubConnected + {githubConnected ? "You don't have any repositories, or we couldn't access them." : "Connect your GitHub account to see your repositories."}

@@ -219,11 +217,10 @@ export default function ConnectRepositoryPage() { {filteredRepos.map((repo) => (
diff --git a/src/app/dashboard/database/connect/page.tsx b/src/app/dashboard/database/connect/page.tsx index 550366e..e89aa97 100644 --- a/src/app/dashboard/database/connect/page.tsx +++ b/src/app/dashboard/database/connect/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { @@ -30,29 +30,29 @@ const databaseTypes = [ // Auto-detect database type from connection string function detectDatabaseType(connectionString: string): DatabaseType | null { const trimmed = connectionString.trim().toLowerCase(); - + if (!trimmed) return null; - + // MongoDB detection if (trimmed.startsWith("mongodb://") || trimmed.startsWith("mongodb+srv://")) { return "mongodb"; } - + // Supabase detection if (trimmed.includes("supabase.co") || trimmed.includes("supabase.com")) { return "supabase"; } - + // PostgreSQL detection if (trimmed.startsWith("postgres://") || trimmed.startsWith("postgresql://")) { return "postgresql"; } - + // MySQL detection if (trimmed.startsWith("mysql://")) { return "mysql"; } - + return null; } @@ -61,18 +61,18 @@ export default function ConnectDatabasePage() { const [selectedType, setSelectedType] = useState("postgresql"); const [connectionMode, setConnectionMode] = useState("string"); const [connectionName, setConnectionName] = useState(""); - + // Connection string mode const [connectionString, setConnectionString] = useState(""); const [detectedType, setDetectedType] = useState(null); - + // Form mode const [host, setHost] = useState(""); const [port, setPort] = useState("5432"); const [database, setDatabase] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - + const [showPassword, setShowPassword] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isTesting, setIsTesting] = useState(false); @@ -125,13 +125,13 @@ export default function ConnectDatabasePage() { const body = connectionMode === "string" ? { connectionString } : { - type: selectedType, - host, - port: parseInt(port), - database, - username, - password, - }; + type: selectedType, + host, + port: parseInt(port), + database, + username, + password, + }; const response = await fetch("/api/database/test", { method: "POST", @@ -163,14 +163,14 @@ export default function ConnectDatabasePage() { const body = connectionMode === "string" ? { name: connectionName, type: detectedType || selectedType, connectionString } : { - name: connectionName, - type: selectedType, - host, - port: parseInt(port), - database, - username, - password, - }; + name: connectionName, + type: selectedType, + host, + port: parseInt(port), + database, + username, + password, + }; const response = await fetch("/api/database/connections", { method: "POST", @@ -191,23 +191,23 @@ export default function ConnectDatabasePage() { } }; - const isFormValid = connectionMode === "string" + const isFormValid = connectionMode === "string" ? connectionName && connectionString : connectionName && host && port && database && username && password; // Generate detection badge based on type const getDetectionBadge = () => { if (!detectedType) return null; - + const badges: Record = { supabase: { label: "Supabase Detected", icon: "⚡", color: "bg-emerald-500/20 text-emerald-400 border-emerald-500/30" }, mongodb: { label: "MongoDB Detected", icon: "🍃", color: "bg-green-500/20 text-green-400 border-green-500/30" }, postgresql: { label: "PostgreSQL Detected", icon: "🐘", color: "bg-blue-500/20 text-blue-400 border-blue-500/30" }, mysql: { label: "MySQL Detected", icon: "🐬", color: "bg-orange-500/20 text-orange-400 border-orange-500/30" }, }; - + const badge = badges[detectedType]; - + return (
@@ -246,22 +246,20 @@ export default function ConnectDatabasePage() {
- + {/* Score */}
@@ -73,7 +73,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {

Overall Score

- + {/* Content */}
{/* Issues */} @@ -93,7 +93,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)} - + {/* Suggestions */} {healthCheck.suggestions.length > 0 && (
@@ -111,7 +111,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)} - + {/* Strengths */} {healthCheck.strengths.length > 0 && (
@@ -129,7 +129,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)} - + {/* Slide Feedback */} {healthCheck.slideFeedback.length > 0 && (
@@ -155,7 +155,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)}
- + {/* Footer */}
- + {/* Controls */}
{currentSlideIndex + 1} / {visibleSlides.length}
- +
); } - + // Error state if (error || !deck) { return ( @@ -399,10 +399,10 @@ export default function PitchDeckStudioPage() {
); } - + // Get theme for CSS variables const theme = getTheme(deck.themeId) || getTheme("minimal-dark")!; - + return (
- + {/* Main Content */}
{/* Left Sidebar: Slides */}
- + {/* Center: Canvas */}
- + {/* Right Sidebar: Properties */}
- + {/* Modals */} {showHealthCheck && healthCheck && ( )} - + {showPresentation && ( account.provider === "github" @@ -90,9 +91,11 @@ function SettingsContent() {
{user?.imageUrl ? ( - Profile ) : ( @@ -170,7 +173,7 @@ function SettingsContent() { {!isGithubConnected && (

- Note: GitHub connection is required for Code Police (code review) + Note: GitHub connection is required for Code Police (code review) and Pitch Deck Generator (repository analysis). Click "Connect GitHub" above to get started.

@@ -276,7 +279,7 @@ function SettingsContent() { > ✕ - ( +
+
+
+); + +// Dynamic imports with loading states for heavy components +const FeaturesSectionDemo = nextDynamic( + () => import("@/components/ui/features-section-demo-3"), + { + loading: () => , + ssr: true, + } +); + +const AnimatedTestimonialsDemo = nextDynamic( + () => import("@/components/ui/animated-testimonials-demo"), + { + loading: () => , + ssr: false, // Disable SSR for client-only animations + } +); + +const SplineSceneDemo = nextDynamic( + () => import("@/components/ui/spline-scene-demo").then(mod => ({ default: mod.SplineSceneDemo })), + { + loading: () => , + ssr: false, // Spline is client-only + } +); + +const TextHoverEffect = nextDynamic( + () => import("@/components/ui/text-hover-effect").then(mod => ({ default: mod.TextHoverEffect })), + { + loading: () => , + ssr: false, + } +); + +const CallToAction = nextDynamic( + () => import("@/components/ui/cta").then(mod => ({ default: mod.CallToAction })), + { + loading: () => , + ssr: true, + } +); export default function LandingPage() { return ( @@ -24,32 +67,42 @@ export default function LandingPage() { highlightedText="redemption." /> - {/* Interactive 3D Spline Scene */} + {/* Interactive 3D Spline Scene - Lazy loaded */}
- + }> + +
{/* Sticky Scroll Features Section */} - {/* Features Section */} - + {/* Features Section - Contains heavy Globe component */} + }> + + {/* GHOSTFOUNDER Text Effect */}
- + }> + +
{/* Animated Testimonials Section */} - + }> + + {/* CTA Section */}
- + }> + +
{/* Sticky Footer Reveal */} @@ -57,3 +110,4 @@ export default function LandingPage() {
); } + diff --git a/src/components/ui/features-section-demo-3.tsx b/src/components/ui/features-section-demo-3.tsx index ef986a0..16dfb90 100644 --- a/src/components/ui/features-section-demo-3.tsx +++ b/src/components/ui/features-section-demo-3.tsx @@ -1,5 +1,6 @@ "use client"; import React from "react"; +import Image from "next/image"; import { cn } from "@/lib/utils"; import createGlobe from "cobe"; import { useEffect, useRef } from "react"; @@ -107,7 +108,7 @@ const FeatureDescription = ({ children }: { children?: React.ReactNode }) => { import { CodeBlock } from "@/components/ui/code-block"; export const SkeletonOne = () => { - const code = `async function processSecurePayment(userId, amount) { + const code = `async function processSecurePayment(userId, amount) { // 🚨 CRITICAL VULNERABILITY: Hardcoded API Key const STRIPE_SECRET = "sk_live_51M..."; @@ -131,16 +132,16 @@ export const SkeletonOne = () => {
- +
@@ -158,9 +159,9 @@ export const SkeletonThree = () => {
{/* TODO */} - header { whileTap="whileTap" className="rounded-xl -mr-4 mt-4 p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 border border-neutral-100 shrink-0 overflow-hidden" > - bali images diff --git a/src/lib/security/rate-limit.ts b/src/lib/security/rate-limit.ts new file mode 100644 index 0000000..cc14df0 --- /dev/null +++ b/src/lib/security/rate-limit.ts @@ -0,0 +1,120 @@ +/** + * ============================================================================ + * RATE LIMITING UTILITY + * ============================================================================ + * Simple in-memory rate limiting for API protection. + * For production scale, consider Redis-based rate limiting. + */ + +interface RateLimitEntry { + count: number; + resetTime: number; +} + +// In-memory store for rate limits +const rateLimitStore = new Map(); + +// Cleanup old entries every 5 minutes +const CLEANUP_INTERVAL = 5 * 60 * 1000; + +let cleanupInitialized = false; + +function initCleanup() { + if (cleanupInitialized) return; + cleanupInitialized = true; + + setInterval(() => { + const now = Date.now(); + for (const [key, entry] of rateLimitStore.entries()) { + if (now > entry.resetTime) { + rateLimitStore.delete(key); + } + } + }, CLEANUP_INTERVAL); +} + +export interface RateLimitConfig { + /** Maximum number of requests allowed in the window */ + limit: number; + /** Time window in seconds */ + windowSeconds: number; +} + +export interface RateLimitResult { + success: boolean; + remaining: number; + resetIn: number; +} + +/** + * Check if a request should be rate limited + * @param identifier - Unique identifier for the client (e.g., IP, userId) + * @param config - Rate limit configuration + * @returns RateLimitResult with success status and remaining requests + */ +export function checkRateLimit( + identifier: string, + config: RateLimitConfig +): RateLimitResult { + initCleanup(); + + const now = Date.now(); + const windowMs = config.windowSeconds * 1000; + const entry = rateLimitStore.get(identifier); + + // No existing entry or window expired + if (!entry || now > entry.resetTime) { + rateLimitStore.set(identifier, { + count: 1, + resetTime: now + windowMs, + }); + return { + success: true, + remaining: config.limit - 1, + resetIn: config.windowSeconds, + }; + } + + // Within window, check count + if (entry.count >= config.limit) { + return { + success: false, + remaining: 0, + resetIn: Math.ceil((entry.resetTime - now) / 1000), + }; + } + + // Increment count + entry.count++; + return { + success: true, + remaining: config.limit - entry.count, + resetIn: Math.ceil((entry.resetTime - now) / 1000), + }; +} + +/** + * Get rate limit headers for response + */ +export function getRateLimitHeaders( + result: RateLimitResult, + config: RateLimitConfig +): Record { + return { + "X-RateLimit-Limit": config.limit.toString(), + "X-RateLimit-Remaining": result.remaining.toString(), + "X-RateLimit-Reset": result.resetIn.toString(), + }; +} + +// Preset configurations for common use cases +export const RATE_LIMITS = { + /** Standard API endpoint: 60 requests per minute */ + standard: { limit: 60, windowSeconds: 60 }, + /** Strict for auth/sensitive: 10 requests per minute */ + strict: { limit: 10, windowSeconds: 60 }, + /** Relaxed for read-only: 120 requests per minute */ + relaxed: { limit: 120, windowSeconds: 60 }, + /** Webhook endpoints: 100 requests per minute */ + webhook: { limit: 100, windowSeconds: 60 }, +} as const; From 5abc4aeba837c2a347fca61f6619302645a4ad86 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 13:47:39 +0530 Subject: [PATCH 2/8] fix: Railway deployment - clear cache before build, skip env validation --- nixpacks.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index cb00d07..13f6334 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -2,10 +2,16 @@ nixPkgs = ["nodejs_20"] [phases.install] -cmds = ["npm ci --legacy-peer-deps"] +cmds = ["npm install --legacy-peer-deps"] [phases.build] -cmds = ["npm run build"] +cmds = [ + "rm -rf .next/cache", + "npm run build" +] + +[variables] +SKIP_ENV_VALIDATION = "true" [start] cmd = "npm start" From 0a4e7eb097a6439ff8ae25f7ce6ead05fdf5f94b Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 13:54:06 +0530 Subject: [PATCH 3/8] fix: Case sensitivity - rename Button.tsx to button.tsx for Linux compatibility --- src/components/ui/{Button.tsx => button.tsx} | 0 src/components/ui/sticky-footer.tsx | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/components/ui/{Button.tsx => button.tsx} (100%) diff --git a/src/components/ui/Button.tsx b/src/components/ui/button.tsx similarity index 100% rename from src/components/ui/Button.tsx rename to src/components/ui/button.tsx diff --git a/src/components/ui/sticky-footer.tsx b/src/components/ui/sticky-footer.tsx index 974400a..2aac4ae 100644 --- a/src/components/ui/sticky-footer.tsx +++ b/src/components/ui/sticky-footer.tsx @@ -10,8 +10,8 @@ import { LinkedInLogoIcon, VideoIcon, } from '@radix-ui/react-icons'; -import { Button } from './button'; -import { TextHoverEffect } from './text-hover-effect'; +import { Button } from '@/components/ui/button'; +import { TextHoverEffect } from '@/components/ui/text-hover-effect'; interface FooterLink { From 40fb34752567bf925c92e20ef1128a673f3ec005 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 13:57:49 +0530 Subject: [PATCH 4/8] fix: Add global-error.tsx for Next.js 16 client-side provider compatibility --- src/app/global-error.tsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/app/global-error.tsx diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx new file mode 100644 index 0000000..76e831e --- /dev/null +++ b/src/app/global-error.tsx @@ -0,0 +1,33 @@ +"use client"; + +/** + * Global Error Boundary + * Required for Next.js 16+ when using client-side providers in layout. + * This handles errors that occur in the root layout. + */ +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + + +
+

Something went wrong

+

+ {error.message || "An unexpected error occurred"} +

+ +
+ + + ); +} From 2a561b37f2d6f817b1442e4929835ce4b700f499 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 14:01:29 +0530 Subject: [PATCH 5/8] fix: Global error with inline styles + aggressive cache clear for Railway --- nixpacks.toml | 1 + src/app/global-error.tsx | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/nixpacks.toml b/nixpacks.toml index 13f6334..d9fd225 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -7,6 +7,7 @@ cmds = ["npm install --legacy-peer-deps"] [phases.build] cmds = [ "rm -rf .next/cache", + "rm -rf node_modules/.cache", "npm run build" ] diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 76e831e..a0a1e8e 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -3,7 +3,7 @@ /** * Global Error Boundary * Required for Next.js 16+ when using client-side providers in layout. - * This handles errors that occur in the root layout. + * Must use inline styles and no external dependencies to avoid SSR issues. */ export default function GlobalError({ error, @@ -14,15 +14,36 @@ export default function GlobalError({ }) { return ( - -
-

Something went wrong

-

- {error.message || "An unexpected error occurred"} + +

+

+ Something went wrong +

+

+ {error?.message || "An unexpected error occurred"}

From 2e083f3eec1cdd4b7d7480ad827ad11417e67149 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 14:07:22 +0530 Subject: [PATCH 6/8] fix: Disable Turbopack features in next.config.ts for build stability --- next.config.ts | 9 +++++++-- package.json | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/next.config.ts b/next.config.ts index 091532c..0dce7a5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -40,9 +40,14 @@ const nextConfig: NextConfig = { }, ], }, - // Disable telemetry + // Disable telemetry and Turbopack for production stability experimental: { - // Improve build performance + // Use Webpack instead of Turbopack for production builds + // Turbopack has issues with global-error.tsx prerendering + }, + // Disable Turbopack for production builds (use Webpack) + turbopack: { + // Turbopack configuration - disabled features that cause issues }, }; diff --git a/package.json b/package.json index 382f5c9..88d56aa 100644 --- a/package.json +++ b/package.json @@ -100,4 +100,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} +} \ No newline at end of file From db5918595cf757b601ed34f7ce3e5aef480ad74a Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 14:12:07 +0530 Subject: [PATCH 7/8] fix: Refactor Landing Page to separate Client/Server components via LandingPageClient.tsx --- next.config.ts | 9 +-- src/app/LandingPageClient.tsx | 112 ++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/app/LandingPageClient.tsx diff --git a/next.config.ts b/next.config.ts index 0dce7a5..091532c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -40,14 +40,9 @@ const nextConfig: NextConfig = { }, ], }, - // Disable telemetry and Turbopack for production stability + // Disable telemetry experimental: { - // Use Webpack instead of Turbopack for production builds - // Turbopack has issues with global-error.tsx prerendering - }, - // Disable Turbopack for production builds (use Webpack) - turbopack: { - // Turbopack configuration - disabled features that cause issues + // Improve build performance }, }; diff --git a/src/app/LandingPageClient.tsx b/src/app/LandingPageClient.tsx new file mode 100644 index 0000000..aa5ee5a --- /dev/null +++ b/src/app/LandingPageClient.tsx @@ -0,0 +1,112 @@ +"use client"; + +import nextDynamic from 'next/dynamic'; +import { Suspense } from 'react'; +import { Header, HeroSection, HeroHighlightSection, StickyScrollRevealDemo } from '@/components/layout'; +import { StickyFooter } from "@/components/ui/sticky-footer"; + +// Loading skeleton for heavy sections +const SectionSkeleton = ({ height = "h-[500px]" }: { height?: string }) => ( +
+
+
+); + +// Dynamic imports with loading states for heavy components +const FeaturesSectionDemo = nextDynamic( + () => import("@/components/ui/features-section-demo-3"), + { + loading: () => , + ssr: true, + } +); + +const AnimatedTestimonialsDemo = nextDynamic( + () => import("@/components/ui/animated-testimonials-demo"), + { + loading: () => , + ssr: false, // Disable SSR for client-only animations + } +); + +const SplineSceneDemo = nextDynamic( + () => import("@/components/ui/spline-scene-demo").then(mod => ({ default: mod.SplineSceneDemo })), + { + loading: () => , + ssr: false, // Spline is client-only + } +); + +const TextHoverEffect = nextDynamic( + () => import("@/components/ui/text-hover-effect").then(mod => ({ default: mod.TextHoverEffect })), + { + loading: () => , + ssr: false, + } +); + +const CallToAction = nextDynamic( + () => import("@/components/ui/cta").then(mod => ({ default: mod.CallToAction })), + { + loading: () => , + ssr: true, + } +); + +export default function LandingPageClient() { + return ( +
+
+ + {/* Hero Section with Background Ripple Effect */} + + + {/* Text Highlight Section */} + + + {/* Interactive 3D Spline Scene - Lazy loaded */} +
+ }> + + +
+ + {/* Sticky Scroll Features Section */} + + + {/* Features Section - Contains heavy Globe component */} + }> + + + + {/* GHOSTFOUNDER Text Effect */} +
+ }> + + +
+ + {/* Animated Testimonials Section */} + }> + + + + {/* CTA Section */} +
+ }> + + +
+ + {/* Sticky Footer Reveal */} + +
+ ); +} From 96845ab194e0efe7123ccb0a46d291dc31d32e30 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 15 Jan 2026 16:53:42 +0530 Subject: [PATCH 8/8] fix: Build errors resolved + CI/CD pipeline configured for Railway - Fixed missing getTokenInfo import in equity/new/page.tsx - Updated nixpacks.toml with SKIP_ENV_VALIDATION and cache clearing - Updated railway-deploy.yml workflow with proper env vars - Removed problematic global-error.tsx that caused prerender issues --- .github/workflows/railway-deploy.yml | 2 + nixpacks.toml | 1 + src/app/api/code-police/analyze/route.ts | 55 +--- .../api/code-police/projects/[id]/route.ts | 41 ++- src/app/api/equity/projects/route.ts | 9 +- src/app/api/equity/verify-owner/route.ts | 21 +- src/app/api/webhooks/github/route.ts | 263 +++++------------- .../dashboard/code-police/connect/page.tsx | 25 +- src/app/dashboard/equity/new/page.tsx | 101 ++++--- src/app/dashboard/pitch-deck/page.tsx | 85 +++++- .../dashboard/pitch-deck/studio/[id]/page.tsx | 90 +++--- src/app/global-error.tsx | 54 ---- src/app/page.tsx | 124 +++------ src/components/pitch-deck/index.ts | 2 - src/lib/agents/equity/contract.ts | 35 ++- src/lib/pitch-deck/ai-generator.ts | 130 ++++----- 16 files changed, 455 insertions(+), 583 deletions(-) delete mode 100644 src/app/global-error.tsx diff --git a/.github/workflows/railway-deploy.yml b/.github/workflows/railway-deploy.yml index 4ba4ad0..b66329c 100644 --- a/.github/workflows/railway-deploy.yml +++ b/.github/workflows/railway-deploy.yml @@ -70,6 +70,8 @@ jobs: - name: Build Next.js application run: npm run build + env: + SKIP_ENV_VALIDATION: true - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/nixpacks.toml b/nixpacks.toml index d9fd225..396d648 100644 --- a/nixpacks.toml +++ b/nixpacks.toml @@ -13,6 +13,7 @@ cmds = [ [variables] SKIP_ENV_VALIDATION = "true" +NODE_ENV = "production" [start] cmd = "npm start" diff --git a/src/app/api/code-police/analyze/route.ts b/src/app/api/code-police/analyze/route.ts index b5af941..694d21c 100644 --- a/src/app/api/code-police/analyze/route.ts +++ b/src/app/api/code-police/analyze/route.ts @@ -20,17 +20,6 @@ import type { DocumentData, QueryDocumentSnapshot, Firestore } from "firebase-ad * Analyzes code from a GitHub repository and optionally sends email report. */ -// Helper to remove undefined values for Firestore -const sanitizeForFirestore = >(obj: T): T => { - const result: Record = {}; - for (const [key, value] of Object.entries(obj)) { - if (value !== undefined) { - result[key] = value; - } - } - return result as T; -}; - export async function POST(request: NextRequest) { try { const { userId } = await auth(); @@ -125,38 +114,8 @@ export async function POST(request: NextRequest) { createdAt: now, }); - // Fetch commit details - if no SHA provided, get latest commit - let commit; - let actualCommitSha = commitSha; - - if (!commitSha || commitSha === "latest") { - console.log("[Analyze] No commit SHA provided, fetching latest commit from main branch..."); - // Fetch the list of commits to get the latest SHA - const commitsResponse = await fetch( - `https://api.github.com/repos/${owner}/${repo}/commits?per_page=1`, - { - headers: { - Authorization: `Bearer ${githubToken}`, - Accept: "application/vnd.github.v3+json", - }, - } - ); - - if (!commitsResponse.ok) { - const errorData = await commitsResponse.json().catch(() => ({})); - throw new Error(`Failed to fetch commits: ${commitsResponse.status} ${errorData.message || commitsResponse.statusText}`); - } - - const commits = await commitsResponse.json(); - if (!commits || commits.length === 0) { - throw new Error("No commits found in repository"); - } - - actualCommitSha = commits[0].sha; - console.log("[Analyze] Using latest commit:", actualCommitSha); - } - - commit = await fetchCommit(githubToken, owner, repo, actualCommitSha); + // Fetch commit details + const commit = await fetchCommit(githubToken, owner, repo, commitSha); // ======================================================================== // FILE FILTERING - Exclude non-source files from analysis @@ -240,7 +199,7 @@ export async function POST(request: NextRequest) { // Ensure commit.files exists const commitFiles = commit.files || []; - console.log(`[Analyze] Commit ${actualCommitSha}: ${commitFiles.length} files changed`); + console.log(`[Analyze] Commit ${commitSha}: ${commitFiles.length} files changed`); if (commitFiles.length === 0) { console.log(`[Analyze] ⚠️ No files in commit to analyze`); @@ -263,7 +222,7 @@ export async function POST(request: NextRequest) { owner, repo, file.filename, - actualCommitSha + commitSha ); // Skip very large files (> 50KB) to avoid token limits @@ -317,13 +276,13 @@ export async function POST(request: NextRequest) { const batch = adminDb.batch(); for (const issue of fullIssues) { const issueRef = analysisRef.collection("issues").doc(issue.id); - batch.set(issueRef, sanitizeForFirestore(issue as unknown as Record)); + batch.set(issueRef, issue); } // Generate summary const summary = await generateAnalysisSummary({ repoName: `${owner}/${repo}`, - commitSha: actualCommitSha, + commitSha, branch: "main", issues: fullIssues, }); @@ -369,7 +328,7 @@ export async function POST(request: NextRequest) { issues: fullIssues, summary, repoName: `${owner}/${repo}`, - commitUrl: `https://github.com/${owner}/${repo}/commit/${actualCommitSha}`, + commitUrl: `https://github.com/${owner}/${repo}/commit/${commitSha || 'HEAD'}`, }); await analysisRef.update({ emailStatus: "sent", emailSentTo: emailTo }); diff --git a/src/app/api/code-police/projects/[id]/route.ts b/src/app/api/code-police/projects/[id]/route.ts index 607b9f3..670bf0a 100644 --- a/src/app/api/code-police/projects/[id]/route.ts +++ b/src/app/api/code-police/projects/[id]/route.ts @@ -31,19 +31,19 @@ export async function GET( hasClientEmail: !!process.env.FIREBASE_CLIENT_EMAIL, hasPrivateKey: !!process.env.FIREBASE_PRIVATE_KEY, }); - return NextResponse.json({ - error: "Database not configured. Please check server logs for Firebase Admin initialization errors." + return NextResponse.json({ + error: "Database not configured. Please check server logs for Firebase Admin initialization errors." }, { status: 503 }); } const projectDoc = await adminDb.collection("projects").doc(id).get(); - + if (!projectDoc.exists) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } - const project = { id: projectDoc.id, ...projectDoc.data() } as { id: string; userId?: string;[key: string]: unknown }; - + const project = { id: projectDoc.id, ...projectDoc.data() } as { id: string; userId?: string; [key: string]: unknown }; + // Verify ownership if (project.userId !== userId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -68,7 +68,7 @@ export async function PATCH( const { id } = await params; const body = await request.json(); - const { status, customRules, ownerEmail, notificationPrefs, autoFixEnabled } = body; + const { status, customRules, ownerEmail, notificationPrefs } = body; const adminDb = getAdminDb(); if (!adminDb) { @@ -77,13 +77,13 @@ export async function PATCH( // Fetch existing project const projectDoc = await adminDb.collection("projects").doc(id).get(); - + if (!projectDoc.exists) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } const existingProject = projectDoc.data(); - + // Verify ownership if (existingProject?.userId !== userId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -101,7 +101,7 @@ export async function PATCH( return NextResponse.json({ error: "Invalid status" }, { status: 400 }); } updateData.status = status; - + // If status is changing to 'stopped', also set isActive to false for backwards compat if (status === 'stopped') { updateData.isActive = false; @@ -131,19 +131,14 @@ export async function PATCH( }; } - // Set auto-fix enabled - if (autoFixEnabled !== undefined) { - updateData.autoFixEnabled = Boolean(autoFixEnabled); - } - await adminDb.collection("projects").doc(id).update(updateData); // Fetch updated project const updatedDoc = await adminDb.collection("projects").doc(id).get(); const updatedProject = { id: updatedDoc.id, ...updatedDoc.data() }; - return NextResponse.json({ - success: true, + return NextResponse.json({ + success: true, project: updatedProject, message: `Project updated successfully`, }); @@ -171,13 +166,13 @@ export async function DELETE( // Fetch existing project const projectDoc = await adminDb.collection("projects").doc(id).get(); - + if (!projectDoc.exists) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } const project = projectDoc.data(); - + // Verify ownership if (project?.userId !== userId) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); @@ -189,7 +184,7 @@ export async function DELETE( // Get user's GitHub token const userDoc = await adminDb.collection("users").doc(userId).get(); const githubToken = userDoc.data()?.githubAccessToken; - + if (githubToken) { await deleteWebhook( githubToken, @@ -213,16 +208,16 @@ export async function DELETE( .collection("analysis_runs") .where("projectId", "==", id) .get(); - + const batch = adminDb.batch(); runsSnapshot.docs.forEach(doc => { batch.delete(doc.ref); }); await batch.commit(); - return NextResponse.json({ - success: true, - message: "Project deleted successfully" + return NextResponse.json({ + success: true, + message: "Project deleted successfully" }); } catch (error) { console.error("Error deleting project:", error); diff --git a/src/app/api/equity/projects/route.ts b/src/app/api/equity/projects/route.ts index cc91e8c..316af6a 100644 --- a/src/app/api/equity/projects/route.ts +++ b/src/app/api/equity/projects/route.ts @@ -92,10 +92,15 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Database not configured" }, { status: 503 }); } + // Normalize githubRepoId to number for consistent querying + const normalizedRepoId = typeof githubRepoId === 'string' + ? parseInt(githubRepoId, 10) + : githubRepoId; + // Check if tokens have already been minted for this repository const existingProjectSnapshot = await adminDb .collection("equity_projects") - .where("githubRepoId", "==", githubRepoId) + .where("githubRepoId", "==", normalizedRepoId) .limit(1) .get(); @@ -114,7 +119,7 @@ export async function POST(request: Request) { symbol, contractAddress, totalSupply: totalSupply || "1000000", - githubRepoId, + githubRepoId: normalizedRepoId, // Always store as number githubRepoFullName, githubRepoOwner, ownerWalletAddress: ownerWalletAddress || null, diff --git a/src/app/api/equity/verify-owner/route.ts b/src/app/api/equity/verify-owner/route.ts index 210395c..09ad6a9 100644 --- a/src/app/api/equity/verify-owner/route.ts +++ b/src/app/api/equity/verify-owner/route.ts @@ -30,16 +30,25 @@ export async function GET(request: Request) { let githubToken: string | null = null; try { - const tokens = await clerk.users.getUserOauthAccessToken(userId, "github"); + // Clerk uses "oauth_github" as the provider name + const tokens = await clerk.users.getUserOauthAccessToken(userId, "oauth_github"); if (tokens.data && tokens.data.length > 0) { githubToken = tokens.data[0].token; } } catch { - return NextResponse.json({ - isOwner: false, - error: "GitHub not connected", - message: "Please connect your GitHub account to verify repository ownership.", - }); + // Also try legacy "github" provider name for backwards compatibility + try { + const tokens = await clerk.users.getUserOauthAccessToken(userId, "github"); + if (tokens.data && tokens.data.length > 0) { + githubToken = tokens.data[0].token; + } + } catch { + return NextResponse.json({ + isOwner: false, + error: "GitHub not connected", + message: "Please connect your GitHub account to verify repository ownership.", + }); + } } if (!githubToken) { diff --git a/src/app/api/webhooks/github/route.ts b/src/app/api/webhooks/github/route.ts index e83ad5a..1375085 100644 --- a/src/app/api/webhooks/github/route.ts +++ b/src/app/api/webhooks/github/route.ts @@ -7,7 +7,6 @@ import { generateAnalysisSummary, } from "@/lib/agents/code-police/analyzer"; import { sendAnalysisReport } from "@/lib/agents/code-police/email"; -import { generateAndCreateFixPR } from "@/lib/agents/code-police/auto-fix"; import { fetchCommit, fetchFileContent, postPRComment, formatPRComment, getDependentFiles } from "@/lib/agents/code-police/github"; import type { CodeIssue, IssueSeverity, ProjectStatus } from "@/types"; @@ -67,17 +66,17 @@ function verifyWebhookSignature( secret: string ): boolean { if (!signature) return false; - + const hmac = crypto.createHmac("sha256", secret); const digest = `sha256=${hmac.update(payload).digest("hex")}`; - + return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature)); } export async function POST(request: NextRequest) { try { console.log("[GitHub Webhook] Received webhook event"); - + const rawBody = await request.text(); const signature = request.headers.get("x-hub-signature-256"); const event = request.headers.get("x-github-event"); @@ -126,9 +125,9 @@ export async function POST(request: NextRequest) { } const projectDoc = projectsSnapshot.docs[0]; - const project = { id: projectDoc.id, ...projectDoc.data() } as { - id: string; - userId: string; + const project = { id: projectDoc.id, ...projectDoc.data() } as { + id: string; + userId: string; webhookSecret?: string; status?: ProjectStatus; customRules?: string[]; @@ -161,7 +160,7 @@ export async function POST(request: NextRequest) { // Get user's GitHub token from Clerk (OAuth tokens are stored in Clerk, not Firestore) let githubToken: string | null = null; - + try { // Fetch GitHub OAuth token from Clerk const clerkResponse = await fetch( @@ -202,7 +201,7 @@ export async function POST(request: NextRequest) { console.log("[GitHub Webhook] Handling push event"); await handlePushEvent( payload as GitHubPushPayload, - project as { id: string; userId: string;[key: string]: unknown }, + project as { id: string; userId: string; [key: string]: unknown }, githubToken ); } else if (event === "pull_request") { @@ -211,7 +210,7 @@ export async function POST(request: NextRequest) { if (["opened", "synchronize"].includes(prPayload.action)) { await handlePREvent( prPayload, - project as { id: string; userId: string;[key: string]: unknown }, + project as { id: string; userId: string; [key: string]: unknown }, githubToken ); } @@ -232,7 +231,7 @@ export async function POST(request: NextRequest) { */ async function handlePushEvent( payload: GitHubPushPayload, - project: { id: string; userId: string;[key: string]: unknown }, + project: { id: string; userId: string; [key: string]: unknown }, githubToken: string ) { const { repository, after: commitSha, commits } = payload; @@ -253,9 +252,9 @@ async function handlePushEvent( // Create analysis run const analysisRef = adminDb.collection("analysis_runs").doc(); - + console.log("[Push Event] Creating analysis run:", analysisRef.id); - + await analysisRef.set({ id: analysisRef.id, userId: project.userId, @@ -272,109 +271,48 @@ async function handlePushEvent( console.log("[Push Event] Fetching commit details..."); // Fetch commit and analyze const commit = await fetchCommit(githubToken, owner, repo, commitSha); - const commitFiles = commit.files || []; - console.log("[Push Event] Commit has", commitFiles.length, "files changed"); - - // ======================================================================== - // FILE FILTERING - Same as analyze route - // ======================================================================== - const EXCLUDED_PATTERNS = [ - /^node_modules\//, - /^\.git\//, - /^dist\//, - /^build\//, - /^\.next\//, - /^out\//, - /^coverage\//, - /package-lock\.json$/, - /yarn\.lock$/, - /pnpm-lock\.yaml$/, - /\.min\.(js|css)$/, - /\.map$/, - /\.d\.ts$/, - ]; - - const ANALYZABLE_EXTENSIONS = [ - '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', - '.py', '.rb', '.go', '.rs', '.java', '.kt', '.swift', - '.c', '.cpp', '.h', '.hpp', '.cs', - '.php', '.sql', '.sol', '.vue', '.svelte', - ]; - - function shouldAnalyzeFile(filename: string): boolean { - for (const pattern of EXCLUDED_PATTERNS) { - if (pattern.test(filename)) return false; - } - const ext = '.' + (filename.split('.').pop()?.toLowerCase() || ''); - return ANALYZABLE_EXTENSIONS.includes(ext); - } - + console.log("[Push Event] Commit has", commit.files.length, "files changed"); + const allIssues: Omit[] = []; - const analyzedFiles: string[] = []; - const skippedFiles: string[] = []; // Get custom rules from project settings const customRules = (project.customRules as string[] | undefined) || []; - for (const file of commitFiles) { + for (const file of commit.files) { if (file.status === "removed") { console.log("[Push Event] Skipping removed file:", file.filename); continue; } - // Apply file filtering - if (!shouldAnalyzeFile(file.filename)) { - console.log("[Push Event] Skipping (filtered):", file.filename); - skippedFiles.push(file.filename); - continue; - } - console.log("[Push Event] Analyzing file:", file.filename); + const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); + const language = detectLanguage(file.filename); + // Get dependent files for graph-aware analysis (optional, may fail due to rate limits) + let dependentContext = ''; try { - const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); - - // Skip large files - if (content.length > 50000) { - console.log("[Push Event] Skipping (too large):", file.filename); - skippedFiles.push(file.filename); - continue; - } - - const language = detectLanguage(file.filename); - - // Get dependent files for graph-aware analysis (optional, may fail due to rate limits) - let dependentContext = ''; - try { - const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); - if (dependentFiles.length > 0) { - dependentContext = dependentFiles - .map(df => `- ${df.path}:\n${df.snippet}`) - .join('\n\n'); - } - } catch (err) { - console.warn("Graph-aware analysis skipped:", err); + const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); + if (dependentFiles.length > 0) { + dependentContext = dependentFiles + .map(df => `- ${df.path}:\n${df.snippet}`) + .join('\n\n'); } + } catch (err) { + console.warn("Graph-aware analysis skipped:", err); + } - const issues = await analyzeCode({ - code: content, - filePath: file.filename, - language, - commitMessage: commit.commit.message, - customRules, - dependentContext: dependentContext || undefined, - }); + const issues = await analyzeCode({ + code: content, + filePath: file.filename, + language, + commitMessage: commit.commit.message, + customRules, + dependentContext: dependentContext || undefined, + }); - allIssues.push(...issues); - analyzedFiles.push(file.filename); - } catch (fileError) { - console.warn("[Push Event] Failed to analyze file:", file.filename, fileError); - skippedFiles.push(file.filename); - } + allIssues.push(...issues); } - console.log(`[Push Event] Analyzed ${analyzedFiles.length} files, skipped ${skippedFiles.length}`); - // Calculate counts const issueCounts: Record = { critical: allIssues.filter((i) => i.severity === "critical").length, @@ -387,7 +325,7 @@ async function handlePushEvent( console.log("[Push Event] Issue counts:", issueCounts); console.log("[Push Event] Total issues found:", allIssues.length); - // Store issues in Firestore SUBCOLLECTION (matching the GET API) + // Store issues in Firestore const fullIssues: CodeIssue[] = allIssues.map((issue, idx) => ({ ...issue, id: `${analysisRef.id}-${idx}`, @@ -396,19 +334,18 @@ async function handlePushEvent( isMuted: false, })); - // Store in SUBCOLLECTION: analysis_runs/{runId}/issues + // Actually store the issues in the issues collection if (fullIssues.length > 0) { const issuesBatch = adminDb.batch(); for (const issue of fullIssues) { - // FIX: Store in subcollection, not top-level collection - const issueRef = analysisRef.collection("issues").doc(issue.id); + const issueRef = adminDb.collection("issues").doc(issue.id); issuesBatch.set(issueRef, { ...issue, createdAt: new Date(), }); } await issuesBatch.commit(); - console.log("[Push Event] Stored", fullIssues.length, "issues in subcollection"); + console.log("[Push Event] Stored", fullIssues.length, "issues in Firestore"); } // Generate summary @@ -465,50 +402,6 @@ async function handlePushEvent( await analysisRef.update({ emailStatus: "sent" }); } - - // Auto-fix: Generate fixes and create PR if enabled - if ((project.autoFixEnabled as boolean | undefined) && fullIssues.length > 0) { - console.log("[Push Event] Auto-fix enabled, generating fixes..."); - - try { - const autoFixResult = await generateAndCreateFixPR({ - githubToken, - owner, - repo, - branch, - commitSha, - issues: fullIssues, - analysisRunId: analysisRef.id, - severityFilter: ["critical", "high", "medium"], - }); - - if (autoFixResult.success) { - console.log(`[Push Event] Auto-fix PR created: ${autoFixResult.prUrl}`); - await analysisRef.update({ - autoFixPrUrl: autoFixResult.prUrl, - autoFixPrNumber: autoFixResult.prNumber, - autoFixBranch: autoFixResult.branchName, - autoFixesGenerated: autoFixResult.fixesGenerated, - autoFixFilesChanged: autoFixResult.filesChanged, - }); - } else { - console.log(`[Push Event] Auto-fix did not create PR: ${autoFixResult.error}`); - if (autoFixResult.fixesGenerated > 0) { - await analysisRef.update({ - autoFixAttempted: true, - autoFixError: autoFixResult.error, - autoFixesGenerated: autoFixResult.fixesGenerated, - }); - } - } - } catch (autoFixError) { - console.error("[Push Event] Auto-fix error:", autoFixError); - await analysisRef.update({ - autoFixAttempted: true, - autoFixError: autoFixError instanceof Error ? autoFixError.message : "Auto-fix failed", - }); - } - } } catch (error) { console.error("Push event analysis failed:", error); await analysisRef.update({ @@ -523,7 +416,7 @@ async function handlePushEvent( */ async function handlePREvent( payload: GitHubPRPayload, - project: { id: string; userId: string;[key: string]: unknown }, + project: { id: string; userId: string; [key: string]: unknown }, githubToken: string ) { const { repository, pull_request: pr } = payload; @@ -541,7 +434,7 @@ async function handlePREvent( // Create analysis run const analysisRef = adminDb.collection("analysis_runs").doc(); - + await analysisRef.set({ id: analysisRef.id, userId: project.userId, @@ -562,60 +455,40 @@ async function handlePREvent( try { // Similar analysis as push event but with PR comment output const commit = await fetchCommit(githubToken, owner, repo, commitSha); - const commitFiles = commit.files || []; - - // File filtering (same as push event) - const EXCLUDED_PATTERNS = [/^node_modules\//, /^\.git\//, /^dist\//, /^build\//, /^\.next\//, /package-lock\.json$/, /yarn\.lock$/, /\.min\.(js|css)$/, /\.map$/, /\.d\.ts$/]; - const ANALYZABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.java', '.vue', '.svelte']; - function shouldAnalyzeFile(filename: string): boolean { - for (const pattern of EXCLUDED_PATTERNS) { - if (pattern.test(filename)) return false; - } - const ext = '.' + (filename.split('.').pop()?.toLowerCase() || ''); - return ANALYZABLE_EXTENSIONS.includes(ext); - } - const allIssues: Omit[] = []; // Get custom rules from project settings const customRules = (project.customRules as string[] | undefined) || []; - for (const file of commitFiles) { + for (const file of commit.files) { if (file.status === "removed") continue; - if (!shouldAnalyzeFile(file.filename)) continue; + const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); + const language = detectLanguage(file.filename); + + // Get dependent files for graph-aware analysis + let dependentContext = ''; try { - const content = await fetchFileContent(githubToken, owner, repo, file.filename, commitSha); - if (content.length > 50000) continue; // Skip large files - - const language = detectLanguage(file.filename); - - // Get dependent files for graph-aware analysis - let dependentContext = ''; - try { - const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); - if (dependentFiles.length > 0) { - dependentContext = dependentFiles - .map(df => `- ${df.path}:\n${df.snippet}`) - .join('\n\n'); - } - } catch (err) { - console.warn("Graph-aware analysis skipped:", err); + const dependentFiles = await getDependentFiles(githubToken, owner, repo, file.filename); + if (dependentFiles.length > 0) { + dependentContext = dependentFiles + .map(df => `- ${df.path}:\n${df.snippet}`) + .join('\n\n'); } + } catch (err) { + console.warn("Graph-aware analysis skipped:", err); + } - const issues = await analyzeCode({ - code: content, - filePath: file.filename, - language, - commitMessage: pr.title, - customRules, - dependentContext: dependentContext || undefined, - }); + const issues = await analyzeCode({ + code: content, + filePath: file.filename, + language, + commitMessage: pr.title, + customRules, + dependentContext: dependentContext || undefined, + }); - allIssues.push(...issues); - } catch (fileError) { - console.warn("[PR Event] Failed to analyze file:", file.filename); - } + allIssues.push(...issues); } const issueCounts: Record = { @@ -637,18 +510,18 @@ async function handlePREvent( isMuted: false, })); - // Store issues in SUBCOLLECTION (matching GET API) + // Store issues in Firestore if (fullIssues.length > 0) { const issuesBatch = adminDb.batch(); for (const issue of fullIssues) { - const issueRef = analysisRef.collection("issues").doc(issue.id); + const issueRef = adminDb.collection("issues").doc(issue.id); issuesBatch.set(issueRef, { ...issue, createdAt: new Date(), }); } await issuesBatch.commit(); - console.log("[PR Event] Stored", fullIssues.length, "issues in subcollection"); + console.log("[PR Event] Stored", fullIssues.length, "issues in Firestore"); } const summary = await generateAnalysisSummary({ diff --git a/src/app/dashboard/code-police/connect/page.tsx b/src/app/dashboard/code-police/connect/page.tsx index be8d64a..f590b55 100644 --- a/src/app/dashboard/code-police/connect/page.tsx +++ b/src/app/dashboard/code-police/connect/page.tsx @@ -8,8 +8,10 @@ import { ArrowLeft, Github, Loader2, + CheckCircle2, AlertCircle, Search, + GitBranch, Star, Lock, Unlock, @@ -39,7 +41,7 @@ export default function ConnectRepositoryPage() { const [filteredRepos, setFilteredRepos] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [isLoading, setIsLoading] = useState(true); - const [selectedRepo] = useState(null); + const [selectedRepo, setSelectedRepo] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(""); const [githubConnected, setGithubConnected] = useState(false); @@ -51,15 +53,15 @@ export default function ConnectRepositoryPage() { try { const response = await fetch("/api/github/repos"); const data = await response.json(); - - console.log("[ConnectRepo] API Response:", { - status: response.status, - connected: data.connected, + + console.log("[ConnectRepo] API Response:", { + status: response.status, + connected: data.connected, repoCount: data.repos?.length || 0, hasError: !!data.error, - message: data.message + message: data.message }); - + if (!response.ok) { throw new Error(data.error || data.message || "Failed to fetch repositories"); } @@ -67,7 +69,7 @@ export default function ConnectRepositoryPage() { setRepos(data.repos || []); setFilteredRepos(data.repos || []); setGithubConnected(data.connected); - + if (!data.connected) { setError(data.message || "GitHub not connected. Please connect your GitHub account in Settings."); } else if (data.repos && data.repos.length === 0) { @@ -193,7 +195,7 @@ export default function ConnectRepositoryPage() {

No Repositories Found

- {githubConnected + {githubConnected ? "You don't have any repositories, or we couldn't access them." : "Connect your GitHub account to see your repositories."}

@@ -217,10 +219,11 @@ export default function ConnectRepositoryPage() { {filteredRepos.map((repo) => (
diff --git a/src/app/dashboard/equity/new/page.tsx b/src/app/dashboard/equity/new/page.tsx index 9ea81c1..197aab0 100644 --- a/src/app/dashboard/equity/new/page.tsx +++ b/src/app/dashboard/equity/new/page.tsx @@ -20,6 +20,7 @@ import { mintInitialTokens, hasUserMinted, getDisplayBalance, + getTokenInfo, } from "@/lib/agents/equity/contract"; import { RepoSelector } from "@/components/equity/repo-selector"; @@ -45,7 +46,6 @@ export default function NewEquityProjectPage() { const [selectedRepo, setSelectedRepo] = useState(null); const [isVerifying, setIsVerifying] = useState(false); const [isMinting, setIsMinting] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [mintStatus, setMintStatus] = useState<"idle" | "minting" | "success" | "error">("idle"); const [errorMessage, setErrorMessage] = useState(""); const [txHash, setTxHash] = useState(""); @@ -113,23 +113,49 @@ export default function NewEquityProjectPage() { try { const { signer } = await connectWallet(); + console.log("[Equity Mint] Starting mint process..."); + console.log("[Equity Mint] Wallet address:", address); + console.log("[Equity Mint] Contract address:", process.env.NEXT_PUBLIC_EQUITY_CONTRACT_ADDRESS); + + // Verify contract exists and is accessible + try { + const tokenInfo = await getTokenInfo(signer); + console.log("[Equity Mint] Contract verified:", tokenInfo); + } catch (verifyError) { + console.error("[Equity Mint] Contract verification failed:", verifyError); + throw new Error("Smart contract not accessible. Please ensure you're on Sepolia testnet."); + } + // Check if already minted on contract + console.log("[Equity Mint] Checking if already minted..."); const alreadyMinted = await hasUserMinted(signer, address!); - let hash = ""; + console.log("[Equity Mint] Already minted:", alreadyMinted); if (alreadyMinted) { - // Wallet has already minted tokens - that's okay! - // We'll still create the project in the database const balance = await getDisplayBalance(signer, address!); - hash = `wallet-has-${balance}-tokens`; - } else { - // Mint tokens on blockchain - this triggers MetaMask - hash = await mintInitialTokens(signer); + console.log("[Equity Mint] User already has balance:", balance); + + // Show success but inform user they already minted + setMintStatus("success"); + setErrorMessage(`You've already minted your initial tokens. Current balance: ${balance} tokens`); + setTxHash("N/A - Already minted"); + setCurrentStep("complete"); + + // Don't try to save to database again, just show the message + setTimeout(() => { + router.push("/dashboard/equity"); + }, 3000); + + return; } + // Mint tokens + console.log("[Equity Mint] Calling mintInitialTokens()..."); + const hash = await mintInitialTokens(signer); + console.log("[Equity Mint] Mint successful! Hash:", hash); setTxHash(hash); - // Save project to database (always - this enables multi-project support) + // Save project to database const projectResponse = await fetch("/api/equity/projects", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -145,27 +171,35 @@ export default function NewEquityProjectPage() { }), }); + const projectData = await projectResponse.json(); + if (!projectResponse.ok) { - const data = await projectResponse.json(); - throw new Error(data.error || "Failed to save project"); + throw new Error(projectData.error || "Failed to save project"); } - const projectData = await projectResponse.json(); + const projectId = projectData.project?.id; - // Record mint transaction (only if we actually minted) - if (!alreadyMinted && hash.startsWith("0x")) { - await fetch("/api/equity/transactions", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - projectId: projectData.project?.id, - type: "mint", - from: "0x0000000000000000000000000000000000000000", - to: address, - amount: "1000000", - txHash: hash, - }), - }); + if (!projectId) { + throw new Error("Failed to get project ID from response"); + } + + // Record mint transaction + const txResponse = await fetch("/api/equity/transactions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + projectId: projectId, + type: "mint", + from: "0x0000000000000000000000000000000000000000", + to: address, + amount: "1000000", + txHash: hash, + }), + }); + + if (!txResponse.ok) { + console.error("Failed to record transaction:", await txResponse.json()); + // Don't throw here - the minting was successful, just the recording failed } setMintStatus("success"); @@ -454,16 +488,11 @@ export default function NewEquityProjectPage() {
-

- {txHash.startsWith("0x") ? "Tokens Minted Successfully!" : "Project Added Successfully!"} -

+

Tokens Minted Successfully!

- {txHash.startsWith("0x") - ? `You now have 1,000,000 equity tokens for ${selectedRepo?.full_name}` - : `Project ${selectedRepo?.full_name} has been added to your portfolio` - } + You now have 1,000,000 equity tokens for {selectedRepo?.full_name}

- {txHash.startsWith("0x") ? ( + {txHash.startsWith("0x") && ( View on Etherscan → - ) : ( -

- Your wallet already has tokens. You can distribute them across your projects. -

)}

Redirecting to dashboard...

diff --git a/src/app/dashboard/pitch-deck/page.tsx b/src/app/dashboard/pitch-deck/page.tsx index 7e2ad12..7a698b6 100644 --- a/src/app/dashboard/pitch-deck/page.tsx +++ b/src/app/dashboard/pitch-deck/page.tsx @@ -1,15 +1,19 @@ import Link from "next/link"; import { auth } from "@clerk/nextjs/server"; import { getAdminDb } from "@/lib/firebase/admin"; -import { DeckList } from "@/components/pitch-deck"; // Force dynamic rendering - requires Clerk auth at runtime export const dynamic = 'force-dynamic'; import { Presentation, Plus, + FileText, + Clock, + Download, + ArrowRight, Sparkles, Wand2, + PenTool, } from "lucide-react"; /** @@ -25,17 +29,17 @@ interface PitchDeck { tagline: string; status: "draft" | "completed"; slidesCount: number; - createdAt: string; + createdAt: Date; } export default async function PitchDeckPage() { // Get authenticated user const { userId } = await auth(); - + // Fetch decks from Firestore let decks: PitchDeck[] = []; const db = getAdminDb(); - + if (db && userId) { try { const decksSnapshot = await db @@ -52,7 +56,7 @@ export default async function PitchDeckPage() { tagline: data.tagline || "", status: data.status || "draft", slidesCount: data.slides?.length || 0, - createdAt: (data.createdAt?.toDate?.() || new Date()).toISOString(), + createdAt: data.createdAt?.toDate?.() || new Date(), }; }); } catch (error) { @@ -60,6 +64,14 @@ export default async function PitchDeckPage() { } } + const formatDate = (date: Date) => { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + return (
{/* Header */} @@ -95,7 +107,11 @@ export default async function PitchDeckPage() { {decks.length === 0 ? ( ) : ( - +
+ {decks.map((deck) => ( + + ))} +
)}
); @@ -124,3 +140,60 @@ function EmptyState() { ); } +function DeckCard({ + deck, + formatDate, +}: { + deck: PitchDeck; + formatDate: (date: Date) => string; +}) { + return ( +
+ {/* Preview placeholder */} + +
+ +
+ + +
+
+

+ {deck.projectName} +

+ + {deck.status} + +
+

{deck.tagline || "No tagline"}

+
+ {deck.slidesCount} slides +
+ + {formatDate(deck.createdAt)} +
+
+
+ +
+ + + + Edit in Studio + +
+
+ ); +} diff --git a/src/app/dashboard/pitch-deck/studio/[id]/page.tsx b/src/app/dashboard/pitch-deck/studio/[id]/page.tsx index 064fef1..1def524 100644 --- a/src/app/dashboard/pitch-deck/studio/[id]/page.tsx +++ b/src/app/dashboard/pitch-deck/studio/[id]/page.tsx @@ -9,9 +9,9 @@ import React, { useEffect, useState, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; -import { - Loader2, - AlertCircle, +import { + Loader2, + AlertCircle, ChevronLeft, X, CheckCircle, @@ -40,13 +40,13 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) { if (score >= 60) return "text-amber-400"; return "text-red-400"; }; - + const getScoreBg = (score: number) => { if (score >= 80) return "bg-green-500/10"; if (score >= 60) return "bg-amber-500/10"; return "bg-red-500/10"; }; - + return (
@@ -63,7 +63,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
- + {/* Score */}
@@ -73,7 +73,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {

Overall Score

- + {/* Content */}
{/* Issues */} @@ -93,7 +93,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)} - + {/* Suggestions */} {healthCheck.suggestions.length > 0 && (
@@ -111,7 +111,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)} - + {/* Strengths */} {healthCheck.strengths.length > 0 && (
@@ -129,7 +129,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)} - + {/* Slide Feedback */} {healthCheck.slideFeedback.length > 0 && (
@@ -155,7 +155,7 @@ function HealthCheckModal({ healthCheck, onClose }: HealthCheckModalProps) {
)}
- + {/* Footer */}
- + {/* Controls */}
{currentSlideIndex + 1} / {visibleSlides.length}
- +
); } - + // Error state if (error || !deck) { return ( @@ -399,10 +399,10 @@ export default function PitchDeckStudioPage() {
); } - + // Get theme for CSS variables const theme = getTheme(deck.themeId) || getTheme("minimal-dark")!; - + return (
- + {/* Main Content */}
{/* Left Sidebar: Slides */}
- + {/* Center: Canvas */}
- + {/* Right Sidebar: Properties */}
- + {/* Modals */} {showHealthCheck && healthCheck && ( )} - + {showPresentation && ( void; -}) { - return ( - - -
-

- Something went wrong -

-

- {error?.message || "An unexpected error occurred"} -

- -
- - - ); -} diff --git a/src/app/page.tsx b/src/app/page.tsx index d437965..c4a3765 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,57 +1,11 @@ -"use client"; +import Link from 'next/link'; +import { Header, Footer, HeroSection, HeroHighlightSection, StickyScrollRevealDemo } from '@/components/layout'; +import { TextHoverEffect } from "@/components/ui/text-hover-effect"; +import FeaturesSectionDemo from "@/components/ui/features-section-demo-3"; +import AnimatedTestimonialsDemo from "@/components/ui/animated-testimonials-demo"; -import nextDynamic from 'next/dynamic'; -import { Suspense } from 'react'; -import { Header, HeroSection, HeroHighlightSection, StickyScrollRevealDemo } from '@/components/layout'; -import { StickyFooter } from "@/components/ui/sticky-footer"; - -// Loading skeleton for heavy sections -const SectionSkeleton = ({ height = "h-[500px]" }: { height?: string }) => ( -
-
-
-); - -// Dynamic imports with loading states for heavy components -const FeaturesSectionDemo = nextDynamic( - () => import("@/components/ui/features-section-demo-3"), - { - loading: () => , - ssr: true, - } -); - -const AnimatedTestimonialsDemo = nextDynamic( - () => import("@/components/ui/animated-testimonials-demo"), - { - loading: () => , - ssr: false, // Disable SSR for client-only animations - } -); - -const SplineSceneDemo = nextDynamic( - () => import("@/components/ui/spline-scene-demo").then(mod => ({ default: mod.SplineSceneDemo })), - { - loading: () => , - ssr: false, // Spline is client-only - } -); - -const TextHoverEffect = nextDynamic( - () => import("@/components/ui/text-hover-effect").then(mod => ({ default: mod.TextHoverEffect })), - { - loading: () => , - ssr: false, - } -); - -const CallToAction = nextDynamic( - () => import("@/components/ui/cta").then(mod => ({ default: mod.CallToAction })), - { - loading: () => , - ssr: true, - } -); +// Force dynamic rendering - Header uses Clerk auth +export const dynamic = 'force-dynamic'; export default function LandingPage() { return ( @@ -67,47 +21,47 @@ export default function LandingPage() { highlightedText="redemption." /> - {/* Interactive 3D Spline Scene - Lazy loaded */} -
- }> - - -
- {/* Sticky Scroll Features Section */} - {/* Features Section - Contains heavy Globe component */} - }> - - - - {/* GHOSTFOUNDER Text Effect */} -
- }> - - -
+ {/* Features Section */} + {/* Animated Testimonials Section */} - }> - - + + + {/* Text Hover Effect Section */} +
+ +
{/* CTA Section */} -
- }> - - +
+
+

+ Ready to get started? +

+

+ Join thousands of teams already using GhostHunter to build better products. +

+
+ + Start Free Trial + + + Contact Sales + +
+
- {/* Sticky Footer Reveal */} - +
); } - diff --git a/src/components/pitch-deck/index.ts b/src/components/pitch-deck/index.ts index 9e5c275..a4aabc4 100644 --- a/src/components/pitch-deck/index.ts +++ b/src/components/pitch-deck/index.ts @@ -9,5 +9,3 @@ export { SlideCanvas } from "./SlideCanvas"; export { SlideList } from "./SlideList"; export { PropertiesPanel } from "./PropertiesPanel"; export { EditorToolbar } from "./EditorToolbar"; -export { DeleteDeckButton } from "./DeleteDeckButton"; -export { DeckList } from "./DeckList"; diff --git a/src/lib/agents/equity/contract.ts b/src/lib/agents/equity/contract.ts index 0e9aa6d..60cbcc3 100644 --- a/src/lib/agents/equity/contract.ts +++ b/src/lib/agents/equity/contract.ts @@ -96,9 +96,38 @@ export async function hasUserMinted(signer: JsonRpcSigner, address: string): Pro */ export async function mintInitialTokens(signer: JsonRpcSigner): Promise { const contract = getContract(signer); - const tx = await contract.mintInitialTokens(); - await tx.wait(); - return tx.hash; + + try { + // First, estimate gas to catch any revert reasons + const gasEstimate = await contract.mintInitialTokens.estimateGas(); + console.log("[Contract] Gas estimate:", gasEstimate.toString()); + + // Execute transaction with extra gas buffer + const tx = await contract.mintInitialTokens({ + gasLimit: gasEstimate * BigInt(120) / BigInt(100), // 20% buffer + }); + + console.log("[Contract] Transaction sent:", tx.hash); + const receipt = await tx.wait(); + console.log("[Contract] Transaction mined:", receipt.hash); + + return tx.hash; + } catch (error: any) { + console.error("[Contract] Mint failed:", error); + + // Parse common error messages + if (error.message?.includes("Already minted")) { + throw new Error("You have already minted your initial tokens"); + } + if (error.message?.includes("user rejected")) { + throw new Error("Transaction was rejected"); + } + if (error.code === 'CALL_EXCEPTION') { + throw new Error("Contract call failed. Please ensure the contract is deployed on Sepolia testnet."); + } + + throw error; + } } /** diff --git a/src/lib/pitch-deck/ai-generator.ts b/src/lib/pitch-deck/ai-generator.ts index 27fd159..3225bab 100644 --- a/src/lib/pitch-deck/ai-generator.ts +++ b/src/lib/pitch-deck/ai-generator.ts @@ -173,15 +173,15 @@ const STYLE_PRIORITIES: Record = { function formatProfile(profile?: Partial): string { if (!profile) return "Not provided"; - + const sections: string[] = []; - + if (profile.companyName) sections.push(`Company: ${profile.companyName}`); if (profile.oneLiner) sections.push(`One-liner: ${profile.oneLiner}`); if (profile.targetCustomer) sections.push(`Target Customer: ${profile.targetCustomer}`); if (profile.problemStatement) sections.push(`Problem: ${profile.problemStatement}`); if (profile.solutionDescription) sections.push(`Solution: ${profile.solutionDescription}`); - + if (profile.metrics) { const m = profile.metrics; const metricLines: string[] = []; @@ -192,44 +192,44 @@ function formatProfile(profile?: Partial): string { if (m.retention) metricLines.push(`Retention: ${m.retention}`); if (metricLines.length) sections.push(`Metrics:\n${metricLines.join("\n")}`); } - + if (profile.marketSize) { const ms = profile.marketSize; if (ms.tam || ms.sam || ms.som) { sections.push(`Market Size: TAM ${ms.tam || "?"}, SAM ${ms.sam || "?"}, SOM ${ms.som || "?"}`); } } - + if (profile.competitors?.length) { sections.push(`Competitors: ${profile.competitors.join(", ")}`); } - + if (profile.competitiveAdvantage) { sections.push(`Competitive Advantage: ${profile.competitiveAdvantage}`); } - + if (profile.team?.length) { sections.push(`Team: ${profile.team.map(t => `${t.name} (${t.role})`).join(", ")}`); } - + if (profile.fundingAsk) { sections.push(`Funding Ask: ${profile.fundingAsk.amount} (${profile.fundingAsk.type})`); if (profile.fundingAsk.useOfFunds?.length) { sections.push(`Use of Funds: ${profile.fundingAsk.useOfFunds.join(", ")}`); } } - + return sections.length ? sections.join("\n\n") : "Not provided"; } function formatGithubMeta(meta?: { stars?: number; forks?: number; contributors?: number }): string { if (!meta) return "Not available"; - + const lines: string[] = []; if (meta.stars) lines.push(`Stars: ${meta.stars}`); if (meta.forks) lines.push(`Forks: ${meta.forks}`); if (meta.contributors) lines.push(`Contributors: ${meta.contributors}`); - + return lines.length ? lines.join(", ") : "Not available"; } @@ -255,10 +255,10 @@ function createSlideFromContent( const slideType = content.type as SlideType; const layout = getDefaultLayout(slideType); const layoutId = layout?.id || `${slideType}-default`; - + const elements: SlideElement[] = []; let elementOrder = 0; - + // Create headline element if (content.headline) { elements.push({ @@ -282,7 +282,7 @@ function createSlideFromContent( }, } as TextElement); } - + // Create subheadline element if (content.subheadline) { elements.push({ @@ -306,7 +306,7 @@ function createSlideFromContent( }, } as TextElement); } - + // Create bullets element if (content.bullets?.length) { elements.push({ @@ -330,7 +330,7 @@ function createSlideFromContent( itemSpacing: 16, } as BulletListElement); } - + // Create body text element if (content.bodyText && !content.bullets?.length) { elements.push({ @@ -353,7 +353,7 @@ function createSlideFromContent( }, } as TextElement); } - + // Create metric elements if (content.metrics?.length) { const metricWidth = Math.min(300, (SLIDE_WIDTH - 120 - (content.metrics.length - 1) * 40) / content.metrics.length); @@ -389,10 +389,10 @@ function createSlideFromContent( } as MetricElement); }); } - + // Create warnings const warnings: SlideWarning[] = []; - + content.warnings?.forEach((warning, index) => { warnings.push({ id: `warn-${slideId}-${index}`, @@ -401,7 +401,7 @@ function createSlideFromContent( message: warning, }); }); - + content.placeholders?.forEach((placeholder, index) => { warnings.push({ id: `placeholder-${slideId}-${index}`, @@ -411,7 +411,7 @@ function createSlideFromContent( suggestion: "Add this information to strengthen your deck", }); }); - + const slide: Slide = { id: slideId, type: slideType, @@ -423,7 +423,7 @@ function createSlideFromContent( contentScore: content.contentScore, warnings, }; - + // Only add notes if there are suggestions if (content.suggestions?.length) { slide.notes = { @@ -431,7 +431,7 @@ function createSlideFromContent( aiSuggestions: content.suggestions, }; } - + return slide; } @@ -444,37 +444,37 @@ export async function generateDeckFromSources( ): Promise { const genId = `gen-${Date.now()}-${Math.random().toString(36).substring(7)}`; const startTime = Date.now(); - + console.log(`${LOG_PREFIX} ----------------------------------------`); console.log(`${LOG_PREFIX} [${genId}] Starting enhanced deck generation`); console.log(`${LOG_PREFIX} [${genId}] Style: ${request.style}, Tone: ${request.tone}`); - + // Validate API key if (!process.env.GOOGLE_AI_API_KEY) { throw new Error("GOOGLE_AI_API_KEY is not configured"); } - + // Initialize model const model = new ChatGoogleGenerativeAI({ - model: "gemini-2.5-flash-lite", + model: "gemini-2.0-flash", apiKey: process.env.GOOGLE_AI_API_KEY, temperature: 0.7, }); - + // Prepare prompt const essentialSlides = getEssentialSlides(request.style); - + const promptTemplate = new PromptTemplate({ template: DECK_GENERATION_PROMPT, inputVariables: [ - "readme", "profile", "githubMeta", "deckStyle", + "readme", "profile", "githubMeta", "deckStyle", "tone", "essentialSlides", "toneGuidelines", "priorities" ], partialVariables: { format_instructions: parser.getFormatInstructions(), }, }); - + const formattedPrompt = await promptTemplate.format({ readme: request.readme || "No README provided", profile: formatProfile(request.profile), @@ -485,18 +485,18 @@ export async function generateDeckFromSources( toneGuidelines: TONE_GUIDELINES[request.tone], priorities: STYLE_PRIORITIES[request.style], }); - + console.log(`${LOG_PREFIX} [${genId}] Prompt prepared, invoking AI...`); - + // Invoke AI const response = await model.invoke(formattedPrompt); const content = response.content as string; - + console.log(`${LOG_PREFIX} [${genId}] AI response received, parsing...`); - + // Parse response let parsedOutput: DeckGenerationOutput; - + try { parsedOutput = await parser.parse(content); } catch { @@ -508,17 +508,17 @@ export async function generateDeckFromSources( throw new Error("Failed to parse AI response"); } } - + console.log(`${LOG_PREFIX} [${genId}] Parsed ${parsedOutput.slides.length} slides`); - + // Build deck const deckId = uuidv4(); const defaultTheme = getDefaultTheme(); - - const slides: Slide[] = parsedOutput.slides.map((slideContent, index) => + + const slides: Slide[] = parsedOutput.slides.map((slideContent, index) => createSlideFromContent(slideContent, index) ); - + const deck: Deck = { id: deckId, userId: "", // Will be set by API route @@ -541,7 +541,7 @@ export async function generateDeckFromSources( repoName: request.repoName, repoOwner: request.repoOwner, }; - + // Collect all warnings const allWarnings: SlideWarning[] = []; slides.forEach(slide => { @@ -549,7 +549,7 @@ export async function generateDeckFromSources( allWarnings.push(...slide.warnings); } }); - + // Add missing slides warnings parsedOutput.missingSlides.forEach((slideType, index) => { allWarnings.push({ @@ -560,11 +560,11 @@ export async function generateDeckFromSources( suggestion: `Add a ${slideType} slide to strengthen your pitch`, }); }); - + const duration = Date.now() - startTime; console.log(`${LOG_PREFIX} [${genId}] Generation completed in ${duration}ms`); console.log(`${LOG_PREFIX} ----------------------------------------`); - + return { deck, warnings: allWarnings, @@ -607,34 +607,34 @@ export async function improveText( deckContext?: { projectName: string; tagline: string; tone: ContentTone } ): Promise { console.log(`${LOG_PREFIX} Improving text: action=${action}, slideType=${slideType}`); - + if (!process.env.GOOGLE_AI_API_KEY) { throw new Error("GOOGLE_AI_API_KEY is not configured"); } - + const model = new ChatGoogleGenerativeAI({ - model: "gemini-2.5-flash-lite", + model: "gemini-2.0-flash", apiKey: process.env.GOOGLE_AI_API_KEY, temperature: 0.7, }); - + const promptTemplate = new PromptTemplate({ template: TEXT_IMPROVEMENT_PROMPT, inputVariables: ["slideType", "deckContext", "text", "action"], }); - + const formattedPrompt = await promptTemplate.format({ slideType, - deckContext: deckContext + deckContext: deckContext ? `Project: ${deckContext.projectName}, Tagline: ${deckContext.tagline}, Tone: ${deckContext.tone}` : "General pitch deck", text, action, }); - + const response = await model.invoke(formattedPrompt); const improvedText = (response.content as string).trim(); - + // Clean up any markdown formatting return improvedText .replace(/^["']|["']$/g, "") @@ -709,17 +709,17 @@ export async function checkDeckHealth( profile?: StartupProfile ): Promise { console.log(`${LOG_PREFIX} Running health check for deck: ${deck.id}`); - + if (!process.env.GOOGLE_AI_API_KEY) { throw new Error("GOOGLE_AI_API_KEY is not configured"); } - + const model = new ChatGoogleGenerativeAI({ - model: "gemini-2.5-flash-lite", + model: "gemini-2.0-flash", apiKey: process.env.GOOGLE_AI_API_KEY, temperature: 0.3, // Lower for more consistent analysis }); - + // Prepare slides summary for the AI const slidesSummary = deck.slides.map(slide => { const textContent = slide.elements @@ -731,38 +731,38 @@ export async function checkDeckHealth( return (el as TextElement).content; }) .join("\n"); - + return { id: slide.id, type: slide.type, content: textContent || "[No text content]", }; }); - + const promptTemplate = new PromptTemplate({ template: HEALTH_CHECK_PROMPT, inputVariables: ["projectName", "tagline", "slidesJson", "profile"], }); - + const formattedPrompt = await promptTemplate.format({ projectName: deck.projectName, tagline: deck.tagline, slidesJson: JSON.stringify(slidesSummary, null, 2), profile: formatProfile(profile), }); - + const response = await model.invoke(formattedPrompt); const content = response.content as string; - + // Parse response const jsonMatch = content.match(/\{[\s\S]*\}/); if (!jsonMatch) { throw new Error("Failed to parse health check response"); } - + const result: HealthCheckResult = JSON.parse(jsonMatch[0]); - + console.log(`${LOG_PREFIX} Health check complete: score=${result.overallScore}`); - + return result; }