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
205 changes: 129 additions & 76 deletions web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,149 @@
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase-server'
import { redirect } from "next/navigation";
import Link from "next/link";
import { Star, GitBranch, ExternalLink } from "lucide-react";
import { createClient } from "@/lib/supabase-server";

type RepoRow = {
repo_owner: string
repo_name: string
is_public: boolean
repo_owner: string;
repo_name: string;
is_public: boolean;
city_state: {
description: string | null
html_url: string
default_branch: string
updated_at: string
language: string | null
stargazers_count: number
} | null
last_synced_at: string | null
}
description: string | null;
html_url: string;
default_branch: string;
updated_at: string;
language: string | null;
stargazers_count: number;
} | null;
last_synced_at: string | null;
};

export default async function DashboardPage() {
const supabase = await createClient()
const supabase = await createClient();

const {
data: { session },
} = await supabase.auth.getSession()
data: { user },
} = await supabase.auth.getUser();

if (!session) {
redirect('/')
if (!user) {
redirect("/");
}

const { data, error } = await supabase
.from('linked_repos')
.select('repo_owner, repo_name, is_public, city_state, last_synced_at')
.eq('user_id', session.user.id)
.order('last_synced_at', { ascending: false })
.from("linked_repos")
.select("repo_owner, repo_name, is_public, city_state, last_synced_at")
.eq("user_id", user.id)
.order("last_synced_at", { ascending: false });

const repos = (data ?? []) as RepoRow[]
const repos = (data ?? []) as RepoRow[];
const username =
(user.user_metadata?.user_name as string | undefined) ??
(user.user_metadata?.preferred_username as string | undefined) ??
user.email ??
"";

return (
<div className="flex flex-col flex-1 p-8 gap-6 max-w-5xl mx-auto w-full">
<header className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Your repositories</h1>
<p className="text-sm text-zinc-500">
Signed in as {session.user.user_metadata.user_name ?? session.user.email}
<main className="min-h-[calc(100vh-64px)] bg-[#F0F9FF] px-6 py-10 text-[#111827] sm:px-8 lg:px-10">
<div className="mx-auto max-w-7xl space-y-8">
<div className="rounded-[2rem] border border-white/60 bg-white/50 p-8 shadow-[0_28px_60px_-30px_rgba(59,130,246,0.24)] backdrop-blur-[12px]">
<p className="text-sm font-semibold uppercase tracking-[0.32em] text-[#4B5563]">
Repositories
</p>
<h1 className="mt-3 text-5xl font-bold text-[#111827]">
Your repositories
</h1>
<p className="mt-4 max-w-2xl text-base leading-8 text-[#4B5563]">
Signed in as{" "}
<span className="font-semibold text-[#111827]">{username}</span>.
Repositories are synced from GitHub on sign-in.
</p>
</div>
</header>

{error && (
<div className="rounded border border-red-300 bg-red-50 p-4 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-200">
Failed to load repos: {error.message}
</div>
)}
{error && (
<div className="rounded-[2rem] border border-red-200 bg-red-50/80 p-6 text-sm text-red-700 backdrop-blur-[12px]">
Failed to load repos: {error.message}
</div>
)}

{!error && repos.length === 0 && (
<div className="rounded-[2rem] border border-white/60 bg-white/50 p-10 text-center shadow-[0_24px_60px_-30px_rgba(59,130,246,0.18)] backdrop-blur-[12px]">
<p className="text-lg font-semibold text-[#111827]">
No repositories synced yet
</p>
<p className="mt-2 text-sm text-[#4B5563]">
Sign out and back in with GitHub to re-sync, or make sure your
account has the correct scopes.
</p>
</div>
)}

{repos.length > 0 && (
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{repos.map((r) => (
<article
key={`${r.repo_owner}/${r.repo_name}`}
className="flex flex-col gap-4 rounded-[2rem] border border-white/60 bg-white/50 p-6 shadow-[0_24px_60px_-30px_rgba(59,130,246,0.18)] backdrop-blur-[12px] transition hover:-translate-y-0.5 hover:shadow-[0_28px_60px_-30px_rgba(59,130,246,0.28)]"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<GitBranch size={18} className="text-[#3B82F6]" />
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-[#4B5563]">
{r.repo_owner}
</p>
</div>
<span
className={`whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold ${
r.is_public
? "bg-[#DCFCE7] text-[#047857]"
: "bg-[#EFF6FF] text-[#2563EB]"
}`}
>
{r.is_public ? "Public" : "Private"}
</span>
</div>

{!error && repos.length === 0 && (
<p className="text-zinc-500">No repositories synced yet.</p>
)}
<div>
<h2 className="text-xl font-bold text-[#111827]">
{r.repo_name}
</h2>
{r.city_state?.description && (
<p className="mt-2 text-sm leading-6 text-[#4B5563]">
{r.city_state.description}
</p>
)}
</div>

{repos.length > 0 && (
<ul className="flex flex-col gap-3">
{repos.map((r) => (
<li
key={`${r.repo_owner}/${r.repo_name}`}
className="rounded border border-zinc-200 p-4 dark:border-zinc-800"
>
<div className="flex items-center justify-between">
<a
href={r.city_state?.html_url ?? '#'}
target="_blank"
rel="noopener noreferrer"
className="font-medium hover:underline"
>
{r.repo_owner}/{r.repo_name}
</a>
<span className="text-xs text-zinc-500">
{r.is_public ? 'public' : 'private'}
</span>
</div>
{r.city_state?.description && (
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{r.city_state.description}
</p>
)}
<div className="mt-2 flex gap-3 text-xs text-zinc-500">
{r.city_state?.language && <span>{r.city_state.language}</span>}
{r.city_state && (
<span>★ {r.city_state.stargazers_count}</span>
)}
</div>
</li>
))}
</ul>
)}
</div>
)
<div className="mt-auto flex items-center justify-between text-xs text-[#4B5563]">
<div className="flex items-center gap-4">
{r.city_state?.language && (
<span className="font-medium text-[#111827]">
{r.city_state.language}
</span>
)}
{r.city_state && (
<span className="flex items-center gap-1">
<Star size={14} className="text-[#3B82F6]" />
{r.city_state.stargazers_count}
</span>
)}
</div>
{r.city_state?.html_url && (
<Link
href={r.city_state.html_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 rounded-full border border-[var(--border)] bg-white px-3 py-1 font-semibold text-[var(--accent)] transition hover:bg-[#EFF6FF]"
>
View
<ExternalLink size={12} />
</Link>
)}
</div>
</article>
))}
</div>
)}
</div>
</main>
);
}
72 changes: 58 additions & 14 deletions web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Inter, JetBrains_Mono } from "next/font/google";
import { createClient } from "@/lib/supabase-server";
import SignInButton from "@/components/SignInButton";
import SignOutButton from "@/components/SignOutButton";
import "./globals.css";

const inter = Inter({
Expand All @@ -18,11 +21,24 @@ export const metadata: Metadata = {
description: "Explore your codebase as a living city.",
};

export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();

const username =
(user?.user_metadata?.user_name as string | undefined) ??
(user?.user_metadata?.preferred_username as string | undefined) ??
user?.email ??
null;
const avatarUrl =
(user?.user_metadata?.avatar_url as string | undefined) ?? null;

return (
<html
lang="en"
Expand All @@ -32,30 +48,58 @@ export default function RootLayout({
<div className="min-h-full">
<header className="sticky top-0 z-20 border-b border-[var(--border)] bg-white/95 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 text-sm text-[#111827]">
<Link href="/" className="font-semibold tracking-wide text-[#111827] transition hover:text-[var(--accent)]">
<Link
href="/"
className="font-semibold tracking-wide text-[#111827] transition hover:text-[var(--accent)]"
>
Codescape
</Link>
<div className="flex items-center gap-3">
<nav className="hidden items-center gap-4 md:flex text-[#111827]">
<Link href="/" className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]">
<Link
href="/"
className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]"
>
Home
</Link>
<Link href="/dashboard" className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]">
<Link
href="/dashboard"
className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]"
>
Dashboard
</Link>
<Link href="/friends" className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]">
<Link
href="/friends"
className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]"
>
Friends
</Link>
<Link href="/profile" className="rounded-full px-3 py-2 transition hover:bg-[#F3F4F6]">
Profile
</Link>
</nav>
<Link
href="/profile"
className="rounded-full border border-[var(--border)] bg-white px-4 py-2 text-sm font-semibold text-[var(--accent)] transition hover:bg-[#EFF6FF]"
>
Sign in
</Link>
{user ? (
<div className="flex items-center gap-3">
<Link
href="/profile"
className="flex items-center gap-2 rounded-full px-3 py-1.5 transition hover:bg-[#F3F4F6]"
>
{avatarUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatarUrl}
alt={username ?? "avatar"}
className="h-7 w-7 rounded-full border border-[var(--border)]"
/>
)}
{username && (
<span className="hidden text-sm font-medium text-[#111827] sm:block">
{username}
</span>
)}
</Link>
<SignOutButton />
</div>
) : (
<SignInButton />
)}
</div>
</div>
</header>
Expand Down
Loading