-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
+
+
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"