Skip to content
Merged
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
63 changes: 63 additions & 0 deletions app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { NextResponse } from 'next/server'
import { getDb } from '@/lib/db'

export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'

interface CheckResult {
ok: boolean
detail?: string
}

async function checkDatabase(): Promise<CheckResult> {
if (!process.env.DATABASE_URL) {
return { ok: false, detail: 'DATABASE_URL not set' }
}
try {
const sql = getDb()
await sql`SELECT 1 as ping`
return { ok: true }
} catch (error) {
return { ok: false, detail: error instanceof Error ? error.message : 'unknown error' }
}
}

function checkEnv(): Record<string, boolean> {
return {
DATABASE_URL: !!process.env.DATABASE_URL,
GITHUB_CLIENT_ID: !!(process.env.GITHUB_CLIENT_ID || process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID),
GITHUB_CLIENT_SECRET: !!process.env.GITHUB_CLIENT_SECRET,
NEXT_PUBLIC_APP_URL: !!process.env.NEXT_PUBLIC_APP_URL,
OPENAI_API_KEY: !!process.env.OPENAI_API_KEY,
ANTHROPIC_API_KEY: !!process.env.ANTHROPIC_API_KEY,
STRIPE_SECRET_KEY: !!process.env.STRIPE_SECRET_KEY,
STRIPE_PRO_PRICE_ID: !!process.env.STRIPE_PRO_PRICE_ID,
}
}

export async function GET() {
const startedAt = Date.now()
const database = await checkDatabase()
const env = checkEnv()

const status = database.ok ? 'ok' : 'degraded'

return NextResponse.json(
{
status,
uptime_ms: Date.now() - startedAt,
commit: process.env.VERCEL_GIT_COMMIT_SHA ?? null,
env,
checks: {
database,
},
timestamp: new Date().toISOString(),
},
{
status: 200,
headers: {
'Cache-Control': 'no-store',
},
},
)
}
46 changes: 46 additions & 0 deletions app/dashboard/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client'

import { useEffect } from 'react'
import Link from 'next/link'

export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('[DashboardError]', error)
}, [error])

return (
<div className="container mx-auto flex min-h-[60vh] max-w-xl flex-col items-center justify-center px-6 py-12 text-center">
<p className="text-xs font-mono uppercase tracking-widest text-cyan-400/80">Dashboard</p>
<h1 className="mt-3 text-2xl font-semibold">This panel could not load.</h1>
<p className="mt-2 text-sm text-muted-foreground">
Try refreshing the panel. If it keeps happening, check your environment variables on Vercel.
</p>
{error?.digest ? (
<code className="mt-3 rounded bg-muted px-2 py-1 text-xs text-muted-foreground">
ref: {error.digest}
</code>
) : null}
<div className="mt-6 flex gap-3">
<button
type="button"
onClick={() => reset()}
className="rounded-full bg-cyan-500 px-4 py-2 text-sm font-semibold text-black transition-colors hover:bg-cyan-400"
>
Retry
</button>
<Link
href="/dashboard"
className="rounded-full border border-white/10 px-4 py-2 text-sm font-semibold transition-colors hover:bg-white/5"
>
Dashboard home
</Link>
</div>
</div>
)
}
8 changes: 8 additions & 0 deletions app/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function DashboardLoading() {
return (
<div className="container mx-auto flex min-h-[60vh] max-w-2xl flex-col items-center justify-center px-6 py-12 text-center">
<div className="h-9 w-9 animate-spin rounded-full border-2 border-cyan-500/60 border-t-transparent" />
<p className="mt-3 text-xs font-mono uppercase tracking-widest text-cyan-400/70">Loading dashboard…</p>
</div>
)
}
50 changes: 50 additions & 0 deletions app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client'

import { useEffect } from 'react'
import Link from 'next/link'

export default function RootError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('[RootError]', error)
}, [error])

return (
<html lang="en">
<body className="min-h-screen bg-[#0a0a0f] text-white antialiased">
<main className="container mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center px-6 py-20 text-center">
<p className="text-xs font-mono uppercase tracking-widest text-cyan-400/80">RepoFuse</p>
<h1 className="mt-4 text-3xl font-bold sm:text-4xl">Something broke loading this page.</h1>
<p className="mt-3 text-sm text-gray-400">
We logged it. Try again, or head back home.
</p>
{error?.digest ? (
<code className="mt-4 rounded bg-white/5 px-2 py-1 text-xs text-gray-500">
ref: {error.digest}
</code>
) : null}
<div className="mt-8 flex gap-3">
<button
type="button"
onClick={() => reset()}
className="rounded-full bg-cyan-500 px-4 py-2 text-sm font-semibold text-black transition-colors hover:bg-cyan-400"
>
Try again
</button>
<Link
href="/"
className="rounded-full border border-white/10 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-white/5"
>
Go home
</Link>
</div>
</main>
</body>
</html>
)
}
47 changes: 47 additions & 0 deletions app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { useEffect } from 'react'

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error('[GlobalError]', error)
}, [error])

return (
<html lang="en">
<body style={{ background: '#0a0a0f', color: '#fff', fontFamily: 'system-ui, sans-serif' }}>
<main style={{ maxWidth: 640, margin: '0 auto', padding: '5rem 1.5rem', textAlign: 'center' }}>
<p style={{ color: '#67e8f9', textTransform: 'uppercase', letterSpacing: '0.2em', fontSize: 12 }}>RepoFuse</p>
<h1 style={{ fontSize: 32, marginTop: 16 }}>The page hit an unrecoverable error.</h1>
<p style={{ color: '#9ca3af', marginTop: 12 }}>We logged it. Try again or go home.</p>
{error?.digest ? (
<code style={{ color: '#6b7280', marginTop: 16, display: 'inline-block' }}>ref: {error.digest}</code>
) : null}
<div style={{ marginTop: 32 }}>
<button
type="button"
onClick={() => reset()}
style={{
background: '#06b6d4',
color: '#000',
padding: '10px 16px',
borderRadius: 9999,
fontWeight: 600,
border: 'none',
cursor: 'pointer',
}}
>
Try again
</button>
</div>
</main>
</body>
</html>
)
}
40 changes: 37 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,44 @@ import './globals.css'
const geist = Geist({ subsets: ['latin'], variable: '--font-geist' })
const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-geist-mono' })

function siteUrl(): string {
return (
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://repofuse.com')
)
}

export const metadata: Metadata = {
title: 'RepoFuse - Discover Apps Hidden in Your Code',
description: 'AI-powered GitHub repository analyzer that discovers what apps you can build from your existing code',
generator: 'v0.app',
metadataBase: new URL(siteUrl()),
title: {
default: 'RepoFuse — Discover Apps Hidden in Your Code',
template: '%s · RepoFuse',
},
description: 'AI-powered GitHub repository analyzer that discovers what apps you can build from your existing code.',
applicationName: 'RepoFuse',
generator: 'Next.js',
keywords: [
'GitHub analyzer',
'AI code analysis',
'reuse code',
'project ideas',
'app blueprints',
'Claude MCP',
'developer tools',
],
openGraph: {
type: 'website',
url: siteUrl(),
title: 'RepoFuse — Discover Apps Hidden in Your Code',
description: 'Scan your GitHub repos, surface buildable app ideas, and ship faster.',
siteName: 'RepoFuse',
},
twitter: {
card: 'summary_large_image',
title: 'RepoFuse',
description: 'Scan your GitHub repos, surface buildable app ideas, and ship faster.',
},
robots: { index: true, follow: true },
icons: {
icon: [
{
Expand Down
10 changes: 10 additions & 0 deletions app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function RootLoading() {
return (
<div className="min-h-screen bg-[#0a0a0f] text-white">
<div className="container mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center px-6 text-center">
<div className="h-10 w-10 animate-spin rounded-full border-2 border-cyan-500/60 border-t-transparent" />
<p className="mt-4 text-sm font-mono uppercase tracking-widest text-cyan-400/70">Loading…</p>
</div>
</div>
)
}
29 changes: 29 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Link from 'next/link'

export default function NotFound() {
return (
<main className="min-h-screen bg-[#0a0a0f] text-white">
<div className="container mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center px-6 py-20 text-center">
<p className="text-xs font-mono uppercase tracking-widest text-cyan-400/80">RepoFuse · 404</p>
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">This route does not exist.</h1>
<p className="mt-3 text-sm text-gray-400">
Either the page moved or never existed. Head back to the dashboard or homepage.
</p>
<div className="mt-8 flex gap-3">
<Link
href="/"
className="rounded-full bg-cyan-500 px-4 py-2 text-sm font-semibold text-black transition-colors hover:bg-cyan-400"
>
Home
</Link>
<Link
href="/dashboard"
className="rounded-full border border-white/10 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-white/5"
>
Dashboard
</Link>
</div>
</div>
</main>
)
}
23 changes: 23 additions & 0 deletions app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { MetadataRoute } from 'next'

function baseUrl(): string {
return (
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://repofuse.com')
)
}

export default function robots(): MetadataRoute.Robots {
const url = baseUrl()
return {
rules: [
{
userAgent: '*',
allow: ['/', '/pricing'],
disallow: ['/api/', '/dashboard/'],
},
],
sitemap: `${url}/sitemap.xml`,
host: url,
}
}
17 changes: 17 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { MetadataRoute } from 'next'

function baseUrl(): string {
return (
process.env.NEXT_PUBLIC_APP_URL ||
(process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : 'https://repofuse.com')
)
}

export default function sitemap(): MetadataRoute.Sitemap {
const url = baseUrl()
const now = new Date()
return [
{ url: `${url}/`, lastModified: now, changeFrequency: 'weekly', priority: 1 },
{ url: `${url}/pricing`, lastModified: now, changeFrequency: 'monthly', priority: 0.8 },
]
}
Loading