From e947655b581a1ff726e2475976fc38fe47f75f78 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink <17958158+jribbink@users.noreply.github.com> Date: Wed, 22 Jan 2025 13:06:47 -0800 Subject: [PATCH] Add GitHub picture to profile (#1100) --- src/components/ProfileModal.tsx | 30 ++++++++- src/hooks/use-debounce.ts | 20 ++++++ src/hooks/use-github-user.ts | 22 +++++++ src/types/github.ts | 65 +++++++++++++++++++ .../src/lib/Components/Modal/index.tsx | 29 +++++---- 5 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 src/hooks/use-debounce.ts create mode 100644 src/hooks/use-github-user.ts create mode 100644 src/types/github.ts diff --git a/src/components/ProfileModal.tsx b/src/components/ProfileModal.tsx index b35000354c..25409ca3b8 100644 --- a/src/components/ProfileModal.tsx +++ b/src/components/ProfileModal.tsx @@ -17,6 +17,9 @@ import * as fcl from '@onflow/fcl'; import { z } from 'zod'; import { parseIdentifier, typeIdentifier } from '../utils/flow'; import { CONTRACT_IDENTIFIER_REGEX } from '../utils/constants'; +import { useGithubUser } from '../hooks/use-github-user'; +import { useDebounce } from '../hooks/use-debounce'; +import { faUser } from '@fortawesome/free-solid-svg-icons'; interface ProfileModalProps { isOpen: boolean; @@ -82,6 +85,13 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => { deployedContracts?: boolean; }>({}); + const { value: debouncedGithubHandle, isDebouncing: githubDebouncing } = + useDebounce(settings?.socials?.[SocialType.GITHUB], 1000); + const { user: githubUser, isLoading: githubFetchLoading } = useGithubUser( + debouncedGithubHandle, + ); + const isGithubLoading = githubFetchLoading || githubDebouncing; + const validate = () => { let hasErrors = false; @@ -236,7 +246,19 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => { return ( -
+
+
+ {(isGithubLoading || !githubUser?.avatar_url) && ( + + )} + {!isGithubLoading && githubUser?.avatar_url && ( + + )} +
+
= ({ isOpen, onClose }) => { socials, }); }} - onBlur={() => setTouched({ ...touched, socials: true })} + onBlur={() => { + setTouched({ ...touched, socials: true }); + }} />
@@ -317,7 +341,7 @@ const ProfileModal: React.FC = ({ isOpen, onClose }) => {
-
+
source.name)} value={settings?.referralSource || ''} diff --git a/src/hooks/use-debounce.ts b/src/hooks/use-debounce.ts new file mode 100644 index 0000000000..fcb1779f2a --- /dev/null +++ b/src/hooks/use-debounce.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return { + value: debouncedValue, + isDebouncing: debouncedValue !== value, + }; +} diff --git a/src/hooks/use-github-user.ts b/src/hooks/use-github-user.ts new file mode 100644 index 0000000000..10c358d609 --- /dev/null +++ b/src/hooks/use-github-user.ts @@ -0,0 +1,22 @@ +import useSWR from 'swr/immutable'; +import { User } from '../types/github'; + +export function useGithubUser(username?: string | null) { + const { + data: user, + isLoading, + error, + } = useSWR( + username ? `https://api.github.com/users/${username}` : null, + async (url) => { + const response = await fetch(url); + return response.json() as Promise; + }, + ); + + return { + user, + isLoading, + error, + }; +} diff --git a/src/types/github.ts b/src/types/github.ts new file mode 100644 index 0000000000..058db01a99 --- /dev/null +++ b/src/types/github.ts @@ -0,0 +1,65 @@ +interface Plan { + collaborators: number; + name: string; + space: number; + private_repos: number; +} + +interface BaseUser { + login: string; + id: number; + user_view_type: string; + node_id: string; + avatar_url: string; + gravatar_id: string | null; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + name: string | null; + company: string | null; + blog: string | null; + location: string | null; + email: string | null; + notification_email?: string | null; + hireable: boolean | null; + bio: string | null; + twitter_username: string | null; + public_repos: number; + public_gists: number; + followers: number; + following: number; + created_at: string; + updated_at: string; + plan: Plan; +} + +interface PrivateUser extends BaseUser { + private_gists: number; + total_private_repos: number; + owned_private_repos: number; + disk_usage: number; + collaborators: number; + two_factor_authentication: boolean; + business_plus?: boolean; + ldap_dn?: string; +} + +interface PublicUser extends BaseUser { + private_gists?: number; + total_private_repos?: number; + owned_private_repos?: number; + disk_usage?: number; + collaborators?: number; +} + +export type User = PrivateUser | PublicUser; diff --git a/src/ui/design-system/src/lib/Components/Modal/index.tsx b/src/ui/design-system/src/lib/Components/Modal/index.tsx index 3abb8fbf8b..833610f7b0 100644 --- a/src/ui/design-system/src/lib/Components/Modal/index.tsx +++ b/src/ui/design-system/src/lib/Components/Modal/index.tsx @@ -12,13 +12,13 @@ interface ModalProps { } const Modal: React.FC = ({ - isOpen, - onClose, - title, - children, - className, - scrollable = true, // default is scrollable - }) => { + isOpen, + onClose, + title, + children, + className, + scrollable = true, // default is scrollable +}) => { return ( @@ -48,9 +48,9 @@ const Modal: React.FC = ({ > = ({ {/* Header */} {title && (
- + {title}
)} {/* Content */} -
- {children} -
+
{children}