diff --git a/packages/shared/src/features/profile/components/githubRepos/GithubRepoCard.tsx b/packages/shared/src/features/profile/components/githubRepos/GithubRepoCard.tsx new file mode 100644 index 0000000000..c0cef33985 --- /dev/null +++ b/packages/shared/src/features/profile/components/githubRepos/GithubRepoCard.tsx @@ -0,0 +1,100 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { GitHubUserRepository } from '../../../../graphql/github'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { StarIcon } from '../../../../components/icons'; +import { IconSize } from '../../../../components/Icon'; +import { githubLanguageColors } from '../../../../lib/githubLanguageColors'; +import { largeNumberFormat } from '../../../../lib/numberFormat'; + +interface GithubRepoCardProps { + repo: GitHubUserRepository; +} + +export function GithubRepoCard({ repo }: GithubRepoCardProps): ReactElement { + const languageColor = repo.language + ? githubLanguageColors[repo.language] + : undefined; + + return ( + +
+ + {repo.name} + +
+ {repo.description && ( + + {repo.description} + + )} +
+ {repo.language && ( + + + + {repo.language} + + + )} + {repo.stars > 0 && ( + + + + {largeNumberFormat(repo.stars)} + + + )} + {repo.forks > 0 && ( + + + + + + {largeNumberFormat(repo.forks)} + + + )} +
+
+ ); +} diff --git a/packages/shared/src/features/profile/components/githubRepos/ProfileUserGithubRepos.tsx b/packages/shared/src/features/profile/components/githubRepos/ProfileUserGithubRepos.tsx new file mode 100644 index 0000000000..6d16030a44 --- /dev/null +++ b/packages/shared/src/features/profile/components/githubRepos/ProfileUserGithubRepos.tsx @@ -0,0 +1,53 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { PublicProfile } from '../../../../lib/user'; +import { useUserGithubRepos } from '../../hooks/useUserGithubRepos'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../../../../components/typography/Typography'; +import { GitHubIcon } from '../../../../components/icons'; +import { IconSize } from '../../../../components/Icon'; +import { GithubRepoCard } from './GithubRepoCard'; + +interface ProfileUserGithubReposProps { + user: PublicProfile; +} + +export function ProfileUserGithubRepos({ + user, +}: ProfileUserGithubReposProps): ReactElement | null { + const { repos, hasGithub, isLoading } = useUserGithubRepos(user); + + if (!hasGithub || (!isLoading && repos.length === 0)) { + return null; + } + + return ( +
+
+ + + GitHub Repositories + +
+
+ {isLoading + ? ['skeleton-1', 'skeleton-2', 'skeleton-3', 'skeleton-4'].map( + (key) => ( +
+ ), + ) + : repos.map((repo) => )} +
+
+ ); +} diff --git a/packages/shared/src/features/profile/hooks/useUserGithubRepos.ts b/packages/shared/src/features/profile/hooks/useUserGithubRepos.ts new file mode 100644 index 0000000000..806bce4ed1 --- /dev/null +++ b/packages/shared/src/features/profile/hooks/useUserGithubRepos.ts @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import type { PublicProfile } from '../../../lib/user'; +import { getUserGithubRepositories } from '../../../graphql/github'; +import { generateQueryKey, RequestKey, StaleTime } from '../../../lib/query'; + +export function useUserGithubRepos(user: PublicProfile | null) { + const hasGithub = useMemo(() => { + if (!user?.socialLinks) { + return false; + } + return user.socialLinks.some((link) => link.platform === 'github'); + }, [user?.socialLinks]); + + const queryKey = generateQueryKey( + RequestKey.UserGithubRepos, + user, + 'profile', + ); + + const query = useQuery({ + queryKey, + queryFn: () => getUserGithubRepositories(user?.id as string), + staleTime: StaleTime.Default, + enabled: !!user?.id && hasGithub, + }); + + const repos = useMemo(() => query.data ?? [], [query.data]); + + return { + ...query, + repos, + hasGithub, + }; +} diff --git a/packages/shared/src/graphql/github.ts b/packages/shared/src/graphql/github.ts new file mode 100644 index 0000000000..cd33fbbde1 --- /dev/null +++ b/packages/shared/src/graphql/github.ts @@ -0,0 +1,41 @@ +import { gql } from 'graphql-request'; +import { gqlClient } from './common'; + +export type GitHubUserRepository = { + id: string; + owner: string; + name: string; + fullName: string; + url: string; + description: string | null; + stars: number; + forks: number; + language: string | null; + updatedAt: string; +}; + +export const USER_GITHUB_REPOSITORIES_QUERY = gql` + query UserGithubRepositories($userId: ID!) { + userGithubRepositories(userId: $userId) { + id + owner + name + fullName + url + description + stars + forks + language + updatedAt + } + } +`; + +export const getUserGithubRepositories = async ( + userId: string, +): Promise => { + const result = await gqlClient.request<{ + userGithubRepositories: GitHubUserRepository[]; + }>(USER_GITHUB_REPOSITORIES_QUERY, { userId }); + return result.userGithubRepositories; +}; diff --git a/packages/shared/src/lib/githubLanguageColors.ts b/packages/shared/src/lib/githubLanguageColors.ts new file mode 100644 index 0000000000..ee125a3ddf --- /dev/null +++ b/packages/shared/src/lib/githubLanguageColors.ts @@ -0,0 +1,33 @@ +export const githubLanguageColors: Record = { + TypeScript: '#3178c6', + JavaScript: '#f1e05a', + Python: '#3572A5', + Java: '#b07219', + Go: '#00ADD8', + Rust: '#dea584', + 'C++': '#f34b7d', + C: '#555555', + 'C#': '#178600', + Ruby: '#701516', + PHP: '#4F5D95', + Swift: '#F05138', + Kotlin: '#A97BFF', + Dart: '#00B4AB', + Scala: '#c22d40', + Shell: '#89e051', + Lua: '#000080', + Haskell: '#5e5086', + R: '#198CE7', + Elixir: '#6e4a7e', + Clojure: '#db5855', + Erlang: '#B83998', + Julia: '#a270ba', + Zig: '#ec915c', + Nim: '#ffc200', + HTML: '#e34c26', + CSS: '#563d7c', + Vue: '#41b883', + SCSS: '#c6538c', + Svelte: '#ff3e00', + 'Objective-C': '#438eff', +}; diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index 0e974640a1..b3eddeaea9 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -249,6 +249,7 @@ export enum RequestKey { UserAchievements = 'user_achievements', TrackedAchievement = 'tracked_achievement', AchievementSyncStatus = 'achievement_sync_status', + UserGithubRepos = 'user_github_repos', } export const getPostByIdKey = (id: string): QueryKey => [RequestKey.Post, id]; diff --git a/packages/webapp/pages/[userId]/index.tsx b/packages/webapp/pages/[userId]/index.tsx index c8f19ef62e..4e679420a1 100644 --- a/packages/webapp/pages/[userId]/index.tsx +++ b/packages/webapp/pages/[userId]/index.tsx @@ -12,6 +12,7 @@ import { ProfileUserExperiences } from '@dailydotdev/shared/src/features/profile import { ProfileUserStack } from '@dailydotdev/shared/src/features/profile/components/stack/ProfileUserStack'; import { ProfileUserHotTakes } from '@dailydotdev/shared/src/features/profile/components/hotTakes/ProfileUserHotTakes'; import { ProfileUserWorkspacePhotos } from '@dailydotdev/shared/src/features/profile/components/workspacePhotos/ProfileUserWorkspacePhotos'; +import { ProfileUserGithubRepos } from '@dailydotdev/shared/src/features/profile/components/githubRepos/ProfileUserGithubRepos'; import { useUploadCv } from '@dailydotdev/shared/src/features/profile/hooks/useUploadCv'; import { ActionType } from '@dailydotdev/shared/src/graphql/actions'; import { ProfileWidgets } from '@dailydotdev/shared/src/features/profile/components/ProfileWidgets/ProfileWidgets'; @@ -130,6 +131,7 @@ const ProfilePage = ({ className="no-scrollbar overflow-auto laptop:hidden" />
+