diff --git a/apps/renderer/package.json b/apps/renderer/package.json index a96edd51cc..f8eca97c6d 100644 --- a/apps/renderer/package.json +++ b/apps/renderer/package.json @@ -46,6 +46,7 @@ "@tanstack/react-query-persist-client": "5.56.2", "@use-gesture/react": "10.3.1", "@yornaath/batshit": "0.10.1", + "camelcase-keys": "9.1.3", "class-variance-authority": "0.7.0", "click-to-react-component": "1.1.0", "clsx": "2.1.1", diff --git a/apps/renderer/src/api/trending.ts b/apps/renderer/src/api/trending.ts new file mode 100644 index 0000000000..8f3e51a300 --- /dev/null +++ b/apps/renderer/src/api/trending.ts @@ -0,0 +1,11 @@ +import camelcaseKeys from "camelcase-keys" + +import { apiFetch } from "~/lib/api-fetch" +import type { Models } from "~/models" + +const v1ApiPrefix = "/v1" +export const getTrendingAggregates = () => { + return apiFetch(`${v1ApiPrefix}/trendings`).then((data) => + camelcaseKeys(data as any, { deep: true }), + ) +} diff --git a/apps/renderer/src/components/icons/crown.tsx b/apps/renderer/src/components/icons/crown.tsx new file mode 100644 index 0000000000..0bf2fbb10e --- /dev/null +++ b/apps/renderer/src/components/icons/crown.tsx @@ -0,0 +1,17 @@ +import type { SVGProps } from "react" +import React from "react" + +export function IconoirBrightCrown(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/renderer/src/components/icons/users.tsx b/apps/renderer/src/components/icons/users.tsx new file mode 100644 index 0000000000..e36c6d9aa5 --- /dev/null +++ b/apps/renderer/src/components/icons/users.tsx @@ -0,0 +1,19 @@ +import type { SVGProps } from "react" +import React from "react" + +export function PhUsersBold(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/renderer/src/components/ui/modal/stacked/custom-modal.tsx b/apps/renderer/src/components/ui/modal/stacked/custom-modal.tsx index c1ab28d61d..5dc2cb0db6 100644 --- a/apps/renderer/src/components/ui/modal/stacked/custom-modal.tsx +++ b/apps/renderer/src/components/ui/modal/stacked/custom-modal.tsx @@ -1,8 +1,8 @@ -import type { PropsWithChildren } from "react" -import { useState } from "react" +import { m, useAnimationControls } from "framer-motion" +import type { FC, PropsWithChildren } from "react" +import { useEffect, useState } from "react" -import { m } from "~/components/common/Motion" -import { stopPropagation } from "~/lib/dom" +import { nextFrame, stopPropagation } from "~/lib/dom" import { cn } from "~/lib/utils" import { ModalClose } from "./components" @@ -61,3 +61,52 @@ SlideUpModal.class = (className: string) => { ) } + +const modalVariant = { + enter: { + x: 0, + opacity: 1, + }, + initial: { + x: 700, + opacity: 0.9, + }, + exit: { + x: 750, + opacity: 0, + }, +} +export const DrawerModalLayout: FC = ({ children }) => { + const { dismiss } = useCurrentModal() + const controller = useAnimationControls() + useEffect(() => { + nextFrame(() => controller.start("enter")) + }, [controller]) + + return ( +
+ + {children} + +
+ ) +} diff --git a/apps/renderer/src/hooks/biz/useFollow.tsx b/apps/renderer/src/hooks/biz/useFollow.tsx new file mode 100644 index 0000000000..5606182b5a --- /dev/null +++ b/apps/renderer/src/hooks/biz/useFollow.tsx @@ -0,0 +1,27 @@ +import { t } from "i18next" +import { useCallback } from "react" + +import { useModalStack } from "~/components/ui/modal/stacked" +import { FeedForm } from "~/modules/discover/feed-form" +import { ListForm } from "~/modules/discover/list-form" + +export const useFollow = () => { + const { present } = useModalStack() + + return useCallback( + (options?: { isList: boolean; id?: string; url?: string }) => { + present({ + title: options?.isList + ? t("sidebar.feed_actions.edit_list") + : t("sidebar.feed_actions.edit_feed"), + content: ({ dismiss }) => + options?.isList ? ( + + ) : ( + + ), + }) + }, + [present], + ) +} diff --git a/apps/renderer/src/models/index.ts b/apps/renderer/src/models/index.ts index 51f739d012..9a907fee85 100644 --- a/apps/renderer/src/models/index.ts +++ b/apps/renderer/src/models/index.ts @@ -1 +1,41 @@ +import type { User } from "@auth/core/types" + +import type { FeedModel, ListModelPoplutedFeeds } from "./types" + export * from "./types" + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace Models { + export interface TrendingList { + id: string + title: string + description: string + image: string + view: number + fee: number + timelineUpdatedAt: string + ownerUserId: string + subscriberCount: number + } + + export interface TrendingAggregates { + trendingFeeds: FeedModel[] + trendingLists: ListModelPoplutedFeeds[] + trendingEntries: TrendingEntry[] + trendingUsers: User[] + } + + export interface TrendingEntry { + id: string + feedId: string + title: string + url: string + content: string + description: string + guid: string + author: string + insertedAt: string + publishedAt: string + readCount: number + } +} diff --git a/apps/renderer/src/modules/discover/recommendations-card.tsx b/apps/renderer/src/modules/discover/recommendations-card.tsx index e9166ac168..d6e9721e32 100644 --- a/apps/renderer/src/modules/discover/recommendations-card.tsx +++ b/apps/renderer/src/modules/discover/recommendations-card.tsx @@ -27,7 +27,7 @@ export const RecommendationCard: FC = memo(({ data, rou {Object.keys(data.routes).map((route) => (
  • { ;(e.target as HTMLElement).querySelector("button")?.click() }} @@ -35,7 +35,7 @@ export const RecommendationCard: FC = memo(({ data, rou > +
  • + ))} + + + ) +} + +const UserCount = ({ count }: { count: number }) => { + return ( + + + {count} + + ) +} + +interface TopUserAvatarProps { + user: User + position: string +} + +const TopUserAvatar: React.FC = ({ user, position }) => ( +
    +
    + + + {user.name?.slice(0, 2)} + + + {user.name && ( + + {user.name} + + )} +
    +) + +const TrendingUsers: FC<{ data: User[] }> = ({ data }) => { + const profile = usePresentUserProfileModal("dialog") + const { t } = useTranslation() + return ( +
    +

    {t("trending.user")}

    +
    +
    + +
    + + {data.slice(0, 3).map((user, index: number) => ( + + ))} +
    + + {data.length > 3 && ( +
      + {data.slice(3).map((user) => ( +
    • + +
    • + ))} +
    + )} +
    + ) +} + +const TrendingFeeds = ({ data }: { data: FeedModel[] }) => { + const follow = useFollow() + const { t } = useTranslation() + return ( +
    +

    {t("trending.feed")}

    + +
      + {data.map((feed) => { + return ( +
    • + +
      + +
      +
      +
      {feed.title}
      +
      +
      + +
      + + + +
      +
    • + ) + })} +
    +
    + ) +} + +const TrendingEntries = ({ data }: { data: Models.TrendingEntry[] }) => { + const filteredData = data.filter((entry) => !entry.url.startsWith("https://x.com")) + return ( +
    +

    Trending Entries

    + +
      + {filteredData.map((entry) => { + return ( +
    • + + {entry.title} + + + + {entry.readCount} + +
    • + ) + })} +
    +
    + ) +} diff --git a/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx b/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx index 1b65112527..e172348c25 100644 --- a/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx +++ b/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx @@ -9,6 +9,7 @@ import { DiscoverInboxList } from "~/modules/discover/inbox-list-form" import { Recommendations } from "~/modules/discover/recommendations" import { DiscoverRSS3 } from "~/modules/discover/rss3-form" import { DiscoverUser } from "~/modules/discover/user-form" +import { Trend } from "~/modules/trending" import { useSubViewTitle } from "../hooks" @@ -64,12 +65,14 @@ export function Component() { }) }} > - + {tabs.map((tab) => ( {t(tab.name)} ))} + + {tabs.map((tab) => ( diff --git a/apps/renderer/src/providers/extension-expose-provider.tsx b/apps/renderer/src/providers/extension-expose-provider.tsx index b920c8cc4c..129c6dad31 100644 --- a/apps/renderer/src/providers/extension-expose-provider.tsx +++ b/apps/renderer/src/providers/extension-expose-provider.tsx @@ -7,8 +7,7 @@ import { toast } from "sonner" import { getGeneralSettings } from "~/atoms/settings/general" import { getUISettings } from "~/atoms/settings/ui" import { useModalStack } from "~/components/ui/modal" -import { FeedForm } from "~/modules/discover/feed-form" -import { ListForm } from "~/modules/discover/list-form" +import { useFollow } from "~/hooks/biz/useFollow" import { usePresentUserProfileModal } from "~/modules/profile/hooks" import { useSettingModal } from "~/modules/settings/modal/hooks" @@ -39,27 +38,16 @@ export const ExtensionExposeProvider = () => { const { t } = useTranslation() + const follow = useFollow() const presentUserProfile = usePresentUserProfileModal("dialog") useEffect(() => { registerGlobalContext({ - follow(options) { - present({ - title: options?.isList - ? t("sidebar.feed_actions.edit_list") - : t("sidebar.feed_actions.edit_feed"), - content: ({ dismiss }) => - options?.isList ? ( - - ) : ( - - ), - }) - }, + follow, profile(id, variant) { presentUserProfile(id, variant) }, }) - }, [present, presentUserProfile, t]) + }, [follow, present, presentUserProfile, t]) return null } diff --git a/icons/mgc/trending_up_cute_re.svg b/icons/mgc/trending_up_cute_re.svg new file mode 100644 index 0000000000..0885468fec --- /dev/null +++ b/icons/mgc/trending_up_cute_re.svg @@ -0,0 +1,4 @@ + + + + diff --git a/locales/app/en.json b/locales/app/en.json index 13be4a464b..606afbd7ab 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -247,6 +247,10 @@ "tip_modal.tip_support": "⭐ Tip to show your support!", "tip_modal.tip_title": "Tip Power", "tip_modal.unclaimed_feed": "No one has claimed this feed yet. The received Power will be securely held in the blockchain contract until it is claimed.", + "trending.entry": "Trending Entries", + "trending.feed": "Trending Feeds", + "trending.list": "Trending Lists", + "trending.user": "Trending Users", "user_button.account": "Account", "user_button.achievement": "Achievements", "user_button.download_desktop_app": "Download Desktop app", @@ -259,7 +263,6 @@ "user_profile.loading": "Loading", "user_profile.share": "Share", "user_profile.toggle_item_style": "Toggle Item Style", - "uu": "Mark as readdddd?", "words.achievement": "Achievements", "words.add": "Add", "words.browser": "Browser", @@ -281,6 +284,7 @@ "words.rsshub": "RSSHub", "words.search": "Search", "words.starred": "Starred", + "words.trending": "Trending", "words.undo": "Undo", "words.unread": "Unread", "words.user": "User", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08fa4fe231..801e1ce491 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -426,6 +426,9 @@ importers: '@yornaath/batshit': specifier: 0.10.1 version: 0.10.1 + camelcase-keys: + specifier: 9.1.3 + version: 9.1.3 class-variance-authority: specifier: 0.7.0 version: 0.7.0 @@ -4133,6 +4136,14 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase-keys@9.1.3: + resolution: {integrity: sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==} + engines: {node: '>=16'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} @@ -6303,6 +6314,10 @@ packages: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} engines: {node: '>=6'} + map-obj@5.0.0: + resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} @@ -7393,6 +7408,10 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} + quick-lru@6.1.2: + resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} + engines: {node: '>=12'} + raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} @@ -13153,6 +13172,15 @@ snapshots: camelcase-css@2.0.1: {} + camelcase-keys@9.1.3: + dependencies: + camelcase: 8.0.0 + map-obj: 5.0.0 + quick-lru: 6.1.2 + type-fest: 4.26.1 + + camelcase@8.0.0: {} + caniuse-api@3.0.0: dependencies: browserslist: 4.23.3 @@ -15736,6 +15764,8 @@ snapshots: dependencies: p-defer: 1.0.0 + map-obj@5.0.0: {} + markdown-table@3.0.3: {} masonic@4.0.1(react@18.3.1): @@ -17019,6 +17049,8 @@ snapshots: quick-lru@5.1.1: {} + quick-lru@6.1.2: {} + raf-schd@4.0.3: {} random-path@0.1.2: