|
1 | 1 | import { Card, CardContent, CardHeader, CardTitle } from "@follow/components/ui/card/index.jsx"
|
| 2 | +import clsx from "clsx" |
| 3 | +import { upperFirst } from "lodash-es" |
2 | 4 | import type { FC } from "react"
|
3 |
| -import { memo } from "react" |
| 5 | +import { memo, useMemo } from "react" |
4 | 6 |
|
5 | 7 | import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
6 | 8 | import { FeedIcon } from "~/modules/feed/feed-icon"
|
7 | 9 |
|
| 10 | +import { RSSHubCategoryMap } from "./constants" |
8 | 11 | import { RecommendationContent } from "./recommendation-content"
|
| 12 | +import styles from "./recommendations.module.css" |
9 | 13 | import type { RSSHubRouteDeclaration } from "./types"
|
10 | 14 |
|
11 | 15 | interface RecommendationCardProps {
|
12 | 16 | data: RSSHubRouteDeclaration
|
13 | 17 | routePrefix: string
|
| 18 | + setCategory: (category: string) => void |
14 | 19 | }
|
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" |
35 | 68 | >
|
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() |
48 | 81 | }}
|
| 82 | + tabIndex={-1} |
49 | 83 | >
|
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 | +) |
0 commit comments