Skip to content

Commit eb70126

Browse files
pseudoyuInnei
andauthored
feat(discover): enhance RSSHub recommendations with filters (#1481)
* feat: add language and categories switcher in discover page * docs: add changelog next * feat: only set zh-CN and en as language options * feat: redesign card Signed-off-by: Innei <tukon479@gmail.com> * feat: card new design Signed-off-by: Innei <tukon479@gmail.com> * feat: redesign discover Signed-off-by: Innei <tukon479@gmail.com> * fix: user guide rsshub Signed-off-by: Innei <tukon479@gmail.com> * fix: request loop Signed-off-by: Innei <tukon479@gmail.com> * fix: scroll to active tab Signed-off-by: Innei <tukon479@gmail.com> * fix: staleTime var name * chore: use useGeneralSettingKey to get locale lang * fix: zindex and flush sync Signed-off-by: Innei <tukon479@gmail.com> * fix: darken in dark mode Signed-off-by: Innei <tukon479@gmail.com> * fix: lottie import Signed-off-by: Innei <tukon479@gmail.com> * docs: change discover page changelog --------- Signed-off-by: Innei <tukon479@gmail.com> Co-authored-by: Innei <i@innei.in> Co-authored-by: Innei <tukon479@gmail.com>
1 parent 198bb17 commit eb70126

File tree

18 files changed

+528
-101
lines changed

18 files changed

+528
-101
lines changed

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
"tailwindCSS.experimental.classRegex": [
1010
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
1111
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
12-
// ["tw\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
13-
["tw`([^`]*)`", "([^`]*)"]
12+
["tw`([^`]*)`", "([^`]*)"],
13+
["[a-zA-Z]+[cC]lass[nN]ame[\"'`]?:\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"],
14+
["[a-zA-Z]+[cC]lass[nN]ame\\s*=\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"]
1415
],
1516
// If you do not want to autofix some rules on save
1617
// You can put this in your user settings or workspace settings

apps/renderer/src/components/ui/lottie-container/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function LottieRenderContainer() {
1414

1515
return (
1616
<RootPortal>
17-
<div className="pointer-events-none fixed z-[999]" data-testid="lottie-render-container">
17+
<div className="pointer-events-none fixed z-[9999]" data-testid="lottie-render-container">
1818
{elements.map((element) => element)}
1919
</div>
2020
</RootPortal>

apps/renderer/src/components/ui/modal/stacked/provider.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ import type { FC, PropsWithChildren } from "react"
44
import { useEffect } from "react"
55

66
import { modalStackAtom } from "./atom"
7+
import { useModalStack } from "./hooks"
78
import { ModalInternal } from "./modal"
9+
import type { ModalProps } from "./types"
10+
11+
declare global {
12+
interface Window {
13+
presentModal: (modal: ModalProps) => void
14+
}
15+
}
816

917
export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => (
1018
<>
@@ -14,6 +22,9 @@ export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => (
1422
)
1523

1624
const ModalStack = () => {
25+
const { present } = useModalStack()
26+
window.presentModal = present
27+
1728
const stack = useAtomValue(modalStackAtom)
1829

1930
const topModalIndex = stack.findLastIndex((item) => item.modal)

apps/renderer/src/lib/query-client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import { FetchError } from "ofetch"
66

77
import { QUERY_PERSIST_KEY } from "../constants/app"
88

9+
const defaultStaleTime = 600_000 // 10min
910
const DO_NOT_RETRY_CODES = new Set([400, 401, 403, 404, 422])
1011
const queryClient = new QueryClient({
1112
defaultOptions: {
1213
queries: {
1314
refetchOnWindowFocus: false,
1415
retryDelay: 1000,
16+
staleTime: defaultStaleTime,
1517
retry(failureCount, error) {
1618
console.error(error)
1719
if (
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
export const RSSHubCategoryOptions: {
2+
name: string
3+
value: string
4+
}[] = [
5+
{
6+
name: "All",
7+
value: "all",
8+
},
9+
{
10+
name: "Social Media",
11+
value: "social-media",
12+
},
13+
{
14+
name: "New Media",
15+
value: "new-media",
16+
},
17+
{
18+
name: "News",
19+
value: "traditional-media",
20+
},
21+
{
22+
name: "BBS",
23+
value: "bbs",
24+
},
25+
{
26+
name: "Blog",
27+
value: "blog",
28+
},
29+
{
30+
name: "Programming",
31+
value: "programming",
32+
},
33+
{
34+
name: "Design",
35+
value: "design",
36+
},
37+
{
38+
name: "Live",
39+
value: "live",
40+
},
41+
{
42+
name: "Multimedia",
43+
value: "multimedia",
44+
},
45+
{
46+
name: "Picture",
47+
value: "picture",
48+
},
49+
{
50+
name: "ACG",
51+
value: "anime",
52+
},
53+
{
54+
name: "Application Updates",
55+
value: "program-update",
56+
},
57+
{
58+
name: "University",
59+
value: "university",
60+
},
61+
{
62+
name: "Forecast",
63+
value: "forecast",
64+
},
65+
{
66+
name: "Travel",
67+
value: "travel",
68+
},
69+
{
70+
name: "Shopping",
71+
value: "shopping",
72+
},
73+
{
74+
name: "Gaming",
75+
value: "game",
76+
},
77+
{
78+
name: "Reading",
79+
value: "reading",
80+
},
81+
{
82+
name: "Government",
83+
value: "government",
84+
},
85+
{
86+
name: "Study",
87+
value: "study",
88+
},
89+
{
90+
name: "Scientific Journal",
91+
value: "journal",
92+
},
93+
{
94+
name: "Finance",
95+
value: "finance",
96+
},
97+
]
98+
99+
export const RSSHubCategoryMap: Record<string, string> = Object.fromEntries(
100+
RSSHubCategoryOptions.map((item) => [item.value, item.name]),
101+
)
Lines changed: 137 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,153 @@
11
import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/card/index.jsx"
2+
import clsx from "clsx"
3+
import { upperFirst } from "lodash-es"
24
import type { FC } from "react"
3-
import { memo } from "react"
5+
import { memo, useMemo } from "react"
46

57
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
68
import { FeedIcon } from "~/modules/feed/feed-icon"
79

10+
import { RSSHubCategoryMap } from "./constants"
811
import { RecommendationContent } from "./recommendation-content"
12+
import styles from "./recommendations.module.css"
913
import type { RSSHubRouteDeclaration } from "./types"
1014

1115
interface RecommendationCardProps {
1216
data: RSSHubRouteDeclaration
1317
routePrefix: string
18+
setCategory: (category: string) => void
1419
}
15-
export const RecommendationCard: FC<RecommendationCardProps> = memo(({ data, routePrefix }) => {
16-
const { present } = useModalStack()
17-
return (
18-
<Card className="shadow-none">
19-
<CardHeader className="p-5 pb-3">
20-
<CardTitle className="flex items-center text-base">
21-
<FeedIcon siteUrl={`https://${data.url}`} />
22-
{data.name}
23-
</CardTitle>
24-
</CardHeader>
25-
<CardContent className="p-5 pt-0">
26-
<ul className="space-y-1 text-sm text-muted-foreground">
27-
{Object.keys(data.routes).map((route) => (
28-
<li
29-
key={route}
30-
className="group hover:text-theme-foreground-hover"
31-
onClick={(e) => {
32-
;(e.target as HTMLElement).querySelector("button")?.click()
33-
}}
34-
tabIndex={-1}
20+
export const RecommendationCard: FC<RecommendationCardProps> = memo(
21+
({ data, routePrefix, setCategory }) => {
22+
const { present } = useModalStack()
23+
24+
const { maintainers, categories } = useMemo(() => {
25+
const maintainers = new Set<string>()
26+
const categories = new Set<string>()
27+
for (const route in data.routes) {
28+
const routeData = data.routes[route]
29+
if (routeData.maintainers) {
30+
routeData.maintainers.forEach((m) => maintainers.add(m))
31+
}
32+
if (routeData.categories) {
33+
routeData.categories.forEach((c) => categories.add(c))
34+
}
35+
}
36+
categories.delete("popular")
37+
return {
38+
maintainers: Array.from(maintainers),
39+
categories: Array.from(categories),
40+
}
41+
}, [data])
42+
43+
return (
44+
<Card className={styles["recommendations-card"]}>
45+
<CardHeader className="relative p-5 pb-3 @container">
46+
<div className="absolute left-0 top-0 h-[50px] w-full overflow-hidden @[280px]:h-[80px] dark:brightness-75">
47+
<span className="opacity-50 blur-2xl">
48+
<FeedIcon
49+
disableFadeIn
50+
siteUrl={`https://${data.url}`}
51+
size={400}
52+
className="pointer-events-none size-[500px]"
53+
/>
54+
</span>
55+
</div>
56+
57+
<CardTitle className="relative z-[1] flex items-center pt-[10px] text-base @[280px]:pt-10">
58+
<span className="center box-content flex aspect-square rounded-full bg-background p-1.5">
59+
<span className="overflow-hidden rounded-full">
60+
<FeedIcon size={28} className="mr-0" siteUrl={`https://${data.url}`} />
61+
</span>
62+
</span>
63+
<a
64+
href={`https://${data.url}`}
65+
target="_blank"
66+
rel="noreferrer"
67+
className="ml-2 translate-y-1.5"
3568
>
36-
<button
37-
type="button"
38-
className="-translate-x-1.5 rounded p-0.5 px-1.5 duration-200 group-hover:translate-x-0 group-hover:bg-muted"
39-
onClick={() => {
40-
present({
41-
id: `recommendation-content-${route}`,
42-
content: () => (
43-
<RecommendationContent routePrefix={routePrefix} route={data.routes[route]} />
44-
),
45-
icon: <FeedIcon className="size-4" size={16} siteUrl={`https://${data.url}`} />,
46-
title: `${data.name} - ${data.routes[route].name}`,
47-
})
69+
{data.name}
70+
</a>
71+
</CardTitle>
72+
</CardHeader>
73+
<CardContent className="flex grow flex-col p-5 pt-0">
74+
<ul className="grow columns-2 gap-2 space-y-1 text-sm text-foreground/90">
75+
{Object.keys(data.routes).map((route) => (
76+
<li
77+
key={route}
78+
className="group ml-1 hover:text-theme-foreground-hover"
79+
onClick={(e) => {
80+
;(e.target as HTMLElement).querySelector("button")?.click()
4881
}}
82+
tabIndex={-1}
4983
>
50-
{data.routes[route].name}
51-
</button>
52-
</li>
53-
))}
54-
</ul>
55-
</CardContent>
56-
</Card>
57-
)
58-
})
84+
<button
85+
type="button"
86+
className="relative rounded p-0.5 px-1 duration-200 before:absolute before:inset-y-0 before:-left-2 before:my-auto before:size-1.5 before:rounded-full before:bg-accent before:content-[''] group-hover:bg-muted"
87+
onClick={() => {
88+
present({
89+
id: `recommendation-content-${route}`,
90+
content: () => (
91+
<RecommendationContent
92+
routePrefix={routePrefix}
93+
route={data.routes[route]}
94+
/>
95+
),
96+
icon: (
97+
<FeedIcon className="size-4" size={16} siteUrl={`https://${data.url}`} />
98+
),
99+
title: `${data.name} - ${data.routes[route].name}`,
100+
})
101+
}}
102+
>
103+
{data.routes[route].name}
104+
</button>
105+
</li>
106+
))}
107+
</ul>
108+
109+
<div className="mt-4 flex flex-col gap-2 text-muted-foreground">
110+
<div className="flex flex-1 items-center text-sm">
111+
<i className="i-mingcute-hammer-line mr-1 shrink-0 translate-y-0.5 self-start" />
112+
113+
<span className="flex flex-wrap gap-1">
114+
{maintainers.map((m) => (
115+
<a
116+
key={m}
117+
href={`https://github.com/${m}`}
118+
className="follow-link--underline"
119+
target="_blank"
120+
rel="noreferrer"
121+
>
122+
@{m}
123+
</a>
124+
))}
125+
</span>
126+
</div>
127+
<div className="flex flex-1 items-center text-sm">
128+
<i className="i-mingcute-tag-2-line mr-1 shrink-0 translate-y-1 self-start" />
129+
<span className="flex flex-wrap gap-1">
130+
{categories.map((c) => (
131+
<button
132+
onClick={() => {
133+
if (!RSSHubCategoryMap[c]) return
134+
setCategory(c)
135+
}}
136+
key={c}
137+
type="button"
138+
className={clsx(
139+
"cursor-pointer rounded bg-muted/50 px-1.5 duration-200 hover:bg-muted",
140+
!RSSHubCategoryMap[c] && "pointer-events-none opacity-50",
141+
)}
142+
>
143+
{RSSHubCategoryMap[c] || upperFirst(c)}
144+
</button>
145+
))}
146+
</span>
147+
</div>
148+
</div>
149+
</CardContent>
150+
</Card>
151+
)
152+
},
153+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.recommendations-grid {
2+
--rows: 4;
3+
--max-item-width: 280px;
4+
--gap: 1em;
5+
display: grid;
6+
grid-template-columns: repeat(
7+
auto-fill,
8+
minmax(
9+
max(var(--max-item-width), calc((100% - var(--gap) * (var(--rows) - 1)) / var(--rows))),
10+
1fr
11+
)
12+
);
13+
14+
gap: 1em;
15+
}
16+
17+
.recommendations-card {
18+
border-radius: 8px;
19+
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.06) inset;
20+
border: 0;
21+
overflow: hidden;
22+
display: flex;
23+
flex-direction: column;
24+
}

0 commit comments

Comments
 (0)