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"
/>
+