diff --git a/src/App.tsx b/src/App.tsx index 31a984d..117720d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(loadAuthState()); @@ -45,14 +48,11 @@ function App() { } > - + ) : ( - + ) } /> @@ -68,4 +68,3 @@ function App() { } export default App; - diff --git a/src/components/AuthorizedHeader.tsx b/src/components/AuthorizedHeader.tsx new file mode 100644 index 0000000..f3cc91d --- /dev/null +++ b/src/components/AuthorizedHeader.tsx @@ -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 ( +
+
+ User Avatar +
+

Welcome,

+

+ {data.name || data.login} +

+
+
+ + +
+ ); +}; diff --git a/src/components/PullRequestList.tsx b/src/components/PullRequestList.tsx index f8cb065..d2aaef0 100644 --- a/src/components/PullRequestList.tsx +++ b/src/components/PullRequestList.tsx @@ -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(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]; @@ -74,10 +94,11 @@ export default function PullRequestList({ count = 20 }: PullRequestListProps) {

- {viewer.login}'s Open Pull Requests + {data.login}'s Pull Requests

- {viewer.pullRequests.totalCount} open pull request{viewer.pullRequests.totalCount !== 1 ? 's' : ''} + {data.pullRequests.totalCount} pull request + {data.pullRequests.totalCount !== 1 ? "s" : ""}

@@ -130,21 +151,32 @@ export default function PullRequestList({ count = 20 }: PullRequestListProps) {
- +{pr.additions} - -{pr.deletions} -
-
- Created {new Date(pr.createdAt).toLocaleDateString()} -
-
- Updated {new Date(pr.updatedAt).toLocaleDateString()} + + +{pr.additions} + + + -{pr.deletions} +
+
Created {new Date(pr.createdAt).toLocaleDateString()}
+
Updated {new Date(pr.updatedAt).toLocaleDateString()}
))}
)} + + {hasNext && ( +
+ +
+ )} ); -} - +}; diff --git a/src/lib/relay/environment.ts b/src/lib/relay/environment.ts index cffd3fd..d305856 100644 --- a/src/lib/relay/environment.ts +++ b/src/lib/relay/environment.ts @@ -44,4 +44,3 @@ export function getRelayEnvironment(accessToken: string): Environment { export function resetRelayEnvironment(): void { clientEnvironment = null; } - diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 488776f..05fe9dc 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -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( + graphql` + query HomePageQuery { + viewer { + ...AuthorizedHeader_user + ...PullRequestList_viewer + } + } + `, + {} + ); + + if (!data.viewer) { return (
@@ -31,26 +47,10 @@ export default function HomePage({ user, onLogout }: HomePageProps) { return (
-
-
-

- Welcome, {user.name || user.login} -

-

- {user.email} -

-
- -
+
- +
); } - diff --git a/vite.config.ts b/vite.config.ts index 6511f7e..a9f7519 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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, }, -}) - +});