diff --git a/apps/frontend/src/pages/Home/Home.tsx b/apps/frontend/src/pages/Home/Home.tsx index f0495a3f..754e9582 100644 --- a/apps/frontend/src/pages/Home/Home.tsx +++ b/apps/frontend/src/pages/Home/Home.tsx @@ -28,7 +28,7 @@ import type { StageIndex } from "../../models/Stage"; import { CustomizeStage } from "./stages/Customize"; import { DisplayStage } from "./stages/Display"; -import { LoginStage } from "./stages/Login"; +import { LoginStage } from "./stages/Login/Login"; import { SelectCardStage } from "./stages/SelectCard"; import { ThemeStage } from "./stages/Theme"; @@ -358,7 +358,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { {stage === 0 && ( { + onContinueAsGuest={() => { setStage(1); }} /> diff --git a/apps/frontend/src/pages/Home/stages/Login.tsx b/apps/frontend/src/pages/Home/stages/Login.tsx deleted file mode 100644 index 459900f8..00000000 --- a/apps/frontend/src/pages/Home/stages/Login.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import type { JSX, RefObject } from "react"; -import clsx from "clsx"; -import { useDispatch } from "react-redux"; - -import { Button } from "../../../components/Generic/Button"; -import { CardImage } from "../../../components/Card/CardImage"; -import { - CLIENT_ID, - DEMO_GIST, - DEMO_REPO, - DEMO_USER, - DEMO_WAKATIME_USER, - GITHUB_PRIVATE_AUTH_URL, - GITHUB_PUBLIC_AUTH_URL, - HOST, -} from "../../../constants"; -import { FaGithub as GithubIcon } from "react-icons/fa"; -import { logout } from "../../../redux/slices/user"; -import { - useIsAuthenticated, - usePrivateAccess, - useUserId, - useUserKey, -} from "../../../redux/selectors/userSelectors"; -import { deleteAccount } from "../../../api/user"; - -function useOutsideAlerter( - ref: RefObject, - action: () => void, -) { - useEffect(() => { - /** - * Alert if clicked on outside of element - */ - function handleClickOutside(event: MouseEvent) { - if (ref.current && !ref.current.contains(event.target as Node)) { - action(); - } - } - - // Bind the event listener - document.addEventListener("mousedown", handleClickOutside); - - return () => { - // Unbind the event listener on clean up - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [action, ref]); -} - -interface LoginStageProps { - onContinueAsGuestClick: () => void; -} - -export function LoginStage({ - onContinueAsGuestClick, -}: LoginStageProps): JSX.Element { - const userId = useUserId(); - const userKey = useUserKey(); - const privateAccess = usePrivateAccess(); - const isAuthenticated = useIsAuthenticated(); - - const dispatch = useDispatch(); - const [showDeleteModal, setShowDeleteModal] = useState(false); - - const handleLogout = useCallback(() => { - dispatch(logout({ userKey: null })); - }, [dispatch]); - - const openDeleteModal = () => { - setShowDeleteModal(true); - }; - - const closeDeleteModal = () => { - setShowDeleteModal(false); - }; - - const wrapperRef = useRef(null); - useOutsideAlerter(wrapperRef, closeDeleteModal); - - const handleAccountDelete = async () => { - const success = await deleteAccount(userId as string, userKey as string); - if (success) { - handleLogout(); - window.location.href = `https://github.com/settings/connections/applications/${CLIENT_ID}`; - } - }; - - // Card data - const cards = [ - { - demoImageSrc: `/pin?repo=${DEMO_REPO}&disable_animations=true`, - }, - { - demoImageSrc: `/top-langs?username=${DEMO_USER}&langs_count=4&disable_animations=true`, - }, - { - demoImageSrc: `?username=${DEMO_USER}&include_all_commits=true&disable_animations=true`, - }, - { - demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450&disable_animations=true`, - }, - { - demoImageSrc: `/gist?id=${DEMO_GIST}&disable_animations=true`, - }, - ]; - - return ( -
-
-
-
- {isAuthenticated ? ( - <> - {/* Access Level Management Buttons */} -
- {privateAccess ? ( -
- - - -

- Switch to public access if you prefer not to share - private contributions. -

-
- ) : ( -
- - - -

- Upgrade to include contributions in private repositories - for more complete and accurate stats. -

-
- )} -
- - {/* Delete Account Button */} -
- -

- This will delete your GitHub-Stats-Extended account and then - redirect you to a GitHub screen where you can revoke your - access token. -

-
- - {/* Logout Button */} -
- -

- Log out from GitHub-Stats-Extended. -

-
- - ) : ( - <> - {/* User is not logged in - show login options */} -
- - - -

- Generate stats based on your contributions in public - repositories. -

-
- -
- - - -

- Include contributions from private repositories for more - complete and accurate stats. -

-
- -
- -

- Explore options using sample data. Insert your own username - in the last step. -

-
- - )} -
-
-
-
- {cards.map((card, index) => { - const radius = 60; - const centerX = 70; - const startAngle = Math.PI * 0.75; // 135 degrees - const endAngle = Math.PI * 1.25; // 225 degrees - - const angle = - startAngle + - (endAngle - startAngle) * (index / (cards.length - 1)); - const x = centerX + radius * Math.cos(angle); - - return ( -
- -
- ); - })} -
-
-
- {showDeleteModal && ( -
-
-
-
-

Delete Account

-
-
-

- Are you sure you want to delete your account from GitHub - Trends? -

-
-
- - -
-
-
-
-
- )} -
- ); -} diff --git a/apps/frontend/src/pages/Home/stages/Login/Login.tsx b/apps/frontend/src/pages/Home/stages/Login/Login.tsx new file mode 100644 index 00000000..2209fa0d --- /dev/null +++ b/apps/frontend/src/pages/Home/stages/Login/Login.tsx @@ -0,0 +1,22 @@ +import type { JSX } from "react"; + +import { useIsAuthenticated } from "../../../../redux/selectors/userSelectors"; + +import { LoginOptions } from "./LoginOptions"; +import { LoginAccountManagement } from "./LoginAccountManagement"; + +interface LoginStageProps { + onContinueAsGuest: () => void; +} + +export function LoginStage({ + onContinueAsGuest, +}: LoginStageProps): JSX.Element { + const isAuthenticated = useIsAuthenticated(); + + if (isAuthenticated) { + return ; + } + + return ; +} diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx b/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx new file mode 100644 index 00000000..7dc653bd --- /dev/null +++ b/apps/frontend/src/pages/Home/stages/Login/LoginAccountDeleteModal.tsx @@ -0,0 +1,76 @@ +import { useEffect, useRef, type JSX, type RefObject } from "react"; +import { createPortal } from "react-dom"; + +import { Button } from "../../../../components/Generic/Button"; + +function useOutsideAlerter( + ref: RefObject, + action: () => void, +) { + useEffect(() => { + /** + * Alert if clicked on outside of element + */ + function handleClickOutside(event: MouseEvent) { + if (ref.current && !ref.current.contains(event.target as Node)) { + action(); + } + } + + // Bind the event listener + document.addEventListener("mousedown", handleClickOutside); + + return () => { + // Unbind the event listener on clean up + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [action, ref]); +} + +interface LoginAccountDeleteModalProps { + onClose: () => void; + onConfirm: () => void; +} + +export function LoginAccountDeleteModal( + props: LoginAccountDeleteModalProps, +): JSX.Element { + const { onConfirm, onClose } = props; + + const wrapperRef = useRef(null); + useOutsideAlerter(wrapperRef, onClose); + + return createPortal( +
+
+
+

Delete Account

+
+
+

+ Are you sure you want to delete your account from GitHub Trends? +

+
+
+ + +
+
+
+
, + document.body, + ); +} diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginAccountManagement.tsx b/apps/frontend/src/pages/Home/stages/Login/LoginAccountManagement.tsx new file mode 100644 index 00000000..a2f30992 --- /dev/null +++ b/apps/frontend/src/pages/Home/stages/Login/LoginAccountManagement.tsx @@ -0,0 +1,125 @@ +import { useCallback, useState, type JSX } from "react"; +import { useDispatch } from "react-redux"; +import { FaGithub as GithubIcon } from "react-icons/fa"; + +import { + usePrivateAccess, + useUserId, + useUserKey, +} from "../../../../redux/selectors/userSelectors"; +import { deleteAccount } from "../../../../api/user"; +import { Button } from "../../../../components/Generic/Button"; +import { + CLIENT_ID, + HOST, + GITHUB_PRIVATE_AUTH_URL, +} from "../../../../constants"; +import { LoginAccountDeleteModal } from "./LoginAccountDeleteModal"; +import { logout } from "../../../../redux/slices/user"; + +import { LoginBox } from "./LoginBox"; + +export function LoginAccountManagement(): JSX.Element { + const userId = useUserId(); + const userKey = useUserKey(); + const privateAccess = usePrivateAccess(); + + const dispatch = useDispatch(); + + const [showDeleteModal, setShowDeleteModal] = useState(false); + const openDeleteModal = () => { + setShowDeleteModal(true); + }; + + const closeDeleteModal = () => { + setShowDeleteModal(false); + }; + + const handleLogout = useCallback(() => { + dispatch(logout({ userKey: null })); + }, [dispatch]); + + const handleAccountDelete = async () => { + const success = await deleteAccount(userId as string, userKey as string); + if (success) { + handleLogout(); + window.location.href = `https://github.com/settings/connections/applications/${CLIENT_ID}`; + } + }; + + return ( + +
+ {privateAccess ? ( +
+ + + +

+ Switch to public access if you prefer not to share private + contributions. +

+
+ ) : ( +
+ + + +

+ Upgrade to include contributions in private repositories for more + complete and accurate stats. +

+
+ )} +
+ + {/* Delete Account Button */} +
+ +

+ This will delete your GitHub-Stats-Extended account and then redirect + you to a GitHub screen where you can revoke your access token. +

+
+ + {/* Logout Button */} +
+ +

+ Log out from GitHub-Stats-Extended. +

+
+ + {showDeleteModal && ( + { + void handleAccountDelete(); + }} + /> + )} +
+ ); +} diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginBox.tsx b/apps/frontend/src/pages/Home/stages/Login/LoginBox.tsx new file mode 100644 index 00000000..c5ca4236 --- /dev/null +++ b/apps/frontend/src/pages/Home/stages/Login/LoginBox.tsx @@ -0,0 +1,28 @@ +import type { JSX, ReactNode } from "react"; +import clsx from "clsx"; + +import { LoginBoxDemoCards } from "./LoginBoxDemoCards"; + +interface LoginBoxProps { + children: ReactNode; + + isOpaque?: boolean; +} + +export function LoginBox(props: LoginBoxProps): JSX.Element { + const { children, isOpaque = false } = props; + + return ( +
+
+
+
+ {children} +
+
+ + +
+
+ ); +} diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginBoxDemoCards.tsx b/apps/frontend/src/pages/Home/stages/Login/LoginBoxDemoCards.tsx new file mode 100644 index 00000000..4d11c249 --- /dev/null +++ b/apps/frontend/src/pages/Home/stages/Login/LoginBoxDemoCards.tsx @@ -0,0 +1,61 @@ +import type { JSX } from "react"; + +import { CardImage } from "../../../../components/Card/CardImage"; +import { + DEMO_REPO, + DEMO_USER, + DEMO_WAKATIME_USER, + DEMO_GIST, +} from "../../../../constants"; + +const cards: Array<{ demoImageSrc: string }> = [ + { + demoImageSrc: `/pin?repo=${DEMO_REPO}&disable_animations=true`, + }, + { + demoImageSrc: `/top-langs?username=${DEMO_USER}&langs_count=4&disable_animations=true`, + }, + { + demoImageSrc: `?username=${DEMO_USER}&include_all_commits=true&disable_animations=true`, + }, + { + demoImageSrc: `/wakatime?username=${DEMO_WAKATIME_USER}&langs_count=6&card_width=450&disable_animations=true`, + }, + { + demoImageSrc: `/gist?id=${DEMO_GIST}&disable_animations=true`, + }, +]; + +function getCardXPosition(cardIndex: number): number { + const radius = 60; + const centerX = 70; + const startAngle = Math.PI * 0.75; // 135 degrees + const endAngle = Math.PI * 1.25; // 225 degrees + + const angle = + startAngle + (endAngle - startAngle) * (cardIndex / (cards.length - 1)); + const x = centerX + radius * Math.cos(angle); + return x; +} + +export function LoginBoxDemoCards(): JSX.Element { + return ( +
+
+ {cards.map((card, index) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/apps/frontend/src/pages/Home/stages/Login/LoginOptions.tsx b/apps/frontend/src/pages/Home/stages/Login/LoginOptions.tsx new file mode 100644 index 00000000..ede4545f --- /dev/null +++ b/apps/frontend/src/pages/Home/stages/Login/LoginOptions.tsx @@ -0,0 +1,60 @@ +import type { JSX } from "react"; +import { FaGithub as GithubIcon } from "react-icons/fa"; + +import { Button } from "../../../../components/Generic/Button"; +import { + GITHUB_PUBLIC_AUTH_URL, + GITHUB_PRIVATE_AUTH_URL, +} from "../../../../constants"; + +import { LoginBox } from "./LoginBox"; + +interface LoginOptionsProps { + onContinueAsGuest: () => void; +} + +export function LoginOptions(props: LoginOptionsProps): JSX.Element { + const { onContinueAsGuest } = props; + + return ( + +
+ + + +

+ Generate stats based on your contributions in public repositories. +

+
+ +
+ + + +

+ Include contributions from private repositories for more complete and + accurate stats. +

+
+ +
+ +

+ Explore options using sample data. Insert your own username in the + last step. +

+
+
+ ); +}