feat(desktop): trending filters
This commit is contained in:
parent
eb6a40954f
commit
58ce9f614a
|
|
@ -2,7 +2,6 @@ import { isMobile } from "@follow/components/hooks/useMobile.js"
|
|||
import { EmptyIcon } from "@follow/components/icons/empty.js"
|
||||
import { ActionButton } from "@follow/components/ui/button/action-button.js"
|
||||
import { Card, CardContent } from "@follow/components/ui/card/index.jsx"
|
||||
import { LoadingCircle } from "@follow/components/ui/loading/index.js"
|
||||
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
|
||||
import { ResponsiveSelect } from "@follow/components/ui/select/responsive.js"
|
||||
import { CategoryMap, RSSHubCategories } from "@follow/constants"
|
||||
|
|
@ -20,7 +19,6 @@ import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/ho
|
|||
import { useAuthQuery } from "~/hooks/common"
|
||||
import { Queries } from "~/queries"
|
||||
|
||||
import { TrendingButton } from "../trending"
|
||||
import { RecommendationCard } from "./recommendations-card"
|
||||
|
||||
const LanguageOptions = [
|
||||
|
|
@ -69,7 +67,7 @@ export function Recommendations() {
|
|||
|
||||
firstLoad = false
|
||||
|
||||
const { data, isLoading, isFetching } = rsshubPopular
|
||||
const { data, isLoading } = rsshubPopular
|
||||
|
||||
const keys = useMemo(() => {
|
||||
if (!data) {
|
||||
|
|
@ -143,10 +141,14 @@ export function Recommendations() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mt-12 w-full max-w-[800px]">
|
||||
<div className="center relative flex">
|
||||
<div className="absolute bottom-0 right-0 flex items-center gap-2">
|
||||
<span className="text-headline text-text font-medium">Preferred language:</span>
|
||||
<div className="mt-12 w-full max-w-[800px] space-y-6">
|
||||
<div className="flex items-center justify-center gap-2 text-center text-xl font-bold">
|
||||
<i className="i-mgc-grid-2-cute-re text-xl" />
|
||||
<span>{t("words.categories")}</span>
|
||||
</div>
|
||||
<div className="center flex justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-headline text-text font-medium">Language:</span>
|
||||
<ResponsiveSelect
|
||||
value={selectedLang}
|
||||
onValueChange={handleLangChange}
|
||||
|
|
@ -154,20 +156,10 @@ export function Recommendations() {
|
|||
size="sm"
|
||||
items={LanguageOptions as any}
|
||||
/>
|
||||
<TrendingButton language={selectedLang} />
|
||||
</div>
|
||||
{isFetching && (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 flex items-center justify-center gap-8">
|
||||
<span className="opacity-0" aria-hidden>
|
||||
{t("discover.popular")}
|
||||
</span>
|
||||
|
||||
<LoadingCircle size="small" className="center flex" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
{RSSHubCategories.map((cat) => (
|
||||
<Card
|
||||
key={cat}
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
import type { FeedModel } from "@follow/models"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { apiFetch } from "~/lib/api-fetch"
|
||||
|
||||
import { FeedCard } from "./feed-card"
|
||||
|
||||
export function Trending() {
|
||||
const { t } = useTranslation()
|
||||
const { data } = useQuery({
|
||||
queryKey: ["trending", "eng"],
|
||||
queryFn: async () => {
|
||||
return await apiFetch<{
|
||||
data: {
|
||||
feed: FeedModel
|
||||
}[]
|
||||
}>("/trending/feeds", {
|
||||
method: "GET",
|
||||
params: {
|
||||
// language: "en",
|
||||
// view: 0,
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="mt-4 w-full max-w-[800px]">
|
||||
<div className="flex items-center justify-center gap-2 text-center text-lg font-medium">
|
||||
<i className="i-mingcute-trending-up-line text-2xl" />
|
||||
<span>{t("words.trending")}</span>
|
||||
</div>
|
||||
<div className="mt-8 grid grid-cols-2 gap-6">
|
||||
{data?.data?.map((item, index) => (
|
||||
<FeedCard
|
||||
key={item.feed.id}
|
||||
item={item}
|
||||
followButtonVariant="ghost"
|
||||
followButtonClassName="border-accent text-accent px-3 -mr-3"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"center absolute -left-5 -top-5 size-12 rounded-br-3xl pl-4 pt-4 text-xs",
|
||||
index < 3 ? "bg-accent text-white" : "bg-zinc-100",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
</FeedCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,330 +1,170 @@
|
|||
import { isMobile, useMobile } from "@follow/components/hooks/useMobile.js"
|
||||
import { PhUsersBold } from "@follow/components/icons/users.jsx"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avatar/index.jsx"
|
||||
import { ActionButton, Button } from "@follow/components/ui/button/index.js"
|
||||
import { LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
|
||||
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/index.js"
|
||||
import type { FeedModel, Models, UserModel } from "@follow/models"
|
||||
import { stopPropagation } from "@follow/utils/dom"
|
||||
import { ResponsiveSelect } from "@follow/components/ui/select/responsive.js"
|
||||
import { Skeleton } from "@follow/components/ui/skeleton/index.jsx"
|
||||
import { views } from "@follow/constants"
|
||||
import type { FeedModel } from "@follow/models"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import type { FC } from "react"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { getTrendingAggregates } from "~/api/trending"
|
||||
import { DrawerModalLayout } from "~/components/ui/modal/stacked/custom-modal"
|
||||
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useFollow } from "~/hooks/biz/useFollow"
|
||||
import { UrlBuilder } from "~/lib/url-builder"
|
||||
import { FeedIcon } from "~/modules/feed/feed-icon"
|
||||
import { useGeneralSettingKey } from "~/atoms/settings/general"
|
||||
import { apiFetch } from "~/lib/api-fetch"
|
||||
|
||||
import { usePresentUserProfileModal } from "../profile/hooks"
|
||||
import { FeedCard } from "../discover/feed-card"
|
||||
|
||||
interface TrendingProps {
|
||||
language: string
|
||||
}
|
||||
export const TrendingButton = ({ language, className }: TrendingProps & { className?: string }) => {
|
||||
const { present } = useModalStack()
|
||||
const LanguageOptions = [
|
||||
{
|
||||
label: "All",
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
label: "English",
|
||||
value: "eng",
|
||||
},
|
||||
{
|
||||
label: "中文",
|
||||
value: "cmn",
|
||||
},
|
||||
]
|
||||
|
||||
type Language = (typeof LanguageOptions)[number]["value"]
|
||||
|
||||
const rangeOptions = [
|
||||
{
|
||||
label: "Today",
|
||||
value: "1d",
|
||||
},
|
||||
{
|
||||
label: "Three days",
|
||||
value: "3d",
|
||||
},
|
||||
{
|
||||
label: "This week",
|
||||
value: "7d",
|
||||
},
|
||||
{
|
||||
label: "This month",
|
||||
value: "30d",
|
||||
},
|
||||
]
|
||||
|
||||
type Range = (typeof rangeOptions)[number]["value"]
|
||||
|
||||
const viewOptions = [
|
||||
{
|
||||
label: "All",
|
||||
value: "all",
|
||||
},
|
||||
...views.map((view) => ({
|
||||
label: view.name,
|
||||
value: `${view.view}`,
|
||||
})),
|
||||
]
|
||||
|
||||
type View = (typeof viewOptions)[number]["value"]
|
||||
|
||||
export function Trending() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
present({
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-mingcute-trending-up-line text-2xl" />
|
||||
<span>{t("words.trending")}</span>
|
||||
</div>
|
||||
),
|
||||
content: () => <TrendContent language={language} />,
|
||||
CustomModalComponent: !isMobile() ? DrawerModalLayout : undefined,
|
||||
})
|
||||
}}
|
||||
buttonClassName={cn("rounded px-2 h-8", className)}
|
||||
>
|
||||
<i className="i-mgc-trending-up-cute-re mr-1" />
|
||||
{t("words.trending")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const TrendContent: FC<TrendingProps> = ({ language }) => {
|
||||
const isMobile = useMobile()
|
||||
const { t: tCommon } = useTranslation("common")
|
||||
const lang = useGeneralSettingKey("language")
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["trending", language],
|
||||
queryFn: () => {
|
||||
return getTrendingAggregates({ language })
|
||||
const [selectedLang, setSelectedLang] = useState<Language>(lang.startsWith("zh") ? "all" : "eng")
|
||||
const [selectedRange, setSelectedRange] = useState<Range>("7d")
|
||||
const [selectedView, setSelectedView] = useState<View>("all")
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["trending", selectedLang, selectedView, selectedRange],
|
||||
queryFn: async () => {
|
||||
return await apiFetch<{
|
||||
data: {
|
||||
feed: FeedModel
|
||||
}[]
|
||||
}>("/trending/feeds", {
|
||||
method: "GET",
|
||||
params: {
|
||||
language: selectedLang === "all" ? undefined : selectedLang,
|
||||
view: selectedView === "all" ? undefined : Number(selectedView),
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { dismiss } = useCurrentModal()
|
||||
if (!data)
|
||||
return (
|
||||
<div className={isMobile ? "mx-auto" : "center absolute inset-0"}>
|
||||
<LoadingWithIcon
|
||||
icon={<i className="i-mingcute-trending-up-line text-3xl" />}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="flex size-full grow flex-col gap-4">
|
||||
{!isMobile && (
|
||||
<div className="-mt-4 flex w-full items-center justify-center gap-2 text-2xl">
|
||||
<i className="i-mingcute-trending-up-line text-3xl" />
|
||||
<span className="font-bold">{t("words.trending")}</span>
|
||||
<div className="mt-4 w-full max-w-[800px] space-y-6">
|
||||
<div className="flex items-center justify-center gap-2 text-center text-xl font-bold">
|
||||
<i className="i-mgc-trending-up-cute-re text-xl" />
|
||||
<span>{t("words.trending")}</span>
|
||||
</div>
|
||||
<div className="center flex justify-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-headline text-text font-medium">Language:</span>
|
||||
<ResponsiveSelect
|
||||
value={selectedLang}
|
||||
onValueChange={(value) => {
|
||||
setSelectedLang(value as Language)
|
||||
}}
|
||||
triggerClassName="w-32 h-8 rounded"
|
||||
size="sm"
|
||||
items={LanguageOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ActionButton
|
||||
className="absolute right-4 top-4"
|
||||
onClick={dismiss}
|
||||
tooltip={t("words.close", { ns: "common" })}
|
||||
>
|
||||
<i className="i-mgc-close-cute-re" />
|
||||
</ActionButton>
|
||||
<ScrollArea.ScrollArea
|
||||
rootClassName="flex h-0 w-[calc(100%+8px)] min-h-[50vh] grow flex-col overflow-visible"
|
||||
viewportClassName="pb-4 [&>div]:!block"
|
||||
scrollbarClassName="-mr-6"
|
||||
>
|
||||
<TrendingUsers data={data.trendingUsers} />
|
||||
<TrendingLists data={data.trendingLists} />
|
||||
<TrendingFeeds data={data.trendingFeeds} />
|
||||
|
||||
<TrendingEntries data={data.trendingEntries} />
|
||||
</ScrollArea.ScrollArea>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-headline text-text font-medium">Range:</span>
|
||||
<ResponsiveSelect
|
||||
value={selectedRange}
|
||||
onValueChange={(value: string) => {
|
||||
setSelectedRange(value as Range)
|
||||
}}
|
||||
triggerClassName="w-32 h-8 rounded"
|
||||
size="sm"
|
||||
items={rangeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-headline text-text font-medium">View:</span>
|
||||
<ResponsiveSelect
|
||||
value={selectedView}
|
||||
onValueChange={(value: string) => {
|
||||
setSelectedView(value as View)
|
||||
}}
|
||||
triggerClassName="w-32 h-8 rounded"
|
||||
size="sm"
|
||||
items={viewOptions}
|
||||
renderItem={(item) => <>{tCommon(item.label as any)}</>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton className="h-[146px]" />
|
||||
<Skeleton className="h-[146px]" />
|
||||
<Skeleton className="h-[146px]" />
|
||||
<Skeleton className="h-[146px]" />
|
||||
<Skeleton className="h-[146px]" />
|
||||
<Skeleton className="h-[146px]" />
|
||||
</>
|
||||
) : (
|
||||
data?.data?.map((item, index) => (
|
||||
<FeedCard
|
||||
key={item.feed.id}
|
||||
item={item}
|
||||
followButtonVariant="ghost"
|
||||
followButtonClassName="border-accent text-accent px-3 -mr-3"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"center absolute -left-5 -top-5 size-12 rounded-br-3xl pl-4 pt-4 text-xs",
|
||||
index < 3 ? "bg-accent text-white" : "bg-zinc-100",
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
</FeedCard>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const TrendingLists: FC<{
|
||||
data: Models.TrendingList[]
|
||||
}> = ({ data }) => {
|
||||
const follow = useFollow()
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<section className="mt-8 w-full text-left">
|
||||
<h2 className="my-2 text-xl font-bold">{t("trending.list")}</h2>
|
||||
|
||||
<ul className="mt-4 flex flex-col gap-3 pb-4">
|
||||
{data.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={"group relative flex w-full min-w-0 items-center pl-2"}
|
||||
onClick={() => {
|
||||
follow({ isList: true, id: item.id })
|
||||
}}
|
||||
>
|
||||
<div className="group-hover:bg-theme-item-hover absolute -inset-y-1 inset-x-0 z-[-1] rounded-lg duration-200" />
|
||||
<FeedIcon feed={item as any} size={40} />
|
||||
|
||||
<div className={cn("ml-1 flex w-full flex-col text-left")}>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className={cn("truncate text-base font-medium")}>{item.title}</div>
|
||||
|
||||
{!!item.subscriberCount && <UserCount count={item.subscriberCount} />}
|
||||
</div>
|
||||
{!!item.description && (
|
||||
<div className={"line-clamp-2 text-xs"}>{item.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const UserCount: Component<{ count: number }> = ({ count, className }) => {
|
||||
return (
|
||||
<span className={cn("flex items-center gap-0.5 text-xs tabular-nums opacity-60", className)}>
|
||||
<PhUsersBold className="size-3" />
|
||||
<span>{count}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
interface TopUserAvatarProps {
|
||||
user: UserModel
|
||||
position: string
|
||||
}
|
||||
|
||||
const TopUserAvatar: React.FC<TopUserAvatarProps> = ({ user, position }) => (
|
||||
<div className={`absolute ${position} group flex w-[50px] flex-col`}>
|
||||
<div className="group-hover:bg-theme-item-hover absolute -inset-x-4 -inset-y-2 rounded-lg duration-200" />
|
||||
<Avatar className="border-border ring-background block aspect-square size-[50px] overflow-hidden rounded-full border ring-1">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{user.name && (
|
||||
<EllipsisHorizontalTextWithTooltip className="mt-2 text-xs font-medium">
|
||||
<span>{user.name}</span>
|
||||
</EllipsisHorizontalTextWithTooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const TrendingUsers: FC<{ data: UserModel[] }> = ({ data }) => {
|
||||
const profile = usePresentUserProfileModal("dialog")
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<section className="w-full text-left">
|
||||
<h2 className="my-2 text-xl font-bold">{t("trending.user")}</h2>
|
||||
<div className="relative h-[100px]">
|
||||
<div className="text-accent absolute left-[calc(50%+15px)] top-[8px] rotate-45 text-[20px]">
|
||||
<i className="i-mgc-vip-2-cute-fi" />
|
||||
</div>
|
||||
|
||||
<div className="text-accent/80 absolute left-[calc(33%+15px)] top-[calc(theme(spacing.3))] rotate-45 text-[20px]">
|
||||
<i className="i-mgc-vip-2-cute-re" />
|
||||
</div>
|
||||
|
||||
{data.slice(0, 3).map((user, index: number) => (
|
||||
<button
|
||||
onFocusCapture={stopPropagation}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
profile(user.id)
|
||||
}}
|
||||
key={user.id}
|
||||
>
|
||||
<TopUserAvatar
|
||||
user={user}
|
||||
position={
|
||||
index === 0
|
||||
? "left-1/2 -translate-x-1/2"
|
||||
: index === 1
|
||||
? "left-1/3 top-6 -translate-x-1/2"
|
||||
: "left-2/3 top-6 -translate-x-1/2"
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.length > 3 && (
|
||||
<ul className="mt-8 flex flex-col gap-4 pl-2">
|
||||
{data.slice(3).map((user) => (
|
||||
<li key={user.id}>
|
||||
<button
|
||||
onFocusCapture={stopPropagation}
|
||||
className="group relative flex w-full items-center gap-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
profile(user.id)
|
||||
}}
|
||||
>
|
||||
<div className="group-hover:bg-theme-item-hover absolute -inset-2 right-0 z-[-1] rounded-lg duration-200" />
|
||||
<Avatar className="border-border ring-background block aspect-square size-[40px] overflow-hidden rounded-full border ring-1">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="font-medium">{user.name}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const TrendingFeeds = ({ data }: { data: FeedModel[] }) => {
|
||||
const follow = useFollow()
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<section className="mt-8 w-full text-left">
|
||||
<h2 className="my-2 text-xl font-bold">{t("trending.feed")}</h2>
|
||||
|
||||
<ul className="mt-2 flex flex-col">
|
||||
{data.map((feed) => {
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"hover:bg-theme-item-hover group flex w-full items-center gap-1 rounded-md py-0.5 pl-2 duration-200",
|
||||
"relative",
|
||||
)}
|
||||
key={feed.id}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href={UrlBuilder.shareFeed(feed.id)}
|
||||
className="flex grow items-center gap-1 py-1"
|
||||
>
|
||||
<FeedIcon feed={feed} size={24} className="rounded" />
|
||||
|
||||
<div className="flex w-full min-w-0 grow items-center">
|
||||
<div className={"truncate"}>{feed.title}</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="pr-2">
|
||||
<UserCount
|
||||
className="-mr-2 group-hover:opacity-0"
|
||||
count={(feed as any).subscriberCount}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
buttonClassName={
|
||||
"absolute inset-y-0.5 right-0 font-medium opacity-0 duration-200 group-hover:opacity-100"
|
||||
}
|
||||
onClick={() => {
|
||||
follow({ isList: false, id: feed.id })
|
||||
}}
|
||||
>
|
||||
{t("feed_form.follow")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const TrendingEntries = ({ data }: { data: Models.TrendingEntry[] }) => {
|
||||
const { t } = useTranslation()
|
||||
const filteredData = data.filter((entry) => !entry.url.startsWith("https://x.com"))
|
||||
return (
|
||||
<section className="mt-8 w-full text-left">
|
||||
<h2 className="my-2 text-xl font-bold">{t("trending.entry")}</h2>
|
||||
|
||||
{filteredData.length > 0 ? (
|
||||
<ul className="mt-2 list-inside list-disc space-y-1">
|
||||
{filteredData.map((entry) => {
|
||||
return (
|
||||
<li
|
||||
key={entry.id}
|
||||
className="marker:text-accent relative grid w-full grid-cols-[1fr_auto] gap-2 whitespace-nowrap py-0.5"
|
||||
>
|
||||
<div className="m-0 min-w-0 truncate p-0">
|
||||
<a
|
||||
href={entry.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="follow-link--underline truncate text-sm"
|
||||
>
|
||||
{entry.title}
|
||||
</a>
|
||||
</div>
|
||||
<span className="flex items-center gap-0.5 text-xs tabular-nums opacity-60">
|
||||
<i className="i-mingcute-book-2-line" />
|
||||
<span>{entry.readCount}</span>
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="mt-2 text-center text-sm opacity-80">{t("trending.entry_no_results")}</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import { DiscoverImport } from "~/modules/discover/import"
|
|||
import { DiscoverInboxList } from "~/modules/discover/inbox-list-form"
|
||||
import { Recommendations } from "~/modules/discover/recommendations"
|
||||
import { DiscoverTransform } from "~/modules/discover/transform-form"
|
||||
import { Trending } from "~/modules/discover/trending"
|
||||
import { DiscoverUser } from "~/modules/discover/user-form"
|
||||
import { Trending } from "~/modules/trending"
|
||||
|
||||
const tabs: {
|
||||
name: I18nKeys
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M16.52 2.278c-.679.146-1.118.389-1.77.981-.587.534-1.302 1.304-1.514 1.632-.196.303-.38.752-.459 1.116-.076.351-.085 1.082-.018 1.414a3.8 3.8 0 0 0 .633 1.419c.167.223 1.362 1.426 1.649 1.66.67.549 1.351.791 2.218.788.7-.003 1.314-.176 1.85-.525.603-.392 1.969-1.793 2.238-2.297.223-.416.293-.603.377-1.006.203-.973-.032-2.047-.608-2.782-.237-.302-1.143-1.225-1.574-1.603a3.424 3.424 0 0 0-3.022-.797m-11.1.28a3.561 3.561 0 0 0-2.119 1.23c-.27.327-.541.844-.667 1.272-.091.309-.094.368-.094 1.7 0 1.338.003 1.389.094 1.68.328 1.045.95 1.773 1.898 2.222.516.245.82.299 1.828.325 1.005.025 1.766-.025 2.14-.141A3.53 3.53 0 0 0 10.846 8.5c.183-.588.205-2.699.035-3.36-.258-1.003-.989-1.89-1.896-2.3-.628-.284-.681-.292-2.085-.304-.704-.005-1.37.004-1.48.022m12.474 1.783c.181.09.396.278.942.823.882.883.944.986.944 1.576 0 .597-.061.704-.864 1.52-.356.362-.739.721-.851.798a1.44 1.44 0 0 1-1.448.101c-.21-.101-.378-.245-.953-.819-.774-.773-.921-.993-.969-1.447-.03-.285.062-.681.211-.913.145-.225 1.445-1.514 1.614-1.601.273-.141.454-.178.8-.166.279.01.382.033.574.128M7.52 4.515c.768.062 1.248.441 1.423 1.125.051.199.061.434.05 1.233-.013.986-.013.987-.126 1.231a1.698 1.698 0 0 1-.767.764l-.24.112h-1.1c-1.056 0-1.109-.004-1.328-.092a1.5 1.5 0 0 1-.757-.713c-.163-.33-.211-.823-.177-1.807.03-.842.081-1.014.408-1.368.293-.316.569-.45.993-.483a12.354 12.354 0 0 1 1.621-.002m-1.9 8.527a3.38 3.38 0 0 0-2.098 1.005c-.43.432-.686.868-.888 1.513-.091.291-.094.342-.094 1.68 0 1.332.003 1.391.094 1.7a3.57 3.57 0 0 0 2.426 2.426c.308.09.37.094 1.66.094 1.444 0 1.526-.009 2.079-.222.938-.362 1.681-1.16 2.028-2.178.217-.638.236-2.816.03-3.525a3.484 3.484 0 0 0-.894-1.498c-.609-.609-1.279-.921-2.139-.997a17.814 17.814 0 0 0-2.204.002m10.5 0c-.99.088-1.94.656-2.507 1.498-.521.773-.643 1.407-.6 3.1.026 1.008.08 1.312.325 1.828.449.948 1.177 1.57 2.222 1.898.291.091.342.094 1.68.094 1.332 0 1.391-.003 1.7-.094a3.57 3.57 0 0 0 2.426-2.426c.09-.308.094-.37.094-1.66 0-1.444-.009-1.526-.222-2.079-.294-.763-.912-1.434-1.678-1.821a3.12 3.12 0 0 0-1.24-.34 18.138 18.138 0 0 0-2.2.002m-8.016 2.091c.309.143.617.452.764.767l.112.24v1.1c0 1.056-.004 1.109-.092 1.328a1.505 1.505 0 0 1-.717.76c-.243.119-.311.133-.834.167-.35.023-.8.023-1.165 0-.693-.043-.9-.118-1.225-.442-.323-.324-.4-.535-.443-1.22-.053-.849.006-1.661.143-1.958.124-.269.346-.524.571-.656.329-.193.52-.217 1.622-.208l1.02.009.244.113m10.35-.052c.443.137.771.45.95.908.085.219.09.286.089 1.271 0 1.27-.022 1.356-.449 1.784-.377.377-.526.425-1.41.456-.985.036-1.479-.012-1.809-.175a1.5 1.5 0 0 1-.713-.757c-.088-.219-.092-.272-.092-1.328v-1.1l.112-.24c.193-.414.603-.759 1.018-.857.082-.019.576-.037 1.096-.039.844-.004.974.005 1.208.077" fill="#10161F" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M7.5 3.257c-.977.227-1.629.909-2.89 3.026-1.038 1.743-1.723 3.052-1.956 3.741a2.45 2.45 0 0 0-.165.897c-.022.555.026.818.221 1.21.102.205.232.377.447.591.6.597 1.2.789 2.816.901.307.021.349.033.326.091a7.123 7.123 0 0 0-.12.429 5.473 5.473 0 0 0 1.439 5.239c.795.794 1.747 1.297 2.887 1.524.483.097 1.498.096 1.99-.001 1.768-.347 3.197-1.446 3.964-3.045.373-.778.517-1.441.516-2.38 0-.522-.019-.743-.086-1.058a4.434 4.434 0 0 1-.077-.404c.004-.002.359-.026.788-.051.922-.055 1.365-.144 1.816-.364a2.406 2.406 0 0 0 1.187-1.187c.31-.635.396-1.375.396-3.416 0-2.654-.156-3.37-.895-4.101-.482-.479-.905-.673-1.758-.808-.381-.06-.725-.071-2.366-.07-1.666 0-1.977.009-2.352.072-.785.132-1.237.329-1.66.725a2.346 2.346 0 0 0-.63.889l-.125.289-.095-.168a19.25 19.25 0 0 0-.487-.757c-.857-1.284-1.577-1.813-2.527-1.854a2.432 2.432 0 0 0-.604.04m.746 2.041c.359.244 1.147 1.453 2.327 3.567.381.683.427.787.427.962v.197l-.339.049c-1.011.146-2.099.663-2.861 1.358l-.26.238-1.02-.033c-1.487-.047-1.917-.138-2.038-.431-.138-.334.393-1.464 1.81-3.845C7.255 5.743 7.684 5.2 8 5.2c.056 0 .166.044.246.098m9.952.781c.485.106.631.262.73.785.074.392.074 3.88 0 4.272-.142.749-.306.818-2.048.852l-1.1.022-.4-.396a5.56 5.56 0 0 0-2.1-1.314l-.26-.088V8.676c.001-1.614.027-1.936.183-2.241.121-.237.402-.346 1.057-.41.385-.037 3.729.009 3.938.054m-6.017 5.997c1.308.27 2.289 1.173 2.686 2.472.095.315.107.415.107.952 0 .537-.012.637-.107.952-.368 1.204-1.211 2.047-2.415 2.415-.315.095-.415.107-.952.107-.537 0-.637-.012-.952-.107-1.204-.368-2.047-1.211-2.415-2.415-.095-.315-.107-.415-.107-.952 0-.537.012-.637.107-.952.545-1.785 2.27-2.839 4.048-2.472" fill="#10161F" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -406,6 +406,7 @@
|
|||
"words.add": "Add",
|
||||
"words.boost": "Boost",
|
||||
"words.browser": "Browser",
|
||||
"words.categories": "Categories",
|
||||
"words.confirm": "Confirm",
|
||||
"words.discover": "Discover",
|
||||
"words.email": "Email",
|
||||
|
|
|
|||
Loading…
Reference in New Issue