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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,66 @@
'use client'

import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase'
import type { Session } from '@supabase/supabase-js'

export default function DashboardPage() {
return <div>Dashboard — coming soon</div>
}
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
setLoading(false)
})

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
setLoading(false)
})

return () => subscription.unsubscribe()
}, [])

useEffect(() => {
if (!loading && !session) {
router.push('/')
}
}, [loading, session, router])

const signOut = async () => {
await supabase.auth.signOut()
}

if (loading || !session) {
return (
<div className="flex flex-1 items-center justify-center">
<p className="text-zinc-500">Loading...</p>
</div>
)
}

return (
<div className="flex flex-col flex-1 p-8 gap-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Dashboard</h1>
<div className="flex items-center gap-4">
<span className="text-zinc-500">
{session.user.user_metadata.user_name}
</span>
<button
onClick={signOut}
className="rounded-full border border-zinc-300 px-4 py-2 text-sm font-medium hover:bg-zinc-100 dark:border-zinc-700 dark:hover:bg-zinc-800 transition-colors"
>
Sign out
</button>
</div>
</div>
<p className="text-zinc-500">Your codescapes will appear here.</p>
</div>
)
}
132 changes: 72 additions & 60 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,77 @@
import Image from "next/image";
'use client'

import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase'
import type { Session } from '@supabase/supabase-js'
import { importUserRepos } from '@/lib/repos'

export default function Home() {
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()

useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
setLoading(false)
})

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setSession(session)
setLoading(false)

if (event === 'SIGNED_IN' && session) {
const githubToken = session.provider_token
const userId = session.user.id
if (githubToken) {
setTimeout(() => {
void importUserRepos(githubToken, userId).catch((err) => {
console.error('importUserRepos failed:', err)
})
}, 0)
}
}
})

return () => subscription.unsubscribe()
}, [])

useEffect(() => {
if (!loading && session) {
router.push('/dashboard')
}
}, [loading, session, router])

const signInWithGitHub = async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: window.location.origin,
},
})
}

if (loading) {
return (
<div className="flex flex-1 items-center justify-center">
<p className="text-zinc-500">Loading...</p>
</div>
)
}

return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
<div className="flex flex-col flex-1 items-center justify-center gap-8">
<h1 className="text-4xl font-bold">Codescape</h1>
<p className="text-zinc-500 text-lg">Visualize your code as a city</p>
<button
onClick={signInWithGitHub}
className="rounded-full bg-zinc-900 text-white px-6 py-3 font-medium hover:bg-zinc-700 dark:bg-white dark:text-black dark:hover:bg-zinc-200 transition-colors"
>
Sign in with GitHub
</button>
</div>
);
)
}
86 changes: 86 additions & 0 deletions web/lib/repos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { supabase } from './supabase'

export async function importUserRepos(githubToken: string, userId: string) {
const response = await fetch('https://api.github.com/user/repos?per_page=100', {
headers: {
Authorization: `Bearer ${githubToken}`,
Accept: 'application/vnd.github.v3+json',
},
})

if (!response.ok) {
const errorBody = await response.text().catch(() => '')
throw new Error(
`Failed to fetch GitHub repositories: ${response.status} ${response.statusText}` +
(errorBody ? ` - ${errorBody}` : '')
)
}

const repos = await response.json()

if (!Array.isArray(repos)) {
throw new Error('Unexpected GitHub API response when listing repositories; expected an array.')
}

for (const repo of repos) {
const codescapeResponse = await fetch(
`https://api.github.com/repos/${repo.full_name}/contents/.codescape`,
{
headers: {
Authorization: `Bearer ${githubToken}`,
Accept: 'application/vnd.github.v3+json',
},
}
)

if (codescapeResponse.ok) {
const fileData = await codescapeResponse.json()

const rawContent =
typeof fileData?.content === 'string' ? fileData.content.replace(/\s+/g, '') : null
if (!rawContent) {
console.warn('Missing or invalid .codescape content for repo', repo.full_name)
continue
}

let cityState: unknown
try {
const decoded = atob(rawContent)
const parsed = JSON.parse(decoded)

if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
console.warn('Invalid .codescape JSON shape for repo', repo.full_name)
continue
}

cityState = parsed
} catch (error) {
console.error('Failed to decode/parse .codescape file for repo', repo.full_name, error)
continue
}

const { error } = await supabase.from('linked_repos').upsert(
{
user_id: userId,
repo_owner: repo.owner.login,
repo_name: repo.name,
is_public: !repo.private,
city_state: cityState,
last_synced_at: new Date().toISOString(),
},
{
onConflict: 'user_id, repo_owner, repo_name',
}
)

if (error) {
console.error('Failed to upsert linked_repos record', {
userId,
repoFullName: repo.full_name,
error,
})
throw error
}
}
}
}
14 changes: 14 additions & 0 deletions web/lib/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

if (!supabaseUrl) {
throw new Error('Missing environment variable NEXT_PUBLIC_SUPABASE_URL')
}

if (!supabaseAnonKey) {
throw new Error('Missing environment variable NEXT_PUBLIC_SUPABASE_ANON_KEY')
}

export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Loading