Skip to content

Commit

Permalink
feat: web app login ux
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jul 2, 2024
1 parent 37efb0b commit 3c34845
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 64 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ out
*.log*
.env
.eslintcache
.env.*
3 changes: 3 additions & 0 deletions src/renderer/src/atoms/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ import { atom } from "jotai"
export const [, , useUser, useSetUser, getUser, setUser] = createAtomHooks(
atom<Nullable<User>>(null),
)

export const [, , useAuthFail, useSetAuthFail, getAuthFail, setAuthFail] =
createAtomHooks(atom<boolean>(false))
3 changes: 2 additions & 1 deletion src/renderer/src/components/ui/image/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback } from "react"

import { useModalStack } from "../modal/stacked/hooks"
import { NoopChildren } from "../modal/stacked/utils"
import { PreviewImageContent } from "./preview-image"

export const usePreviewImages = () => {
Expand All @@ -15,7 +16,7 @@ export const usePreviewImages = () => {
),
title: "Image",
overlay: true,
CustomModalComponent: ({ children }) => children,
CustomModalComponent: NoopChildren,
clickOutsideToDismiss: true,
})
},
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/src/components/ui/modal/stacked/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { PropsWithChildren } from "react"

export const NoopChildren = ({ children }: PropsWithChildren) => children
63 changes: 6 additions & 57 deletions src/renderer/src/components/user-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,17 @@ import {
AvatarImage,
} from "@renderer/components/ui/avatar"
import { useSignOut } from "@renderer/hooks"
import { loginHandler } from "@renderer/lib/auth"
import { APP_NAME } from "@renderer/lib/constants"
import { nextFrame, stopPropagation } from "@renderer/lib/dom"
import { nextFrame } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { LoginModalContent } from "@renderer/modules/auth/LoginModalContent"
import { useSettingModal } from "@renderer/modules/settings/modal/hooks"
import { useSession } from "@renderer/queries/auth"
import { m } from "framer-motion"
import type { FC } from "react"
import { memo } from "react"
import { Link } from "react-router-dom"

import { FollowIcon } from "./icons/follow"
import { UserArrowLeftIcon } from "./icons/user"
import { ActionButton, Button } from "./ui/button"
import { ActionButton } from "./ui/button"
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -26,56 +23,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu/dropdown-menu"
import { useCurrentModal } from "./ui/modal"
import { modalMontionConfig } from "./ui/modal/stacked/constants"
import { useModalStack } from "./ui/modal/stacked/hooks"

const LoginModalContent = () => {
const modal = useCurrentModal()
return (
<div className="center flex h-full" onClick={modal.dismiss}>
<m.div
className="shadow-modal rounded-lg border border-border bg-theme-background p-4 px-8 pb-8"
onClick={stopPropagation}
{...modalMontionConfig}
>
<div className="mb-8 mt-4 text-center align-middle font-sans text-2xl font-bold leading-relaxed">
<span className="text-xl">Sign in to </span>
<span className="center flex translate-y-px gap-2 font-theme text-theme-accent">
<FollowIcon className="size-4" />
{APP_NAME}
</span>
</div>
<div className="flex flex-col gap-4">
<Button
className="h-[48px] w-[320px] rounded-[8px] !bg-black font-sans text-base text-white hover:!bg-black/80"
size="lg"
onClick={() => {
loginHandler("github")
}}
>
<i className="i-mgc-github-cute-fi mr-2 text-xl" />
{" "}
Continue with
GitHub
</Button>
<Button
className="h-[48px] w-[320px] rounded-[8px] bg-blue-500 font-sans text-base text-white hover:bg-blue-500/90"
size="xl"
onClick={() => {
loginHandler("google")
}}
>
<i className="i-mgc-google-cute-fi mr-2 text-xl" />
{" "}
Continue with
Google
</Button>
</div>
</m.div>
</div>
)
}
import { NoopChildren } from "./ui/modal/stacked/utils"

interface LoginProps {
method?: "redirect" | "modal"
Expand All @@ -90,9 +39,9 @@ export const LoginButton: FC<LoginProps> = (props) => {
method === "modal" ?
() => {
modalStack.present({
CustomModalComponent: ({ children }) => children,
CustomModalComponent: NoopChildren,
title: "Login",
content: LoginModalContent,
content: () => <LoginModalContent runtime={window.electron ? "app" : "browser"} />,
clickOutsideToDismiss: true,
})
} :
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/src/lib/api-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getCsrfToken } from "@hono/auth-js/react"
import { setAuthFail } from "@renderer/atoms"
import type { AppType } from "@renderer/hono"
import { hc } from "hono/client"
import { FetchError, ofetch } from "ofetch"
Expand Down Expand Up @@ -28,7 +29,9 @@ export const apiFetch = ofetch.create({
const { router } = window
if (context.response.status === 401) {
// Or we can present LoginModal here.
router.navigate("/login")
// router.navigate("/login")
// If any response status is 401, we can set auth fail. Maybe some bug, but if navigate to login page, had same issues
setAuthFail(true)
}
try {
const json = JSON.parse(context.response._data)
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { signIn } from "@hono/auth-js/react"

export const LOGIN_CALLBACK_URL = `${import.meta.env.VITE_WEB_URL}/redirect?app=follow`
export const loginHandler = (provider: string) => {
export type LoginRuntime = "browser" | "app"
export const loginHandler = (provider: string, runtime: LoginRuntime = "app") => {
if (window.electron) {
window.open(
`${import.meta.env.VITE_WEB_URL}/login?provider=${provider}`,
)
} else {
signIn(provider, {
callbackUrl: LOGIN_CALLBACK_URL,
callbackUrl: runtime === "app" ? LOGIN_CALLBACK_URL : import.meta.env.VITE_WEB_URL,
})
}
}
59 changes: 59 additions & 0 deletions src/renderer/src/modules/auth/LoginModalContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { FollowIcon } from "@renderer/components/icons/follow"
import { Button } from "@renderer/components/ui/button"
import { useCurrentModal } from "@renderer/components/ui/modal"
import { modalMontionConfig } from "@renderer/components/ui/modal/stacked/constants"
import type { LoginRuntime } from "@renderer/lib/auth"
import { loginHandler } from "@renderer/lib/auth"
import { APP_NAME } from "@renderer/lib/constants"
import { stopPropagation } from "@renderer/lib/dom"
import { m } from "framer-motion"

interface LoginModalContentProps {
runtime?: LoginRuntime
}
export const LoginModalContent = (props: LoginModalContentProps) => {
const modal = useCurrentModal()
return (
<div className="center flex h-full" onClick={modal.dismiss}>
<m.div
className="shadow-modal rounded-lg border border-border bg-theme-background p-4 px-8 pb-8"
onClick={stopPropagation}
{...modalMontionConfig}
>
<div className="mb-8 mt-4 text-center align-middle font-sans text-2xl font-bold leading-relaxed">
<span className="text-xl">Sign in to </span>
<span className="center flex translate-y-px gap-2 font-theme text-theme-accent">
<FollowIcon className="size-4" />
{APP_NAME}
</span>
</div>
<div className="flex flex-col gap-4">
<Button
className="h-[48px] w-[320px] rounded-[8px] !bg-black font-sans text-base text-white hover:!bg-black/80"
size="lg"
onClick={() => {
loginHandler("github", props.runtime)
}}
>
<i className="i-mgc-github-cute-fi mr-2 text-xl" />
{" "}
Continue with
GitHub
</Button>
<Button
className="h-[48px] w-[320px] rounded-[8px] bg-blue-500 font-sans text-base text-white hover:bg-blue-500/90"
size="xl"
onClick={() => {
loginHandler("google", props.runtime)
}}
>
<i className="i-mgc-google-cute-fi mr-2 text-xl" />
{" "}
Continue with
Google
</Button>
</div>
</m.div>
</div>
)
}
2 changes: 1 addition & 1 deletion src/renderer/src/pages/(external)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function Component() {
<Logo className="size-20" />
<h1 className="text-3xl font-bold">
Log in to
{APP_NAME}
{` ${APP_NAME}`}
</h1>
{redirecting ? (
<div>Redirecting</div>
Expand Down
25 changes: 23 additions & 2 deletions src/renderer/src/pages/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { setMainContainerElement } from "@renderer/atoms"
import {
setMainContainerElement,
useAuthFail,
useUser,
} from "@renderer/atoms"
import { DeclarativeModal } from "@renderer/components/ui/modal/stacked/declarative-modal"
import { NoopChildren } from "@renderer/components/ui/modal/stacked/utils"
import { RootPortal } from "@renderer/components/ui/portal"
import { preventDefault } from "@renderer/lib/dom"
import { LoginModalContent } from "@renderer/modules/auth/LoginModalContent"
import { FeedColumn } from "@renderer/modules/feed-column"
import { Outlet } from "react-router-dom"

export function Component() {
const isAuthFail = useAuthFail()
const user = useUser()

return (
<div className="flex h-full" onContextMenu={preventDefault}>
<div className="w-64 shrink-0 border-r">
Expand All @@ -17,7 +28,17 @@ export function Component() {
>
<Outlet />
</main>

{isAuthFail && !user && (
<RootPortal>
<DeclarativeModal
CustomModalComponent={NoopChildren}
open
title="Login"
>
<LoginModalContent runtime={window.electron ? "app" : "browser"} />
</DeclarativeModal>
</RootPortal>
)}
</div>
)
}
12 changes: 12 additions & 0 deletions types/vite.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {

VITE_WEB_URL: string
VITE_API_URL: string
VITE_IMGPROXY_URL: string
}

interface ImportMeta {
readonly env: ImportMetaEnv
}

0 comments on commit 3c34845

Please sign in to comment.