feat: profiles page
This commit is contained in:
parent
7a6bc2fdbf
commit
ddb1cd9769
|
|
@ -21,6 +21,7 @@ export function FeedIcon({
|
|||
return (
|
||||
<Image
|
||||
src={image}
|
||||
loading="lazy"
|
||||
className={cn("mr-2 shrink-0 rounded-sm", className)}
|
||||
style={{
|
||||
width: size,
|
||||
|
|
|
|||
|
|
@ -551,7 +551,7 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
view: number;
|
||||
feedId: string;
|
||||
category: string | null;
|
||||
isPrivate: boolean | null;
|
||||
isPrivate: boolean;
|
||||
} | undefined;
|
||||
};
|
||||
};
|
||||
|
|
@ -882,6 +882,7 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
$get: {
|
||||
input: {
|
||||
query: {
|
||||
userId?: string | undefined;
|
||||
view?: string | undefined;
|
||||
};
|
||||
};
|
||||
|
|
@ -909,7 +910,7 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
ownerUserId: string | null;
|
||||
};
|
||||
category: string | null;
|
||||
isPrivate: boolean | null;
|
||||
isPrivate: boolean;
|
||||
}[];
|
||||
};
|
||||
outputFormat: "json";
|
||||
|
|
@ -921,7 +922,7 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
view: number;
|
||||
url: string;
|
||||
category?: string | null | undefined;
|
||||
isPrivate?: boolean | null | undefined;
|
||||
isPrivate?: boolean | undefined;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
|
|
@ -949,7 +950,7 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
view: number;
|
||||
feedId: string;
|
||||
category?: string | null | undefined;
|
||||
isPrivate?: boolean | null | undefined;
|
||||
isPrivate?: boolean | undefined;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export function Component() {
|
|||
{feed.data.feed.title}
|
||||
{" "}
|
||||
|
|
||||
{" "}
|
||||
{APP_NAME}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
|
@ -98,8 +99,6 @@ export function Component() {
|
|||
<a className="mb-8" href={`${DEEPLINK_SCHEME}add?id=${id}`}>
|
||||
<StyledButton>
|
||||
<FollowIcon className="mr-1 size-3" />
|
||||
follow on
|
||||
{" "}
|
||||
{APP_NAME}
|
||||
</StyledButton>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
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 { useAuthQuery } from "@renderer/hooks/common"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { APP_NAME } from "@renderer/lib/constants"
|
||||
import { defineQuery } from "@renderer/lib/defineQuery"
|
||||
import { capitalizeFirstLetter } from "@renderer/lib/utils"
|
||||
import { DEEPLINK_SCHEME } from "@shared/constants"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { parse } from "tldts"
|
||||
|
||||
export function Component() {
|
||||
const { id } = useParams()
|
||||
|
||||
const user = useAuthQuery(defineQuery(["profiles", id], async () => {
|
||||
const res = await apiClient.profiles.$get({
|
||||
query: { id: id! },
|
||||
})
|
||||
return res.data
|
||||
}), {
|
||||
enabled: !!id,
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{user.data && (
|
||||
<div className="mx-auto mt-12 flex max-w-5xl flex-col items-center justify-center p-4 lg:p-0">
|
||||
<Helmet>
|
||||
<title>
|
||||
{user.data.name}
|
||||
{" "}
|
||||
|
|
||||
{" "}
|
||||
{APP_NAME}
|
||||
</title>
|
||||
</Helmet>
|
||||
<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-96 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" href={subscription.feeds.siteUrl!} target="_blank">
|
||||
<FeedIcon feed={subscription.feeds} size={22} className="mr-3" />
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
<a className="absolute left-full top-5 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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { useUser } from "@renderer/atoms/user"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@renderer/components/ui/avatar"
|
||||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { useSignOut } from "@renderer/hooks/biz/useSignOut"
|
||||
import { views } from "@renderer/lib/constants"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { FeedList } from "@renderer/modules/feed-column/list"
|
||||
|
||||
export function Component() {
|
||||
const user = useUser()
|
||||
const signOut = useSignOut()
|
||||
return (
|
||||
<div className="flex w-full gap-10 px-10 py-16">
|
||||
<div className="flex min-w-40 flex-1 flex-col items-center gap-8 pt-20">
|
||||
<Avatar className="aspect-square size-32">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user?.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="text-4xl font-bold">{user?.name}</div>
|
||||
|
||||
<div>
|
||||
<StyledButton
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
signOut()
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</StyledButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-[2] flex-col gap-8">
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<div className="text-2xl font-bold">Subscriptions</div>
|
||||
<div className="overflow-y-auto">
|
||||
{views.map((view, index) => (
|
||||
<div key={view.name}>
|
||||
<div className="mb-1 flex items-center gap-2 font-semibold">
|
||||
<span className={cn("flex", view.className)}>
|
||||
{view.icon}
|
||||
</span>
|
||||
<span>{view.name}</span>
|
||||
</div>
|
||||
<FeedList
|
||||
key={view.name}
|
||||
className="mb-4"
|
||||
view={index}
|
||||
hideTitle
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue