Skip to content

Commit

Permalink
feat: add home page and profile page (#36)
Browse files Browse the repository at this point in the history
* feat: add tailwind plugin to centering dividers

divider from tailiwind are based on border,
often times it's not centered enough because the padding is not balanced
between dividers, here I tried to balance it using this plugin.
It also can adjust the amount padding needed.

* feat: add home and test profile page

+ move token retrieval in custom hooks useAuth
+ add custom hooks to check current tab is github profile url and
opensauced user
+ add activeTab permission to manifest.json to check url of current tab
from extension popup
+ refactor routing logic to use context api, so it can be used anywhere
without prop drilling
+ refactor setRenderedPage to accept props, because I need to pass
username to profile page
+ wrap setRenderedPage with setCurrentPage function to
make it easy if we want to update route state without passing props
+ add new util cacheFetch to be able cache data from api,
reducing api calls.
+ add isOpenSaucedUser function for ease of use and clarity

* add real profile page

+ add getUserData on fetchOpenSaucedApiData to fetch  more detailed
user data
+ add getUserPRData to fetch user PR data

Note:
I use inline styling 'flexWrap' in profile page because it's interfering
with github page (again)
maybe we should use shadow DOM approach?

* refactor: remove comments

* fix: add target blank to links in profile page

to make it work

* chore:  Replaced literals with config values

* chore: replaced literals with values from cofig

* Replaced literals in fetchOpenSaucedApiData.ts with config vals

* fix dashboard url and link text as suggested

Co-authored-by: Brian Douglas <[email protected]>

* clarify tools button text as suggested

Co-authored-by: Brian Douglas <[email protected]>
Co-authored-by: Anush <[email protected]>

* fix profile blog url as suggested

Co-authored-by: Brian Douglas <[email protected]>

* patching user bio text in profile page as suggested

Co-authored-by: Anush <[email protected]>

* patch and refactor cachedFetch as suggeted

Changes:
+ Replaced || with ?? to avoid a falsy result when expiry is set to 0 seconds.
+ Replaced localStorage with chrome.storage.local .
+ Combined the text/*, application/json check into a single match() call.
+ Formatted the file with semi-colons(Linting will be added soon, so this shouldn't be a problem).

Co-authored-by: Anush <[email protected]>

* removing console.log (s), as suggested

Co-authored-by: Anush <[email protected]>

* removing console.log (s), as suggested

Co-authored-by: Anush <[email protected]>

* refactor: use Promise.all for concurrent network calls, as suggested

Suggestion:
Using Promise.all() will prove useful here as the calls can be made simultaneously

Co-authored-by: Anush <[email protected]>

* fix: user.blog_url to user.blog to
match api response

---------

Co-authored-by: Anush <[email protected]>
Co-authored-by: Brian Douglas <[email protected]>
  • Loading branch information
3 people authored Apr 27, 2023
1 parent 1c337c2 commit 8a43288
Show file tree
Hide file tree
Showing 10 changed files with 441 additions and 46 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
"128": "src/assets/os-icons/os-icon-128.png"
},
"host_permissions": ["<all_urls>"],
"permissions": ["storage","webRequest"]
"permissions": ["storage","webRequest", "activeTab"]
}
70 changes: 38 additions & 32 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
import { useState, useEffect } from "react";
import { OPEN_SAUCED_AUTH_TOKEN_KEY } from "./constants";
import { useState, useEffect, createContext } from "react";

import Start from "./pages/start";
import Home from "./pages/home";
import Loading from "./pages/loading";
import { checkTokenValidity } from "./utils/fetchOpenSaucedApiData";
import { Profile } from "./pages/profile";
import { useAuth } from "./hooks/useAuth";

export const RouteContext = createContext<{page: {name: string, props?: any}, setCurrentPage: (page: RouteKeys, props?: any) => void}>({page: {name: "loading"}, setCurrentPage: () => {}});

const routes = {
start: <Start />,
home: <Home />,
loading: <Loading />,
profile: <Profile />
}

type RouteKeys = keyof typeof routes;

function App() {
const [osAccessToken, setOsAccessToken] = useState("");
// renderedPage can be either "start", "home" or "loading"
const [renderedPage, setRenderedPage] = useState("loading");
const {isTokenValid} = useAuth()
const [renderedPage, setRenderedPage] = useState<{name: RouteKeys, props?: any}>({name: "loading", props: {}});

const setCurrentPage = (name: RouteKeys, props: any = {}) => {
setRenderedPage({name: name, props})
}


useEffect(() => {
chrome.storage.sync.get([OPEN_SAUCED_AUTH_TOKEN_KEY], (result) => {
const authToken: string | undefined = result[OPEN_SAUCED_AUTH_TOKEN_KEY];
if (authToken) {
checkTokenValidity(authToken).then((valid) => {
if (!valid) {
setOsAccessToken("");
setRenderedPage("signin");
} else {
setOsAccessToken(authToken);
setRenderedPage("home");
}
});
} else {
setRenderedPage("start");
}
});
}, []);
if(isTokenValid === null) {
setCurrentPage("loading")
}
else if(isTokenValid) {
setCurrentPage("home")
}
else {
setCurrentPage("start")
}
}, [isTokenValid]);

return (
<div className="p-4 bg-slate-800">
{renderedPage === "start" ? (
<Start setRenderedPage={setRenderedPage} />
) : renderedPage === "home" ? (
<Home osAccessToken={osAccessToken} setRenderedPage={setRenderedPage} />
) : (
<Loading />
)}
</div>
<RouteContext.Provider value={{page: renderedPage, setCurrentPage: setCurrentPage}}>
<div className="p-4 bg-slate-800">
{routes[renderedPage.name]}
</div>
</RouteContext.Provider>

);
}

Expand Down
52 changes: 52 additions & 0 deletions src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect, useState } from "react"
import { OPEN_SAUCED_AUTH_TOKEN_KEY, OPEN_SAUCED_SESSION_ENDPOINT } from "../constants"
import { cachedFetch } from "../utils/cache"

const removeTokenFromStorage = () => {
return new Promise((resolve, reject) => {

chrome.storage.sync.remove(OPEN_SAUCED_AUTH_TOKEN_KEY, () => {
resolve(true)
})
})
}

export const useAuth = () => {
const [authToken, setAuthToken] = useState<null | string>(null)
const [user, setUser] = useState<null | { id: string, user_name: string }>(null)
const [isTokenValid, setIsTokenValid] = useState<boolean|null>(null)

useEffect(() => {
chrome.storage.sync.get([OPEN_SAUCED_AUTH_TOKEN_KEY], (result) => {
if (result[OPEN_SAUCED_AUTH_TOKEN_KEY]) {
setAuthToken(result[OPEN_SAUCED_AUTH_TOKEN_KEY])
//get account data
cachedFetch(OPEN_SAUCED_SESSION_ENDPOINT, {
expireInSeconds: 2 * 60 * 60, // 2 hours
headers: {
Authorization: `Bearer ${result[OPEN_SAUCED_AUTH_TOKEN_KEY]}`,
Accept: 'application/json',
},
}).then((resp) => {
if (!resp.ok) {
console.log('error getting user info')
removeTokenFromStorage().then(() => {
setAuthToken(null)
setUser(null)
setIsTokenValid(false)
})
}
return resp.json()
})
.then((json) => {
setUser(json)
setIsTokenValid(true)
})
} else {
setIsTokenValid(false)
}
});
}, [])

return { authToken, user, isTokenValid }
}
27 changes: 27 additions & 0 deletions src/hooks/useOpensaucedUserCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useEffect, useState } from "react"
import { isOpenSaucedUser } from "../utils/fetchOpenSaucedApiData"
import { getGithubUsername } from "../utils/urlMatchers"

export const useOpensaucedUserCheck = () => {
const [currentTabIsOpensaucedUser, setCurrentTabIsOpensaucedUser] = useState(false)
const [checkedUser, setCheckedUser] = useState<string|null>(null)
useEffect(() => {
//get active tab
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
if (tabs.length > 0) {
const tab = tabs[0]
const username = getGithubUsername(tab.url!)
if(username != null) {
setCheckedUser(username)
setCurrentTabIsOpensaucedUser(await isOpenSaucedUser(username))
} else {
setCheckedUser(null)
setCurrentTabIsOpensaucedUser(false)
}
}
})
}, [])


return { currentTabIsOpensaucedUser, checkedUser }
}
66 changes: 58 additions & 8 deletions src/pages/home.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,64 @@
import React from "react";
import React, { useContext } from "react";
import { HiArrowTopRightOnSquare, HiUserCircle } from 'react-icons/hi2'
import { RouteContext } from "../App";
import OpenSaucedLogo from "../assets/opensauced-logo.svg";
import { useAuth } from "../hooks/useAuth";
import { useOpensaucedUserCheck } from "../hooks/useOpensaucedUserCheck";

interface HomeProps {
osAccessToken: string;
setRenderedPage: (page: string) => void;
}
function Home() {
const {setCurrentPage} = useContext(RouteContext)
const {user} = useAuth()
const {currentTabIsOpensaucedUser, checkedUser} = useOpensaucedUserCheck()

function Home({ osAccessToken, setRenderedPage }: HomeProps) {
return (
<div>
<p className="text-white">Home</p>
<div className="grid grid-cols-1 divide-y divide-white/40 divider-y-center-2 min-w-[320px] text-white">
<header className='flex justify-between'>
<img src={OpenSaucedLogo} alt="OpenSauced logo" className='w-[45%]' />
{user &&
<button
onClick={e=> {
setCurrentPage('profile', {userName: user.user_name})
}}
className='flex gap-1 items-center hover:text-orange text-white no-underline'>
{user?.user_name}
<img
src={`https://github.com/${user?.user_name}.png`}
alt="profile image"
className='rounded-full w-6 aspect-square border border-orange' />
</button>}
</header>
<main className='main-content'>
<h3 className='text font-medium text-base leading-10'>Tools:</h3>
<div className='tools flex flex-col gap-2'>
<a
target='_blank'
href={`https://insights.opensauced.pizza/feed`}
className='flex items-center bg-slate-700 hover:bg-slate-700/70 text-white hover:text-orange no-underline gap-2 p-1.5 px-3 w-full rounded-sm font-medium text-sm'
>
<HiArrowTopRightOnSquare />
Go to Highlights feed
</a>
<a
target='_blank'
href={`https://insights.opensauced.pizza`}
className='flex items-center bg-slate-700 hover:bg-slate-700/70 hover:text-orange text-white gap-2 p-1.5 px-3 w-full rounded-sm font-medium text-sm'
>
<HiArrowTopRightOnSquare />
Go to Dashboard
</a>
{
currentTabIsOpensaucedUser &&
<button
onClick={e => {
setCurrentPage('profile', {userName: checkedUser})
}}
className="flex items-center bg-slate-700 hover:bg-slate-700/70 hover:text-orange text-white gap-2 p-1.5 px-3 w-full rounded-sm font-medium text-sm">
<HiUserCircle />
View {checkedUser}'s profile
</button>
}
</div>
</main>
</div>
);
}
Expand Down
140 changes: 140 additions & 0 deletions src/pages/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { useContext, useEffect, useState } from 'react';
import { FaBrain, FaChevronLeft, FaRobot } from 'react-icons/fa';
import { RiLinkedinFill, RiLinkM, RiTwitterFill } from 'react-icons/ri';
import { AiOutlineReload } from 'react-icons/ai';
import { SiC, SiCplusplus, SiCsharp, SiGoland, SiJavascript, SiPhp, SiPython, SiReact, SiRuby, SiRust, SiTypescript } from 'react-icons/si';
import { DiJava } from 'react-icons/di'
import OpenSaucedLogo from "../assets/opensauced-logo.svg";
import { getUserData, getUserPRData } from '../utils/fetchOpenSaucedApiData';
import { RouteContext } from '../App';

const interestIcon = {
'python': <SiPython />,
'java': <DiJava />,
'javascript': <SiJavascript />,
'typescript': <SiTypescript />,
'csharp': <SiCsharp />,
'cpp': <SiCplusplus />,
'c': <SiC />,
'php': <SiPhp />,
'ruby': <SiRuby />,
'react': <SiReact />,
'ml': <FaBrain />,
'ai': <FaRobot />,
'golang': <SiGoland />,
'rust': <SiRust />
}

type InterestIconKeys = keyof typeof interestIcon;

export const Profile = () => {
const { page, setCurrentPage } = useContext(RouteContext)
const [user, setUser] = useState<null | { id: string, user_name: string, bio: string, created_at: string, linkedin_url: string, twitter_username: string, blog: string, interests: string, open_issues: number }>(null)
const [userPR, setUserPR] = useState<null | { meta: {itemCount: number} }>(null)

useEffect(() => {
const fetchUserData = async () => {
const [userData, userPRData] = await Promise.all([getUserData(page.props.userName), getUserPRData(page.props.userName)]);
setUser(userData);
setUserPR(userPRData);
}
fetchUserData();
}, [])


return (
<div className="grid grid-cols-1 divide-y divider-y-center-2 min-w-[320px] text-white">
<header className='flex justify-between'>
<div className="flex items-center gap-2">
<button onClick={() => { setCurrentPage("home") }} className='rounded-full p-2 bg-slate-700 hover:bg-slate-700/50'>
<FaChevronLeft className='text-osOrange' />
</button>
<img src={OpenSaucedLogo} alt="OpenSauced logo" className='w-[100%]' />
</div>
<button
title='Refresh user data'
className='hover:text-orange text-lg'
onClick={async () => {
const [userData, userPRData] = await Promise.all([getUserData(page.props.userName), getUserPRData(page.props.userName)]);
setUser(userData);
setUserPR(userPRData);
}}>
<AiOutlineReload />
</button>
</header>
<main>
<div className='flex flex-col items-center gap-1 mb-4'>
<img
src={`https://github.com/${page.props.userName}.png`}
alt="profile image"
className='rounded-full w-14 aspect-square p-1 bg-slate-700' />
<p className='font-medium'>@{page.props.userName}</p>
{(user?.linkedin_url || user?.twitter_username) &&
<div className='social flex gap-0.5'>
{user?.linkedin_url &&
<a
target={'_blank'}
href={user.linkedin_url}
title={user.linkedin_url}
className='rounded-sm border bg-slate-700 hover:bg-slate-700/50 hover:text-orange p-1'>
<RiLinkedinFill className='text-lg' />
</a>
}
{user?.twitter_username &&
<a
target={'_blank'}
href={`https://twitter.com/${user.twitter_username}`}
title={`https://twitter.com/${user.twitter_username}`}
className='rounded-sm border bg-slate-700 hover:bg-slate-700/50 hover:text-orange p-1'>
<RiTwitterFill className='text-lg' />
</a>
}
</div>
}
{user?.bio && <span>{user.bio}</span>}
{user?.blog &&
<a target={'_blank'} href={user.blog} className='flex text-orange items-center gap-0.5'>
<RiLinkM />
{user.blog}
</a>
}
</div>
<div className='grid grid-cols-2 text-white bg-osOrange -mx-4 mb-4 p-4 py-8'>
<div className='flex flex-col items-center justify-center p-2 text-xs'>
<p>Open Issues</p>
<p className='font-medium text-5xl'>{user?.open_issues}</p>
</div>
<div className='flex flex-col items-center justify-center p-2 text-xs'>
<p>PRs Made</p>
<p className='font-medium text-5xl'>{userPR?.meta.itemCount}</p>
</div>
<div className='flex flex-col items-center justify-center p-2 text-xs'>
<p>Avg PRs Velocity</p>
<p className='font-medium text-5xl'>-</p>
</div>
<div className='flex flex-col items-center justify-center p-2 text-xs'>
<p>Contributed Repos</p>
<p className='font-medium text-5xl'>-</p>
</div>
</div>
{
<div>
<h2 className='font-medium text-lg mb-2'>Current Interest</h2>
<div className='flex gap-1.5' style={{flexWrap: 'wrap'}}>
{user?.interests.split(',').map((interest) => (
<a target="_blank" key={interest}
href={`https://insights.opensauced.pizza/${interest}/dashboard/filter/recent`}
title={`https://insights.opensauced.pizza/${interest}/dashboard/filter/recent`}
className='flex gap-1 items-center p-1.5 px-4 rounded-full bg-slate-700 hover:bg-slate-700/50'>
{interestIcon[interest as InterestIconKeys] ?? null}
{interest}
</a>
)
)}
</div>
</div>
}
</main>
</div>
)
}
5 changes: 1 addition & 4 deletions src/pages/start.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import OpenSaucedLogo from "../assets/opensauced-logo.svg";
import { SUPABASE_LOGIN_URL } from "../constants";

interface StartProps {
setRenderedPage: (page: string) => void;
}
function Start() {

function Start({ setRenderedPage }: StartProps) {
return (
<div>
<img src={OpenSaucedLogo} alt="Open Sauced Logo" />
Expand Down
Loading

0 comments on commit 8a43288

Please sign in to comment.