Skip to content

Commit c339d55

Browse files
committed
caching graphql requests, profile loading state, profile layout refactoring
1 parent f4aa4f8 commit c339d55

File tree

15 files changed

+222
-179
lines changed

15 files changed

+222
-179
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { formatDistanceToNow } from 'date-fns';
2+
import { BriefcaseBusiness, ExternalLink, Hourglass, Link2, Mail, MapPin, Timer, UsersRound } from 'lucide-react';
3+
import Image from 'next/image';
4+
import Link from 'next/link';
5+
import { FC } from 'react';
6+
7+
import { Page } from '@/components/page/page';
8+
import { AspectRatio } from '@/components/ui/aspect-ratio';
9+
import { Avatar, AvatarImage } from '@/components/ui/avatar';
10+
import { Button } from '@/components/ui/button';
11+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
12+
import { UserQuery } from '@/types/generated/graphql';
13+
14+
import { ProfileListItem } from './profile-list-item';
15+
import { RefreshButton } from './refresh-button';
16+
import { getSocialIcon } from '../utils/get-social-icon';
17+
18+
type LayoutLeftColumnProps = Readonly<{
19+
user: UserQuery['user'];
20+
children: React.ReactNode;
21+
}>;
22+
23+
export const LayoutLeftColumn: FC<LayoutLeftColumnProps> = ({ user, children }) => {
24+
const showContact = !!user?.email || !!user?.websiteUrl || !!user?.socialAccounts?.nodes?.length;
25+
26+
if (!user) {
27+
return null;
28+
}
29+
30+
return (
31+
<Page className="gap-6 flex-col md:flex-row">
32+
<div className="w-full md:w-3xs xl:w-2xs flex flex-col shrink-0 gap-4">
33+
<div className="flex flex-row md:flex-col items-center md:items-start gap-4">
34+
<div className="w-[64] sm:w-[128] md:w-full">
35+
<AspectRatio ratio={1}>
36+
<Avatar className="w-full h-full rounded-full" asChild>
37+
<AvatarImage src={user.avatarUrl!} />
38+
</Avatar>
39+
</AspectRatio>
40+
</div>
41+
<div>
42+
<h1 className="font-semibold text-2xl">{user.name}</h1>
43+
<h2 className="text-muted-foreground">@{user.login}</h2>
44+
</div>
45+
</div>
46+
<div className="flex flex-row md:flex-col gap-4">
47+
<RefreshButton />
48+
<Button size="sm" variant="secondary" className="flex-grow" asChild>
49+
<Link href={`https://github.com/${user.login}`} target="_blank" rel="noopener noreferrer">
50+
Open GitHub
51+
<ExternalLink className="size-4" />
52+
</Link>
53+
</Button>
54+
</div>
55+
<div className="flex flex-col gap-6">
56+
<div className="flex flex-col gap-1.5">
57+
<ProfileListItem value={user.location} Icon={MapPin} />
58+
<ProfileListItem value={user.company} Icon={BriefcaseBusiness} />
59+
<ProfileListItem value={'Profile age: ' + formatDistanceToNow(user.githubCreatedAt)} Icon={Hourglass} />
60+
<ProfileListItem
61+
value={'Updated ' + formatDistanceToNow(user.githubFetchedAt, { addSuffix: true })}
62+
Icon={Timer}
63+
/>
64+
<ProfileListItem
65+
value={`${user.followersCount?.toLocaleString('en-US')} followers • ${user.followingCount?.toLocaleString(
66+
'en-US',
67+
)} following`}
68+
Icon={UsersRound}
69+
/>
70+
</div>
71+
{showContact && (
72+
<div className="flex flex-col gap-1.5">
73+
<h4 className="text-lg font-semibold">Contacts</h4>
74+
<ProfileListItem value={user.email} url={`mailto:${user.email}`} Icon={Mail} />
75+
<ProfileListItem value={user.websiteUrl} url={user.websiteUrl!} Icon={Link2} />
76+
{user.socialAccounts?.nodes?.map((account) => (
77+
<ProfileListItem
78+
key={`${account.provider}${account.displayName}`}
79+
value={account.displayName}
80+
url={account.url}
81+
Icon={getSocialIcon(account.provider, account.url)}
82+
/>
83+
))}
84+
</div>
85+
)}
86+
{!!user.organizations?.length && (
87+
<div className="flex flex-col gap-1.5">
88+
<h4 className="text-lg font-semibold">Organizations</h4>
89+
<div className="flex gap-1 flex-wrap">
90+
{user.organizations?.map((org) => (
91+
<TooltipProvider key={org.login}>
92+
<Tooltip>
93+
<TooltipTrigger asChild>
94+
<Link href={`https://github.com/${org.login}`} target="_blank" rel="noopener noreferrer">
95+
<Image
96+
src={org.avatarUrl!}
97+
alt={org.login}
98+
width={32}
99+
height={32}
100+
className="rounded-sm bg-secondary"
101+
/>
102+
</Link>
103+
</TooltipTrigger>
104+
<TooltipContent>{org.login}</TooltipContent>
105+
</Tooltip>
106+
</TooltipProvider>
107+
))}
108+
</div>
109+
</div>
110+
)}
111+
</div>
112+
</div>
113+
{children}
114+
</Page>
115+
);
116+
};

app/profile/[login]/components/ranks-overview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { UserQuery } from '@/types/generated/graphql';
66
import { ProfileCard } from './profile-card';
77

88
type RanksOverviewProps = {
9-
ranksData: UserQuery['user']['rank'];
9+
ranksData: NonNullable<UserQuery['user']>['rank'];
1010
};
1111

1212
export const RanksOverview: FC<RanksOverviewProps> = ({ ranksData }) => {

app/profile/[login]/components/repositories-overiview.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { UserQuery } from '@/types/generated/graphql';
44

55
import { ProfileCard } from './profile-card';
66

7+
type User = NonNullable<UserQuery['user']>;
8+
79
type RepositoriesOverviewProps = {
8-
repositories: UserQuery['user']['repositories'];
9-
contributions: UserQuery['user']['contributions'];
10-
ownedStars: UserQuery['user']['ownedStars'];
11-
contributedStars: UserQuery['user']['contributedStars'];
10+
repositories: User['repositories'];
11+
contributions: User['contributions'];
12+
ownedStars: User['ownedStars'];
13+
contributedStars: User['contributedStars'];
1214
};
1315

1416
export const RepositoriesOverview: FC<RepositoriesOverviewProps> = ({

app/profile/[login]/layout.tsx

Lines changed: 4 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,12 @@
1-
import { formatDistanceToNow } from 'date-fns';
2-
import { BriefcaseBusiness, ExternalLink, Hourglass, Link2, Mail, MapPin, Timer, UsersRound } from 'lucide-react';
3-
import Image from 'next/image';
4-
import Link from 'next/link';
5-
61
import { Header } from '@/components/header/header';
7-
import { Page } from '@/components/page/page';
8-
import { AspectRatio } from '@/components/ui/aspect-ratio';
9-
import { Avatar, AvatarImage } from '@/components/ui/avatar';
10-
import { Button } from '@/components/ui/button';
11-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
122
import { graphqlDirect } from '@/lib/graphql/graphql-direct';
133
import { TopRanksDocument } from '@/types/generated/graphql';
144

15-
import { ProfileListItem } from './components/profile-list-item';
16-
import { RefreshButton } from './components/refresh-button';
17-
import { fetchProfileData } from './utils/fetch-profile-data';
18-
import { getSocialIcon } from './utils/get-social-icon';
19-
205
type ProfileLayoutProps = Readonly<{ children: React.ReactNode; params: Promise<{ login: string }> }>;
216

227
// Next.js will invalidate the cache when a
23-
// request comes in, at most once every 3600 seconds.
24-
export const revalidate = 3600;
8+
// request comes in, at most once every 10800 seconds.
9+
export const revalidate = 10800;
2510

2611
// We'll prerender only the params from `generateStaticParams` at build time.
2712
// If a request comes in for a path that hasn't been generated,
@@ -43,110 +28,11 @@ export async function generateStaticParams() {
4328
return [...uniqueLogins].map((login) => ({ login }));
4429
}
4530

46-
export default async function ProfileLayout({ params, children }: ProfileLayoutProps) {
47-
const { login } = await params;
48-
const { user } = await fetchProfileData(login);
49-
50-
if (!user) {
51-
return (
52-
<Page className="gap-6 flex-col md:flex-row">
53-
<div className="flex-grow flex flex-col gap-6">
54-
<h1 className="text-2xl font-semibold">User not found</h1>
55-
<p className="text-muted-foreground">The user you are looking for does not exist.</p>
56-
</div>
57-
</Page>
58-
);
59-
}
60-
61-
const showContact = !!user?.email || !!user?.websiteUrl || !!user?.socialAccounts?.nodes?.length;
62-
31+
export default async function ProfileLayout({ children }: ProfileLayoutProps) {
6332
return (
6433
<>
6534
<Header />
66-
<Page className="gap-6 flex-col md:flex-row">
67-
<div className="w-full md:w-3xs xl:w-2xs flex flex-col shrink-0 gap-4">
68-
<div className="flex flex-row md:flex-col items-center md:items-start gap-4">
69-
<div className="w-[64] sm:w-[128] md:w-full">
70-
<AspectRatio ratio={1}>
71-
<Avatar className="w-full h-full rounded-full" asChild>
72-
<AvatarImage src={user.avatarUrl!} />
73-
</Avatar>
74-
</AspectRatio>
75-
</div>
76-
<div>
77-
<h1 className="font-semibold text-2xl">{user.name}</h1>
78-
<h2 className="text-muted-foreground">@{user.login}</h2>
79-
</div>
80-
</div>
81-
<div className="flex flex-row md:flex-col gap-4">
82-
<RefreshButton />
83-
<Button size="sm" variant="secondary" className="flex-grow" asChild>
84-
<Link href={`https://github.com/${user.login}`} target="_blank" rel="noopener noreferrer">
85-
Open GitHub
86-
<ExternalLink className="size-4" />
87-
</Link>
88-
</Button>
89-
</div>
90-
<div className="flex flex-col gap-6">
91-
<div className="flex flex-col gap-1.5">
92-
<ProfileListItem value={user.location} Icon={MapPin} />
93-
<ProfileListItem value={user.company} Icon={BriefcaseBusiness} />
94-
<ProfileListItem value={'Profile age: ' + formatDistanceToNow(user.githubCreatedAt)} Icon={Hourglass} />
95-
<ProfileListItem
96-
value={'Updated ' + formatDistanceToNow(user.githubFetchedAt, { addSuffix: true })}
97-
Icon={Timer}
98-
/>
99-
<ProfileListItem
100-
value={`${user.followersCount?.toLocaleString(
101-
'en-US',
102-
)} followers • ${user.followingCount?.toLocaleString('en-US')} following`}
103-
Icon={UsersRound}
104-
/>
105-
</div>
106-
{showContact && (
107-
<div className="flex flex-col gap-1.5">
108-
<h4 className="text-lg font-semibold">Contacts</h4>
109-
<ProfileListItem value={user.email} url={`mailto:${user.email}`} Icon={Mail} />
110-
<ProfileListItem value={user.websiteUrl} url={user.websiteUrl!} Icon={Link2} />
111-
{user.socialAccounts?.nodes?.map((account) => (
112-
<ProfileListItem
113-
key={account.displayName}
114-
value={account.displayName}
115-
url={account.url}
116-
Icon={getSocialIcon(account.provider, account.url)}
117-
/>
118-
))}
119-
</div>
120-
)}
121-
{!!user.organizations?.length && (
122-
<div className="flex flex-col gap-1.5">
123-
<h4 className="text-lg font-semibold">Organizations</h4>
124-
<div className="flex gap-1 flex-wrap">
125-
{user.organizations?.map((org) => (
126-
<TooltipProvider key={org.login}>
127-
<Tooltip>
128-
<TooltipTrigger asChild>
129-
<Link href={`https://github.com/${org.login}`} target="_blank" rel="noopener noreferrer">
130-
<Image
131-
src={org.avatarUrl!}
132-
alt={org.login}
133-
width={32}
134-
height={32}
135-
className="rounded-sm bg-secondary"
136-
/>
137-
</Link>
138-
</TooltipTrigger>
139-
<TooltipContent>{org.login}</TooltipContent>
140-
</Tooltip>
141-
</TooltipProvider>
142-
))}
143-
</div>
144-
</div>
145-
)}
146-
</div>
147-
</div>
148-
{children}
149-
</Page>
35+
{children}
15036
</>
15137
);
15238
}

app/profile/[login]/loading.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { AspectRatio } from '@radix-ui/react-aspect-ratio';
2+
3+
import { Page } from '@/components/page/page';
4+
import { Skeleton } from '@/components/ui/skeleton';
5+
6+
export default function Loading() {
7+
return (
8+
<Page className="gap-6 flex-col md:flex-row">
9+
<div className="w-full md:w-3xs xl:w-2xs flex flex-col shrink-0 gap-12">
10+
<div className="flex flex-row md:flex-col items-center md:items-start gap-4">
11+
<div className="w-[64] sm:w-[128] md:w-full">
12+
<AspectRatio ratio={1}>
13+
<Skeleton className="w-full h-full rounded-full" />
14+
</AspectRatio>
15+
</div>
16+
<div className="flex flex-col gap-3">
17+
<Skeleton className="h-5 w-[200]" />
18+
<Skeleton className="h-4 w-[150]" />
19+
</div>
20+
</div>
21+
<div className="flex flex-col gap-3">
22+
<Skeleton className="h-4 flex grow" />
23+
<Skeleton className="h-4 flex grow" />
24+
</div>
25+
</div>
26+
<div className="flex-grow flex flex-col gap-6">
27+
<div className="flex flex-col md:flex-row flex-wrap gap-6">
28+
<Skeleton className="h-[150px] grow min-w-xs rounded-xl" />
29+
<Skeleton className="h-[150px] grow min-w-xs rounded-xl" />
30+
</div>
31+
</div>
32+
</Page>
33+
);
34+
}

app/profile/[login]/not-found.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Page } from '@/components/page/page';
2+
3+
export default async function NotFound() {
4+
return (
5+
<Page className="gap-6 flex-col md:flex-row">
6+
<div className="flex-grow flex flex-col gap-6">
7+
<h1 className="text-2xl font-semibold">User not found</h1>
8+
<p className="text-muted-foreground">The user you are looking for does not exist.</p>
9+
</div>
10+
</Page>
11+
);
12+
}

0 commit comments

Comments
 (0)