Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for user login #94

Merged
merged 3 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,34 @@
"test-watch": "jest --watch "
},
"dependencies": {
"@headlessui/react": "^1.7.16",
"@heroicons/react": "^2.1.5",
"@tanstack/react-query": "^4.33.0",
"@tanstack/react-query-devtools": "^4.33.0",
"@tanstack/react-query": "^5.51.15",
"@tanstack/react-query-devtools": "^5.51.15",
"@testing-library/react-hooks": "^8.0.1",
"autoprefixer": "10.4.14",
"axios": "^1.6.7",
"axios": "^1.7.2",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"next": "13.4.4",
"next": "^14.2.4",
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^4.10.1",
"react-test-renderer": "^18.2.0",
"tailwindcss": "3.3.2",
"typescript": "5.0.4",
"zod": "^3.23.8"
"typescript": "^5.5.3",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.3",
"@types/node": "20.2.5",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"eslint": "8.41.0",
"eslint-config-next": "13.4.4",
"eslint-config-next": "^14.2.4",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
Expand All @@ -63,7 +61,7 @@
"local-ssl-proxy": "^2.0.5",
"msw": "^1.3.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.8",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"ts-jest": "^29.1.1"
},
Expand Down
1 change: 1 addition & 0 deletions src/api/rds/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RdsApi } from "./rds.api"
9 changes: 9 additions & 0 deletions src/api/rds/rds.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { rdsClient } from "@/utils/client"
import { RdsUserSelfResDto } from "./rds.dto"

export class RdsApi {
public static async getCurrentUserData(): Promise<RdsUserSelfResDto> {
const { data } = await rdsClient.get("/users/self")
return data
}
}
33 changes: 33 additions & 0 deletions src/api/rds/rds.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export type RdsUserSelfResDto = {
id: string
incompleteUserDetails: boolean
discordJoinedAt: string
discordId: string
roles: {
archived: boolean
in_discord: boolean
member: boolean
}
linkedin_id: boolean
picture?: {
url: string
publicId: string
}
yoe: number
github_created_at: number
github_display_name: string
github_id: string
twitter_id: string
username: string
github_user_id: string
first_name: string
profileURL: string
website: string
last_name: string
company: string
designation: string
instagram_id: string
profileStatus: string // TODO: Make this a enum
updated_at: number
created_at: number
}
45 changes: 45 additions & 0 deletions src/components/navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { config } from "@/config"
import Link from "next/link"
import { SignInWithRds } from "./signin-with-rds"

type NavLinksList = {
className?: string
children: React.ReactNode
}

const NavLinksList = ({ className, children }: NavLinksList) => {
return <ul className={className}>{children}</ul>
}

type NavLinkProps = {
href: string
children: React.ReactNode
}

const NavLink = ({ href, children }: NavLinkProps) => {
return (
<li className="rounded-full px-4 py-1 transition-all ease-in-out hover:bg-gray-100">
<Link href={href}>{children}</Link>
</li>
)
}

export const Navbar = () => {
return (
<nav className="fixed left-0 top-0 w-full bg-white">
<div className="mx-auto flex h-14 w-full max-w-screen-2xl items-center gap-6 px-6">
<h1 className="text-xl font-bold">Rds</h1>

<NavLinksList className="flex items-center">
<NavLink href={config.welcomeSiteUrl}>Welcome</NavLink>
<NavLink href={config.membersSiteUrl}>Members</NavLink>
<NavLink href={config.membersSiteUrl}>Status</NavLink>
</NavLinksList>

<div className="ml-auto">
<SignInWithRds />
</div>
</div>
</nav>
)
}
43 changes: 43 additions & 0 deletions src/components/signin-with-rds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { config } from "@/config"
import { ROUTES } from "@/routes"
import { useGlobalStore } from "@/store/global-store"
import Image from "next/image"
import { useRouter } from "next/navigation"

export const SignInWithRds = () => {
const { push } = useRouter()
const { user } = useGlobalStore((store) => ({ user: store.user }))

const handleSignIn = async () => {
const redirectUrl = `${config.skillTreeUrl}${ROUTES.requests}`
const url = `${config.rdsBackendBaseUrl}/auth/github/login?redirectURL=${redirectUrl}?v2=true`

push(url)
}

// TODO : make a separate component for this
if (user) {
return (
<div className="flex items-center gap-2">
<Image
src={user.profilePicture}
alt={user.name}
width={64}
height={64}
className="h-7 w-7 overflow-hidden rounded-full"
/>

<span className="inline-block">{user.name}</span>
</div>
)
}

return (
<button
onClick={handleSignIn}
className="h-9 rounded-lg bg-gray-200 px-4 font-medium text-gray-700 transition hover:bg-gray-300"
>
SignIn within rds
</button>
)
}
51 changes: 51 additions & 0 deletions src/components/user-guard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { RdsApi } from "@/api/rds"
import { ROUTES } from "@/routes"
import { TGlobalStoreUser, useGlobalStore } from "@/store/global-store"
import { useQuery } from "@tanstack/react-query"
import { usePathname } from "next/navigation"
import { useEffect } from "react"

type Props = {
children: React.ReactNode
}

const UNPROTECTED_PATHS = [ROUTES.root, ROUTES.signIn]

export const UserGuard = ({ children }: Props) => {
const pathname = usePathname()
const isUnprotectedPath = UNPROTECTED_PATHS.includes(pathname)
const { setGlobalStore } = useGlobalStore((store) => ({ setGlobalStore: store.setGlobalStore }))

const { data, isLoading, isError } = useQuery({
enabled: !isUnprotectedPath,
queryKey: ["RdsApi.getCurrentUserData"],
queryFn: RdsApi.getCurrentUserData,
})

useEffect(() => {
if (!data) {
return
}

const firstName = data?.first_name ?? ""
const lastName = data?.last_name ?? ""

const user: TGlobalStoreUser = {
id: data.id,
name: firstName + " " + lastName,
profilePicture: data?.picture?.url ?? "",
}

setGlobalStore({ user })
}, [data])

if (isLoading) {
return <div>Loading...</div>
}

if (isError) {
return <div>Error...</div>
}

return <>{children}</>
}
16 changes: 8 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod"

const configSchema = z.object({
appEnv: z.enum(["dev", "staging", "prod"]),
Expand All @@ -10,9 +10,9 @@ const configSchema = z.object({
welcomeSiteUrl: z.string(),
wwwSiteUrl: z.string(),
mySiteUrl: z.string(),
});
})

type TConfig = z.infer<typeof configSchema>;
type TConfig = z.infer<typeof configSchema>

export const config = {
appEnv: process.env.NEXT_PUBLIC_APP_ENV,
Expand All @@ -24,16 +24,16 @@ export const config = {
welcomeSiteUrl: process.env.NEXT_PUBLIC_WELCOME_SITE_URL,
wwwSiteUrl: process.env.NEXT_PUBLIC_WWW_SITE_URL,
mySiteUrl: process.env.NEXT_PUBLIC_MY_SITE_URL,
} as TConfig;
} as TConfig

/**
* Validate if all the required environment variables defined in the schema above are set
* and are in the correct format else throw an error
* ---
*/
export const validateEnv = () => {
const result = configSchema.safeParse(config);
const errors = result.error?.flatten().fieldErrors;
const result = configSchema.safeParse(config)
const errors = result.error?.flatten().fieldErrors

if (!result.success) {
throw new Error(
Expand All @@ -45,6 +45,6 @@ export const validateEnv = () => {
null,
2
)
);
)
}
};
}
1 change: 1 addition & 0 deletions src/modules/requests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Requests } from "./requests"
13 changes: 13 additions & 0 deletions src/modules/requests/requests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Navbar } from "@/components/navbar"

export const Requests = () => {
return (
<div>
<Navbar />

<main className="mx-auto mt-14 max-w-screen-2xl p-6">
<h1 className="pb-6">Requests</h1>
</main>
</div>
)
}
39 changes: 13 additions & 26 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
import type { ReactElement, ReactNode } from "react";
import type { NextPage } from "next";
import type { AppProps } from "next/app";

import { validateEnv } from "@/config";
import "@/styles/global.css";
import { Providers } from "@/utils/providers";
import type { AppProps } from "next/app"

import { validateEnv } from "@/config"
import "@/styles/global.css"
import { Providers } from "@/utils/providers"
import { UserGuard } from "@/components/user-guard"

/**
* Validate if all the required environment variables are set
* this will allow us to fail fast if any of the required environment variables are not set
* ---
*/
validateEnv();


export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}

type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}

export default function App({ Component, pageProps }: AppPropsWithLayout) {
// For more info on this pattern visit: https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts#with-typescript
const getLayout = Component.getLayout ?? ((page) => page);
* ---
*/
validateEnv()

export default function MyApp({ Component, pageProps }: AppProps) {
return (
<Providers>
{/* @ts-ignore */}
{getLayout(<Component {...pageProps} />)}
<UserGuard>
<Component {...pageProps} />
</UserGuard>
</Providers>
);
)
}
22 changes: 16 additions & 6 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
export default function Home() {
import { ROUTES } from "@/routes"
import Link from "next/link"

const Homepage = () => {
return (
<div>
<div>
Homepage
</div>
<div className="flex h-screen w-screen flex-col items-center justify-center">
<h1 className="pb-1 text-4xl font-bold">Welcome to Skilltree</h1>
<p className="text-sm text-gray-500">
Visit{" "}
<Link className="text-blue-500 hover:text-blue-600 hover:underline" href={ROUTES.requests}>
/requests
</Link>{" "}
to view all pending skill requests
</p>
</div>
);
)
}

export default Homepage
3 changes: 3 additions & 0 deletions src/pages/requests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Requests } from "@/modules/requests"

export default Requests
Loading