fix: user profile modal
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
7a8594d579
commit
374c1d0632
|
|
@ -6,5 +6,13 @@ export const [, , useMe, useSetMe, getMe, setMe] = createAtomHooks(
|
|||
atom<Nullable<User>>(null),
|
||||
)
|
||||
|
||||
export const [, , useLoginModalShow, useSetLoginModalShow, getLoginModalShow, setLoginModalShow] =
|
||||
createAtomHooks(atom<boolean>(false))
|
||||
export { useMe as useWhoAmI }
|
||||
|
||||
export const [
|
||||
,
|
||||
,
|
||||
useLoginModalShow,
|
||||
useSetLoginModalShow,
|
||||
getLoginModalShow,
|
||||
setLoginModalShow,
|
||||
] = createAtomHooks(atom<boolean>(false))
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import { defineQuery } from "@renderer/lib/defineQuery"
|
|||
import { nextFrame } from "@renderer/lib/dom"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { LoginModalContent } from "@renderer/modules/auth/LoginModalContent"
|
||||
import { usePresentUserProfileModal } from "@renderer/modules/profile/hooks"
|
||||
import { useSettingModal } from "@renderer/modules/settings/modal/hooks"
|
||||
import { useSession } from "@renderer/queries/auth"
|
||||
import { WEB_URL } from "@shared/constants"
|
||||
import type { FC } from "react"
|
||||
import { memo } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
|
|
@ -68,9 +68,11 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
|
|||
const { user } = session || {}
|
||||
const signOut = useSignOut()
|
||||
const settingModalPresent = useSettingModal()
|
||||
const presentUserProfile = usePresentUserProfileModal()
|
||||
if (status !== "authenticated") {
|
||||
return <LoginButton {...props} />
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="!outline-none focus-visible:bg-theme-item-hover">
|
||||
|
|
@ -96,10 +98,7 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
|
|||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${WEB_URL}/profile/${user?.handle || user?.id}`,
|
||||
"_blank",
|
||||
)
|
||||
presentUserProfile(user?.id)
|
||||
}}
|
||||
>
|
||||
<i className="i-mgc-user-3-cute-re mr-1.5" />
|
||||
|
|
|
|||
|
|
@ -72,11 +72,11 @@ export const FeedForm: Component<{
|
|||
},
|
||||
})
|
||||
|
||||
const { setClickOutSideToDismiss } = useCurrentModal()
|
||||
const { setClickOutSideToDismiss } = useCurrentModal() || {}
|
||||
|
||||
useEffect(() => {
|
||||
if (form.formState.isDirty) {
|
||||
setClickOutSideToDismiss(false)
|
||||
setClickOutSideToDismiss?.(false)
|
||||
}
|
||||
}, [form.formState.isDirty])
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { useEntryReadHistory } from "@renderer/store/entry"
|
|||
import { useUserById } from "@renderer/store/user"
|
||||
import { Fragment } from "react"
|
||||
|
||||
import { usePresentUserProfileModal } from "../profile/hooks"
|
||||
|
||||
export const EntryReadHistory: Component<{ entryId: string }> = ({
|
||||
entryId,
|
||||
}) => {
|
||||
|
|
@ -66,6 +68,7 @@ const EntryUser: Component<{
|
|||
i: number
|
||||
}> = ({ userId, i }) => {
|
||||
const user = useUserById(userId)
|
||||
const presentUserProfile = usePresentUserProfileModal()
|
||||
if (!user) return null
|
||||
return (
|
||||
<Tooltip>
|
||||
|
|
@ -76,14 +79,21 @@ const EntryUser: Component<{
|
|||
zIndex: i,
|
||||
}}
|
||||
>
|
||||
<Avatar className="aspect-square size-8 border border-border ring-1 ring-background">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<button
|
||||
className="no-drag-region"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
presentUserProfile(userId)
|
||||
}}
|
||||
>
|
||||
<Avatar className="aspect-square size-8 border border-border ring-1 ring-background">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Recent reader:
|
||||
{" "}
|
||||
{user.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { useModalStack } from "@renderer/components/ui/modal"
|
||||
import { NoopChildren } from "@renderer/components/ui/modal/stacked/utils"
|
||||
import { useAuthQuery } from "@renderer/hooks/common"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { defineQuery } from "@renderer/lib/defineQuery"
|
||||
import { capitalizeFirstLetter } from "@renderer/lib/utils"
|
||||
import { createElement, useCallback } from "react"
|
||||
import { parse } from "tldts"
|
||||
|
||||
import { UserProfileModalContent } from "./user-profile-modal"
|
||||
|
||||
export const useUserSubscriptionsQuery = (userId: string | undefined) => {
|
||||
const subscriptions = useAuthQuery(
|
||||
defineQuery(["subscriptions", userId], async () => {
|
||||
const res = await apiClient.subscriptions.$get({
|
||||
query: { userId },
|
||||
})
|
||||
const groupFolder = {} as Record<string, typeof res.data>
|
||||
|
||||
for (const subscription of res.data || []) {
|
||||
if (!subscription.category && subscription.feeds) {
|
||||
const { siteUrl } = subscription.feeds
|
||||
if (!siteUrl) continue
|
||||
const parsed = parse(siteUrl)
|
||||
parsed.domain &&
|
||||
(subscription.category = capitalizeFirstLetter(parsed.domain))
|
||||
}
|
||||
if (subscription.category) {
|
||||
if (!groupFolder[subscription.category]) {
|
||||
groupFolder[subscription.category] = []
|
||||
}
|
||||
groupFolder[subscription.category].push(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
return groupFolder
|
||||
}),
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
)
|
||||
return subscriptions
|
||||
}
|
||||
|
||||
export const usePresentUserProfileModal = () => {
|
||||
const { present } = useModalStack()
|
||||
|
||||
return useCallback(
|
||||
(userId: string | undefined) => {
|
||||
if (!userId) return
|
||||
present({
|
||||
title: "User Profile",
|
||||
content: () =>
|
||||
createElement(UserProfileModalContent, {
|
||||
userId,
|
||||
}),
|
||||
CustomModalComponent: NoopChildren,
|
||||
clickOutsideToDismiss: true,
|
||||
})
|
||||
},
|
||||
[present],
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
import { getSidebarActiveView } from "@renderer/atoms/sidebar"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
import { FeedIcon } from "@renderer/components/feed-icon"
|
||||
import { FollowIcon } from "@renderer/components/icons/follow"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@renderer/components/ui/avatar"
|
||||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { LoadingCircle } from "@renderer/components/ui/loading"
|
||||
import { useCurrentModal, useModalStack } from "@renderer/components/ui/modal"
|
||||
import { useAuthQuery } from "@renderer/hooks/common"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { defineQuery } from "@renderer/lib/defineQuery"
|
||||
import { nextFrame } from "@renderer/lib/dom"
|
||||
import type { FeedViewType } from "@renderer/lib/enum"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { useUserSubscriptionsQuery } from "@renderer/modules/profile/hooks"
|
||||
import { useAnimationControls } from "framer-motion"
|
||||
import type { FC } from "react"
|
||||
import { Fragment, useEffect, useState } from "react"
|
||||
|
||||
import { FeedForm } from "../discover/feed-form"
|
||||
|
||||
export const UserProfileModalContent: FC<{
|
||||
userId: string
|
||||
}> = ({ userId }) => {
|
||||
const user = useAuthQuery(
|
||||
defineQuery(["profiles", userId], async () => {
|
||||
const res = await apiClient.profiles.$get({
|
||||
query: { id: userId! },
|
||||
})
|
||||
return res.data
|
||||
}),
|
||||
)
|
||||
|
||||
const subscriptions = useUserSubscriptionsQuery(user.data?.id)
|
||||
const modal = useCurrentModal()
|
||||
const controller = useAnimationControls()
|
||||
useEffect(() => {
|
||||
nextFrame(() => controller.start("enter"))
|
||||
}, [controller])
|
||||
|
||||
const { present } = useModalStack()
|
||||
const winHeight = useState(() => window.innerHeight)[0]
|
||||
|
||||
return (
|
||||
<div className="container center h-full" onClick={modal.dismiss}>
|
||||
<m.div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
tabIndex={-1}
|
||||
initial="initial"
|
||||
animate={controller}
|
||||
variants={{
|
||||
enter: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
initial: {
|
||||
y: "100%",
|
||||
opacity: 0.9,
|
||||
},
|
||||
exit: {
|
||||
y: winHeight,
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
type: "spring",
|
||||
|
||||
mass: 0.4,
|
||||
tension: 100,
|
||||
friction: 1,
|
||||
}}
|
||||
exit="exit"
|
||||
className="shadow-perfect perfect-sm relative flex max-h-[80vh] flex-col items-center overflow-hidden rounded-xl border bg-theme-background p-8"
|
||||
>
|
||||
<button
|
||||
className="absolute right-2 top-2 z-10 p-2"
|
||||
onClick={modal.dismiss}
|
||||
type="button"
|
||||
aria-label="close profile"
|
||||
>
|
||||
<i className="i-mgc-close-cute-re text-[20px] opacity-80 hover:to-theme-button-hover" />
|
||||
</button>
|
||||
{user.data && (
|
||||
<Fragment>
|
||||
<div className="center m-12 flex shrink-0 flex-col">
|
||||
<Avatar className="aspect-square size-16">
|
||||
<AvatarImage src={user.data.image || undefined} />
|
||||
<AvatarFallback>{user.data.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
|
||||
<h1>{user.data.name}</h1>
|
||||
</div>
|
||||
<div className="mb-8 text-sm text-zinc-500">
|
||||
{user.data.handle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 h-full w-[70ch] max-w-full space-y-10 overflow-auto px-5">
|
||||
{Object.keys(subscriptions.data || {}).map((category) => (
|
||||
<div key={category}>
|
||||
<div className="mb-4 flex items-center text-2xl font-bold">
|
||||
<h3>{category}</h3>
|
||||
</div>
|
||||
<div>
|
||||
{subscriptions.data?.[category].map((subscription) => (
|
||||
<div
|
||||
key={subscription.feedId}
|
||||
className="group relative border-b py-5"
|
||||
>
|
||||
<a
|
||||
className="flex flex-1 cursor-default"
|
||||
href={subscription.feeds.siteUrl!}
|
||||
target="_blank"
|
||||
>
|
||||
<FeedIcon
|
||||
feed={subscription.feeds}
|
||||
size={22}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"w-0 flex-1 grow",
|
||||
"group-hover:grow-[0.85]",
|
||||
)}
|
||||
>
|
||||
<div className="truncate font-medium leading-none">
|
||||
{subscription.feeds?.title}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||
{subscription.feeds?.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<StyledButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
const defaultView =
|
||||
getSidebarActiveView() as FeedViewType
|
||||
|
||||
present({
|
||||
title: "Add follow",
|
||||
content: ({ dismiss }) => (
|
||||
<FeedForm
|
||||
asWidget
|
||||
url={subscription.feeds.url}
|
||||
defaultView={defaultView}
|
||||
onSuccess={dismiss}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}}
|
||||
>
|
||||
<FollowIcon className="mr-1 size-3" />
|
||||
{APP_NAME}
|
||||
</StyledButton>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{!user.data && (
|
||||
<LoadingCircle size="large" className="center h-48 w-[46.125rem] max-w-full" />
|
||||
)}
|
||||
</m.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -9,10 +9,11 @@ import { StyledButton } from "@renderer/components/ui/button"
|
|||
import { useAuthQuery, useTitle } from "@renderer/hooks/common"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { defineQuery } from "@renderer/lib/defineQuery"
|
||||
import { capitalizeFirstLetter, cn } from "@renderer/lib/utils"
|
||||
import { stopPropagation } from "@renderer/lib/dom"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { useUserSubscriptionsQuery } from "@renderer/modules/profile/hooks"
|
||||
import { DEEPLINK_SCHEME } from "@shared/constants"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { parse } from "tldts"
|
||||
|
||||
export function Component() {
|
||||
const { id } = useParams()
|
||||
|
|
@ -29,104 +30,75 @@ export function Component() {
|
|||
},
|
||||
)
|
||||
|
||||
const subscriptions = useAuthQuery(
|
||||
defineQuery(["subscriptions", user.data?.id], async () => {
|
||||
const res = await apiClient.subscriptions.$get({
|
||||
query: { userId: user.data?.id },
|
||||
})
|
||||
const groupFolder = {} as Record<string, typeof res.data>
|
||||
|
||||
for (const subscription of res.data || []) {
|
||||
if (!subscription.category && subscription.feeds) {
|
||||
const { siteUrl } = subscription.feeds
|
||||
if (!siteUrl) continue
|
||||
const parsed = parse(siteUrl)
|
||||
parsed.domain &&
|
||||
(subscription.category = capitalizeFirstLetter(parsed.domain))
|
||||
}
|
||||
if (subscription.category) {
|
||||
if (!groupFolder[subscription.category]) {
|
||||
groupFolder[subscription.category] = []
|
||||
}
|
||||
groupFolder[subscription.category].push(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
return groupFolder
|
||||
}),
|
||||
{
|
||||
enabled: !!user.data?.id,
|
||||
},
|
||||
)
|
||||
const subscriptions = useUserSubscriptionsQuery(user.data?.id)
|
||||
useTitle(user.data?.name)
|
||||
return (
|
||||
<>
|
||||
{user.data && (
|
||||
<div className="container mx-auto mt-12 flex flex-col items-center justify-center p-4 lg:p-0">
|
||||
<Avatar className="aspect-square size-16">
|
||||
<AvatarImage src={user.data.image || undefined} />
|
||||
<AvatarFallback>{user.data.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
|
||||
<h1>{user.data.name}</h1>
|
||||
</div>
|
||||
<div className="mb-8 text-sm text-zinc-500">{user.data.handle}</div>
|
||||
</div>
|
||||
<div className="mb-12 w-[70ch] max-w-full space-y-10">
|
||||
{Object.keys(subscriptions.data || {}).map((category) => (
|
||||
<div key={category}>
|
||||
<div className="mb-4 flex items-center text-2xl font-bold">
|
||||
<h3>{category}</h3>
|
||||
</div>
|
||||
<div>
|
||||
{subscriptions.data?.[category].map((subscription) => (
|
||||
<div
|
||||
key={subscription.feedId}
|
||||
className="group relative border-b py-5"
|
||||
>
|
||||
<a
|
||||
className="flex flex-1 cursor-default"
|
||||
href={subscription.feeds.siteUrl!}
|
||||
target="_blank"
|
||||
>
|
||||
<FeedIcon
|
||||
feed={subscription.feeds}
|
||||
size={22}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"w-0 flex-1 grow",
|
||||
"group-hover:grow-[0.85]",
|
||||
)}
|
||||
>
|
||||
<div className="truncate font-medium leading-none">
|
||||
{subscription.feeds?.title}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||
{subscription.feeds?.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
href={`${DEEPLINK_SCHEME}#add?id=${subscription.feeds?.id}`}
|
||||
>
|
||||
<StyledButton>
|
||||
<FollowIcon className="mr-1 size-3" />
|
||||
{APP_NAME}
|
||||
</StyledButton>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
if (!user.data) return null
|
||||
return (
|
||||
<div className="container mx-auto mt-12 flex flex-col items-center justify-center p-4 lg:p-0">
|
||||
<Avatar className="aspect-square size-16">
|
||||
<AvatarImage src={user.data.image || undefined} />
|
||||
<AvatarFallback>{user.data.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
|
||||
<h1>{user.data.name}</h1>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className="mb-8 text-sm text-zinc-500">{user.data.handle}</div>
|
||||
</div>
|
||||
<div className="mb-12 w-[70ch] max-w-full space-y-10">
|
||||
{Object.keys(subscriptions.data || {}).map((category) => (
|
||||
<div key={category}>
|
||||
<div className="mb-4 flex items-center text-2xl font-bold">
|
||||
<h3>{category}</h3>
|
||||
</div>
|
||||
<div>
|
||||
{subscriptions.data?.[category].map((subscription) => (
|
||||
<div
|
||||
key={subscription.feedId}
|
||||
className="group relative border-b py-5"
|
||||
>
|
||||
<a
|
||||
className="flex flex-1 cursor-default"
|
||||
href={subscription.feeds.siteUrl!}
|
||||
target="_blank"
|
||||
>
|
||||
<FeedIcon
|
||||
feed={subscription.feeds}
|
||||
size={22}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"w-0 flex-1 grow",
|
||||
"group-hover:grow-[0.85]",
|
||||
)}
|
||||
>
|
||||
<div className="truncate font-medium leading-none">
|
||||
{subscription.feeds?.title}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||
{subscription.feeds?.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
href={`${DEEPLINK_SCHEME}#add?id=${subscription.feeds?.id}`}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<StyledButton>
|
||||
<FollowIcon className="mr-1 size-3" />
|
||||
{APP_NAME}
|
||||
</StyledButton>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue