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
27 changes: 13 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useState, useEffect, Suspense } from 'react';
import { RelayEnvironmentProvider } from 'react-relay';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { getRelayEnvironment, resetRelayEnvironment } from './lib/relay/environment';
import { loadAuthState, clearAuthState, type AuthState } from './lib/auth';
import { ErrorBoundary } from './components/ErrorBoundary';
import HomePage from './pages/HomePage';
import CallbackPage from './pages/CallbackPage';
import { useState, useEffect, Suspense } from "react";
import { RelayEnvironmentProvider } from "react-relay";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import {
getRelayEnvironment,
resetRelayEnvironment,
} from "./lib/relay/environment";
import { loadAuthState, clearAuthState, type AuthState } from "./lib/auth";
import { ErrorBoundary } from "./components/ErrorBoundary";
import HomePage from "./pages/HomePage";
import CallbackPage from "./pages/CallbackPage";

function App() {
const [authState, setAuthState] = useState<AuthState>(loadAuthState());
Expand Down Expand Up @@ -45,14 +48,11 @@ function App() {
</div>
}
>
<HomePage
user={authState.user}
onLogout={handleLogout}
/>
<HomePage onLogout={handleLogout} />
</Suspense>
</RelayEnvironmentProvider>
) : (
<HomePage user={null} onLogout={handleLogout} />
<HomePage onLogout={handleLogout} />
)
}
/>
Expand All @@ -68,4 +68,3 @@ function App() {
}

export default App;

48 changes: 48 additions & 0 deletions src/components/AuthorizedHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { graphql, useFragment } from "react-relay";
import { AuthorizedHeader_user$key } from "./__generated__/AuthorizedHeader_user.graphql";

export const AuthorizedHeader = ({
user,
onLogout,
}: {
user: AuthorizedHeader_user$key;
onLogout: () => void;
}) => {
const data = useFragment(
graphql`
fragment AuthorizedHeader_user on User {
name
login
email

avatarUrl(size: 96)
}
`,
user
);

return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<img
src={data.avatarUrl}
alt="User Avatar"
className="w-12 h-12 rounded-full"
/>
<div>
<p className="text-zinc-600 dark:text-zinc-400">Welcome,</p>
<h1 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">
{data.name || data.login}
</h1>
</div>
</div>

<button
onClick={onLogout}
className="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 rounded-lg hover:bg-zinc-300 dark:hover:bg-zinc-700 transition-colors cursor-pointer"
>
Sign out
</button>
</div>
);
};
154 changes: 93 additions & 61 deletions src/components/PullRequestList.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,83 @@
import { graphql, useLazyLoadQuery } from "react-relay";
import type { PullRequestListQuery as PullRequestListQueryType } from "@/__generated__/PullRequestListQuery.graphql";

const PullRequestListQuery = graphql`
query PullRequestListQuery($first: Int!) {
viewer {
login
pullRequests(first: $first, states: [OPEN], orderBy: { field: UPDATED_AT, direction: DESC }) {
totalCount
nodes {
id
number
title
url
state
isDraft
createdAt
updatedAt
repository {
name
nameWithOwner
}
baseRefName
headRefName
additions
deletions
reviewDecision
}
}
}
}
`;
import { graphql, usePaginationFragment } from "react-relay";
import type { PullRequestList_viewer$key } from "./__generated__/PullRequestList_viewer.graphql";

interface PullRequestListProps {
count?: number;
viewer: PullRequestList_viewer$key;
}

type PullRequest = NonNullable<
NonNullable<
PullRequestListQueryType["response"]["viewer"]["pullRequests"]["nodes"]
>[number]
>;
export const PullRequestList = ({ viewer }: PullRequestListProps) => {
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
graphql`
fragment PullRequestList_viewer on User
@refetchable(queryName: "PullRequestListPaginationQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 10 }
after: { type: "String" }
) {
login
pullRequests(
first: $first
after: $after
orderBy: { field: UPDATED_AT, direction: DESC }
) @connection(key: "PullRequestList_pullRequests") {
totalCount
edges {
node {
id
number
title
url
state
isDraft
createdAt
updatedAt
repository {
name
nameWithOwner
}
baseRefName
headRefName
additions
deletions
reviewDecision

export default function PullRequestList({ count = 20 }: PullRequestListProps) {
const data = useLazyLoadQuery<PullRequestListQueryType>(PullRequestListQuery, {
first: count,
});
author {
... on User {
name
login
}
}
}
}
}
}
`,
viewer
);

const { viewer } = data;
const pullRequests = (viewer.pullRequests.nodes?.filter(
(pr): pr is PullRequest => pr !== null && pr !== undefined
) ?? []) as PullRequest[];
const pullRequests =
data.pullRequests.edges
?.filter((edge) => edge?.node != null)
.map((edge) => edge!.node!) ?? [];

const getReviewDecisionBadge = (decision: string | null | undefined) => {
if (!decision) return null;

const badges = {
APPROVED: { text: "Approved", color: "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200" },
CHANGES_REQUESTED: { text: "Changes Requested", color: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200" },
REVIEW_REQUIRED: { text: "Review Required", color: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200" },
APPROVED: {
text: "Approved",
color:
"bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200",
},
CHANGES_REQUESTED: {
text: "Changes Requested",
color: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200",
},
REVIEW_REQUIRED: {
text: "Review Required",
color:
"bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200",
},
};

const badge = badges[decision as keyof typeof badges];
Expand All @@ -74,10 +94,11 @@ export default function PullRequestList({ count = 20 }: PullRequestListProps) {
<div className="w-full max-w-4xl mx-auto p-6">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">
{viewer.login}'s Open Pull Requests
{data.login}'s Pull Requests
</h1>
<p className="text-zinc-600 dark:text-zinc-400">
{viewer.pullRequests.totalCount} open pull request{viewer.pullRequests.totalCount !== 1 ? 's' : ''}
{data.pullRequests.totalCount} pull request
{data.pullRequests.totalCount !== 1 ? "s" : ""}
</p>
</div>

Expand Down Expand Up @@ -130,21 +151,32 @@ export default function PullRequestList({ count = 20 }: PullRequestListProps) {

<div className="flex items-center gap-4 text-sm text-zinc-500 dark:text-zinc-500">
<div className="flex items-center gap-1">
<span className="text-green-600 dark:text-green-400">+{pr.additions}</span>
<span className="text-red-600 dark:text-red-400">-{pr.deletions}</span>
</div>
<div>
Created {new Date(pr.createdAt).toLocaleDateString()}
</div>
<div>
Updated {new Date(pr.updatedAt).toLocaleDateString()}
<span className="text-green-600 dark:text-green-400">
+{pr.additions}
</span>
<span className="text-red-600 dark:text-red-400">
-{pr.deletions}
</span>
</div>
<div>Created {new Date(pr.createdAt).toLocaleDateString()}</div>
<div>Updated {new Date(pr.updatedAt).toLocaleDateString()}</div>
</div>
</a>
))}
</div>
)}

{hasNext && (
<div className="mt-6 text-center">
<button
onClick={() => loadNext(10)}
disabled={isLoadingNext}
className="px-6 py-3 bg-zinc-800 dark:bg-zinc-200 text-white dark:text-zinc-900 rounded-lg font-medium hover:bg-zinc-700 dark:hover:bg-zinc-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isLoadingNext ? "Loading..." : "Load Next Page"}
</button>
</div>
)}
</div>
);
}

};
1 change: 0 additions & 1 deletion src/lib/relay/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,3 @@ export function getRelayEnvironment(accessToken: string): Environment {
export function resetRelayEnvironment(): void {
clientEnvironment = null;
}

46 changes: 23 additions & 23 deletions src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { initiateGitHubLogin, type GitHubUser } from '../lib/auth';
import PullRequestList from '../components/PullRequestList';
import { useLazyLoadQuery, graphql } from "react-relay";

import { initiateGitHubLogin } from "../lib/auth";
import { PullRequestList } from "../components/PullRequestList";
import { AuthorizedHeader } from "@/components/AuthorizedHeader";

import { HomePageQuery } from "./__generated__/HomePageQuery.graphql";

interface HomePageProps {
user: GitHubUser | null;
onLogout: () => void;
}

export default function HomePage({ user, onLogout }: HomePageProps) {
if (!user) {
export default function HomePage({ onLogout }: HomePageProps) {
const data = useLazyLoadQuery<HomePageQuery>(
graphql`
query HomePageQuery {
viewer {
...AuthorizedHeader_user
...PullRequestList_viewer
}
}
`,
{}
);

if (!data.viewer) {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
<div className="text-center">
Expand All @@ -31,26 +47,10 @@ export default function HomePage({ user, onLogout }: HomePageProps) {
return (
<div className="min-h-screen bg-zinc-50 dark:bg-black py-8">
<div className="max-w-4xl mx-auto px-6 mb-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
Welcome, {user.name || user.login}
</h1>
<p className="text-zinc-600 dark:text-zinc-400">
{user.email}
</p>
</div>
<button
onClick={onLogout}
className="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100 rounded-lg hover:bg-zinc-300 dark:hover:bg-zinc-700 transition-colors"
>
Sign out
</button>
</div>
<AuthorizedHeader user={data.viewer} onLogout={onLogout} />
</div>

<PullRequestList />
<PullRequestList viewer={data.viewer} />
</div>
);
}

13 changes: 6 additions & 7 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";

// https://vite.dev/config/
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['relay'],
plugins: ["relay"],
},
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
"@": path.resolve(__dirname, "./src"),
},
},
server: {
port: 3000,
},
})

});