diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 4397669..abd620e 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -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
Dashboard — coming soon
-} \ No newline at end of file + const [session, setSession] = useState(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 ( +
+

Loading...

+
+ ) + } + + return ( +
+
+

Dashboard

+
+ + {session.user.user_metadata.user_name} + + +
+
+

Your codescapes will appear here.

+
+ ) +} diff --git a/web/app/page.tsx b/web/app/page.tsx index 3f36f7c..6ef8ea2 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -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(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 ( +
+

Loading...

+
+ ) + } + return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
+
+

Codescape

+

Visualize your code as a city

+
- ); + ) } diff --git a/web/lib/repos.ts b/web/lib/repos.ts new file mode 100644 index 0000000..045e705 --- /dev/null +++ b/web/lib/repos.ts @@ -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 + } + } + } +} diff --git a/web/lib/supabase.ts b/web/lib/supabase.ts new file mode 100644 index 0000000..8cc5a2a --- /dev/null +++ b/web/lib/supabase.ts @@ -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) diff --git a/web/package-lock.json b/web/package-lock.json index b598ee0..66c798d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,8 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@supabase/ssr": "^0.10.0", + "@supabase/supabase-js": "^2.102.1", "next": "16.2.2", "react": "19.2.4", "react-dom": "19.2.4" @@ -1234,6 +1236,105 @@ "dev": true, "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.102.1.tgz", + "integrity": "sha512-2uH2WB0H98TOGDtaFWhxIcR42Dro/VB7VDZanz/4bVJsqioIue1m3TUqu3xciDm2W9r+1LXQvYNsYbQfWmD+uQ==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.102.1.tgz", + "integrity": "sha512-UcrcKTPnAIo+Yp9Jjq9XXwFbsmgRYY637mwka9ZjmTIWcX/xr1pote4OVvaGQycVY1KTiQgjMvpC0Q0yJhRq3w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.102.1.tgz", + "integrity": "sha512-InLvXKAYf8BIqiv9jWOYudWB3rU8A9uMbcip5BQ5sLLNPrbO1Ekkr79OvlhZBgMNSppxVyC7wPPGzLxMcTZhlA==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.102.1.tgz", + "integrity": "sha512-h2fCumib/v6u7XMwSPgxnpfimjX4xCEayUHrxWLC7UurfQjUZJ0pmJDgm6yj80DnUerxuulRghwm5zXYysFG/Q==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.0", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.0.tgz", + "integrity": "sha512-36jIu+DuKzg5EgA3fnH+zHvwASvpKcL4zPgmHoZaULroS5Q4mzeHcM69zJ0sXUHddO5IcHjQNZJ9Vyhl/DdbRw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.100.1" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.102.1.tgz", + "integrity": "sha512-eCL9T4Xpe40nmKlkUJ7Zq/hk34db1xPiT0WL3Iv5MbJqHuCAe5TxhV8Rjqd6DNZrzjtfYObZtYl9jKJaHrivqw==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.102.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.102.1.tgz", + "integrity": "sha512-bChxPVeLDnYN9M2d/u4fXsvylwSQG5grAl+HN8f+ZD9a9PuVU+Ru+xGmEsk+b9Iz3rJC9ZQnQUJYQ28fApdWYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@supabase/auth-js": "2.102.1", + "@supabase/functions-js": "2.102.1", + "@supabase/postgrest-js": "2.102.1", + "@supabase/realtime-js": "2.102.1", + "@supabase/storage-js": "2.102.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1550,7 +1651,6 @@ "version": "20.19.37", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1577,6 +1677,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", @@ -2644,6 +2753,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3933,6 +4055,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6363,7 +6494,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6557,6 +6687,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/web/package.json b/web/package.json index 173ebf3..c3e4772 100644 --- a/web/package.json +++ b/web/package.json @@ -9,6 +9,8 @@ "lint": "eslint" }, "dependencies": { + "@supabase/ssr": "^0.10.0", + "@supabase/supabase-js": "^2.102.1", "next": "16.2.2", "react": "19.2.4", "react-dom": "19.2.4"