diff --git a/.github/workflows/u3-firebase-hosting-merge.yml b/.github/workflows/u3-firebase-hosting-merge.yml index 2c86fed8..1d835ee6 100644 --- a/.github/workflows/u3-firebase-hosting-merge.yml +++ b/.github/workflows/u3-firebase-hosting-merge.yml @@ -7,6 +7,7 @@ name: Deploy u3 to Firebase Hosting on merge branches: - u3 - u3-dev + - u3-v2 - u3-pwa jobs: prod_build_and_deploy: @@ -123,7 +124,7 @@ jobs: env: CI: false REACT_APP_NAME: "U3 DEV" - NODE_OPTIONS: "--max_old_space_size=4096" + NODE_OPTIONS: "--max_old_space_size=8192" REACT_APP_API_BASE_URL: "${{ vars.REACT_APP_API_BASE_URL }}" REACT_APP_S3_API_BASE_URL: "${{ vars.REACT_APP_S3_API_BASE_URL }}" REACT_APP_US3R_UPLOAD_IMAGE_ENDPOINT: "${{ vars.REACT_APP_US3R_UPLOAD_IMAGE_ENDPOINT }}" @@ -153,3 +154,51 @@ jobs: projectId: us3r-network target: u3-pwa entryPoint: "./apps/u3/" + v2_build_and_deploy: + if: github.ref == 'refs/heads/u3-v2' + runs-on: ubuntu-latest + environment: + name: development + url: https://v2.u3.xyz + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: "20" + - run: | + cd apps/u3 + yarn install --ignore-engines + yarn build + env: + CI: false + REACT_APP_NAME: "U3 V2 DEV" + NODE_OPTIONS: "--max_old_space_size=8192" + REACT_APP_API_BASE_URL: "${{ vars.REACT_APP_API_BASE_URL }}" + REACT_APP_S3_API_BASE_URL: "${{ vars.REACT_APP_S3_API_BASE_URL }}" + REACT_APP_US3R_UPLOAD_IMAGE_ENDPOINT: "${{ vars.REACT_APP_US3R_UPLOAD_IMAGE_ENDPOINT }}" + REACT_APP_CERAMIC_HOST: "${{ vars.REACT_APP_CERAMIC_HOST }}" + REACT_APP_CHROME_EXTENSION_URL: "${{ vars.REACT_APP_CHROME_EXTENSION_URL }}" + REACT_APP_API_SOCIAL_URL: "${{ vars.REACT_APP_API_SOCIAL_URL }}" + REACT_APP_XMTP_ENV: "${{ vars.REACT_APP_XMTP_ENV }}" + REACT_APP_LENS_ENV: "${{ vars.REACT_APP_LENS_ENV }}" + REACT_APP_FARCASTER_HUB_URL: "${{ vars.REACT_APP_FARCASTER_HUB_URL }}" + REACT_APP_FARCASTER_NETWORK: "${{ vars.REACT_APP_FARCASTER_NETWORK }}" + REACT_APP_NFT_STORAGE_API_KEY: "${{ vars.REACT_APP_NFT_STORAGE_API_KEY }}" + REACT_APP_DAPP_NFT_TO_MINT: "${{ vars.REACT_APP_DAPP_NFT_TO_MINT }}" + REACT_APP_DAPP_NFT_FIXED_PRICE_STRATEGY: "${{ vars.REACT_APP_DAPP_NFT_FIXED_PRICE_STRATEGY }}" + REACT_APP_DAPP_NFT_CHAIN_ID: "${{ vars.REACT_APP_DAPP_NFT_CHAIN_ID }}" + REACT_APP_DAPP_NFT_RECIPIENT_ADDRESS: "${{ vars.REACT_APP_DAPP_NFT_RECIPIENT_ADDRESS }}" + REACT_APP_CASTER_NFT_TO_MINT: "${{ vars.REACT_APP_CASTER_NFT_TO_MINT }}" + REACT_APP_CASTER_NFT_FIXED_PRICE_STRATEGY: "${{ vars.REACT_APP_CASTER_NFT_FIXED_PRICE_STRATEGY }}" + REACT_APP_CASTER_NFT_CHAIN_ID: "${{ vars.REACT_APP_CASTER_NFT_CHAIN_ID }}" + REACT_APP_CASTER_NFT_RECIPIENT_ADDRESS: "${{ vars.REACT_APP_CASTER_NFT_RECIPIENT_ADDRESS }}" + REACT_APP_VAPID_PUBLIC_KEY: "${{ vars.REACT_APP_VAPID_PUBLIC_KEY }}" + REACT_APP_RED_ENVELOPE_PLEDGE_ADDRESS: "${{ vars.REACT_APP_RED_ENVELOPE_PLEDGE_ADDRESS }}" + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_US3R_NETWORK }}" + channelId: live + projectId: us3r-network + target: u3-v2 + entryPoint: "./apps/u3/" diff --git a/apps/u3/.firebaserc b/apps/u3/.firebaserc index 5e26d933..4e343b0d 100644 --- a/apps/u3/.firebaserc +++ b/apps/u3/.firebaserc @@ -13,6 +13,9 @@ ], "u3-pwa": [ "us3r-u3-pwa" + ], + "u3-v2": [ + "us3r-u3-v2" ] } } diff --git a/apps/u3/.vscode/settings.json b/apps/u3/.vscode/settings.json index 6fdb18f0..c7742ce1 100644 --- a/apps/u3/.vscode/settings.json +++ b/apps/u3/.vscode/settings.json @@ -22,6 +22,7 @@ "unpinup", "upsert", "Upvote", + "videojs", "viem", "Warpcast", "Whatsnew", diff --git a/apps/u3/README.md b/apps/u3/README.md index f8459628..3446525c 100644 --- a/apps/u3/README.md +++ b/apps/u3/README.md @@ -1,3 +1,5 @@ # U3 use --ignore-engines when install deps ```yarn install --ignore-engines``` +this project use more mem than node default +```export NODE_OPTIONS=--max_old_space_size=8192``` diff --git a/apps/u3/firebase.json b/apps/u3/firebase.json index 8ed4d836..86707f7b 100644 --- a/apps/u3/firebase.json +++ b/apps/u3/firebase.json @@ -44,6 +44,21 @@ "destination": "/index.html" } ] + }, + { + "target": "u3-v2", + "public": "build", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] } ] } diff --git a/apps/u3/package.json b/apps/u3/package.json index 88cc561e..8471fe0d 100644 --- a/apps/u3/package.json +++ b/apps/u3/package.json @@ -22,11 +22,14 @@ "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", @@ -57,7 +60,7 @@ "dompurify": "^2.3.10", "ethers": "^5.7.2", "formik": "^2.2.9", - "frames.js": "^0.7.1", + "frames.js": "^0.8", "html2canvas-strengthen": "0.0.1", "http-proxy-middleware": "^2.0.6", "immer": "^10.0.3", @@ -110,6 +113,8 @@ "tslib": "^2.3.0", "typescript": "^5.3.3", "validator": "^13.11.0", + "vaul": "^0.9.0", + "video.js": "^8.10.0", "viem": "~2.7.11", "wagmi": "^2.5.7", "web-vitals": "^2.1.4", diff --git a/apps/u3/src/App.tsx b/apps/u3/src/App.tsx index e9d66996..84306f5f 100644 --- a/apps/u3/src/App.tsx +++ b/apps/u3/src/App.tsx @@ -29,11 +29,11 @@ import { injectStore, injectU3Token } from './services/shared/api/request'; import U3LoginProvider from './contexts/U3LoginContext'; import { XmtpClientProvider } from './contexts/message/XmtpClientCtx'; import { AppLensProvider } from './contexts/social/AppLensCtx'; -import { NavProvider } from './contexts/NavCtx'; import FarcasterProvider from './contexts/social/FarcasterCtx'; import LensGlobalModals from './components/social/lens/LensGlobalModals'; import { GlobalModalsProvider } from './contexts/shared/GlobalModalsCtx'; import GlobalModals from './components/shared/modal/GlobalModals'; +import { ProfileInfoProvider } from './contexts/profile/ProfileInfoCtx'; init(AIRSTACK_API_KEY); dayjs.extend(relativeTime); @@ -47,6 +47,10 @@ function App() { @@ -61,13 +65,13 @@ function App() { - - + + - - + + diff --git a/apps/u3/src/components/common/button/ColorButton.tsx b/apps/u3/src/components/common/button/ColorButton.tsx index 86d75d68..48ed7018 100644 --- a/apps/u3/src/components/common/button/ColorButton.tsx +++ b/apps/u3/src/components/common/button/ColorButton.tsx @@ -9,7 +9,9 @@ export default function ColorButton({ return ( + ); +} diff --git a/apps/u3/src/components/community/SidebarCommunityItem.tsx b/apps/u3/src/components/community/SidebarCommunityItem.tsx new file mode 100644 index 00000000..000152cd --- /dev/null +++ b/apps/u3/src/components/community/SidebarCommunityItem.tsx @@ -0,0 +1,56 @@ +import { useNavigate } from 'react-router-dom'; +import { CommunityInfo } from '@/services/community/types/community'; +import { getCommunityPath } from '@/route/path'; +import { cn } from '@/lib/utils'; +import { + HoverCard, + HoverCardArrow, + HoverCardContent, + HoverCardTrigger, +} from '@/components/ui/hover-card'; +import CommunityInfoAndAction from './CommunityInfoAndAction'; + +export default function SidebarCommunityItem({ + communityInfo, + active, +}: { + communityInfo: CommunityInfo; + active?: boolean; +}) { + const navigate = useNavigate(); + const url = getCommunityPath(communityInfo.channelId); + return ( + + + { + e.preventDefault(); + navigate(url); + }} + className="w-full flex justify-center items-center cursor-pointer relative" + > +
+ {communityInfo.name} + + + + + + + + ); +} diff --git a/apps/u3/src/components/dapp/launcher/DappMenu.tsx b/apps/u3/src/components/dapp/launcher/DappMenu.tsx index 6879ca73..28928d36 100644 --- a/apps/u3/src/components/dapp/launcher/DappMenu.tsx +++ b/apps/u3/src/components/dapp/launcher/DappMenu.tsx @@ -6,6 +6,7 @@ import DappWebsiteModal from './DappWebsiteModal'; import { ReactComponent as PlusSquareSvg } from '../../common/assets/svgs/plus-square.svg'; import DappInstallList from './DappInstallList'; import ExploreDappsNavBtn from './ExploreDappsNavBtn'; +import { cn } from '@/lib/utils'; export default function DappMenu() { const navigate = useNavigate(); @@ -13,8 +14,12 @@ export default function DappMenu() { const [isOpen, setIsOpen] = useState(false); const dappInstallListRef = useRef(null); return ( - setIsOpen(true)} onMouseLeave={() => { setIsOpen(false); @@ -46,26 +51,9 @@ export default function DappMenu() { )} - +
); } -const Wrapper = styled.div<{ isOpen: boolean }>` - background: #1b1e23; - width: ${({ isOpen }) => (isOpen ? '60px' : '30px')}; - height: 100vh; - padding: 20px 0px; - position: fixed; - top: 0; - right: 0; - z-index: 1; - border-left: 1px solid #39424c; - box-sizing: border-box; - overflow-x: hidden; - display: flex; - justify-content: center; - gap: 20; - transition: all 0.3s ease-out; -`; const OpenIcon = styled.div` width: 30px; diff --git a/apps/u3/src/components/explore/ExploreLayout.tsx b/apps/u3/src/components/explore/ExploreLayout.tsx deleted file mode 100644 index 491e05c9..00000000 --- a/apps/u3/src/components/explore/ExploreLayout.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import styled from 'styled-components'; -import { isMobile } from 'react-device-detect'; -import { MainWrapper } from '../layout/Index'; -import HotPosts from './posts/HotPosts'; -import TopLinks from './links/TopLinks'; -import HighScoreDapps from './dapps/HighScoreDapps'; -import type { ExploreState } from '../../container/Explore'; -import PosterMenu from '../poster/PosterMenu'; -import TopChannels from './channels/TopChannels'; - -export default function ExploreLayout({ - hotPosts, - topLinks, - topChannels, - highScoreDapps, -}: ExploreState) { - return ( - - {!isMobile && ( - - )} -
- - - - - - -
- -
- -
-
- ); -} - -const Wrapper = styled(MainWrapper)` - padding-bottom: 80px; - min-height: 100vh; - height: auto; - display: flex; - flex-direction: column; - gap: 40px; - ${isMobile && - ` - gap: 20px; - margin-bottom: 40px; - `} -`; -const Main = styled.div` - display: flex; - gap: 20px; - ${isMobile && - ` - flex-direction: column; - `} -`; -const MainLeft = styled.div` - width: 0; - flex: 3; - ${isMobile && - ` - width: 100%; - felx: none; - `} -`; -const MainRight = styled.div` - width: 0; - flex: 1; - ${isMobile && - ` - width: 100%; - felx: none; - `} -`; -const Footer = styled.div` - ${isMobile && - ` - width: 100%; - overflow-x: auto; - `} -`; diff --git a/apps/u3/src/components/explore/HomeLayout.tsx b/apps/u3/src/components/explore/HomeLayout.tsx new file mode 100644 index 00000000..12bb984d --- /dev/null +++ b/apps/u3/src/components/explore/HomeLayout.tsx @@ -0,0 +1,44 @@ +import { useNavigate } from 'react-router-dom'; +import { MainWrapper } from '../layout/Index'; +import HotPosts from './posts/HotPosts'; +import type { ExploreHomeState } from '../../container/explore/Home'; +import ColorButton from '../common/button/ColorButton'; +import HotCommunities from './community/HotCommunities'; + +export default function HomeLayout({ + hotPosts, + hotCommunities, +}: ExploreHomeState) { + const navigate = useNavigate(); + return ( + + + +
+ { + navigate('/caster-daily'); + }} + > + Mint Poster + + { + navigate('/poster-gallery'); + }} + > + Poster Gallery + +
+
+ ); +} diff --git a/apps/u3/src/components/explore/channels/TopChannels.tsx b/apps/u3/src/components/explore/channels/TopChannels.tsx index 1033c978..194d7e21 100644 --- a/apps/u3/src/components/explore/channels/TopChannels.tsx +++ b/apps/u3/src/components/explore/channels/TopChannels.tsx @@ -18,9 +18,9 @@ export default function TopChannels({ return (
{ - navigate(`/social/trends`); + navigate(`/communities`); }} /> <div className={cn('w-full mt-[20px]', 'max-sm:mt-[10px]')}> diff --git a/apps/u3/src/components/explore/community/CommunityItem.tsx b/apps/u3/src/components/explore/community/CommunityItem.tsx new file mode 100644 index 00000000..97d5f385 --- /dev/null +++ b/apps/u3/src/components/explore/community/CommunityItem.tsx @@ -0,0 +1,65 @@ +import { ComponentPropsWithRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { CommunityInfo } from '@/services/community/types/community'; +import { cn } from '@/lib/utils'; +import JoinCommunityBtn from '@/components/community/JoinCommunityBtn'; + +export default function CommunityItem({ + className, + communityInfo, + ...props +}: ComponentPropsWithRef<'div'> & { + communityInfo: CommunityInfo; +}) { + const navigate = useNavigate(); + const { logo, name, description, memberInfo, types } = communityInfo || {}; + return ( + <div + className={cn( + 'h-[160px] flex p-[20px] box-border items-center gap-[20px] bg-[#1B1E23] rounded-[20px] cursor-pointer', + 'max-sm:p-[10px] max-sm:h-[120px]', + className + )} + onClick={() => { + navigate(`/community/${communityInfo.channelId}`); + }} + {...props} + > + <img + src={logo} + alt="" + className="w-[120px] h-[120px] rounded-[20px] max-sm:w-[100px] max-sm:h-[100px]" + /> + <div className="flex-1 flex h-full flex-col justify-between items-start"> + <div className="w-full flex justify-between items-start gap-[10px]"> + <div className="flex-1 flex flex-col gap-[10px]"> + {types?.length > 0 && ( + <div className="text-[#718096] text-[12px] font-normal line-clamp-1"> + {types.reduce((acc, cur) => { + return `${acc}, ${cur}`; + })} + </div> + )} + <div className="text-[#FFF] text-[16px] font-medium">{name}</div> + </div> + <JoinCommunityBtn communityInfo={communityInfo} /> + </div> + <div className="flex gap-[10px] items-center"> + {memberInfo?.newPostNumber > 0 && ( + <div className="text-[#718096] text-[12px] font-normal leading-[15px]"> + {memberInfo?.newPostNumber} new posts + </div> + )} + {memberInfo?.totalNumber > 0 && ( + <div className="text-[#718096] text-[12px] font-normal leading-[15px]"> + {memberInfo?.totalNumber} members + </div> + )} + </div> + <div className="text-[#FFF] text-[14px] font-normal leading-[20px] line-clamp-2 max-sm:line-clamp-1"> + {description} + </div> + </div> + </div> + ); +} diff --git a/apps/u3/src/components/explore/community/HotCommunities.tsx b/apps/u3/src/components/explore/community/HotCommunities.tsx new file mode 100644 index 00000000..61ad002b --- /dev/null +++ b/apps/u3/src/components/explore/community/HotCommunities.tsx @@ -0,0 +1,51 @@ +import { useNavigate } from 'react-router-dom'; +import Title from '../Title'; +import Loading from '../../common/loading/Loading'; +import { cn } from '@/lib/utils'; +import { CommunityInfo } from '@/services/community/types/community'; +import CommunityItem from './CommunityItem'; + +export type HotCommunitiesData = Array<CommunityInfo>; + +export default function HotCommunities({ + communities, + isLoading, +}: { + communities: HotCommunitiesData; + isLoading: boolean; +}) { + const navigate = useNavigate(); + return ( + <div className="w-full"> + <Title + text="Hot Communities" + viewAllAction={() => { + navigate(`/communities`); + }} + /> + <div className={cn('w-full mt-[20px]', 'max-sm:mt-[10px]')}> + {isLoading ? ( + <div + className={cn( + 'w-full h-full flex justify-center items-center', + 'max-sm:h-[430px]' + )} + > + <Loading /> + </div> + ) : ( + <div + className={cn( + 'w-full grid gap-[20px] grid-cols-3', + 'max-sm:grid-cols-1 max-sm:gap-[10px]' + )} + > + {communities.map((item) => { + return <CommunityItem key={item.id} communityInfo={item} />; + })} + </div> + )} + </div> + </div> + ); +} diff --git a/apps/u3/src/components/explore/posts/HotPosts.tsx b/apps/u3/src/components/explore/posts/HotPosts.tsx index e440d892..e41107bc 100644 --- a/apps/u3/src/components/explore/posts/HotPosts.tsx +++ b/apps/u3/src/components/explore/posts/HotPosts.tsx @@ -1,11 +1,10 @@ -import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; -import { isMobile } from 'react-device-detect'; import CardBase from '../../common/card/CardBase'; import Title from '../Title'; import { SocialPlatform } from '../../../services/social/types'; import FarcasterPostCard from './FarcasterPostCard'; import Loading from '../../common/loading/Loading'; +import { cn } from '@/lib/utils'; export type HotPostsData = Array<{ data: any; platform: SocialPlatform }>; export default function HotPosts({ @@ -19,20 +18,30 @@ export default function HotPosts({ }) { const navigate = useNavigate(); return ( - <Wrapper> + <div className="w-full"> <Title - text="🔥 Hot Posts" + text="Hot Posts" viewAllAction={() => { navigate(`/social`); }} /> - <CardsWrapper> + <CardBase + className={cn( + 'w-full h-[534px] mt-[20px]', + 'max-sm:bg-transparent max-sm:h-auto max-sm:p-0 max-sm:border-none max-sm:mt-[10px] max-sm:bg-none max-sm:overflow-visible' + )} + > {isLoading ? ( - <LoadingWrapper> + <div className="w-full h-full flex justify-center items-center"> <Loading /> - </LoadingWrapper> + </div> ) : ( - <CardsLayout> + <div + className={cn( + 'w-full h-full grid gap-[20px] auto-cols-auto auto-rows-auto', + 'max-sm:flex max-sm:flex-col max-sm:gap-[10px]' + )} + > {posts.map(({ platform, data }, idx) => { if (platform === SocialPlatform.Farcaster) { const id = Buffer.from(data.hash.data).toString('hex'); @@ -48,51 +57,9 @@ export default function HotPosts({ } return null; })} - </CardsLayout> + </div> )} - </CardsWrapper> - </Wrapper> + </CardBase> + </div> ); } -const Wrapper = styled.div` - width: 100%; -`; -const CardsWrapper = styled(CardBase)` - width: 100%; - height: 534px; - margin-top: 20px; - ${isMobile && - ` - height: auto; - padding: 0; - border: none; - margin-top: 10px; - background: none; - overflow: visible; - `} -`; -const LoadingWrapper = styled.div` - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - ${isMobile && - ` - height: 430px; - `} -`; -const CardsLayout = styled.div` - width: 100%; - height: 100%; - display: grid; - grid-gap: 20px; - grid-auto-columns: auto; - grid-auto-rows: auto; - ${isMobile && - ` - display: flex; - flex-direction: column; - gap: 10px; - `} -`; diff --git a/apps/u3/src/components/info/AddWalletModal.tsx b/apps/u3/src/components/info/AddWalletModal.tsx deleted file mode 100644 index 4eaa3c4c..00000000 --- a/apps/u3/src/components/info/AddWalletModal.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useState } from 'react'; -import Modal from 'react-modal'; -import styled from 'styled-components'; -import { Close } from '../common/icons/close'; - -export default function AddWalletModal({ - show, - closeModal, - confirmAction, -}: { - show: boolean; - closeModal: () => void; - confirmAction: (addr: string) => Promise<boolean>; -}) { - const [text, setText] = useState(''); - if (!show) return null; - return ( - <Modal - isOpen={show} - style={{ - overlay: { - backgroundColor: 'rgba(0,0,0,0.3)', - zIndex: 200, - backdropFilter: 'blur(12px)', - }, - content: { - display: 'flex', - alignItems: 'center', - margin: '0 auto', - background: 'none', - border: 'none', - }, - }} - > - <ContentBox> - <div className="title"> - <h2>Add New Wallet</h2> - <span onClick={closeModal}> - <Close /> - </span> - </div> - <div className="text"> - <input - title="text" - type="text" - placeholder="Wallet address" - value={text} - onChange={(e) => { - setText(e.target.value); - }} - /> - </div> - <div className="btns"> - <button - className="confirm" - type="button" - onClick={async () => { - const t = text.trim(); - if (!t) return; - const r = await confirmAction(t); - if (r) { - setText(''); - } - }} - > - Add Wallet - </button> - </div> - </ContentBox> - </Modal> - ); -} - -const ContentBox = styled.div` - margin: 0 auto; - text-align: start; - display: flex; - flex-direction: column; - gap: 20px; - width: 380px; - - background: #1b1e23; - border-radius: 20px; - padding: 20px; - box-sizing: border-box; - - & .title { - display: flex; - justify-content: space-between; - align-items: center; - - & span { - cursor: pointer; - } - } - - & h2 { - margin: 0; - font-weight: 700; - font-size: 24px; - line-height: 28px; - font-style: italic; - color: #ffffff; - } - - & p { - font-weight: 400; - font-size: 16px; - line-height: 24px; - margin: 0; - color: #ffffff; - } - - & .text { - & input { - outline: none; - background: inherit; - height: 48px; - padding: 12px 16px; - background: #1a1e23; - border: 1px solid #39424c; - border-radius: 12px; - box-sizing: border-box; - width: calc(100% - 2px); - font-weight: 400; - font-size: 16px; - line-height: 24px; - color: #fff; - &::placeholder { - color: #4e5a6e; - } - } - } - - & .btns { - display: flex; - justify-content: space-between; - & button { - cursor: pointer; - width: 100%; - height: 48px; - - background: #1a1e23; - border: 1px solid #39424c; - border-radius: 12px; - font-weight: 600; - font-size: 16px; - line-height: 24px; - - text-align: center; - - color: #718096; - } - - & button.confirm { - background: #ffffff; - color: #14171a; - } - } -`; diff --git a/apps/u3/src/components/info/DailyDigest.tsx b/apps/u3/src/components/info/DailyDigest.tsx deleted file mode 100644 index d973b3f7..00000000 --- a/apps/u3/src/components/info/DailyDigest.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-12-17 14:50:43 - * @LastEditors: shixuewen friendlysxw@163.com - * @LastEditTime: 2022-12-17 15:50:29 - * @Description: file description - */ -import React from 'react'; -import styled from 'styled-components'; -import { Email } from '../common/icons/email'; - -const DailyDigestBox = styled.div` - display: flex; - justify-content: space-between; - flex-direction: column; - box-sizing: border-box; - gap: 10px; - padding: 20px; - width: 360px; - min-width: 360px; - height: 170px; - - background: #1b1e23; - border-bottom: 1px solid #14171a; - border-radius: 20px; - h2 { - margin: 0; - font-weight: 700; - font-size: 24px; - line-height: 28px; - font-style: italic; - color: #ffffff; - } - - & .desc { - font-weight: 400; - font-size: 16px; - line-height: 19px; - - color: #718096; - } - - & button { - border: none; - width: 100%; - - isolation: isolate; - - height: 44px; - - background: #5ba85a; - border-radius: 12px; - font-weight: 500; - font-size: 14px; - line-height: 20px; - text-align: center; - - color: #ffffff; - - & svg { - vertical-align: middle; - margin-right: 10px; - } - } -`; - -export default function DailyDigest() { - return ( - <DailyDigestBox> - <h2>Daily Digest</h2> - <div className="desc"> - Daily recommends everything you are interested web3 - </div> - - <div> - {/* <button type="button"> - <Email /> - Email - </button> */} - </div> - </DailyDigestBox> - ); -} diff --git a/apps/u3/src/components/info/WalletList.tsx b/apps/u3/src/components/info/WalletList.tsx deleted file mode 100644 index fee434c7..00000000 --- a/apps/u3/src/components/info/WalletList.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; -import { toast } from 'react-toastify'; -import { sortPubKey } from '../../utils/shared/solana'; -import Badge from '../news/contents/Badge'; -import { Add } from '../common/icons/add'; -import { ChevronDown } from '../common/icons/chevron-down'; -import { Copy2 } from '../common/icons/copy'; -import { Trash } from '../common/icons/trash'; -import { Wallet } from '../common/icons/wallet'; -import { ProfileWallet } from '../../services/profile/types/profile'; -import { messages } from '../../utils/shared/message'; - -export default function WalletList({ - wallets, - currAddr, - addAction, - delAction, -}: { - wallets: ProfileWallet[]; - currAddr: string; - addAction: () => void; - delAction: (addr: string) => void; -}) { - const titleRef = useRef(); - const [showList, setShowList] = useState(false); - useEffect(() => { - const windowClick = (e: MouseEvent) => { - if ( - e.target === titleRef.current || - (e.target as HTMLElement).parentNode === titleRef.current - ) - return; - setShowList(false); - }; - window.addEventListener('click', windowClick); - return () => { - window.removeEventListener('click', windowClick); - }; - }, []); - return ( - <WalletBox> - <div - className="title" - ref={titleRef} - onClick={() => { - setShowList(!showList); - }} - > - <span> - <Wallet /> - </span> - <span>{wallets.length} Wallet</span> - <span className={showList ? 'show' : ''}> - <ChevronDown /> - </span> - </div> - {showList && ( - <div - className="list" - onClick={(e) => { - e.stopPropagation(); - }} - > - <div className="add"> - <h3>My Wallets</h3> - <span onClick={addAction}> - <Add /> - </span> - </div> - {wallets.map((item) => { - const { wallet } = item; - return ( - <div className="item" key={wallet}> - <div> - <span className="wallet-addr">{sortPubKey(wallet, 4)}</span> - {currAddr === wallet && <Badge text="Owner" />} - </div> - - <div> - <span - onClick={() => { - navigator.clipboard.writeText(wallet).then( - () => { - toast.success(messages.common.copy); - }, - (err) => { - console.error('Async: Could not copy text: ', err); - } - ); - }} - > - <Copy2 /> - </span> - {currAddr !== wallet && ( - <span - onClick={() => { - delAction(wallet); - }} - > - <Trash /> - </span> - )} - </div> - </div> - ); - })} - </div> - )} - </WalletBox> - ); -} - -const WalletBox = styled.div` - position: relative; - > div.title { - cursor: pointer; - display: flex; - align-items: center; - padding: 8px 20px 8px 16px; - justify-content: center; - box-sizing: border-box; - gap: 8px; - min-width: 150px; - height: 36px; - background: #1a1e23; - border: 1px solid #39424c; - border-radius: 100px; - - > span { - & svg { - vertical-align: middle; - } - &.show { - transform: rotate(180deg); - } - } - } - - > div.list { - position: absolute; - padding: 0px; - top: 45px; - position: absolute; - width: 260px; - - background: #1b1e23; - border: 1px solid #39424c; - border-radius: 10px; - - > div { - box-sizing: border-box; - padding: 20px; - height: 60px; - display: flex; - align-items: center; - justify-content: space-between; - } - > div.add { - border-bottom: 1px solid #39424c; - & span { - cursor: pointer; - } - } - - > div.item { - > div { - display: flex; - gap: 10px; - - > span { - cursor: pointer; - } - } - - & .wallet-addr { - font-weight: 400; - font-size: 16px; - line-height: 19px; - - color: #718096; - } - } - } -`; diff --git a/apps/u3/src/components/info/index.tsx b/apps/u3/src/components/info/index.tsx deleted file mode 100644 index a16a0bf0..00000000 --- a/apps/u3/src/components/info/index.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import styled from 'styled-components'; -import { toast } from 'react-toastify'; -import { useEffect, useState } from 'react'; -import { UserAvatar, UserName } from '@us3r-network/profile'; -import { sortPubKey } from '../../utils/shared/solana'; -import { Copy } from '../common/icons/copy'; - -import { Refresh } from '../common/icons/refresh'; -import { Edit } from '../common/icons/edit'; -import WalletList from './WalletList'; -import AddWalletModal from './AddWalletModal'; -import { ProfileWallet } from '../../services/profile/types/profile'; -import { defaultFormatDate } from '../../utils/shared/time'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; - -import { messages } from '../../utils/shared/message'; -import { - selectFrensHandlesState, - getFollower, - getFollowing, -} from '../../features/frens/frensHandles'; - -export default function Info({ - walletAddr, - date, - wallets, - addWallet, - delWallet, -}: { - walletAddr: string; - date: number; - wallets: ProfileWallet[]; - delWallet: (addr: string) => void; - addWallet: (addr: string) => Promise<boolean>; -}) { - const [showAddModal, setShowAddModal] = useState(false); - - const dispatch = useAppDispatch(); - const { following, follower } = useAppSelector(selectFrensHandlesState); - - useEffect(() => { - dispatch(getFollowing({ reset: true })); - dispatch(getFollower({ reset: true })); - }, []); - - return ( - <InfoBox> - <div className="user-info"> - <div className="img-edit"> - <div - onClick={() => { - console.log('TODO'); - }} - > - <Edit /> - </div> - <UserAvatar className="user-avatar" /> - </div> - - <div className="info"> - <div className="nickname"> - <div> - <span className="name"> - <UserName /> - </span> - </div> - <div className="wallet"> - <WalletList - currAddr={walletAddr} - wallets={[{ wallet: walletAddr, chain: 'eth' }, ...wallets]} - addAction={() => { - setShowAddModal(true); - }} - delAction={(addr) => { - if (addr === walletAddr) return; - delWallet(addr); - }} - /> - <span className="share"> - <Refresh /> - </span> - </div> - </div> - <div className="addr"> - <span>{sortPubKey(walletAddr || '', 10)}</span> - <span - className="copy" - onClick={() => { - navigator.clipboard.writeText(walletAddr).then( - () => { - toast.success(messages.common.copy); - }, - (err) => { - console.error('Async: Could not copy text: ', err); - } - ); - }} - > - <Copy /> - </span> - </div> - <div className="attach"> - <div> - <span> - <span className="num">{following?.total || 0}</span>Following - </span> - <span> - <span className="num">{follower?.total || 0}</span>Follower - </span> - <span>|</span> - <span>{defaultFormatDate(date || Date.now())}</span> - </div> - </div> - </div> - </div> - <AddWalletModal - show={showAddModal} - closeModal={() => { - setShowAddModal(false); - }} - confirmAction={async (addr) => { - const r = await addWallet(addr); - if (r) { - setShowAddModal(false); - } - return r; - }} - /> - </InfoBox> - ); -} - -const InfoBox = styled.div` - padding: 25px 25px 25px 20px; - box-sizing: border-box; - color: white; - width: 100%; - height: 170px; - - background: #1b1e23; - border-radius: 20px; - .user-info { - display: flex; - gap: 20px; - & img.user-avatar { - border-radius: 50%; - - width: 120px; - height: 120px; - } - & > div.img-edit { - position: relative; - &:hover { - > div { - display: flex; - } - } - > div { - cursor: pointer; - position: absolute; - display: none; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - border-radius: 50%; - background: linear-gradient( - 0deg, - rgba(0, 0, 0, 0.5), - rgba(0, 0, 0, 0.5) - ); - } - } - & > div.info { - flex-grow: 1; - display: flex; - flex-direction: column; - gap: 10px; - justify-content: space-between; - & .nickname { - display: flex; - justify-content: space-between; - & > div { - &:first-child { - display: flex; - gap: 10px; - align-items: center; - } - } - - & .name div { - font-size: 25px; - font-weight: 700; - font-style: italic; - font-weight: 700; - font-size: 24px; - line-height: 28px; - - color: #ffffff; - } - } - - & .wallet { - display: flex; - gap: 20px; - & > div { - margin-top: -8px; - } - } - } - - div.addr { - display: flex; - gap: 5px; - color: #718096; - align-items: center; - font-weight: 400; - font-size: 16px; - line-height: 24px; - & .copy { - cursor: pointer; - } - } - - & .share { - cursor: pointer; - } - } - - .attach { - display: flex; - - justify-content: space-between; - align-items: center; - font-weight: 400; - font-size: 16px; - line-height: 24px; - /* identical to box height, or 150% */ - - /* #718096 */ - - color: #718096; - > div { - display: flex; - gap: 10px; - } - - & .num { - line-height: 19px; - color: #ffffff; - margin-right: 5px; - } - - & .twitter, - & .discord { - width: 40px; - height: 40px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: #14171a; - } - & .discord { - /* background: #14171a; */ - } - } -`; diff --git a/apps/u3/src/components/layout/ContactUsModal.tsx b/apps/u3/src/components/layout/ContactUsModal.tsx deleted file mode 100644 index dadd0933..00000000 --- a/apps/u3/src/components/layout/ContactUsModal.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import styled from 'styled-components'; -import { useNav } from '../../contexts/NavCtx'; -import feedbackIconUrl from '../common/assets/platform/pngs/feedback.png'; -import telegramIconUrl from '../common/assets/platform/pngs/telegram.png'; -import twitterIconUrl from '../common/assets/platform/pngs/twitter.png'; -import discordIconUrl from '../common/assets/platform/pngs/discord.png'; -import warpcastIconUrl from '../common/assets/platform/svgs/warpcast.svg'; -import { CONTACT_US_LINKS } from '../../constants'; - -const links = [ - { - link: CONTACT_US_LINKS.feedback, - iconUrl: feedbackIconUrl, - name: 'Feedback', - }, - { - link: CONTACT_US_LINKS.farcaster, - iconUrl: warpcastIconUrl, - name: 'Farcaster', - }, - { - link: CONTACT_US_LINKS.discord, - iconUrl: discordIconUrl, - name: 'Discord', - }, - { - link: CONTACT_US_LINKS.twitter, - iconUrl: twitterIconUrl, - name: 'Twitter', - }, - { - link: CONTACT_US_LINKS.telegram, - iconUrl: telegramIconUrl, - name: 'Telegram', - }, -]; -export default function ContactUsModal() { - const { openContactUsModal } = useNav(); - return ( - <Wrapper open={openContactUsModal}> - <Body> - {links.map((link) => ( - <Link - href={link.link} - key={link.link} - target="_blank" - rel="noreferrer" - > - <Icon src={link.iconUrl} /> - <Name>{link.name}</Name> - </Link> - ))} - </Body> - </Wrapper> - ); -} - -const Wrapper = styled.div<{ open: boolean }>` - z-index: 3; - position: absolute; - bottom: 20px; - right: 0; - transform: translateX(100%); - - display: ${({ open }) => (open ? 'block' : 'none')}; -`; -const Body = styled.div` - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 20px; - - padding: 20px; - border-radius: 10px; - border: 1px solid #39424c; - background: #1b1e23; - - margin-left: 10px; -`; -const Link = styled.a` - display: flex; - align-items: center; - gap: 10px; - text-decoration: none; -`; -const Icon = styled.img` - width: 16px; - height: 16px; -`; -const Name = styled.span` - color: #fff; - font-family: Rubik; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: normal; -`; diff --git a/apps/u3/src/components/layout/Index.tsx b/apps/u3/src/components/layout/Index.tsx index 24405c77..d8bed38f 100644 --- a/apps/u3/src/components/layout/Index.tsx +++ b/apps/u3/src/components/layout/Index.tsx @@ -8,59 +8,65 @@ import styled from 'styled-components'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.min.css'; -import { isMobile } from 'react-device-detect'; import { useLocation, useSearchParams } from 'react-router-dom'; -import { useAuthentication } from '@us3r-network/auth-with-rainbowkit'; +import { ComponentPropsWithRef, useEffect } from 'react'; import { MEDIA_BREAK_POINTS } from '../../constants/index'; import Main from './Main'; import { useGAPageView } from '../../hooks/shared/useGoogleAnalytics'; import Menu from './menu'; import DappMenu from '../dapp/launcher/DappMenu'; -import MobileHeader from './mobile/MobileHeader'; -import MobileNav from './mobile/MobileNav'; +import MobileMainNav from './mobile/MobileMainNav'; import { MobileGuide } from './mobile/MobileGuide'; -import AddPostMobile from '../social/AddPostMobile'; import ClaimOnboard from '../onboard/Claim'; import RedEnvelopeFloatingWindow from '../social/frames/red-envelope/RedEnvelopeFloatingWindow'; +import { cn } from '@/lib/utils'; +import { isCommunityPath } from '@/route/path'; +import useAllJoinedCommunities from '@/hooks/community/useAllJoinedCommunities'; +import useLogin from '@/hooks/shared/useLogin'; function Layout() { - const { ready } = useAuthentication(); - const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const claim = searchParams.get('claim'); + const { pathname } = useLocation(); + const isCommunity = isCommunityPath(pathname); useGAPageView(); + const { isLogin } = useLogin(); + const { loadAllJoinedCommunities, clearJoinedCommunities } = + useAllJoinedCommunities(); + useEffect(() => { + if (isLogin) { + loadAllJoinedCommunities(); + } else { + clearJoinedCommunities(); + } + }, [isLogin]); + return ( - <LayoutWrapper id="layout-wrapper"> - <> - {ready ? isMobile ? <MobileHeader /> : <Menu /> : null} - {ready && isMobile ? <MobileNav /> : null} - {isMobile ? ( - <MobileContentBox> - <Main /> - <div className="fixed right-[20px] bottom-[80px]"> - <AddPostMobile /> - </div> - </MobileContentBox> - ) : ( - <RightBox> - <RightInner id="layout-main-wrapper"> - {location.pathname.includes('social') ? ( - <Main /> - ) : ( - <MainBox className="main-box"> - <Main /> - </MainBox> - )} - </RightInner> - <DappMenu /> - <RedEnvelopeFloatingWindow /> - </RightBox> + <div + id="layout-wrapper" + className="w-screen h-screen bg-[#14171a] overflow-x-hidden overflow-y-auto flex" + > + <Menu + className={cn( + '', + !isCommunity ? 'max-sm:hidden' : ' max-sm:h-[calc(100vh-80px)]' )} - <MobileGuide /> - {claim === 'true' && <ClaimOnboard />} - </> + /> + <DappMenu /> + <Main + className={cn( + 'h-full w-[calc(100vw-60px-30px)] bg-[#20262F] box-border overflow-y-auto overflow-x-hidden', + 'max-sm:ml-0 max-sm:w-full max-sm:h-[calc(100vh-80px)]', + isCommunity ? 'max-sm:w-[calc(100vw-60px)]' : '' + )} + id="layout-main-wrapper" + /> + <MobileMainNav /> + <RedEnvelopeFloatingWindow /> + <MobileGuide /> + {claim === 'true' && <ClaimOnboard />} <ToastContainer position="top-right" autoClose={3000} @@ -73,32 +79,11 @@ function Layout() { pauseOnHover theme="dark" /> - </LayoutWrapper> + </div> ); } export default Layout; -const LayoutWrapper = styled.div` - width: 100vw; - height: 100vh; - background: #14171a; - overflow: hidden; - overflow-y: ${isMobile ? 'auto' : 'hidden'}; -`; -const RightBox = styled.div` - margin-left: 60px; - height: 100%; - // menu: 60px , dappSideBarList: 30px - width: calc(100% - 60px - 30px); - display: flex; -`; -const RightInner = styled.div` - height: 100%; - width: 0; - flex: 1; - box-sizing: border-box; - overflow-y: auto; - overflow-x: hidden; -`; + export const MainBox = styled.div` @media (max-width: ${MEDIA_BREAK_POINTS.xxxl}px) { width: 100%; @@ -110,30 +95,18 @@ export const MainBox = styled.div` margin: 0 auto; `; -export const MainWrapper = styled.div` - width: 100%; - height: 100vh; - padding: 24px; - box-sizing: border-box; - overflow-y: auto; - overflow-x: hidden; - @media (max-width: ${MEDIA_BREAK_POINTS.xl}px) { - width: ${MEDIA_BREAK_POINTS.xl}px; - } - ${isMobile && - ` - padding: 10px; - height: calc(100vh - 56px); - @media (max-width: ${MEDIA_BREAK_POINTS.xl}px) { - width: 100%; - } - `} -`; - -/** - * mobile styles - */ -const MobileContentBox = styled.div` - margin-top: 60px; - width: 100%; -`; +export function MainWrapper({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( + <div + className={cn( + 'w-full min-h-full h-auto p-[24px] box-border overflow-y-auto overflow-x-hidden flex flex-col gap-[40px]', + 'max-sm:gap-[20px] max-sm:p-[10px]', + className + )} + {...props} + /> + ); +} diff --git a/apps/u3/src/components/layout/ListRoutelLayout.tsx b/apps/u3/src/components/layout/ListRoutelLayout.tsx deleted file mode 100644 index c0e86bed..00000000 --- a/apps/u3/src/components/layout/ListRoutelLayout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-11-30 18:50:14 - * @LastEditors: shixuewen friendlysxw@163.com - * @LastEditTime: 2022-12-01 20:12:57 - * @Description: file description - */ -import { PropsWithChildren } from 'react'; -import { Outlet } from 'react-router-dom'; -import styled from 'styled-components'; - -function ListRouteLayout({ children }: PropsWithChildren) { - return ( - <ListRouteLayoutWrapper> - <ListBox>{children}</ListBox> - <RouteOutLetBox> - <Outlet /> - </RouteOutLetBox> - </ListRouteLayoutWrapper> - ); -} -export default ListRouteLayout; -const ListRouteLayoutWrapper = styled.div` - width: 100%; - height: 100%; - display: flex; - gap: 20px; -`; -const ListBox = styled.div` - width: 400px; -`; -const RouteOutLetBox = styled.div` - flex: 1; -`; diff --git a/apps/u3/src/components/layout/LoginButton.tsx b/apps/u3/src/components/layout/LoginButton.tsx deleted file mode 100644 index 3db65629..00000000 --- a/apps/u3/src/components/layout/LoginButton.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-12-12 14:36:31 - * @LastEditors: shixuewen friendlysxw@163.com - * @LastEditTime: 2023-02-08 16:44:26 - * @Description: file description - */ -// import { useEffect, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import { UserAvatar, UserName } from '@us3r-network/profile'; -import useLogin from '../../hooks/shared/useLogin'; -import { ButtonPrimaryLine } from '../common/button/ButtonBase'; -import LogoutSvg from '../common/assets/svgs/logout.svg'; - -type Props = { - onlyIcon?: boolean; - onLogout?: () => void; -}; -export default function LoginButton({ onlyIcon, onLogout }: Props) { - const { user, isLogin, login } = useLogin(); - const navigate = useNavigate(); - - return ( - <LoginButtonWrapper> - {isLogin ? ( - <LoginUser - onClick={() => { - navigate('/u'); - }} - onlyIcon={onlyIcon} - > - <UserAvatar /> - <UserName /> - </LoginUser> - ) : ( - <Button onClick={login} onlyIcon={onlyIcon}> - <NoLoginText className="wl-user-button_no-login-text"> - Login - </NoLoginText> - </Button> - )} - </LoginButtonWrapper> - ); -} - -export function LogoutButton({ - onlyIcon, - ...otherProps -}: StyledComponentPropsWithRef<'button'> & { - onlyIcon?: boolean; -}) { - return ( - <Button onlyIcon={onlyIcon} {...otherProps}> - {!onlyIcon && `Logout`} - <LogoutIconButton src={LogoutSvg} /> - </Button> - ); -} - -const LoginButtonWrapper = styled.div` - width: 100%; - box-sizing: border-box; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; -`; -const Button = styled(ButtonPrimaryLine)<{ onlyIcon?: boolean }>` - width: 100%; - box-sizing: border-box; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 12px; - gap: 10px; - isolation: isolate; - transition: all 0.3s ease-out; - ${({ onlyIcon }) => - onlyIcon && - ` - padding: 0; - border: none; - `} -`; -const LoginUser = styled(Button)<{ onlyIcon?: boolean }>` - gap: 4px !important; - padding: 8px 10px !important; - [data-us3r-component='UserAvatar'] { - width: 16px !important; - height: 16px !important; - } - [data-us3r-component='UserName'] { - flex: 1 !important; - text-align: left !important; - font-size: 16px !important; - font-weight: 500 !important; - line-height: 17px !important; - - color: #fff !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - white-space: nowrap !important; - } - ${({ onlyIcon }) => - onlyIcon && - ` - padding: 0px !important; - [data-us3r-component='UserAvatar'] { - width: 40px !important; - height: 40px !important; - } - [data-us3r-component="UserName"] { - flex: 0 !important; - width: 0 !important; - } - `} -`; -const LogoutIconButton = styled.img` - width: 24px; - height: 24px; -`; - -const NoLoginText = styled.span` - font-weight: 500; - font-size: 16px; - color: #ffffff; -`; diff --git a/apps/u3/src/components/layout/LoginButtonV2.tsx b/apps/u3/src/components/layout/LoginButtonV2.tsx index 9edef489..26c3271a 100644 --- a/apps/u3/src/components/layout/LoginButtonV2.tsx +++ b/apps/u3/src/components/layout/LoginButtonV2.tsx @@ -1,34 +1,264 @@ -import { useNavigate } from 'react-router-dom'; import { UserAvatar, UserName } from '@us3r-network/profile'; +import React, { ComponentPropsWithRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import useLogin from '../../hooks/shared/useLogin'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '../ui/dropdown-menu'; +import { cn } from '@/lib/utils'; +import { LogoutIcon2 } from '../common/icons/LogoutIcon'; +import NotificationIcon from '../common/icons/NotificationIcon'; +import BookmarkIcon from '../common/icons/BookmarkIcon'; +import { ChatRoomIcon2 } from '../common/icons/ChatRoomIcon'; +import SocialAccountIcon from '../common/icons/SocialAccountIcon'; +import ContactUsIcon from '../common/icons/ContactUsIcon'; +import EmailIcon from '../common/icons/EmailIcon'; +import LogoutConfirmModal from './LogoutConfirmModal'; +import feedbackIconUrl from '../common/assets/platform/pngs/feedback.png'; +import telegramIconUrl from '../common/assets/platform/pngs/telegram.png'; +import twitterIconUrl from '../common/assets/platform/pngs/twitter.png'; +import discordIconUrl from '../common/assets/platform/pngs/discord.png'; +import warpcastIconUrl from '../common/assets/platform/svgs/warpcast.svg'; +import { CONTACT_US_LINKS } from '@/constants'; +import { + FarcasterAccount, + LensAccount, +} from '../profile/info/PlatformAccounts'; +import LoginIcon from './nav-icons/LoginIcon'; + +const CONTACT_LINKS = [ + { + link: CONTACT_US_LINKS.feedback, + iconUrl: feedbackIconUrl, + name: 'Feedback', + }, + { + link: CONTACT_US_LINKS.farcaster, + iconUrl: warpcastIconUrl, + name: 'Farcaster', + }, + { + link: CONTACT_US_LINKS.discord, + iconUrl: discordIconUrl, + name: 'Discord', + }, + { + link: CONTACT_US_LINKS.twitter, + iconUrl: twitterIconUrl, + name: 'Twitter', + }, + { + link: CONTACT_US_LINKS.telegram, + iconUrl: telegramIconUrl, + name: 'Telegram', + }, +]; export default function LoginButtonV2() { - const { isLogin, login } = useLogin(); + const { isLogin, login, logout } = useLogin(); + const [openMenu, setOpenMenu] = useState(false); + const [openLogoutConfirm, setOpenLogoutConfirm] = useState(false); const navigate = useNavigate(); + if (!isLogin) { + return ( + <ButtonWrapper onClick={login}> + <LoginIcon className="w-[40px] h-[40px]" /> + </ButtonWrapper> + ); + } + return ( + <> + <LogoutConfirmModal + isOpen={openLogoutConfirm} + onClose={() => { + setOpenLogoutConfirm(false); + }} + onConfirm={() => { + logout(); + setOpenLogoutConfirm(false); + }} + /> + <DropdownMenu open={openMenu} onOpenChange={setOpenMenu}> + <DropdownMenuTrigger + className=" + w-full + focus:outline-none focus:border-none + active:outline-none active:border-none + " + > + <ButtonWrapper + onClick={() => { + setOpenMenu((pre) => !pre); + }} + > + <UserAvatar style={{ width: '40px', height: '40px' }} /> + </ButtonWrapper> + </DropdownMenuTrigger> + <DropdownMenuContent + className={cn( + 'inline-flex w-[280px] box-border p-[20px] flex-col items-start gap-[20px] rounded-[20px] border-[1px] border-solid border-[#39424C] bg-[#14171A]' + )} + side="right" + align="end" + > + <DropdownMenuItemWarper onClick={() => navigate('/u')}> + <UserAvatar + className="size-4 flex-shrink-0" + style={{ width: '20px', height: '20px' }} + /> + My Profile + </DropdownMenuItemWarper> + + <DropdownMenuItemWarper onClick={() => navigate('/fav/posts')}> + <BookmarkIcon /> + My favorites + </DropdownMenuItemWarper> + + <DropdownMenuItemWarper + className="max-sm:hidden" + onClick={() => navigate('/notification/activity')} + > + <NotificationIcon /> + Notifications + </DropdownMenuItemWarper> + + <DropdownMenuItemWarper + className="max-sm:hidden" + onClick={() => navigate('/message')} + > + <ChatRoomIcon2 /> + Message + </DropdownMenuItemWarper> + + <DropdownMenuSub> + <DropdownMenuSubTriggerWarper> + <SocialAccountIcon /> + Social Accounts + </DropdownMenuSubTriggerWarper> + <DropdownMenuPortal> + <DropdownMenuSubContent + className={cn( + 'inline-flex w-[280px] box-border p-[20px] flex-col items-start gap-[20px] rounded-[20px] border-[1px] border-solid border-[#39424C] bg-[#14171A]' + )} + sideOffset={30} + > + <DropdownMenuItemWarper> + <FarcasterAccount /> + </DropdownMenuItemWarper> + <DropdownMenuItemWarper> + <LensAccount /> + </DropdownMenuItemWarper> + </DropdownMenuSubContent> + </DropdownMenuPortal> + </DropdownMenuSub> + + <DropdownMenuItemWarper disabled> + <EmailIcon /> + Subscribe + </DropdownMenuItemWarper> + + <DropdownMenuSub> + <DropdownMenuSubTriggerWarper> + <ContactUsIcon /> + Contact us + </DropdownMenuSubTriggerWarper> + <DropdownMenuPortal> + <DropdownMenuSubContent + className={cn( + 'inline-flex w-[280px] box-border p-[20px] flex-col items-start gap-[20px] rounded-[20px] border-[1px] border-solid border-[#39424C] bg-[#14171A]' + )} + sideOffset={30} + > + {CONTACT_LINKS.map((link) => ( + <DropdownMenuItemWarper + key={link.link} + onClick={() => window.open(link.link, '_blank')} + > + <img + className="size-4" + src={link.iconUrl} + alt={link.name} + /> + <span>{link.name}</span> + </DropdownMenuItemWarper> + ))} + </DropdownMenuSubContent> + </DropdownMenuPortal> + </DropdownMenuSub> + + <DropdownMenuItemWarper + onClick={(e) => { + e.preventDefault(); + setOpenLogoutConfirm(true); + setOpenMenu(false); + }} + > + <LogoutIcon2 /> + Logout + </DropdownMenuItemWarper> + </DropdownMenuContent> + </DropdownMenu> + </> + ); +} + +function ButtonWrapper({ className, ...props }: ComponentPropsWithRef<'div'>) { return ( - <button - type="button" - className="flex items-center gap-[8px] rounded-[12px] text-white text-[16px] font-bold" - onClick={() => { - if (isLogin) { - navigate('/u'); - } else { - login(); - } - }} - > - {isLogin ? ( - <> - <UserAvatar - className="w-[40px] h-[40px] flex-shrink-0" - style={{ width: '40px', height: '40px' }} - /> - <UserName className="text-[#FFF] text-[16px] font-normal" /> - </> - ) : ( - <span className="wl-user-button_no-login-text">Login</span> + <div + className={cn( + `w-full flex items-center justify-center + text-white text-[16px] font-bold + p-[0px] box-border + outline-none border-none`, + className )} - </button> + {...props} + /> ); } + +const DropdownMenuItemWarper = React.forwardRef< + React.ElementRef<typeof DropdownMenuItem>, + React.ComponentPropsWithoutRef<typeof DropdownMenuItem> + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + <DropdownMenuItem + ref={ref} + className={cn( + `w-full p-[10px] box-border select-none rounded-[10px] leading-none no-underline outline-none transition-colors + text-[#718096] text-[16px] font-medium + flex gap-[10px] items-center`, + `hover:bg-[#20262F]`, + 'max-sm:text-[14px]', + className + )} + {...props} + /> +)); + +const DropdownMenuSubTriggerWarper = React.forwardRef< + React.ElementRef<typeof DropdownMenuSubTrigger>, + React.ComponentPropsWithoutRef<typeof DropdownMenuSubTrigger> + // eslint-disable-next-line react/prop-types +>(({ className, ...props }, ref) => ( + <DropdownMenuSubTrigger + ref={ref} + className={cn( + `w-full p-[10px] box-border select-none rounded-[10px] leading-none no-underline outline-none transition-colors + text-[#718096] text-[16px] font-medium + flex gap-[10px] items-center`, + `hover:bg-[#20262F]`, + 'max-sm:text-[14px]', + className + )} + {...props} + /> +)); diff --git a/apps/u3/src/components/layout/LoginButtonV2Mobile.tsx b/apps/u3/src/components/layout/LoginButtonV2Mobile.tsx new file mode 100644 index 00000000..9d87ddd1 --- /dev/null +++ b/apps/u3/src/components/layout/LoginButtonV2Mobile.tsx @@ -0,0 +1,210 @@ +import { UserAvatar, UserName } from '@us3r-network/profile'; +import React, { ComponentPropsWithRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useLogin from '../../hooks/shared/useLogin'; +import { + Drawer, + DrawerContent, + DrawerPortal, + DrawerTrigger, +} from '../ui/drawer'; +import { cn } from '@/lib/utils'; +import { LogoutIcon2 } from '../common/icons/LogoutIcon'; +import SocialAccountIcon from '../common/icons/SocialAccountIcon'; +import ContactUsIcon from '../common/icons/ContactUsIcon'; +import EmailIcon from '../common/icons/EmailIcon'; +import LogoutConfirmModal from './LogoutConfirmModal'; +import feedbackIconUrl from '../common/assets/platform/pngs/feedback.png'; +import telegramIconUrl from '../common/assets/platform/pngs/telegram.png'; +import twitterIconUrl from '../common/assets/platform/pngs/twitter.png'; +import discordIconUrl from '../common/assets/platform/pngs/discord.png'; +import warpcastIconUrl from '../common/assets/platform/svgs/warpcast.svg'; +import { CONTACT_US_LINKS } from '@/constants'; +import { + FarcasterAccount, + LensAccount, +} from '../profile/info/PlatformAccounts'; +import LoginIcon from './nav-icons/LoginIcon'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '../ui/collapsible'; + +const CONTACT_LINKS = [ + { + link: CONTACT_US_LINKS.feedback, + iconUrl: feedbackIconUrl, + name: 'Feedback', + }, + { + link: CONTACT_US_LINKS.farcaster, + iconUrl: warpcastIconUrl, + name: 'Farcaster', + }, + { + link: CONTACT_US_LINKS.discord, + iconUrl: discordIconUrl, + name: 'Discord', + }, + { + link: CONTACT_US_LINKS.twitter, + iconUrl: twitterIconUrl, + name: 'Twitter', + }, + { + link: CONTACT_US_LINKS.telegram, + iconUrl: telegramIconUrl, + name: 'Telegram', + }, +]; + +export default function LoginButtonV2Mobile() { + const { isLogin, login, logout } = useLogin(); + const [openMenu, setOpenMenu] = useState(false); + const [openLogoutConfirm, setOpenLogoutConfirm] = useState(false); + const navigate = useNavigate(); + + if (!isLogin) { + return ( + <ButtonWrapper onClick={login}> + <div className="flex items-center gap-4"> + <LoginIcon /> + </div> + </ButtonWrapper> + ); + } + return ( + <> + <LogoutConfirmModal + isOpen={openLogoutConfirm} + onClose={() => { + setOpenLogoutConfirm(false); + }} + onConfirm={() => { + logout(); + setOpenLogoutConfirm(false); + }} + /> + <Drawer open={openMenu} onOpenChange={setOpenMenu}> + <DrawerTrigger + className=" + focus:outline-none focus:border-none + active:outline-none active:border-none + " + > + <ButtonWrapper + onClick={() => { + setOpenMenu((pre) => !pre); + }} + > + <div className="flex items-center gap-4"> + <UserAvatar style={{ width: '32px', height: '32px' }} /> + </div> + </ButtonWrapper> + </DrawerTrigger> + <DrawerContent + className={cn( + 'inline-flex w-full box-border p-[20px] flex-col items-start gap-4 rounded-[20px] border-[1px] border-solid border-[#39424C] bg-[#14171A]' + )} + > + <ItemWarper onClick={() => navigate('/u')}> + <UserAvatar + className="size-4 flex-shrink-0" + style={{ width: '20px', height: '20px' }} + /> + My Profile + </ItemWarper> + <Collapsible className="w-full"> + <CollapsibleTrigger asChild className="w-full"> + <ItemWarper> + <SocialAccountIcon /> + Social Accounts + </ItemWarper> + </CollapsibleTrigger> + <CollapsibleContent className="w-full px-4 gap-2"> + <ItemWarper + onClick={() => { + setOpenMenu(false); + }} + > + <FarcasterAccount /> + </ItemWarper> + <ItemWarper + onClick={() => { + setOpenMenu(false); + }} + > + <LensAccount /> + </ItemWarper> + </CollapsibleContent> + </Collapsible> + + <ItemWarper> + <EmailIcon /> + Subscribe + </ItemWarper> + + <Collapsible className="w-full"> + <CollapsibleTrigger className="w-full"> + <ItemWarper> + <ContactUsIcon /> + Contact us + </ItemWarper> + </CollapsibleTrigger> + <CollapsibleContent className="w-full px-4 gap-2"> + {CONTACT_LINKS.map((link) => ( + <ItemWarper + key={link.link} + onClick={() => window.open(link.link, '_blank')} + > + <img className="size-4" src={link.iconUrl} alt={link.name} /> + <span>{link.name}</span> + </ItemWarper> + ))} + </CollapsibleContent> + </Collapsible> + <ItemWarper + onClick={(e) => { + e.preventDefault(); + setOpenLogoutConfirm(true); + setOpenMenu(false); + }} + > + <LogoutIcon2 /> + Logout + </ItemWarper> + </DrawerContent> + </Drawer> + </> + ); +} + +function ButtonWrapper({ className, ...props }: ComponentPropsWithRef<'div'>) { + return ( + <div + className={cn( + `flex items-center justify-between + text-white text-[16px] font-bold + sm:h-[76px] sm:w-full sm:p-[20px] box-border + outline-none border-none`, + className + )} + {...props} + /> + ); +} + +function ItemWarper({ className, ...props }: ComponentPropsWithRef<'div'>) { + return ( + <div + className={cn( + `w-full p-[10px] box-border select-none rounded-[10px] leading-none no-underline outline-none transition-colors + text-[#718096] text-[16px] font-medium + flex gap-[10px] items-center`, + className + )} + {...props} + /> + ); +} diff --git a/apps/u3/src/components/layout/Main.tsx b/apps/u3/src/components/layout/Main.tsx index b6b9db83..c2f0486b 100644 --- a/apps/u3/src/components/layout/Main.tsx +++ b/apps/u3/src/components/layout/Main.tsx @@ -6,11 +6,7 @@ * @Description: 站点主体内容(路由导航) */ import { useRoutes } from 'react-router-dom'; -import styled from 'styled-components'; -import { useCallback, useEffect } from 'react'; -// import { isMobile } from 'react-device-detect'; -// import { useProfileState } from '@us3r-network/profile'; -// import { useSession } from '@us3r-network/auth-with-rainbowkit'; +import { ComponentPropsWithRef, useCallback, useEffect } from 'react'; import { CutomRouteObject, RoutePermission, routes } from '../../route/routes'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import useU3Extension from '../../hooks/shared/useU3Extension'; @@ -18,28 +14,21 @@ import { selectWebsite, setU3ExtensionInstalled, } from '../../features/shared/websiteSlice'; -import EventCompleteGuideModal from '../news/event/EventCompleteGuideModal'; +// import EventCompleteGuideModal from '../news/event/EventCompleteGuideModal'; import useLogin from '../../hooks/shared/useLogin'; import NoLogin from './NoLogin'; -// import usePreference from '../../hooks/usePreference'; -// import OnboardModal from '../onboard/OnboardModal'; +import { cn } from '@/lib/utils'; -// import { store } from '../../store/store'; - -function Main() { +export default function Main(props: ComponentPropsWithRef<'div'>) { const dispatch = useAppDispatch(); - // const session = useSession(); - // const { profile, updateProfile, profileLoading } = useProfileState(); - const { isLogin, user, isAdmin } = useLogin(); - const { openEventCompleteGuideModal, eventCompleteGuideEndCallback } = - useAppSelector(selectWebsite); + const { isLogin, isAdmin } = useLogin(); + // const { openEventCompleteGuideModal, eventCompleteGuideEndCallback } = + // useAppSelector(selectWebsite); const { u3ExtensionInstalled } = useU3Extension(); useEffect(() => { dispatch(setU3ExtensionInstalled(u3ExtensionInstalled)); }, [u3ExtensionInstalled]); - // const { preferenceList } = usePreference(user?.token); - const renderElement = useCallback( ({ element, permissions }: CutomRouteObject) => { if (permissions) { @@ -50,7 +39,7 @@ function Main() { if (isAdmin) { return element; } - return <NoPermission>Need Admin Permission</NoPermission>; + return <NoAdminPermission />; } return element; } @@ -70,43 +59,20 @@ function Main() { const renderRoutes = useRoutes(routesMap); return ( - <MainWrapper id="main-wrapper"> + <div className={cn('w-full h-full relative')} {...props}> {renderRoutes} - <EventCompleteGuideModal + {/* <EventCompleteGuideModal isOpen={openEventCompleteGuideModal} onGuideEnd={eventCompleteGuideEndCallback} - /> - {/* {!isMobile && ( - <OnboardModal - show={ - !!session?.id && - !profileLoading && - !!profile && - (!profile?.tags || profile.tags.length === 0) - } - lists={preferenceList} - finishAction={async (data) => { - await updateProfile({ - tags: data.tags, - }); - }} - /> - )} */} - </MainWrapper> + /> */} + </div> + ); +} + +function NoAdminPermission() { + return ( + <div className={cn('w-full h-full flex justify-center items-center')}> + <div className={cn('text-white text-2xl')}>Need Admin Permission</div> + </div> ); } -export default Main; -const MainWrapper = styled.div` - width: 100%; - height: 100%; - position: relative; -`; -const NoPermission = styled.div` - width: 100%; - height: 50vh; - display: flex; - justify-content: center; - align-items: center; - font-size: 30px; - color: #ffffff; -`; diff --git a/apps/u3/src/components/layout/Nav.tsx b/apps/u3/src/components/layout/Nav.tsx deleted file mode 100644 index f13ff9b4..00000000 --- a/apps/u3/src/components/layout/Nav.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-11-30 18:17:08 - * @LastEditors: bufan bufan@hotmail.com - * @LastEditTime: 2023-10-30 14:54:49 - * @Description: file description - */ -import { ComponentPropsWithRef, useCallback, useRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; -import { isMobile } from 'react-device-detect'; -import useLogin from '../../hooks/shared/useLogin'; -import { CustomNavObject, navs } from '../../route/nav'; -import useRoute from '../../route/useRoute'; -import { cn } from '@/lib/utils'; - -type Props = { - onlyIcon?: boolean; -}; - -export default function Nav({ onlyIcon }: Props) { - const { isLogin, isAdmin } = useLogin(); - const navigate = useNavigate(); - const { firstRouteMeta } = useRoute(); - const [openGroupKeys, setOpenGroupKeys] = useState<Array<string>>([]); - const handleGroupClick = useCallback( - (key: string) => { - if (openGroupKeys.includes(key)) { - setOpenGroupKeys([...openGroupKeys.filter((item) => item !== key)]); - } else { - setOpenGroupKeys([...openGroupKeys, key]); - } - }, - [openGroupKeys, setOpenGroupKeys] - ); - const navItemIsActive = useCallback( - (nav: CustomNavObject) => nav.activeRouteKeys.includes(firstRouteMeta.key), - [firstRouteMeta] - ); - - const groupChidrenInnerEls = useRef(new WeakMap()); - const navItemTextInnerEls = useRef(new WeakMap()); - const renderNavItemText = useCallback( - (nav: CustomNavObject) => { - if (navItemTextInnerEls.current.has(nav)) { - const innerEl = navItemTextInnerEls.current.get(nav); - innerEl.parentElement.style.width = onlyIcon - ? '0px' - : `${innerEl.scrollWidth}px`; - } - return ( - <PcNavItemTextBox> - <PcNavItemTextInner - ref={(el) => { - if (el) { - navItemTextInnerEls.current.set(nav, el); - } - }} - > - {nav.name} - </PcNavItemTextInner> - </PcNavItemTextBox> - ); - }, - [onlyIcon] - ); - const renderNavItem = useCallback( - (nav: CustomNavObject) => { - const isActive = navItemIsActive(nav); - return ( - <PcNavItem - key={nav.route.path} - isActive={isActive} - onClick={() => navigate(nav.route.path)} - > - <NavItemIconBox isActive={isActive}>{nav.icon}</NavItemIconBox> - {!isMobile && renderNavItemText(nav)} - </PcNavItem> - ); - }, - [navItemIsActive, onlyIcon] - ); - return ( - <NavWrapper> - {navs.map((item) => { - if (item?.component) { - return item.component; - } - // feed submit 特殊处理 - if (item.key === 'feed-submit') { - // 未登录不显示 - if (!isLogin) return null; - // 如果不是admin只显示submit content - if (!isAdmin) { - return renderNavItem(item); - } - } - if (item.children) { - const groupIsOpen = openGroupKeys.includes(item.key); - const childrenHasActive = !!item.children.find((nav) => - navItemIsActive(nav) - ); - const groupIsActive = onlyIcon - ? childrenHasActive - : groupIsOpen - ? false - : childrenHasActive; - - if (groupChidrenInnerEls.current.has(item)) { - const innerEl = groupChidrenInnerEls.current.get(item); - innerEl.parentElement.style.height = - onlyIcon || !groupIsOpen ? '0px' : `${innerEl.offsetHeight}px`; - } - return ( - <PcNavGroupBox key={item.name}> - <PcNavItem - isActive={groupIsActive} - onClick={() => handleGroupClick(item.key)} - > - <NavItemIconBox isActive={groupIsActive}> - {item.icon} - </NavItemIconBox> - {!isMobile && renderNavItemText(item)} - </PcNavItem> - <GroupChildrenBox> - <GroupChildrenInner - ref={(el) => { - if (el) { - groupChidrenInnerEls.current.set(item, el); - } - }} - > - {item.children.map((nav) => renderNavItem(nav))} - </GroupChildrenInner> - </GroupChildrenBox> - </PcNavGroupBox> - ); - } - return renderNavItem(item); - })} - </NavWrapper> - ); -} -export const NavWrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 10px; - transition: all 0.3s ease-out; -`; -export const PcNavItem = styled.div<{ isActive?: boolean; disabled?: boolean }>` - overflow: hidden; - height: 40px; - font-weight: 400; - font-size: 16px; - line-height: 19px; - text-transform: capitalize; - padding: 10px; - border-radius: 10px; - box-sizing: border-box; - display: flex; - align-items: center; - gap: 10px; - cursor: pointer; - background: ${(props) => (props?.isActive ? '#14171A' : 'none')}; - color: ${(props) => (props?.isActive ? '#fff' : '#718096')}; - &:hover { - ${(props) => - !props?.isActive && - ` - background: #14171a; - opacity: 0.8; - `}; - } - transition: all 0.3s ease-out; - ${(props) => - props?.disabled && - ` - opacity: 0.5; - cursor: not-allowed; - `}; -`; -export function NavItemIconBox({ - isActive, - className, - children, - ...props -}: ComponentPropsWithRef<'div'> & { isActive?: boolean }) { - return ( - <div - className={cn( - 'flex-shrink-0 transition-all ease-out delay-300 relative', - className, - 'max-sm:w-[24px] max-sm:h-[24px]' - )} - {...props} - > - <NavItemIconInner isActive={isActive}>{children}</NavItemIconInner> - </div> - ); -} -export const NavItemIconInner = styled.div<{ isActive?: boolean }>` - width: 100%; - height: 100%; - svg { - width: 100%; - height: 100%; - path { - stroke: ${({ isActive }) => (isActive ? `#fff` : '#718096')}; - transition: all 0.3s ease-out; - } - } -`; -export const PcNavItemTextBox = styled.div` - overflow: hidden; - transition: all 0.5s ease-out; -`; -export const PcNavItemTextInner = styled.div` - white-space: nowrap; -`; -const PcNavGroupBox = styled.div` - max-height: 100vh; - transition: height 1s; -`; -const GroupChildrenBox = styled.div` - overflow: hidden; - transition: all 0.5s ease-out; -`; -const GroupChildrenInner = styled.div``; diff --git a/apps/u3/src/components/layout/NavLinkItem.tsx b/apps/u3/src/components/layout/NavLinkItem.tsx new file mode 100644 index 00000000..91b7ff66 --- /dev/null +++ b/apps/u3/src/components/layout/NavLinkItem.tsx @@ -0,0 +1,37 @@ +import { ComponentPropsWithRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { cn } from '@/lib/utils'; + +export type NavLinkItemProps = ComponentPropsWithRef<'a'> & { + active?: boolean; +}; +export default function NavLinkItem({ + active, + href, + className, + children, + ...props +}: NavLinkItemProps) { + const navigate = useNavigate(); + return ( + <a + href={href} + onClick={(e) => { + e.preventDefault(); + if (href) navigate(href); + }} + className={cn( + `w-full p-[10px] box-border select-none rounded-[10px] leading-none no-underline outline-none transition-colors + text-[#718096] text-[16px] font-medium + flex gap-[10px] items-center`, + `hover:bg-[#20262F]`, + 'max-sm:text-[14px]', + active && 'bg-[#20262F] text-[#FFF]', + className + )} + {...props} + > + {children} + </a> + ); +} diff --git a/apps/u3/src/components/layout/SearchIconBtn.tsx b/apps/u3/src/components/layout/SearchIconBtn.tsx new file mode 100644 index 00000000..d1e1b50a --- /dev/null +++ b/apps/u3/src/components/layout/SearchIconBtn.tsx @@ -0,0 +1,8 @@ +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { QuickSearchModalName } from '../social/QuickSearchModal'; +import SearchIcon from '../common/icons/SearchIcon'; + +export default function SearchIconBtn() { + const { setOpenModalName } = useFarcasterCtx(); + return <SearchIcon onClick={() => setOpenModalName(QuickSearchModalName)} />; +} diff --git a/apps/u3/src/components/layout/menu.tsx b/apps/u3/src/components/layout/menu.tsx index 3f5fb393..41945f80 100644 --- a/apps/u3/src/components/layout/menu.tsx +++ b/apps/u3/src/components/layout/menu.tsx @@ -6,216 +6,111 @@ * @Description: file description */ import styled from 'styled-components'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useMemo, useState } from 'react'; -import LoginButton from './LoginButton'; -import Nav, { NavWrapper, PcNavItem, NavItemIconBox } from './Nav'; +import { useNavigate } from 'react-router-dom'; +import { ComponentPropsWithRef } from 'react'; import { ReactComponent as LogoIconSvg } from '../common/assets/imgs/logo-icon.svg'; -import { ReactComponent as MessageChatSquareSvg } from '../common/assets/svgs/message-chat-square.svg'; -import { ReactComponent as ContactUsSvg } from '../common/assets/svgs/contact-us.svg'; -import MessageModal from '../message/MessageModal'; -import { NavModalName, useNav } from '../../contexts/NavCtx'; -import ContactUsModal from './ContactUsModal'; -import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; -import { getCommunityPath } from '@/route/path'; - -export default function Menu() { - const [isOpen, setIsOpen] = useState(false); - const navigate = useNavigate(); - const { pathname } = useLocation(); - const isCommunityRoute = pathname.includes('/community/'); +import { cn } from '@/lib/utils'; +import useAllJoinedCommunities from '@/hooks/community/useAllJoinedCommunities'; +import useBrowsingCommunity from '@/hooks/community/useBrowsingCommunity'; +import SidebarCommunityItem from '../community/SidebarCommunityItem'; +import ExploreIcon from './nav-icons/ExploreIcon'; +import useRoute from '@/route/useRoute'; +import { RouteKey } from '@/route/routes'; +import LoginButtonV2 from './LoginButtonV2'; + +export default function Menu({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { return ( - <MenuWrapper - onMouseEnter={() => setIsOpen(true)} - onMouseLeave={() => setIsOpen(false)} - isOpen={isOpen} + <div + className={cn( + 'bg-[#14171A] w-[60px] h-full py-[20px] box-border flex flex-col gap-[20] items-start', + className + )} + {...props} > - <LogoBox onlyIcon={!isOpen} onClick={() => navigate('/')}> - <LogoIconBox onlyIcon={!isOpen}> + <div className="w-full flex flex-col items-center gap-[4px] cursor-pointer max-sm:hidden"> + <LogoIconBox> <LogoIconSvg /> </LogoIconBox> - {isOpen ? ( - <span className={'font-medium text-[24px] text-[#ffffff]'}> - U3.XYZ - </span> - ) : ( - <span - className={ - 'w-[fit-content] flex px-[4px] py-[2px] items-center rounded-[22px] bg-[#454C99] text-[#ffffff] text-[10px] font-medium' - } - > - Alpha - </span> - )} - </LogoBox> - <NavListBox> - <Nav onlyIcon={!isOpen} /> - </NavListBox> - - <hr className="border-t border-[#39424C] my-4 w-full" /> - - <UserChannels /> - - <hr className="border-t border-[#39424C] my-4 w-full" /> - - <div className="flex-grow" /> - - <FooterBox> - <NavWrapper> - <MessageButton /> - <ContactUsButton /> - </NavWrapper> - {!isCommunityRoute && ( - <LoginButtonBox> - <LoginButton onlyIcon={!isOpen} /> - </LoginButtonBox> - )} - </FooterBox> - </MenuWrapper> - ); -} - -function UserChannels() { - const { userChannels, currFid } = useFarcasterCtx(); - - if (!currFid) return null; - - return ( - <div className="w-full overflow-scroll h-full flex gap-5 flex-col"> - {userChannels.map(({ parent_url }) => ( - <ChannelItem parent_url={parent_url} key={parent_url} /> - ))} + <span + className={ + 'w-[fit-content] flex px-[4px] py-[2px] items-center rounded-[22px] bg-[#454C99] text-[#ffffff] text-[10px] font-medium' + } + > + Alpha + </span> + </div> + <hr className="border-t border-[#39424C] my-4 w-full max-sm:hidden" /> + <SidebarHomeLink /> + <UserCommunities /> + <hr className="border-t border-[#39424C] my-4 w-full max-sm:hidden" /> + <div className="w-full max-sm:hidden"> + <LoginButtonV2 /> + </div> </div> ); } -function ChannelItem({ parent_url }: { parent_url: string }) { - const { getChannelFromUrl } = useFarcasterCtx(); +function SidebarHomeLink() { const navigate = useNavigate(); - - const item = useMemo(() => { - return getChannelFromUrl(parent_url); - }, [parent_url, getChannelFromUrl]); - - if (!item) return null; - + const { firstRouteMeta } = useRoute(); + const firstRouteKey = firstRouteMeta?.key; + const active = firstRouteKey === RouteKey.home; return ( - <div - onClick={() => { - navigate(getCommunityPath(item.channel_id)); + <a + href={'/'} + onClick={(e) => { + e.preventDefault(); + navigate('/'); }} - className="cursor-pointer relative" + className="w-full flex justify-center items-center cursor-pointer relative mb-[20px] max-sm:hidden" > - <div className="flex items-center gap-3"> - <img - src={item.image} - alt={item.name} - className="rounded-md w-[40px] h-[40px]" - /> - <div className="text-white font-bold">{item.name}</div> + <div + className={cn( + 'w-[5px] h-[40px] rounded-tl-none rounded-br-[10px] rounded-tr-[10px] rounded-bl-none bg-[#FFF] absolute left-0', + 'transition-all duration-300', + active ? 'block' : 'hidden' + )} + /> + <div className="flex w-[39px] h-[39px] justify-center items-center gap-[10px] rounded-[10px] bg-[#F41F4C]"> + <ExploreIcon active /> </div> - </div> + </a> ); } -function ContactUsButton() { - const { openContactUsModal, renderNavItemText, switchNavModal } = useNav(); - - return ( - <> - <PcNavItem - isActive={openContactUsModal} - onClick={() => { - switchNavModal(NavModalName.ContactUs); - }} - > - <NavItemIconBox isActive={openContactUsModal}> - <ContactUsSvg /> - </NavItemIconBox> - {renderNavItemText('Contact US')} - </PcNavItem> - <ContactUsModal /> - </> - ); -} - -function MessageButton() { - const { openMessageModal, renderNavItemText, switchNavModal } = useNav(); +function UserCommunities() { + const { joinedCommunities } = useAllJoinedCommunities(); + const { browsingCommunity } = useBrowsingCommunity(); + const showCommunities = [...joinedCommunities]; + if (browsingCommunity) { + const findCommunity = showCommunities.find( + (c) => c.id === browsingCommunity.id + ); + if (!findCommunity) { + showCommunities.unshift(browsingCommunity); + } + } return ( - <> - <PcNavItem - isActive={openMessageModal} - onClick={() => { - switchNavModal(NavModalName.Message); - }} - > - <NavItemIconBox isActive={openMessageModal}> - <MessageChatSquareSvg /> - </NavItemIconBox> - {renderNavItemText('Message')} - </PcNavItem> - <MessageModal /> - </> + <div className="flex-1 w-full overflow-scroll flex gap-5 flex-col"> + {showCommunities.map((item) => ( + <SidebarCommunityItem + key={item.id} + communityInfo={item} + active={browsingCommunity?.id === item.id} + /> + ))} + </div> ); } -const MenuWrapper = styled.div<{ isOpen: boolean }>` - background: #1b1e23; - width: ${({ isOpen }) => (isOpen ? '200px' : '60px')}; - height: 100%; - position: fixed; - top: 0; - left: 0; - z-index: 2; - padding: 20px 10px; - border-right: 1px solid #39424c; - box-sizing: border-box; - transition: all 0.3s ease-out; - display: flex; - flex-direction: column; - gap: 20; - align-items: flex-start; -`; -const LogoBox = styled.div<{ onlyIcon?: boolean }>` - width: ${({ onlyIcon }) => (onlyIcon ? '36px' : '142px')}; - height: ${({ onlyIcon }) => (onlyIcon ? '194px' : '94px')}; - display: flex; - flex-direction: ${({ onlyIcon }) => (onlyIcon ? 'column' : 'row')}; - gap: ${({ onlyIcon }) => (onlyIcon ? '4px' : '10px')}; - align-items: 'flex-start'; - overflow: hidden; - transition: all 0.3s ease-out; - cursor: pointer; -`; -const LogoIconBox = styled.div<{ onlyIcon?: boolean }>` +const LogoIconBox = styled.div` width: 36px; height: 36px; path { - transition: all 0.3s ease-out; + fill: #fff; } - ${({ onlyIcon }) => - onlyIcon && - `path { - fill: #fff; - } - `}; -`; -const NavListBox = styled.div` - width: 100%; - flex: 1; - display: flex; - align-items: start; -`; -const FooterBox = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 10px; - justify-content: space-between; - align-items: flex-start; -`; -const LoginButtonBox = styled.div` - width: 100%; - transition: all 0.3s ease-out; `; diff --git a/apps/u3/src/components/layout/mobile/MobileHeader.tsx b/apps/u3/src/components/layout/mobile/MobileHeader.tsx index 38554190..ffaeeee9 100644 --- a/apps/u3/src/components/layout/mobile/MobileHeader.tsx +++ b/apps/u3/src/components/layout/mobile/MobileHeader.tsx @@ -5,27 +5,80 @@ * @LastEditTime: 2023-02-28 23:32:58 * @Description: file description */ +import { ComponentPropsWithRef } from 'react'; +import { useNavigate } from 'react-router-dom'; import useRoute from '../../../route/useRoute'; import { RouteKey } from '../../../route/routes'; -import MobileHomeHeader from './MobileHomeHeader'; -import MobileSubPageHeader from './MobileSubPageHeader'; -import { capitalizeFirstLetter } from '../../../utils/shared/string'; +import { cn } from '@/lib/utils'; +import { MobileHeaderBackBtn, MobileHeaderWrapper } from './MobileHeaderCommon'; +import SearchIconBtn from '../SearchIconBtn'; +import AddPostMobileBtn from '@/components/social/AddPostMobileBtn'; // import MobileDappHeader from './MobileDappHeader'; // import MobileContentHeader from './MobileContentHeader'; -export default function MobileHeader() { - const { firstRouteMeta } = useRoute(); +export default function MobileHeader({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const navigate = useNavigate(); + const { firstRouteMeta, lastRouteMeta } = useRoute(); + const firstRouteKey = firstRouteMeta?.key; + const lastRouteKey = lastRouteMeta?.key; + console.log(firstRouteMeta, lastRouteMeta); - // if ([RouteKey.contents, RouteKey.content].includes(firstRouteMeta.key)) { - // const type = firstRouteMeta.key === RouteKey.contents ? 'list' : 'detail'; - // return <MobileContentHeader type={type} />; - // } + const isHomeRoute = + firstRouteKey === RouteKey.home && lastRouteKey !== RouteKey.communities; - if ([RouteKey.dapp, RouteKey.profile].includes(firstRouteMeta.key)) { + const isCommunityRoute = firstRouteKey === RouteKey.community; + + const isCommunitiesRoute = lastRouteKey === RouteKey.communities; + + const isExplorePostsRoute = false; + const isMessageRoute = false; + const isNotificationRoute = false; + + if (isCommunityRoute) { + return null; + } + if (isHomeRoute) { return ( - <MobileSubPageHeader name={capitalizeFirstLetter(firstRouteMeta?.key)} /> + <MobileHeaderWrapper> + <div + className="text-[#FFF] text-[16px] font-medium" + onClick={() => navigate('/')} + > + Explore + </div> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + </div> + </MobileHeaderWrapper> ); } - - return <MobileHomeHeader />; + if (isCommunitiesRoute) { + return ( + <MobileHeaderWrapper> + <MobileHeaderBackBtn title="Communities" /> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + </div> + </MobileHeaderWrapper> + ); + } + if (isExplorePostsRoute) { + return ( + <MobileHeaderWrapper> + <MobileHeaderBackBtn title="Posts" /> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <AddPostMobileBtn /> + </div> + </MobileHeaderWrapper> + ); + } + return ( + <MobileHeaderWrapper> + <MobileHeaderBackBtn title={lastRouteMeta.title} /> + </MobileHeaderWrapper> + ); } diff --git a/apps/u3/src/components/layout/mobile/MobileHeaderCommon.tsx b/apps/u3/src/components/layout/mobile/MobileHeaderCommon.tsx new file mode 100644 index 00000000..6221e6cc --- /dev/null +++ b/apps/u3/src/components/layout/mobile/MobileHeaderCommon.tsx @@ -0,0 +1,68 @@ +import { ComponentPropsWithRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { cn } from '@/lib/utils'; + +export function MobileHeaderWrapper({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + return ( + <div + className={cn( + 'w-full h-[56px] p-[10px] box-border bg-[#14171A] flex items-center justify-between', + 'hidden max-sm:flex', + className + )} + {...props} + /> + ); +} + +export function MobileHeaderBackBtn({ + title, + backToPath, + onBackClick, + className, + ...props +}: ComponentPropsWithRef<'button'> & { + title?: string; + backToPath?: string; + onBackClick?: () => void; +}) { + const navigate = useNavigate(); + return ( + <button + type="button" + className={cn( + 'flex items-center gap-[10px] text-[#FFF] text-[16px] font-medium', + className + )} + onClick={() => { + if (onBackClick) { + onBackClick(); + return; + } + if (backToPath) { + navigate(backToPath); + } else { + navigate(-1); + } + }} + {...props} + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + > + <path + d="M2.6294 11.1007C2.02129 10.4926 2.02129 9.50739 2.6294 8.89928L9.85721 1.67147C10.149 1.37969 10.5447 1.21578 10.9574 1.21578C11.37 1.21578 11.7657 1.37969 12.0575 1.67147C12.3493 1.96325 12.5132 2.35898 12.5132 2.77161C12.5132 3.18424 12.3493 3.57997 12.0575 3.87175L5.92923 10L12.0575 16.1283C12.3493 16.42 12.5132 16.8158 12.5132 17.2284C12.5132 17.641 12.3493 18.0368 12.0575 18.3285C11.7657 18.6203 11.37 18.7842 10.9574 18.7842C10.5447 18.7842 10.149 18.6203 9.85721 18.3285L2.6294 11.1007Z" + fill="white" + /> + </svg> + {title} + </button> + ); +} diff --git a/apps/u3/src/components/layout/mobile/MobileHomeHeader.tsx b/apps/u3/src/components/layout/mobile/MobileHomeHeader.tsx index 8081a057..09335a32 100644 --- a/apps/u3/src/components/layout/mobile/MobileHomeHeader.tsx +++ b/apps/u3/src/components/layout/mobile/MobileHomeHeader.tsx @@ -5,83 +5,23 @@ * @LastEditTime: 2023-02-28 15:54:26 * @Description: file description */ -import { useState } from 'react'; -import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; -import { ReactComponent as LogoIconSvg } from '../../common/assets/imgs/logo-icon.svg'; -import LogoutConfirmModal from '../LogoutConfirmModal'; -import useLogin from '../../../hooks/shared/useLogin'; -import MobileLoginButton from './MobileLoginButton'; -import useRoute from '@/route/useRoute'; -import SearchIcon from '@/components/common/icons/SearchIcon'; -import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; -import { QuickSearchModalName } from '@/components/social/QuickSearchModal'; +import { MobileHeaderWrapper } from './MobileHeaderCommon'; +import SearchIconBtn from '../SearchIconBtn'; export default function MobileHomeHeader() { - const { logout } = useLogin(); const navigate = useNavigate(); - const [openLogoutConfirm, setOpenLogoutConfirm] = useState(false); - const { firstRouteMeta } = useRoute(); - const { setOpenModalName } = useFarcasterCtx(); return ( - <MobileHomeHeaderWrapper> - <LogoBox onClick={() => navigate('/')}> - <LogoIconBox> - <LogoIconSvg /> - </LogoIconBox> - - {/* <LogoText>Alpha</LogoText> */} - </LogoBox> - <div className="flex-1 flex justify-between items-center"> - <span className="font-bold text-[20px] leading-[24px] text-[#ffffff]"> - {firstRouteMeta?.title || 'U3.XYZ'} - </span> - <SearchIcon onClick={() => setOpenModalName(QuickSearchModalName)} /> + <MobileHeaderWrapper> + <div + className="text-[#FFF] text-[16px] font-medium" + onClick={() => navigate('/')} + > + Explore + </div> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> </div> - <MobileLoginButton - onLogout={() => { - setOpenLogoutConfirm(true); - }} - /> - <LogoutConfirmModal - isOpen={openLogoutConfirm} - onClose={() => { - setOpenLogoutConfirm(false); - }} - onConfirm={() => { - logout(); - setOpenLogoutConfirm(false); - }} - /> - </MobileHomeHeaderWrapper> + </MobileHeaderWrapper> ); } -const MobileHomeHeaderWrapper = styled.div` - background: #1b1e23; - width: 100%; - height: 56px; - position: fixed; - top: 0; - left: 0; - z-index: 1; - padding: 20px 10px; - border-bottom: 1px solid #39424c; - box-sizing: border-box; - display: flex; - gap: 20px; - justify-content: space-between; - align-items: center; -`; -const LogoBox = styled.div` - display: flex; - gap: 10px; - align-items: flex-end; - overflow: hidden; - transition: all 0.3s ease-out; - cursor: pointer; -`; -const LogoIconBox = styled.div` - path { - fill: #5057aa; - } -`; diff --git a/apps/u3/src/components/layout/mobile/MobileMainNav.tsx b/apps/u3/src/components/layout/mobile/MobileMainNav.tsx new file mode 100644 index 00000000..7671a7da --- /dev/null +++ b/apps/u3/src/components/layout/mobile/MobileMainNav.tsx @@ -0,0 +1,88 @@ +/* + * @Author: shixuewen friendlysxw@163.com + * @Date: 2022-12-29 18:44:14 + * @LastEditors: bufan bufan@hotmail.com + * @LastEditTime: 2023-11-22 16:03:33 + * @Description: file description + */ + +import { ComponentPropsWithRef } from 'react'; +import useRoute from '../../../route/useRoute'; +import { RouteKey } from '../../../route/routes'; +import NavLinkItem, { NavLinkItemProps } from '../NavLinkItem'; +import CommunityIcon from '../nav-icons/CommunityIcon'; +import { cn } from '@/lib/utils'; +import NotificationIcon from '../nav-icons/NotificationIcon'; +import MessageIcon from '../nav-icons/MessageIcon'; +import ExploreIcon from '../nav-icons/ExploreIcon'; +import FavIcon from '../nav-icons/FavIcon'; + +export default function MobileMainNav({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { firstRouteMeta, lastRouteMeta } = useRoute(); + + const firstRouteKey = firstRouteMeta?.key; + const lastRouteKey = lastRouteMeta?.key; + + const isCommunitiesRoute = [ + RouteKey.communities, + RouteKey.trendingCommunities, + RouteKey.newestCommunities, + RouteKey.joinedCommunities, + ].includes(lastRouteKey); + + const isExploreRoute = firstRouteKey === RouteKey.home && !isCommunitiesRoute; + + const isCommunityRoute = + isCommunitiesRoute || firstRouteKey === RouteKey.community; + + const isMessageRoute = firstRouteKey === RouteKey.message; + const isNotificationRoute = firstRouteKey === RouteKey.notification; + const isFavRoute = firstRouteKey === RouteKey.fav; + return ( + <div + className={cn( + 'fixed bottom-[0] w-screen h-[80px] px-[10px] py-[20px] box-border bg-[#14171A] flex justify-between items-center z-10', + 'hidden max-sm:flex', + className + )} + {...props} + > + <MobileNavItem href="/" active={isExploreRoute}> + <ExploreIcon active={isExploreRoute} /> + Explore + </MobileNavItem> + <MobileNavItem href="/communities" active={isCommunityRoute}> + <CommunityIcon active={isCommunityRoute} /> + Communities + </MobileNavItem> + <MobileNavItem href="/notification" active={isNotificationRoute}> + <NotificationIcon active={isNotificationRoute} /> + Notification + </MobileNavItem> + <MobileNavItem active={isMessageRoute} href="/message"> + <MessageIcon active={isMessageRoute} /> + Message + </MobileNavItem> + <MobileNavItem href="/fav/posts" active={isFavRoute}> + <FavIcon active={isFavRoute} /> + Favorites + </MobileNavItem> + </div> + ); +} + +function MobileNavItem({ active, className, ...props }: NavLinkItemProps) { + return ( + <NavLinkItem + className={cn( + 'flex-col gap-[4px] text-[10px] p-0 bg-transparent hover:bg-transparent max-sm:text-[10px]', + active && 'bg-transparent text-[#FFF]', + className + )} + {...props} + /> + ); +} diff --git a/apps/u3/src/components/layout/mobile/MobileNav.tsx b/apps/u3/src/components/layout/mobile/MobileNav.tsx deleted file mode 100644 index 1fc39172..00000000 --- a/apps/u3/src/components/layout/mobile/MobileNav.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-12-29 18:44:14 - * @LastEditors: bufan bufan@hotmail.com - * @LastEditTime: 2023-11-22 16:03:33 - * @Description: file description - */ -import styled from 'styled-components'; - -import useRoute from '../../../route/useRoute'; -import { RouteKey } from '../../../route/routes'; -import Nav from '../Nav'; - -export default function MobileNav() { - const { firstRouteMeta } = useRoute(); - - if ([RouteKey.dapp, RouteKey.profile].includes(firstRouteMeta.key)) { - return null; - } - - return ( - <MobileNavWrapper> - <Nav /> - </MobileNavWrapper> - ); -} - -const MobileNavWrapper = styled.div` - position: fixed; - bottom: 0; - width: 100vw; - z-index: 2; - background: #1b1e23; - border-top: 1px solid #39424c; - & > div { - flex-direction: row; - & > div { - flex: 1; - height: 60px; - align-items: center; - justify-content: center; - background: transparent !important; - } - } -`; diff --git a/apps/u3/src/components/layout/nav-icons/CommunityIcon.tsx b/apps/u3/src/components/layout/nav-icons/CommunityIcon.tsx new file mode 100644 index 00000000..baccef86 --- /dev/null +++ b/apps/u3/src/components/layout/nav-icons/CommunityIcon.tsx @@ -0,0 +1,42 @@ +import { ComponentPropsWithRef } from 'react'; + +export default function CommunityIcon({ + active, + ...props +}: ComponentPropsWithRef<'svg'> & { active?: boolean }) { + const color = active ? '#fff' : '#718096'; + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + {...props} + > + <g clipPath="url(#clip0_4162_8637)"> + <path + d="M8.35303 6.17995C8.35303 6.70508 8.56123 7.2087 8.93183 7.58002C9.30242 7.95134 9.80506 8.15995 10.3292 8.15995C10.8533 8.15995 11.3559 7.95134 11.7265 7.58002C12.0971 7.2087 12.3053 6.70508 12.3053 6.17995C12.3053 5.65482 12.0971 5.1512 11.7265 4.77988C11.3559 4.40856 10.8533 4.19995 10.3292 4.19995C9.80506 4.19995 9.30242 4.40856 8.93183 4.77988C8.56123 5.1512 8.35303 5.65482 8.35303 6.17995Z" + fill={color} + /> + <path + d="M5.41895 12.6C5.41895 12.9023 5.53882 13.1923 5.75219 13.4061C5.96557 13.6199 6.25497 13.74 6.55672 13.74C6.85848 13.74 7.14788 13.6199 7.36125 13.4061C7.57463 13.1923 7.6945 12.9023 7.6945 12.6C7.6945 12.2976 7.57463 12.0077 7.36125 11.7939C7.14788 11.5801 6.85848 11.46 6.55672 11.46C6.25497 11.46 5.96557 11.5801 5.75219 11.7939C5.53882 12.0077 5.41895 12.2976 5.41895 12.6Z" + fill={color} + /> + <path + d="M14.7603 18.18C14.7603 18.3869 14.8423 18.5853 14.9883 18.7316C15.1343 18.8778 15.3323 18.96 15.5387 18.96C15.7452 18.96 15.9432 18.8778 16.0892 18.7316C16.2352 18.5853 16.3172 18.3869 16.3172 18.18C16.3172 17.9732 16.2352 17.7748 16.0892 17.6285C15.9432 17.4822 15.7452 17.4 15.5387 17.4C15.3323 17.4 15.1343 17.4822 14.9883 17.6285C14.8423 17.7748 14.7603 17.9732 14.7603 18.18Z" + fill={color} + /> + <path + d="M22.0063 5.52001C22.1261 5.34001 22.2459 5.16001 22.3656 4.98001C22.8447 4.08001 23.6232 2.70001 23.204 1.44001C22.9645 0.840008 22.5453 0.540008 22.2459 0.420008C21.168 0.0600083 19.7308 0.960008 19.4913 1.14001C19.1918 1.32001 19.132 1.68001 19.3116 1.98001C19.4913 2.28001 19.8505 2.34001 20.15 2.16001C20.629 1.86001 21.4674 1.44001 21.8866 1.62001C21.9465 1.62001 22.0063 1.68001 22.0662 1.86001C22.3058 2.58001 21.7069 3.66001 21.2877 4.44001C20.2697 6.30001 18.8924 7.98001 17.5151 9.72001C14.2814 13.68 10.5687 17.04 6.13733 19.92C5.95768 20.04 5.77803 20.16 5.5385 20.28C3.14318 18.36 1.58622 15.36 1.58622 12C1.58622 6.12001 6.31698 1.38001 12.1855 1.38001C13.5029 1.38001 14.7006 1.62001 15.8384 2.04001C15.8983 2.04001 15.9582 2.10001 16.018 2.10001C16.3773 2.10001 16.6169 1.86001 16.6169 1.50001C16.6169 1.26001 16.4372 1.02001 16.1977 0.960008C14.9401 0.480008 13.5628 0.240008 12.1256 0.240008C5.65827 0.180008 0.388561 5.46001 0.388561 12C0.388561 14.04 0.927508 15.96 1.82575 17.64C1.58622 18 0.747859 19.14 0.208912 20.46C-0.150386 21.3 -0.0306202 22.08 0.448444 22.62C0.807742 23.04 1.40657 23.28 2.06529 23.28C2.30482 23.28 2.60423 23.22 2.84377 23.16C3.80189 22.86 4.70014 22.32 5.47862 21.78C7.39488 23.1 9.67043 23.88 12.1855 23.88C18.7128 23.82 23.9825 18.54 23.9825 12C23.9825 9.60001 23.2639 7.38001 22.0063 5.52001ZM2.48447 21.96C2.06529 22.08 1.58622 22.02 1.34669 21.78C1.10716 21.54 1.22692 21.18 1.28681 20.88C1.6461 20.04 2.12517 19.2 2.48447 18.72C3.0833 19.56 3.74201 20.28 4.46061 20.94C3.86178 21.36 3.20306 21.72 2.48447 21.96ZM12.1855 22.62C10.1495 22.62 8.23324 22.02 6.6164 21C6.67628 20.94 6.73616 20.94 6.79605 20.88C11.2873 17.94 15.1198 14.52 18.4134 10.44C19.4314 9.18001 20.3895 7.98001 21.2877 6.66001C22.186 8.22001 22.7848 10.08 22.7848 12C22.7848 17.88 18.0541 22.62 12.1855 22.62Z" + fill={color} + /> + </g> + <defs> + <clipPath id="clip0_4162_8637"> + <rect width="24" height="24" fill={color} /> + </clipPath> + </defs> + </svg> + ); +} diff --git a/apps/u3/src/components/layout/nav-icons/ExploreIcon.tsx b/apps/u3/src/components/layout/nav-icons/ExploreIcon.tsx new file mode 100644 index 00000000..27f6073b --- /dev/null +++ b/apps/u3/src/components/layout/nav-icons/ExploreIcon.tsx @@ -0,0 +1,31 @@ +import { ComponentPropsWithRef } from 'react'; + +export default function ExploreIcon({ + active, + ...props +}: ComponentPropsWithRef<'svg'> & { active?: boolean }) { + const color = active ? '#fff' : '#718096'; + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="25" + height="24" + viewBox="0 0 25 24" + fill="none" + {...props} + > + <path + d="M12.25 22C17.7728 22 22.25 17.5228 22.25 12C22.25 6.47715 17.7728 2 12.25 2C6.72715 2 2.25 6.47715 2.25 12C2.25 17.5228 6.72715 22 12.25 22Z" + stroke={color} + strokeLinecap="round" + strokeLinejoin="round" + /> + <path + d="M14.9721 8.26596C15.4607 8.10312 15.7049 8.02169 15.8674 8.07962C16.0087 8.13003 16.12 8.24127 16.1704 8.38263C16.2283 8.54507 16.1469 8.78935 15.984 9.27789L14.4965 13.7405C14.4501 13.8797 14.4269 13.9492 14.3874 14.007C14.3524 14.0582 14.3082 14.1024 14.257 14.1374C14.1992 14.1769 14.1297 14.2001 13.9905 14.2465L9.52789 15.734C9.03935 15.8969 8.79507 15.9783 8.63263 15.9204C8.49127 15.87 8.38003 15.7587 8.32962 15.6174C8.27169 15.4549 8.35312 15.2107 8.51596 14.7221L10.0035 10.2595C10.0499 10.1203 10.0731 10.0508 10.1126 9.99299C10.1476 9.94182 10.1918 9.8976 10.243 9.8626C10.3008 9.82308 10.3703 9.79989 10.5095 9.75351L14.9721 8.26596Z" + stroke={color} + strokeLinecap="round" + strokeLinejoin="round" + /> + </svg> + ); +} diff --git a/apps/u3/src/components/layout/nav-icons/FavIcon.tsx b/apps/u3/src/components/layout/nav-icons/FavIcon.tsx new file mode 100644 index 00000000..1a962a0a --- /dev/null +++ b/apps/u3/src/components/layout/nav-icons/FavIcon.tsx @@ -0,0 +1,22 @@ +import { ComponentPropsWithRef } from 'react'; + +export default function ExploreIcon({ + active, + ...props +}: ComponentPropsWithRef<'svg'> & { active?: boolean }) { + const color = active ? '#fff' : '#718096'; + return ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M18.1321 23.0648V23.0648H18.128C17.8799 23.0648 17.5767 22.9917 17.2829 22.8448L12.2605 20.3327L12.0377 20.2213L11.8145 20.332L6.76097 22.84L6.75457 22.8432L6.74827 22.8466C6.50318 22.9771 6.21792 23.0478 5.92328 23.0478L5.92134 23.0478C5.54419 23.0493 5.17663 22.9292 4.87318 22.7052C4.31388 22.2848 4.03459 21.557 4.1624 20.9176L4.16305 20.9143L5.19662 15.5524L5.249 15.2807L5.04656 15.092L1.09307 11.4073C0.857649 11.1663 0.689029 10.8681 0.603902 10.5421C0.518243 10.214 0.520128 9.86923 0.609338 9.54214L0.61328 9.52858C0.845675 8.84232 1.40617 8.37972 2.0712 8.29064L2.08277 8.28909L2.09426 8.287L7.5983 7.28622L7.84615 7.24115L7.95743 7.01515L10.4266 2.00025C10.4268 1.99983 10.427 1.99942 10.4272 1.999C10.7435 1.36696 11.3818 0.971924 12.0368 0.971924C12.7367 0.971924 13.3804 1.3914 13.6446 1.99246L13.649 2.00237L13.6538 2.01208L16.1163 7.01364L16.2285 7.24156L16.4788 7.28531L21.9837 8.24775L21.9909 8.24901L21.9981 8.25005C22.3304 8.2982 22.6417 8.44134 22.8945 8.66224C23.1474 8.88314 23.331 9.1724 23.4234 9.49522L23.4259 9.50397L23.4287 9.51263C23.5332 9.83344 23.5465 10.177 23.4671 10.5049C23.3876 10.8329 23.2185 11.1322 22.9787 11.3696L22.9766 11.3717L22.9685 11.3798L19.0238 15.0945L18.8251 15.2816L18.875 15.5499L19.8734 20.9213L19.8735 20.9219C19.9984 21.5896 19.7353 22.2697 19.1703 22.6993L19.1701 22.6992L19.1596 22.7076C18.8685 22.9416 18.5055 23.0678 18.1321 23.0648ZM22.2571 11.2169L22.2639 11.2106L22.2703 11.204C22.6606 10.8083 22.7982 10.2353 22.6242 9.70745L22.616 9.68159C22.5431 9.42632 22.3978 9.19754 22.1976 9.02298C21.9963 8.84752 21.7484 8.7345 21.484 8.69766L21.4688 8.69527L15.8465 7.71238L13.3188 2.57831C13.1002 2.08438 12.5634 1.77089 12.0364 1.77089C11.482 1.77089 10.9887 2.11236 10.7455 2.59752L10.7439 2.60071L8.22721 7.71187L2.57955 8.73779C2.01938 8.81383 1.60057 9.19627 1.41799 9.72322L1.41222 9.73989L1.40763 9.75693C1.2695 10.27 1.42818 10.8609 1.79958 11.2405L1.80766 11.2488L1.8161 11.2566L5.86569 15.0302L4.80295 20.5423C4.80281 20.543 4.80267 20.5437 4.80254 20.5444C4.69494 21.0856 4.94653 21.6566 5.37733 21.9801C5.61817 22.1612 5.91621 22.2519 6.21489 22.2519C6.44584 22.2519 6.67562 22.1981 6.88047 22.088L6.8888 22.0835L6.89695 22.0787L6.90004 22.0769L12.0378 19.5283L17.1496 22.0846C17.15 22.0848 17.1503 22.085 17.1507 22.0852C17.3521 22.1865 17.6104 22.2668 17.8356 22.2668C18.1243 22.2668 18.4188 22.1783 18.6512 21.985C18.6516 21.9846 18.652 21.9843 18.6524 21.9839L18.6665 21.9724C19.1045 21.6381 19.3352 21.095 19.2326 20.5457C19.2325 20.5454 19.2325 20.5452 19.2324 20.5449L18.2086 15.0295L22.2571 11.2169Z" + stroke={color} + /> + </svg> + ); +} diff --git a/apps/u3/src/components/layout/nav-icons/LoginIcon.tsx b/apps/u3/src/components/layout/nav-icons/LoginIcon.tsx new file mode 100644 index 00000000..7c5b5557 --- /dev/null +++ b/apps/u3/src/components/layout/nav-icons/LoginIcon.tsx @@ -0,0 +1,24 @@ +import { ComponentPropsWithRef } from 'react'; + +export default function LoginIcon({ + active, + unread, + ...props +}: ComponentPropsWithRef<'svg'> & { active?: boolean; unread?: boolean }) { + const color = active ? '#fff' : '#718096'; + return ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <path + d="M11.314 16.957C11.0723 16.9257 10.8321 16.884 10.594 16.832C10.531 16.819 10.466 16.808 10.403 16.794C10.1468 16.733 9.89321 16.6613 9.643 16.579C8.916 16.336 8.097 16.519 7.618 17.081C7.558 17.154 7.504 17.231 7.448 17.305C7.378 17.399 7.304 17.489 7.24 17.586C7.183 17.669 7.136 17.759 7.084 17.845C7.026 17.944 6.964 18.041 6.912 18.143C6.865 18.233 6.828 18.329 6.787 18.422C6.74 18.526 6.691 18.629 6.651 18.737C6.616 18.833 6.588 18.933 6.559 19.031C6.525 19.141 6.489 19.249 6.461 19.361C6.437 19.462 6.421 19.566 6.404 19.668C6.383 19.782 6.36 19.895 6.347 20.012C6.336 20.117 6.334 20.225 6.328 20.331C6.322 20.44 6.314 20.548 6.315 20.659C6.428 23.78 18.569 23.78 18.681 20.659C18.682 20.549 18.675 20.439 18.669 20.331C18.664 20.225 18.662 20.117 18.649 20.012C18.636 19.895 18.614 19.782 18.593 19.668C18.576 19.566 18.56 19.462 18.536 19.361C18.509 19.249 18.473 19.141 18.439 19.031C18.409 18.933 18.382 18.833 18.346 18.737C18.307 18.629 18.258 18.527 18.211 18.422C18.169 18.329 18.131 18.234 18.085 18.143C18.033 18.041 17.971 17.943 17.912 17.845C17.861 17.759 17.813 17.669 17.758 17.586C17.692 17.489 17.62 17.399 17.549 17.305C17.492 17.23 17.439 17.154 17.379 17.081C16.9 16.519 16.081 16.336 15.354 16.579C15.1038 16.6613 14.8502 16.733 14.594 16.794C14.53 16.808 14.466 16.819 14.404 16.832C14.1656 16.884 13.925 16.9257 13.683 16.957C13.598 16.968 13.514 16.979 13.429 16.988C13.1201 17.0219 12.8097 17.0399 12.499 17.042C12.184 17.042 11.875 17.019 11.569 16.988C11.483 16.979 11.399 16.968 11.314 16.957ZM19 9.047C19 12.387 16.089 15.096 12.5 15.096C8.91 15.096 6 12.388 6 9.048C6 5.708 8.91 3 12.5 3C16.089 3 19 5.707 19 9.048" + fill={color} + /> + </svg> + ); +} diff --git a/apps/u3/src/components/layout/nav-icons/MessageIcon.tsx b/apps/u3/src/components/layout/nav-icons/MessageIcon.tsx new file mode 100644 index 00000000..a75c8da0 --- /dev/null +++ b/apps/u3/src/components/layout/nav-icons/MessageIcon.tsx @@ -0,0 +1,25 @@ +import { ComponentPropsWithRef } from 'react'; + +export default function MessageIcon({ + active, + ...props +}: ComponentPropsWithRef<'svg'> & { active?: boolean }) { + const color = active ? '#fff' : '#718096'; + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="25" + height="24" + viewBox="0 0 25 24" + fill="none" + {...props} + > + <path + d="M10.5 15L7.42474 18.1137C6.99579 18.548 6.78131 18.7652 6.59695 18.7805C6.43701 18.7938 6.28042 18.7295 6.17596 18.6076C6.05556 18.4672 6.05556 18.162 6.05556 17.5515V15.9916C6.05556 15.444 5.60707 15.0477 5.0652 14.9683V14.9683C3.75374 14.7762 2.72378 13.7463 2.53168 12.4348C2.5 12.2186 2.5 11.9605 2.5 11.4444V6.8C2.5 5.11984 2.5 4.27976 2.82698 3.63803C3.1146 3.07354 3.57354 2.6146 4.13803 2.32698C4.77976 2 5.61984 2 7.3 2H14.7C16.3802 2 17.2202 2 17.862 2.32698C18.4265 2.6146 18.8854 3.07354 19.173 3.63803C19.5 4.27976 19.5 5.11984 19.5 6.8V11M19.5 22L17.3236 20.4869C17.0177 20.2742 16.8647 20.1678 16.6982 20.0924C16.5504 20.0255 16.3951 19.9768 16.2356 19.9474C16.0558 19.9143 15.8695 19.9143 15.4969 19.9143H13.7C12.5799 19.9143 12.0198 19.9143 11.592 19.6963C11.2157 19.5046 10.9097 19.1986 10.718 18.8223C10.5 18.3944 10.5 17.8344 10.5 16.7143V14.2C10.5 13.0799 10.5 12.5198 10.718 12.092C10.9097 11.7157 11.2157 11.4097 11.592 11.218C12.0198 11 12.5799 11 13.7 11H19.3C20.4201 11 20.9802 11 21.408 11.218C21.7843 11.4097 22.0903 11.7157 22.282 12.092C22.5 12.5198 22.5 13.0799 22.5 14.2V16.9143C22.5 17.8462 22.5 18.3121 22.3478 18.6797C22.1448 19.1697 21.7554 19.5591 21.2654 19.762C20.8978 19.9143 20.4319 19.9143 19.5 19.9143V22Z" + stroke={color} + strokeLinecap="round" + strokeLinejoin="round" + /> + </svg> + ); +} diff --git a/apps/u3/src/components/layout/nav-icons/NotificationIcon.tsx b/apps/u3/src/components/layout/nav-icons/NotificationIcon.tsx new file mode 100644 index 00000000..dd721832 --- /dev/null +++ b/apps/u3/src/components/layout/nav-icons/NotificationIcon.tsx @@ -0,0 +1,41 @@ +import { ComponentPropsWithRef } from 'react'; + +export default function NotificationIcon({ + active, + unread, + ...props +}: ComponentPropsWithRef<'svg'> & { active?: boolean; unread?: boolean }) { + const color = active ? '#fff' : '#718096'; + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="25" + height="24" + viewBox="0 0 25 24" + fill="none" + {...props} + > + <g clipPath="url(#clip0_4162_8672)"> + <path + d="M10.1039 21C10.8091 21.6224 11.7353 22 12.7498 22C13.7642 22 14.6905 21.6224 15.3956 21M18.7498 8C18.7498 6.4087 18.1176 4.88258 16.9924 3.75736C15.8672 2.63214 14.3411 2 12.7498 2C11.1585 2 9.63235 2.63214 8.50713 3.75736C7.38192 4.88258 6.74977 6.4087 6.74977 8C6.74977 11.0902 5.97024 13.206 5.09944 14.6054C4.3649 15.7859 3.99763 16.3761 4.0111 16.5408C4.02601 16.7231 4.06463 16.7926 4.21155 16.9016C4.34423 17 4.94237 17 6.13863 17H19.3609C20.5572 17 21.1553 17 21.288 16.9016C21.4349 16.7926 21.4735 16.7231 21.4884 16.5408C21.5019 16.3761 21.1346 15.7859 20.4001 14.6054C19.5293 13.206 18.7498 11.0902 18.7498 8Z" + stroke={color} + strokeLinecap="round" + strokeLinejoin="round" + /> + {unread && ( + <rect x="18.75" width="6" height="6" rx="3" fill="#F81775" /> + )} + </g> + <defs> + <clipPath id="clip0_4162_8672"> + <rect + width="24" + height="24" + fill="white" + transform="translate(0.75)" + /> + </clipPath> + </defs> + </svg> + ); +} diff --git a/apps/u3/src/components/message/Avatar.tsx b/apps/u3/src/components/message/Avatar.tsx deleted file mode 100644 index 7f68ae6b..00000000 --- a/apps/u3/src/components/message/Avatar.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { UserAvatarProps } from '@us3r-network/profile'; -import { getDidPkhWithAddress } from '../../utils/shared/did'; -import S3UserAvatar from './S3UserAvatar'; - -export default function Avatar({ - address, - ...otherProps -}: UserAvatarProps & { address: string }) { - return <S3UserAvatar did={getDidPkhWithAddress(address)} {...otherProps} />; -} diff --git a/apps/u3/src/components/message/ConversationList.tsx b/apps/u3/src/components/message/ConversationList.tsx deleted file mode 100644 index 2e6d932c..00000000 --- a/apps/u3/src/components/message/ConversationList.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import { DecodedMessage } from '@xmtp/xmtp-js'; -import dayjs from 'dayjs'; -import { useEffect, useState } from 'react'; -import { - getAttachmentUrl, - isAttachment, - truncate, -} from '../../utils/message/xmtp'; -import useConversationList from '../../hooks/message/xmtp/useConversationList'; -import { - useXmtpClient, - MessageRoute, -} from '../../contexts/message/XmtpClientCtx'; -import Name from './Name'; -import Avatar from './Avatar'; -import Loading from '../common/loading/Loading'; -import NoConversations from './NoConversations'; - -export default function ConversationList( - props: StyledComponentPropsWithRef<'div'> -) { - const { setMessageRouteParams } = useXmtpClient(); - - const { isLoading, conversationList } = useConversationList(); - - return ( - <ConversationListWrap {...props}> - {isLoading ? ( - <LoadingWrapper> - <Loading /> - </LoadingWrapper> - ) : conversationList.length > 0 ? ( - <CardListWrap> - {conversationList.map(({ conversation, latestMessage }) => ( - <ConversationCard - key={`Convo_${conversation.peerAddress}`} - selectConvoAction={(address) => { - setMessageRouteParams({ - route: MessageRoute.DETAIL, - peerAddress: address, - }); - }} - address={conversation.peerAddress} - latestMessage={latestMessage} - /> - ))} - </CardListWrap> - ) : ( - <NoConversations /> - )} - </ConversationListWrap> - ); -} -const ConversationListWrap = styled.div` - width: 100%; -`; -const LoadingWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; -`; -const CardListWrap = styled.div` - display: flex; - flex-direction: column; -`; -function ConversationCard({ - selectConvoAction, - address, - latestMessage, -}: { - selectConvoAction: (address: string) => void; - address: string; - latestMessage: DecodedMessage | null; -}) { - const { xmtpClient } = useXmtpClient(); - const [attachmentUrl, setAttachmentUrl] = useState<string>(''); - useEffect(() => { - (async () => { - if (latestMessage && isAttachment(latestMessage)) { - const url = await getAttachmentUrl(latestMessage, xmtpClient); - setAttachmentUrl(url); - } - })(); - }, [latestMessage, xmtpClient]); - return ( - <ConversationCardWrap onClick={() => selectConvoAction(address)}> - <Avatar address={address} /> - <Center> - <Name address={address} /> - <LatestMessage> - {latestMessage && - (() => { - if (isAttachment(latestMessage)) { - return attachmentUrl; - } - return truncate(latestMessage.content, 75); - })()} - </LatestMessage> - </Center> - <LatestMessageTime> - {latestMessage && dayjs(latestMessage.sent).fromNow()} - </LatestMessageTime> - </ConversationCardWrap> - ); -} - -const ConversationCardWrap = styled.div` - padding: 12px 0px; - display: flex; - gap: 10px; - align-items: flex-start; - cursor: pointer; - color: #fff; -`; -const Center = styled.div` - width: 0; - flex: 1; - display: flex; - flex-direction: column; - gap: 5px; -`; - -const LatestMessage = styled.div` - color: #9c9c9c; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: normal; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const LatestMessageTime = styled.span` - color: #9c9c9c; - font-size: 12px; - font-style: normal; - font-weight: 400; - line-height: normal; -`; diff --git a/apps/u3/src/components/message/Conversations.tsx b/apps/u3/src/components/message/Conversations.tsx new file mode 100644 index 00000000..3c610fa8 --- /dev/null +++ b/apps/u3/src/components/message/Conversations.tsx @@ -0,0 +1,112 @@ +import { DecodedMessage } from '@xmtp/xmtp-js'; +import dayjs from 'dayjs'; +import { ComponentPropsWithRef, useEffect, useState } from 'react'; +import { + getAttachmentUrl, + isAttachment, + truncate, +} from '../../utils/message/xmtp'; +import useConversationList from '../../hooks/message/xmtp/useConversationList'; +import { + useXmtpClient, + MessageRoute, +} from '../../contexts/message/XmtpClientCtx'; +import Loading from '../common/loading/Loading'; +import NoConversations from './NoConversations'; +import { cn } from '@/lib/utils'; +import ProfileInfoHeadless from '../profile/info/ProfileInfoHeadless'; + +export default function Conversations({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { setMessageRouteParams } = useXmtpClient(); + const { isLoading, conversationList } = useConversationList(); + + return ( + <div className={cn('w-full', className)} {...props}> + {isLoading ? ( + <div className="flex justify-center items-center"> + <Loading /> + </div> + ) : conversationList.length > 0 ? ( + <div className="flex flex-col"> + {conversationList.map(({ conversation, latestMessage }) => ( + <ConversationCard + key={`Convo_${conversation.peerAddress}`} + selectConvoAction={(address) => { + setMessageRouteParams({ + route: MessageRoute.PRIVATE_CHAT, + peerAddress: address, + }); + }} + address={conversation.peerAddress} + latestMessage={latestMessage} + /> + ))} + </div> + ) : ( + <NoConversations /> + )} + </div> + ); +} + +function ConversationCard({ + selectConvoAction, + address, + latestMessage, +}: { + selectConvoAction: (address: string) => void; + address: string; + latestMessage: DecodedMessage | null; +}) { + const { xmtpClient } = useXmtpClient(); + const [attachmentUrl, setAttachmentUrl] = useState<string>(''); + useEffect(() => { + (async () => { + if (latestMessage && isAttachment(latestMessage)) { + const url = await getAttachmentUrl(latestMessage, xmtpClient); + setAttachmentUrl(url); + } + })(); + }, [latestMessage, xmtpClient]); + return ( + <div + className="px-0 py-[12px] flex gap-[10px] items-start cursor-pointer text-[#fff]" + onClick={() => selectConvoAction(address)} + > + <ProfileInfoHeadless identity={address} isSelf={false}> + {({ displayAvatar, displayName }) => { + return ( + <> + <img + src={displayAvatar} + alt="" + className="w-[50px] h-[50px] rounded-full" + /> + <div className="w-[0] flex-[1] flex flex-col gap-[5px]"> + {/* <Name address={address} /> */} + <span className="text-[#FFF] text-[16px] font-medium"> + {displayName} + </span> + <div className="text-[#9c9c9c] text-[12px] font-normal line-clamp-1"> + {latestMessage && + (() => { + if (isAttachment(latestMessage)) { + return attachmentUrl; + } + return truncate(latestMessage.content, 75); + })()} + </div> + </div> + <span className="text-[#9c9c9c] text-[12px] font-normal"> + {latestMessage && dayjs(latestMessage.sent).fromNow()} + </span> + </> + ); + }} + </ProfileInfoHeadless> + </div> + ); +} diff --git a/apps/u3/src/components/message/ConversationsPage.tsx b/apps/u3/src/components/message/ConversationsPage.tsx deleted file mode 100644 index 447a24c3..00000000 --- a/apps/u3/src/components/message/ConversationsPage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import styled from 'styled-components'; -import ConversationList from './ConversationList'; -// import SearchConversation from './SearchConversation'; -import MessageModalCloseBtn from './MessageModalCloseBtn'; -import StartNewConversation from './StartNewConversation'; - -export default function ConversationsPage() { - return ( - <Wrapper> - <Header> - <Title>Message - - - {/* */} - - - - ); -} -const Wrapper = styled.div` - width: 100%; - height: 100%; - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: 20px; -`; -const Header = styled.div` - display: flex; - justify-content: space-between; - align-items: center; -`; -const Title = styled.h1` - margin: 0; - padding: 0; - color: #fff; - font-size: 20px; - font-style: normal; - font-weight: 700; - line-height: normal; -`; -const Conversations = styled(ConversationList)` - height: 0; - flex: 1; - overflow-y: auto; -`; diff --git a/apps/u3/src/components/message/ConvoMessagesPage.tsx b/apps/u3/src/components/message/ConvoMessagesPage.tsx deleted file mode 100644 index ed80f6b2..00000000 --- a/apps/u3/src/components/message/ConvoMessagesPage.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import styled from 'styled-components'; -import { useNavigate } from 'react-router-dom'; -import { useMemo } from 'react'; -import BackIcon from '../common/icons/BackIcon'; -import Avatar from './Avatar'; -import Name from './Name'; -import SendMessageForm from './SendMessageForm'; -import MessageList from './MessageList'; -import MessageModalCloseBtn from './MessageModalCloseBtn'; -import { useNav } from '../../contexts/NavCtx'; -import { - useXmtpClient, - MessageRoute, -} from '../../contexts/message/XmtpClientCtx'; - -export default function ConvoMessagesPage() { - const navigate = useNavigate(); - const { setOpenMessageModal } = useNav(); - const { messageRouteParams, setMessageRouteParams } = useXmtpClient(); - const { peerAddress } = messageRouteParams; - const profileUrl = useMemo(() => `/u/${peerAddress}`, [peerAddress]); - return ( - -
- { - setMessageRouteParams({ - route: MessageRoute.SEARCH, - }); - }} - /> - - { - e.stopPropagation(); - e.preventDefault(); - navigate(profileUrl); - setOpenMessageModal(false); - }} - > - - - - { - e.stopPropagation(); - e.preventDefault(); - navigate(profileUrl); - setOpenMessageModal(false); - }} - > - - - - - - -
-
- -
- -
- ); -} - -const Wrapper = styled.div` - width: 100%; - height: 100%; - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: 20px; -`; -const BackBtn = styled(BackIcon)` - width: 20px; - height: 20px; - cursor: pointer; -`; -const Header = styled.div` - display: flex; - align-items: center; -`; -const HeaderCenter = styled.div` - width: 0; - flex: 1; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; -`; -const HeaderRight = styled.div` - width: 20px; - display: flex; - justify-content: end; -`; -const AvatarStyled = styled(Avatar)` - width: 20px; - height: 20px; -`; -const NameStyled = styled(Name)` - color: #fff; - font-size: 20px; - font-style: normal; - font-weight: 700; - line-height: normal; -`; -const Main = styled.div` - width: 100%; - height: 0; - flex: 1; - overflow-y: auto; -`; diff --git a/apps/u3/src/components/message/MessageList.tsx b/apps/u3/src/components/message/MessageList.tsx index 9ffcf957..52b967eb 100644 --- a/apps/u3/src/components/message/MessageList.tsx +++ b/apps/u3/src/components/message/MessageList.tsx @@ -1,22 +1,35 @@ import styled from 'styled-components'; import { DecodedMessage } from '@xmtp/xmtp-js'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useXmtpClient } from '../../contexts/message/XmtpClientCtx'; import { useXmtpStore } from '../../contexts/message/XmtpStoreCtx'; import { getAttachmentUrl, isAttachment } from '../../utils/message/xmtp'; -import Avatar from './Avatar'; -import { useNav } from '../../contexts/NavCtx'; +import ProfileInfoHeadless from '../profile/info/ProfileInfoHeadless'; export default function MessageList() { const { xmtpClient, messageRouteParams } = useXmtpClient(); const { loadingConversations, convoMessages } = useXmtpStore(); - const messages = - convoMessages.get(messageRouteParams?.peerAddress || '') || []; + const { peerAddress } = messageRouteParams; + const messages = convoMessages.get(peerAddress || '') || []; + const messageListEndRef = useRef(null); + const megLen = messages.length; + useEffect(() => { + if (messageListEndRef.current) { + messageListEndRef?.current?.scrollIntoView({ + behavior: 'smooth', + }); + } + }, [megLen]); + useEffect(() => { + if (messageListEndRef.current) { + messageListEndRef?.current?.scrollIntoView(); + } + }, [peerAddress]); return ( - +
{!loadingConversations && messages.map((msg) => { const isMe = xmtpClient?.address === msg.senderAddress; @@ -25,19 +38,13 @@ export default function MessageList() { } return ; })} - +
+
); } -const MessageListWrap = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 20px; -`; function MessageRow({ msg }: { msg: DecodedMessage }) { const navigate = useNavigate(); - const { setOpenMessageModal } = useNav(); const { xmtpClient } = useXmtpClient(); const [attachmentUrl, setAttachmentUrl] = useState(''); useEffect(() => { @@ -54,17 +61,26 @@ function MessageRow({ msg }: { msg: DecodedMessage }) { ); return ( - { - e.stopPropagation(); - e.preventDefault(); - navigate(profileUrl); - setOpenMessageModal(false); + + {({ displayAvatar }) => { + return ( + { + e.stopPropagation(); + e.preventDefault(); + navigate(profileUrl); + }} + > + + + ); }} - > - - + {(() => { @@ -99,7 +115,6 @@ function MyMessageRow({ msg }: { msg: DecodedMessage }) { return {msg.content}; })()} - ); } @@ -110,29 +125,27 @@ const MessageRowWrapper = styled.div` gap: 15px; `; -const AvatarStyled = styled(Avatar)` - width: 20px; - height: 20px; -`; const MessageWrapper = styled.div` max-width: calc(100% - (15px + 20px) * 2); flex-shrink: 0; border-radius: 10px 10px 10px 0px; background: #000; + overflow: hidden; + padding: 20px; + box-sizing: border-box; `; -const Text = styled.span` +const Text = styled.div` + width: 100%; color: #fff; font-size: 16px; font-style: normal; font-weight: 400; line-height: normal; - - margin: 20px; + word-break: break-all; `; const Image = styled.img` max-width: 120px; object-fit: cover; - margin: 20px; `; const MyMessageRowWrapper = styled(MessageRowWrapper)` diff --git a/apps/u3/src/components/message/MessageModal.tsx b/apps/u3/src/components/message/MessageModal.tsx deleted file mode 100644 index 37e48dad..00000000 --- a/apps/u3/src/components/message/MessageModal.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import styled from 'styled-components'; -import loadable from '@loadable/component'; -import { useNav } from '../../contexts/NavCtx'; - -const MessageModalBody = loadable(() => import('./MessageModalBody')); - -export default function MessageModal() { - const { openMessageModal } = useNav(); - return ( - - {openMessageModal && } - - ); -} - -const Wrapper = styled.div<{ open: boolean }>` - z-index: 3; - - position: absolute; - bottom: 20px; - right: 0px; - transform: translateX(100%); - - display: ${({ open }) => (open ? 'block' : 'none')}; -`; diff --git a/apps/u3/src/components/message/MessageModalBody.tsx b/apps/u3/src/components/message/MessageModalBody.tsx deleted file mode 100644 index 2329ca02..00000000 --- a/apps/u3/src/components/message/MessageModalBody.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import styled from 'styled-components'; -import { - useXmtpClient, - MessageRoute, -} from '../../contexts/message/XmtpClientCtx'; -import NoEnableXmtp from './NoEnableXmtp'; -import ConversationsPage from './ConversationsPage'; -import ConvoMessagesPage from './ConvoMessagesPage'; -import { XmtpStoreProvider } from '../../contexts/message/XmtpStoreCtx'; - -export default function MessageModalBody() { - const { xmtpClient, messageRouteParams } = useXmtpClient(); - return ( - - - {(() => { - if (!xmtpClient) { - return ; - } - if (messageRouteParams.route === MessageRoute.SEARCH) { - return ; - } - if (messageRouteParams.route === MessageRoute.DETAIL) { - return ; - } - return null; - })()} - - - ); -} - -const Wrapper = styled.div` - width: 400px; - height: 760px; - max-height: 80vh; - padding: 20px; - box-sizing: border-box; - flex-shrink: 0; - border-radius: 10px; - border: 1px solid #39424c; - background: #1b1e23; - - margin-left: 10px; -`; diff --git a/apps/u3/src/components/message/MessageModalCloseBtn.tsx b/apps/u3/src/components/message/MessageModalCloseBtn.tsx deleted file mode 100644 index 6fc9f485..00000000 --- a/apps/u3/src/components/message/MessageModalCloseBtn.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { ModalCloseBtn } from '../common/modal/ModalWidgets'; -import { useNav } from '../../contexts/NavCtx'; - -export default function MessageModalCloseBtn() { - const { setOpenMessageModal } = useNav(); - return setOpenMessageModal(false)} />; -} diff --git a/apps/u3/src/components/message/Name.tsx b/apps/u3/src/components/message/Name.tsx deleted file mode 100644 index a0d727fa..00000000 --- a/apps/u3/src/components/message/Name.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { UserName, UserNameProps } from '@us3r-network/profile'; -import { getDidPkhWithAddress } from '../../utils/shared/did'; - -export default function Name({ - address, - ...otherProps -}: UserNameProps & { address: string }) { - return ; -} diff --git a/apps/u3/src/components/message/NoConversations.tsx b/apps/u3/src/components/message/NoConversations.tsx index f86efde5..fc0f4d4f 100644 --- a/apps/u3/src/components/message/NoConversations.tsx +++ b/apps/u3/src/components/message/NoConversations.tsx @@ -1,25 +1,22 @@ import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; -import { useNav } from '../../contexts/NavCtx'; -import { SocialButtonPrimary } from '../social/button/SocialButton'; -import { FollowType } from '../profile/ProfilePageFollowNav'; +import ColorButton from '../common/button/ColorButton'; export default function NoConversations() { const navigate = useNavigate(); - const { setOpenMessageModal } = useNav(); return ( There is nothing here. Send a message to your friend? - { - setOpenMessageModal(false); - navigate(`/u?followType=${FollowType.FOLLOWERS}`); + navigate(`/u/contacts`); }} > - Find from my following/follower list - + Find from
+ my following/follower list +
); } diff --git a/apps/u3/src/components/message/NoEnableXmtp.tsx b/apps/u3/src/components/message/NoEnableXmtp.tsx index d51eeff4..1e01dd63 100644 --- a/apps/u3/src/components/message/NoEnableXmtp.tsx +++ b/apps/u3/src/components/message/NoEnableXmtp.tsx @@ -1,75 +1,56 @@ import styled from 'styled-components'; import { useAccount } from 'wagmi'; import { useXmtpClient } from '../../contexts/message/XmtpClientCtx'; -import MessageModalCloseBtn from './MessageModalCloseBtn'; -import { SocialButtonPrimary } from '../social/button/SocialButton'; +import ColorButton from '../common/button/ColorButton'; export default function NoEnableXmtp() { const { isConnected, isConnecting } = useAccount(); const { xmtpClient, enablingXmtp, enableXmtp } = useXmtpClient(); return ( - <> -
- -
- - {(() => { - if (!xmtpClient) { - return ( - <> - - {(() => { - if (enablingXmtp) { - return 'Enabling your XMTP identity...'; - } - if (isConnecting) { - return 'Connecting Wallet...'; - } - if (!isConnected) { - return 'Connect Your Wallet'; - } - return ''; - })()} - - enableXmtp()} - > - {(() => { - if (enablingXmtp) { - return 'Enabling...'; - } - if (isConnecting) { - return 'Connecting...'; - } - if (!isConnected) { - return 'Connect'; - } - return ''; - })()} - - - ); - } - return Enabled Xmtp; - })()} - - +
+ {(() => { + if (!xmtpClient) { + return ( + <> + + {(() => { + if (enablingXmtp) { + return 'Enabling your XMTP identity...'; + } + if (isConnecting) { + return 'Connecting Wallet...'; + } + if (!isConnected) { + return 'Connect Your Wallet'; + } + return 'Enable Xmtp'; + })()} + + enableXmtp()} + > + {(() => { + if (enablingXmtp) { + return 'Enabling...'; + } + if (isConnecting) { + return 'Connecting...'; + } + if (!isConnected) { + return 'Connect'; + } + return 'Enable'; + })()} + + + ); + } + return Enabled Xmtp; + })()} +
); } - -const Wrapper = styled.div` - padding-top: 42px; - width: 100%; - display: flex; - flex-direction: column; - gap: 20px; - align-items: center; -`; -const Header = styled.div` - display: flex; - justify-content: end; -`; const Description = styled.span` color: #9c9c9c; text-align: center; @@ -78,7 +59,3 @@ const Description = styled.span` font-weight: 400; line-height: normal; `; - -const LoginButton = styled(SocialButtonPrimary)` - width: 240px; -`; diff --git a/apps/u3/src/components/message/SearchConversation.tsx b/apps/u3/src/components/message/SearchConversation.tsx deleted file mode 100644 index 41afff79..00000000 --- a/apps/u3/src/components/message/SearchConversation.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import styled from 'styled-components'; -import SearchInput from '../common/input/SearchInput'; - -export default function SearchConversation() { - return ( - - {}} disabled placeholder="Comming soon" /> - - ); -} -const SearchConversationWrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 10px; -`; -const Search = styled(SearchInput)` - height: 40px; - border-radius: 10px; - background: #2b2c31; -`; diff --git a/apps/u3/src/components/message/SendMessageForm.tsx b/apps/u3/src/components/message/SendMessageForm.tsx index d7c97a4b..03e55e57 100644 --- a/apps/u3/src/components/message/SendMessageForm.tsx +++ b/apps/u3/src/components/message/SendMessageForm.tsx @@ -33,7 +33,8 @@ export default function SendMessageForm() { const fileInputRef = useRef(null); return ( - { e.preventDefault(); if (!peerAddress) return; @@ -86,7 +87,7 @@ export default function SendMessageForm() { /> - + - + ); } -const SendMessageFormWrap = styled.form` - width: 100%; - min-height: 40px; - border-radius: 10px; - background: #fff; - padding: 10px; - box-sizing: border-box; -`; const FormWrapper = styled.div` width: 100%; border-radius: 10px; - background: #fff; display: flex; align-items: center; gap: 10px; @@ -134,7 +126,6 @@ const TextArea = styled(TextareaBase)` background: none; border: none; &:focus-within { - color: black; border-color: none; background: none; } @@ -158,7 +149,6 @@ const DelFileIcon = styled.div` height: 20px; width: 20px; background-color: red; - color: white; display: flex; align-items: center; justify-content: center; @@ -166,6 +156,7 @@ const DelFileIcon = styled.div` cursor: pointer; `; const SubmitButton = styled(ButtonPrimary)` - width: 16px; height: 16px; + background: red; + color: white; `; diff --git a/apps/u3/src/components/message/StartNewConversation.tsx b/apps/u3/src/components/message/StartNewConversation.tsx index 3b7fbcd6..89c36782 100644 --- a/apps/u3/src/components/message/StartNewConversation.tsx +++ b/apps/u3/src/components/message/StartNewConversation.tsx @@ -1,12 +1,11 @@ -import styled from 'styled-components'; import { useState } from 'react'; import { MessageRoute, useXmtpClient, } from '../../contexts/message/XmtpClientCtx'; import useStartNewConvo from '../../hooks/message/xmtp/useStartNewConvo'; +import ColorButton from '../common/button/ColorButton'; import InputBase from '../common/input/InputBase'; -import { ButtonPrimaryLine } from '../common/button/ButtonBase'; export default function StartNewConversation() { const { setMessageRouteParams } = useXmtpClient(); @@ -14,25 +13,10 @@ export default function StartNewConversation() { const [errMsg, setErrMsg] = useState(''); const [convoAddress, setConvoAddress] = useState(''); return ( - { - e.preventDefault(); - startNewConvo(convoAddress, { - onSuccess: () => { - setConvoAddress(''); - setMessageRouteParams({ - route: MessageRoute.DETAIL, - peerAddress: convoAddress, - }); - }, - onFail: (error) => { - setErrMsg(error.message); - }, - }); - }} - > - - +
+ - + { + e.preventDefault(); + startNewConvo(convoAddress, { + onSuccess: () => { + setConvoAddress(''); + setMessageRouteParams({ + route: MessageRoute.PRIVATE_CHAT, + peerAddress: convoAddress, + }); + }, + onFail: (error) => { + setErrMsg(error.message); + }, + }); + }} + > Start - - - {errMsg && {errMsg}} - + +
+ {errMsg &&

{errMsg}

} +
); } -const StartNewConversationWrap = styled.form` - width: 100%; - display: flex; - flex-direction: column; - gap: 10px; -`; -const StartFormWrap = styled.div` - display: flex; - gap: 10px; -`; -const ConvoAddress = styled(InputBase)` - width: 0; - flex: 1; -`; -const SubmitButton = styled(ButtonPrimaryLine)` - height: 40px; -`; -const ErrMsg = styled.p` - color: red; -`; diff --git a/apps/u3/src/components/news/links/community/CommunityLinks.tsx b/apps/u3/src/components/news/links/community/CommunityLinks.tsx index a6324d91..0f725fbd 100644 --- a/apps/u3/src/components/news/links/community/CommunityLinks.tsx +++ b/apps/u3/src/components/news/links/community/CommunityLinks.tsx @@ -85,6 +85,7 @@ export default function CommunityLinks({ const Box = styled(MainWrapper)` width: 100%; + height: 100%; display: flex; flex-direction: column; gap: 24px; diff --git a/apps/u3/src/components/news/links/community/LinksListItem.tsx b/apps/u3/src/components/news/links/community/LinksListItem.tsx index 550f0f6a..68fca145 100644 --- a/apps/u3/src/components/news/links/community/LinksListItem.tsx +++ b/apps/u3/src/components/news/links/community/LinksListItem.tsx @@ -32,7 +32,6 @@ export default function ListItem({ data, ...otherProps }: Props) { const CardWrapper = styled.div` width: 100%; - background: #1b1e23; overflow: hidden; cursor: pointer; `; diff --git a/apps/u3/src/components/notification/MobileNotificationNavBtn.tsx b/apps/u3/src/components/notification/MobileNotificationNavBtn.tsx deleted file mode 100644 index 8a154623..00000000 --- a/apps/u3/src/components/notification/MobileNotificationNavBtn.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useIsAuthenticated } from '@us3r-network/auth-with-rainbowkit'; -import { useMemo } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { PcNavItem, NavItemIconBox } from '../layout/Nav'; -import { ReactComponent as BellSvg } from '../../route/svgs/bell.svg'; -import { - useNotificationStore, - NotificationStoreProvider, -} from '../../contexts/notification/NotificationStoreCtx'; -import useFarcasterCurrFid from '../../hooks/social/farcaster/useFarcasterCurrFid'; -import useRoute from '@/route/useRoute'; -import { RouteKey } from '@/route/routes'; - -export default function NotificationButtonContainer() { - const isAuthenticated = useIsAuthenticated(); - const fid = Number(useFarcasterCurrFid()); - if (!isAuthenticated) return null; - return ( - - - - ); -} - -function NotificationButton() { - const navigate = useNavigate(); - const { unreadCount, clearUnread } = useNotificationStore(); - const { firstRouteMeta } = useRoute(); - const isActive = useMemo( - () => firstRouteMeta.key === RouteKey.notification, - [firstRouteMeta] - ); - - return ( - { - if (unreadCount && !isActive) clearUnread(); - navigate('/notification'); - }} - /> - ); -} - -function NotificationButtonStyled({ - unreadCount, - isActive, - onClick, -}: { - unreadCount: number; - isActive: boolean; - onClick: () => void; -}) { - return ( - - - - {unreadCount > 0 && ( -
- )} - - - ); -} diff --git a/apps/u3/src/components/notification/NotificationModal.tsx b/apps/u3/src/components/notification/NotificationModal.tsx deleted file mode 100644 index 2fda6759..00000000 --- a/apps/u3/src/components/notification/NotificationModal.tsx +++ /dev/null @@ -1,534 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import { useNavigate } from 'react-router-dom'; -import { Notification as LensNotification } from '@lens-protocol/react-web'; - -import { MessageType, ReactionType } from '@farcaster/hub-web'; -import InfiniteScroll from 'react-infinite-scroll-component'; -import dayjs from 'dayjs'; -import getContent from 'src/utils/social/lens/getContent'; -import { getHandle, getName } from 'src/utils/social/lens/profile'; -import { useNotificationStore } from '../../contexts/notification/NotificationStoreCtx'; -import { ModalCloseBtn } from '../common/modal/ModalWidgets'; -import { FarcasterNotification } from '../../services/social/api/farcaster'; -import useFarcasterUserData from '../../hooks/social/farcaster/useFarcasterUserData'; -import getAvatar from '../../utils/social/lens/getAvatar'; -import LensIcon from '../common/icons/LensIcon'; -import FarcasterIcon from '../common/icons/FarcasterIcon'; -import Loading from '../common/loading/Loading'; -import { useNav } from '../../contexts/NavCtx'; -// import { NotificationSettingsGroup } from './PushNotificationsToogleBtn'; - -export default function NotificationModal() { - const { openNotificationModal, setOpenNotificationModal } = useNav(); - const { notifications, loading, hasMore, loadMore, farcasterUserData } = - useNotificationStore(); - return ( - - -
- Notifications - {/*
- -
*/} - setOpenNotificationModal(false)} /> -
- {notifications && notifications.length > 0 && ( - - { - if (loading) return; - loadMore(); - }} - hasMore={hasMore} - loader={ - loading ? ( - - - - ) : null - } - scrollableTarget="notification-list-wraper" - scrollThreshold="200px" - endMessage="No More Notifications!" - style={{ - color: '#718096', - textAlign: 'center', - lineHeight: '32px', - fontSize: '14px', - }} - > - - {notifications.map((notification, index) => { - if ('message_hash' in notification) { - return ( - - ); - } - if ('id' in notification) { - return ( - - ); - } - return null; - })} - {/* {hasMore && ( - - )} */} - - - - )} - -
- ); -} - -interface FarcasterNotificationItemProps { - notification: FarcasterNotification; - farcasterUserData: { [key: string]: { type: number; value: string }[] }; -} - -export function FarcasterNotificationItem({ - notification, - farcasterUserData, -}: StyledComponentPropsWithRef<'div'> & FarcasterNotificationItemProps) { - const navigate = useNavigate(); - const { setOpenNotificationModal } = useNav(); - const userData = useFarcasterUserData({ - fid: String(notification.message_fid), - farcasterUserData, - }); - switch (notification.message_type) { - case MessageType.CAST_ADD: - if (notification.replies_text && notification.replies_parent_hash) { - return ( - { - navigate( - `/social/post-detail/fcast/${Buffer.from( - notification.replies_parent_hash - ).toString('hex')}#${Buffer.from( - notification.replies_hash - ).toString('hex')}` - ); - setOpenNotificationModal(false); - }} - > - - - - {userData.userName} commented on your cast - - {notification.replies_text} - - {dayjs(notification.message_timestamp).fromNow()} - - - - - ); - } - if ( - notification.casts_mentions && - notification.casts_mentions.length > 0 - ) { - return ( - { - navigate( - `/social/post-detail/fcast/${Buffer.from( - notification.casts_hash - ).toString('hex')}` - ); - setOpenNotificationModal(false); - }} - > - - - - {userData.userName} mentions you in his cast - - {notification.casts_text} - - {dayjs(notification.message_timestamp).fromNow()} - - - - - ); - } - break; - case MessageType.REACTION_ADD: - switch (notification.reaction_type) { - case ReactionType.LIKE: - return ( - { - navigate( - `/social/post-detail/fcast/${Buffer.from( - notification.casts_hash - ).toString('hex')}` - ); - setOpenNotificationModal(false); - }} - > - - - - {userData.userName} like your cast - - {notification.casts_text} - - {dayjs(notification.message_timestamp).fromNow()} - - - - - ); - break; - case ReactionType.RECAST: - return ( - { - navigate( - `/social/post-detail/fcast/${Buffer.from( - notification.casts_hash - ).toString('hex')}` - ); - setOpenNotificationModal(false); - }} - > - - - - {userData.userName} recast your cast - - {notification.casts_text} - - {dayjs(notification.message_timestamp).fromNow()} - - - - - ); - break; - default: - break; - } - break; - case MessageType.LINK_ADD: - return ( - { - navigate(`/u/${userData.userName}.fcast`); - setOpenNotificationModal(false); - }} - > - - - - {userData.userName} follows you - - - {dayjs(notification.message_timestamp).fromNow()} - - - - - ); - break; - default: - return null; - } -} - -interface LensNotificationItemProps { - notification: LensNotification; -} - -enum LensNotificationType { - NEW_FOLLOWER = 'FollowNotification', - NEW_COMMENT = 'CommentNotification', - NEW_MIRROR = 'MirrorNotification', - NEW_MENTION = 'MentionNotification', - NEW_REACTION = 'ReactionNotification', -} - -function LensNotificationItem({ - notification, -}: StyledComponentPropsWithRef<'div'> & LensNotificationItemProps) { - const navigate = useNavigate(); - const { setOpenNotificationModal } = useNav(); - switch (notification.__typename) { - case LensNotificationType.NEW_COMMENT: - return ( - { - navigate( - `/social/post-detail/lens/${notification?.comment?.commentOn?.id}#${notification?.comment?.id}` - ); - setOpenNotificationModal(false); - }} - > - - - - - {getName(notification.comment.by) || - getHandle(notification.comment.by)} - {' '} - commented on your post - - {getContent(notification.comment.metadata)} - - {dayjs(notification.comment.createdAt).fromNow()} - - - - - ); - break; - case LensNotificationType.NEW_REACTION: - return ( - { - navigate(`/social/post-detail/lens/${notification.publication.id}`); - setOpenNotificationModal(false); - }} - > - - - - - {getName(notification.reactions[0].profile) || - getHandle(notification.reactions[0].profile)} - {' '} - like your post - - {getContent(notification.publication.metadata)} - - {dayjs( - notification.reactions[0].reactions[0].reactedAt - ).fromNow()} - - - - - ); - break; - case LensNotificationType.NEW_MIRROR: - return ( - { - navigate(`/social/post-detail/lens/${notification.publication.id}`); - setOpenNotificationModal(false); - }} - > - - - - - {getName(notification.mirrors[0].profile) || - getHandle(notification.mirrors[0].profile)} - {' '} - mirror your post - - {getContent(notification.publication.metadata)} - - {dayjs(notification.mirrors[0].mirroredAt).fromNow()} - - - - - ); - break; - case LensNotificationType.NEW_FOLLOWER: - return ( - { - navigate(`/u/${notification.followers[0].handle.fullHandle}.lens`); - setOpenNotificationModal(false); - }} - > - - - - - {getName(notification.followers[0]) || - getHandle(notification.followers[0])} - {' '} - follows you - - - {dayjs(notification.followers[0].createdAt).fromNow()} - - - - - ); - break; - case LensNotificationType.NEW_MENTION: - return ( - { - navigate(`/social/post-detail/lens/${notification.publication.id}`); - setOpenNotificationModal(false); - }} - > - - - - - {getName(notification.publication.by) || - getHandle(notification.publication.by)} - {' '} - mentions you - - - {dayjs(notification.publication.createdAt).fromNow()} - - - - - ); - break; - default: - return null; - } -} - -export const Wrapper = styled.div<{ open: boolean }>` - z-index: 3; - - position: absolute; - bottom: 20px; - right: 0px; - transform: translateX(100%); - - display: ${({ open }) => (open ? 'block' : 'none')}; -`; -export const Body = styled.div` - width: 400px; - height: 760px; - max-height: 80vh; - padding: 20px; - box-sizing: border-box; - flex-shrink: 0; - border-radius: 10px; - border: 1px solid #39424c; - background: #1b1e23; - - margin-left: 10px; -`; -export const Header = styled.div` - display: flex; - justify-content: space-between; -`; -export const Title = styled.h1` - margin: 0; - padding: 0; - color: #fff; - font-size: 20px; - font-style: normal; - font-weight: 700; - line-height: normal; -`; -export const NotificationListWraper = styled.div` - width: 100%; - height: 95%; - margin-top: 20px; - overflow: auto; -`; -export const NotificationList = styled.div` - display: flex; - flex-direction: column; - justify-content: start; - gap: 0px; - height: 70%; - overflow: hidden; -`; -const NotificationItem = styled.div` - display: flex; - justify-content: start; - align-items: start; - gap: 20px; - color: #fff; - font-size: 16px; - font-style: normal; - font-weight: 600; - padding: 10px; - &:hover { - background-color: #39424c; - } - cursor: pointer; - /* line-height: normal; */ - /* :first-child { - flex-grow: 0; - flex-shrink: 0; - } - :last-child { - flex-grow: 0; - flex-shrink: 0; - } */ -`; -const Avatar = styled.img` - width: 24px; - height: 24px; - border-radius: 50%; - margin-top: 6px; - object-fit: cover; -`; -const UserActionWraper = styled.div` - flex-grow: 1; - flex-shrink: 1; - display: flex; - flex-direction: column; - justify-content: start; - align-items: start; - gap: 10px; - width: 100%; -`; -const UserAction = styled.div` - max-width: 100%; - text-align: start; - line-height: 24px; -`; -const PostText = styled.div` - width: 250px; - color: #718096; - font-size: 16px; - font-weight: 400; - text-align: start; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - line-height: 24px; -`; -const DateText = styled.div` - max-width: 100%; - color: #718096; - font-size: 12px; - font-weight: 400; -`; -const LoadingMoreWrapper = styled.div` - width: 100%; - display: flex; - justify-content: center; - align-items: center; - margin-top: 20px; -`; diff --git a/apps/u3/src/components/notification/NotificationNavBtn.tsx b/apps/u3/src/components/notification/NotificationNavBtn.tsx deleted file mode 100644 index 844312d8..00000000 --- a/apps/u3/src/components/notification/NotificationNavBtn.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-12-29 18:44:14 - * @LastEditors: bufan bufan@hotmail.com - * @LastEditTime: 2023-10-30 15:02:12 - * @Description: file description - */ -import { useIsAuthenticated } from '@us3r-network/auth-with-rainbowkit'; -import { PcNavItem, NavItemIconBox } from '../layout/Nav'; -import { ReactComponent as BellSvg } from '../../route/svgs/bell.svg'; -import { - useNotificationStore, - NotificationStoreProvider, -} from '../../contexts/notification/NotificationStoreCtx'; -import NotificationModal from './NotificationModal'; -import useFarcasterCurrFid from '../../hooks/social/farcaster/useFarcasterCurrFid'; -import { NavModalName, useNav } from '../../contexts/NavCtx'; - -export default function NotificationButtonContainer() { - const isAuthenticated = useIsAuthenticated(); - const fid = Number(useFarcasterCurrFid()); - if (!isAuthenticated) return null; - return ( - - - - ); -} - -function NotificationButton() { - const { openNotificationModal, switchNavModal } = useNav(); - const { unreadCount, clearUnread } = useNotificationStore(); - - return ( - <> - { - if (unreadCount && !openNotificationModal) clearUnread(); - switchNavModal(NavModalName.Notification); - }} - /> - - - - ); -} - -function NotificationButtonStyled({ - unreadCount, - isActive, - onClick, -}: { - unreadCount: number; - isActive: boolean; - onClick: () => void; -}) { - const { renderNavItemText } = useNav(); - return ( - - - - {unreadCount > 0 && ( -
- )} - - {renderNavItemText(`Notifications`)} - {unreadCount > 0 && ( -
- {unreadCount} -
- )} - - ); -} diff --git a/apps/u3/src/components/notification/PushNotificationsToogleBtn.tsx b/apps/u3/src/components/notification/PushNotificationsToogleBtn.tsx index 2fae5858..37b50ddb 100644 --- a/apps/u3/src/components/notification/PushNotificationsToogleBtn.tsx +++ b/apps/u3/src/components/notification/PushNotificationsToogleBtn.tsx @@ -151,24 +151,25 @@ export function NotificationSettingsGroup() { return ( //
//

Web Push

- <> +
- {(() => { - if (webpushLoading) { - if (webpushSubscribed) { - return 'Unsubscribing...'; + + {(() => { + if (webpushLoading) { + if (webpushSubscribed) { + return 'Unsubscribing...'; + } + return 'Subscribing...'; } - return 'Subscribing...'; - } - return 'Subscribe Notifications'; - })()} + return 'Subscribe'; + })()} - {/* {!(isLoginFarcaster && farcasterUserInfo) && ( + {/* {!(isLoginFarcaster && farcasterUserInfo) && ( openFarcasterQR()} @@ -176,7 +177,8 @@ export function NotificationSettingsGroup() { Login Farcaster )} */} - + +
//
); } diff --git a/apps/u3/src/components/notification/farcaster/FarcasterNotificationItem.tsx b/apps/u3/src/components/notification/farcaster/FarcasterNotificationItem.tsx index e5d80053..e86375dc 100644 --- a/apps/u3/src/components/notification/farcaster/FarcasterNotificationItem.tsx +++ b/apps/u3/src/components/notification/farcaster/FarcasterNotificationItem.tsx @@ -1,11 +1,12 @@ import { useNavigate } from 'react-router-dom'; -import { MessageType, ReactionType } from '@farcaster/hub-web'; +import { ReactionType } from '@farcaster/hub-web'; import { useMemo } from 'react'; import { FarcasterNotification } from '@/services/social/api/farcaster'; import useFarcasterUserData from '@/hooks/social/farcaster/useFarcasterUserData'; import NotificationItem, { NotificationActionType, } from '../ui/NotificationItem'; +import { NotificationType as FarcasterNotificationType } from '@/services/notification/types/notifications'; interface FarcasterNotificationItemProps { notification: FarcasterNotification; @@ -23,12 +24,12 @@ export default function FarcasterNotificationItem({ const viewData = useMemo(() => { let actionType: NotificationActionType; - switch (notification.message_type) { - case MessageType.CAST_ADD: + switch (notification.type) { + case FarcasterNotificationType.REPLY: actionType = NotificationActionType.comment; break; - case MessageType.REACTION_ADD: - switch (notification.reaction_type) { + case FarcasterNotificationType.REACTION: + switch (notification.reactions_type) { case ReactionType.LIKE: actionType = NotificationActionType.like; break; @@ -39,9 +40,12 @@ export default function FarcasterNotificationItem({ break; } break; - case MessageType.LINK_ADD: + case FarcasterNotificationType.FOLLOW: actionType = NotificationActionType.follow; break; + case FarcasterNotificationType.MENTION: + actionType = NotificationActionType.mention; + break; default: break; } @@ -49,23 +53,25 @@ export default function FarcasterNotificationItem({ actionType, userName: userData.userName, userAvatar: userData.pfp, - text: notification.replies_text, + timeStamp: notification.message_timestamp, + text: notification.replies_text || notification.casts_text, }; }, [notification, userData]); const clickAction = () => { - switch (notification.message_type) { - case MessageType.CAST_ADD: + console.log('notification', notification); + switch (notification.type) { + case FarcasterNotificationType.REPLY: navigate( `/social/post-detail/fcast/${Buffer.from( notification.replies_parent_hash - ).toString('hex')}#${Buffer.from(notification.casts_hash).toString( + ).toString('hex')}#${Buffer.from(notification.replies_hash).toString( 'hex' )}` ); break; - case MessageType.REACTION_ADD: - switch (notification.reaction_type) { + case FarcasterNotificationType.REACTION: + switch (notification.reactions_type) { case ReactionType.LIKE: case ReactionType.RECAST: navigate( @@ -78,6 +84,18 @@ export default function FarcasterNotificationItem({ break; } break; + case FarcasterNotificationType.MENTION: + navigate( + `/social/post-detail/fcast/${Buffer.from( + notification.casts_hash + ).toString('hex')}#${Buffer.from(notification.casts_hash).toString( + 'hex' + )}` + ); + break; + case FarcasterNotificationType.FOLLOW: + navigate('/u/contacts?type=follower'); + break; default: break; } diff --git a/apps/u3/src/components/notification/ui/NotificationItem.tsx b/apps/u3/src/components/notification/ui/NotificationItem.tsx index d3665742..6946af86 100644 --- a/apps/u3/src/components/notification/ui/NotificationItem.tsx +++ b/apps/u3/src/components/notification/ui/NotificationItem.tsx @@ -1,4 +1,5 @@ import { ComponentPropsWithRef } from 'react'; +import dayjs from 'dayjs'; import { cn } from '@/lib/utils'; export enum NotificationActionType { @@ -28,6 +29,7 @@ type NotificationData = { actionType: NotificationActionType; userName: string; userAvatar?: string; + timeStamp?: string; text?: string; }; interface NotificationItemProps extends ComponentPropsWithRef<'div'> { @@ -38,7 +40,7 @@ export default function NotificationItem({ className, ...divProps }: NotificationItemProps) { - const { actionType, userName, userAvatar, text } = data; + const { actionType, userName, userAvatar, text, timeStamp } = data; return (
- - {userName} - {getActionTypeTitle(actionType)} - +
+

+ {userName} + {getActionTypeTitle(actionType)} +

+

+ {dayjs(timeStamp).fromNow()} +

+
{text?.trim() && ( - +

{text} - +

)}
diff --git a/apps/u3/src/components/poster/DailyPosterBtn.tsx b/apps/u3/src/components/poster/DailyPosterBtn.tsx new file mode 100644 index 00000000..25f8964e --- /dev/null +++ b/apps/u3/src/components/poster/DailyPosterBtn.tsx @@ -0,0 +1,43 @@ +import { ComponentPropsWithRef, useState } from 'react'; +import DailyPosterModal from './DailyPosterModal'; +import { DailyPosterLayoutData } from './layout/DailyPosterLayout'; +import { cn } from '@/lib/utils'; +import ColorButton from '../common/button/ColorButton'; + +export default function DailyPosterBtn({ + className, + posts, + farcasterUserData, + topics, + dapps, + links, + ...props +}: ComponentPropsWithRef<'button'> & DailyPosterLayoutData) { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)} + {...props} + > + Mint Daily Poster + + setOpen(false)} + /> + + ); +} diff --git a/apps/u3/src/components/poster/DailyPosterModal.tsx b/apps/u3/src/components/poster/DailyPosterModal.tsx index f6c4b547..68960206 100644 --- a/apps/u3/src/components/poster/DailyPosterModal.tsx +++ b/apps/u3/src/components/poster/DailyPosterModal.tsx @@ -3,7 +3,7 @@ import DailyPosterLayout, { DailyPosterLayoutProps, } from './layout/DailyPosterLayout'; import ModalBase from '../common/modal/ModalBase'; -import { API_BASE_URL } from '@/constants'; +import { POSTER_IMG_URL } from '@/constants'; import { ModalCloseBtn } from '@/components/common/modal/ModalWidgets'; import PosterShare from './PosterShare'; import PosterMint from './mint/PosterMint'; @@ -13,7 +13,7 @@ type Props = DailyPosterLayoutProps & { open: boolean; closeModal: () => void; }; -const posterImg = `${API_BASE_URL}/static-assets/poster/poster.webp`; +const posterImg = POSTER_IMG_URL; export default function DailyPosterModal({ posts, farcasterUserData, diff --git a/apps/u3/src/components/poster/PosterMenu.tsx b/apps/u3/src/components/poster/PosterMenu.tsx deleted file mode 100644 index d4fcedc9..00000000 --- a/apps/u3/src/components/poster/PosterMenu.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { ComponentPropsWithRef, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import DailyPosterModal from './DailyPosterModal'; -import { DailyPosterLayoutProps } from './layout/DailyPosterLayout'; -import { cn } from '@/lib/utils'; - -export default function PosterMenu({ - disabled, - ...layoutProps -}: DailyPosterLayoutProps & { disabled?: boolean }) { - const navigate = useNavigate(); - const [open, setOpen] = useState(false); - return ( - <> -
- setOpen(true)}> - - - - Free Mint - - - navigate('/poster-gallery')}> - - - - Poster Gallery - - - toast.info('Coming Soon!')}> - - - - Subscribe - -
- {DailyPosterModal && ( - setOpen(false)} - /> - )} - - ); -} -function MenuItem({ className, ...props }: ComponentPropsWithRef<'div'>) { - return ( -
- ); -} -function MenuText({ className, ...props }: ComponentPropsWithRef<'span'>) { - return ( - - ); -} -function MenuLine({ className, ...props }: ComponentPropsWithRef<'div'>) { - return ( -
- ); -} diff --git a/apps/u3/src/components/poster/gallery/GalleryItem.tsx b/apps/u3/src/components/poster/gallery/GalleryItem.tsx index 14b5b3b5..67d41a41 100644 --- a/apps/u3/src/components/poster/gallery/GalleryItem.tsx +++ b/apps/u3/src/components/poster/gallery/GalleryItem.tsx @@ -83,7 +83,7 @@ export default function GalleryItem({ closeModal={() => setOpenPreviewModal(false)} /> setOpenPreviewModal(true)} diff --git a/apps/u3/src/components/poster/layout/DailyPosterLayout.tsx b/apps/u3/src/components/poster/layout/DailyPosterLayout.tsx index 8668dc74..65a49742 100644 --- a/apps/u3/src/components/poster/layout/DailyPosterLayout.tsx +++ b/apps/u3/src/components/poster/layout/DailyPosterLayout.tsx @@ -10,13 +10,13 @@ import TopTopics, { TopTopicsProps } from './topics/TopTopics'; export type DailyPosterLayoutData = TopPostsProps & TopTopicsProps & HighScoreDappsProps & - TopLinksProps; - -export type DailyPosterLayoutProps = ComponentPropsWithRef<'div'> & - DailyPosterLayoutData & { + TopLinksProps & { createAt?: number; }; +export type DailyPosterLayoutProps = ComponentPropsWithRef<'div'> & + DailyPosterLayoutData; + export default function DailyPosterLayout({ posts, farcasterUserData, diff --git a/apps/u3/src/components/poster/mint/MintSuccessModalBody.tsx b/apps/u3/src/components/poster/mint/MintSuccessModalBody.tsx index 1123688d..3e92bf60 100644 --- a/apps/u3/src/components/poster/mint/MintSuccessModalBody.tsx +++ b/apps/u3/src/components/poster/mint/MintSuccessModalBody.tsx @@ -1,4 +1,5 @@ import { toast } from 'react-toastify'; +import { ComponentPropsWithRef } from 'react'; import { ModalCloseBtn } from '@/components/common/modal/ModalWidgets'; import TokenShare from './TokenShare'; import { @@ -7,12 +8,13 @@ import { } from '@/constants/zora'; import { getZoraMintLink } from '@/utils/shared/zora'; import CopyIcon from '@/components/common/icons/CopyIcon'; +import { cn } from '@/lib/utils'; -type Props = { +type Props = ComponentPropsWithRef<'div'> & { img: string; tokenId: number; referrerAddress: string; - closeModal: () => void; + closeModal?: () => void; }; export default function MintSuccessModalBody({ @@ -20,6 +22,8 @@ export default function MintSuccessModalBody({ tokenId, referrerAddress, closeModal, + className, + ...props }: Props) { const mintLink = getZoraMintLink({ chainId: casterZoraChainId, @@ -28,12 +32,19 @@ export default function MintSuccessModalBody({ referrerAddress, }); return ( -
-
+
+

Minted - U3 Caster

- + { + closeModal?.(); + }} + />
diff --git a/apps/u3/src/components/profile/Activities.tsx b/apps/u3/src/components/profile/Activities.tsx deleted file mode 100644 index 4599c6ca..00000000 --- a/apps/u3/src/components/profile/Activities.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import dayjs from 'dayjs'; -import styled from 'styled-components'; -import { UserAvatar } from '@us3r-network/profile'; -import Badge from '../news/contents/Badge'; -import { Copy } from '../common/icons/copy'; -import { CurrencyETH } from '../common/icons/currency-eth'; -import { GasPump } from '../common/icons/gas-pump'; -import Rss3Content from '../fren/Rss3Content'; - -export default function Activities() { - return ( - -
- } - /> -
- {/* {(transList.length > 0 && - transList.map((item, idx) => { - return ( -
- -
- ); - })) || } */} -
- ); -} - -export function NoActivities() { - return ( -
- -

No transactions found on Ethereum.

-
- ); -} - -function ActivityItem({ id }: { id: number }) { - return ( - - -
-
-
- Nicole - fas...df - { - // TODO - }} - > - - -
-
- gasFee -
-
-
- - fasdfasf | {dayjs('1999-01-01').fromNow()} -
-

- I am afraid looking how things are we are going towards this direction -

-

- A Cosmos app chain that honors Apples 30% fee on gas at the protocol - level... Its called iChain. DM me if you want access to the 🍏Seed - round... 😉 -

-
- - Opensea -
-
-
- ); -} - -const ContentBox = styled.div` - display: flex; - gap: 40px; - margin-top: 40px; - width: 100%; - padding-bottom: 24px; - /* > div { - max-height: calc(100vh - 170px - 24px - 24px - 73px - 40px); - height: fit-content; - } */ - - & .no-item { - box-sizing: border-box; - text-align: center; - height: fit-content; - background: #1b1e23; - border-radius: 20px; - padding: 40px 0 40px 0; - flex-grow: 1; - & p { - font-weight: 400; - font-size: 16px; - line-height: 19px; - - color: #748094; - } - } - - & .lists { - background: #1b1e23; - border-radius: 20px; - padding: 0 20px; - flex-grow: 1; - display: flex; - min-width: 37.5rem; - /* max-height: 700px; */ - height: fit-content; - } - & .activity { - &:last-child { - border-bottom: none; - } - - & .info { - display: flex; - gap: 10px; - flex-direction: column; - > div { - display: flex; - gap: 10px; - } - & p { - margin: 0; - } - - & p.quote { - padding: 10px 20px; - gap: 10px; - background: #14171a; - border-radius: 10px; - } - & .header { - display: flex; - align-items: center; - justify-content: space-between; - > div { - display: flex; - gap: 10px; - align-items: center; - & span { - font-weight: 400; - font-size: 14px; - line-height: 17px; - color: #718096; - } - & .nickname { - font-weight: 500; - font-size: 16px; - line-height: 19px; - } - } - } - - & .intro { - display: flex; - align-items: center; - - font-weight: 400; - font-size: 14px; - line-height: 17px; - - color: #718096; - } - - & .source { - display: flex; - padding: 8px 20px 8px 16px; - box-sizing: border-box; - gap: 8px; - height: 40px; - width: fit-content; - background: #1a1e23; - border: 1px solid #39424c; - border-radius: 100px; - > img { - width: 20px; - height: 20px; - border-radius: 50%; - } - } - } - } -`; - -const ActivityBox = styled.div` - display: flex; - gap: 20px; - padding: 20px 0; - border-bottom: 1px solid #39424c; - color: #ffffff; -`; - -const ActivityAvatar = styled(UserAvatar)` - width: 48px; - height: 48px; - border-radius: 50%; -`; diff --git a/apps/u3/src/components/profile/OffChainInterest.tsx b/apps/u3/src/components/profile/OffChainInterest.tsx deleted file mode 100644 index c5777bfd..00000000 --- a/apps/u3/src/components/profile/OffChainInterest.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-12-17 14:50:43 - * @LastEditors: shixuewen friendlysxw@163.com - * @LastEditTime: 2022-12-17 15:39:15 - * @Description: file description - */ -import styled from 'styled-components'; -import { Discord } from '../common/icons/discord'; -import { Twitter } from '../common/icons/twitter'; - -export default function OffChainInterest() { - return ( - -

Coming soon

-

- Filter the Projects you follow based on your Twitter following and - Discord server. -

-

You can start by authorizing a Twitter and Discord account.

-
- {/* - */} - {/* - */} -
-
- ); -} - -const ContentBox = styled.div` - margin-top: 40px; - padding: 40px 20px; - box-sizing: border-box; - gap: 20px; - - height: 230px; - - background: #1b1e23; - border-radius: 20px; - - & h2 { - margin: 0; - font-style: italic; - font-weight: 700; - font-size: 24px; - line-height: 28px; - text-align: center; - color: #ffffff; - margin-bottom: 20px; - } - - & p { - text-align: center; - font-weight: 400; - font-size: 16px; - line-height: 19px; - color: #748094; - margin: 2px; - } - - & > div.btns { - margin-top: 20px; - display: flex; - align-items: center; - justify-content: center; - gap: 40px; - - & button { - padding: 10px 16px; - isolation: isolate; - border: none; - width: 240px; - height: 44px; - - border-radius: 12px; - font-size: 14px; - line-height: 20px; - font-weight: 500; - text-align: center; - color: #ffffff; - & svg { - vertical-align: middle; - margin-right: 8px; - } - } - & button.twitter { - background: #4097ff; - } - & button.discord { - background: #4f40ff; - } - } -`; diff --git a/apps/u3/src/components/profile/ProfilePageFollowNav.tsx b/apps/u3/src/components/profile/ProfilePageFollowNav.tsx deleted file mode 100644 index 1f97ad42..00000000 --- a/apps/u3/src/components/profile/ProfilePageFollowNav.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { isMobile } from 'react-device-detect'; -import styled from 'styled-components'; -import MobilePageHeader from '../layout/mobile/MobilePageHeader'; -import { ArrowLeft } from '../common/icons/ArrowLeft'; -import { ButtonPrimaryLine } from '../common/button/ButtonBase'; -import { SocialPlatform } from '../../services/social/types'; - -export enum FollowType { - FOLLOWING = 'following', - FOLLOWERS = 'followers', -} -export default function ProfilePageFollowNav({ - followType, - activePlatform, - platformCount, - onChangePlatform, - goBack, -}: { - followType: FollowType; - activePlatform: SocialPlatform; - platformCount: { - [platform in SocialPlatform]: number; - }; - onChangePlatform: (platform: SocialPlatform) => void; - goBack: () => void; -}) { - return ( - - {!isMobile && ( - - { - goBack(); - }} - > - - - - )} - - - {isMobile ? ( - - ) : ( - - )} - - {!isMobile && } - - ); -} -function PcFollowPlatformTable({ - followType, - activePlatform, - platformCount, - onChangePlatform, -}: { - followType: FollowType; - activePlatform: SocialPlatform; - platformCount: { - [platform in SocialPlatform]: number; - }; - onChangePlatform: (platform: SocialPlatform) => void; -}) { - const followTypeText = - followType.slice(0, 1).toUpperCase() + followType.slice(1); - const tabs = [ - { - name: `Farcaster ${followTypeText}(${ - platformCount[SocialPlatform.Farcaster] || 0 - })`, - value: SocialPlatform.Farcaster, - }, - { - name: `Lens ${followTypeText}(${ - platformCount[SocialPlatform.Lens] || 0 - })`, - value: SocialPlatform.Lens, - }, - ]; - return ( - - {tabs.map((tab) => ( - onChangePlatform(tab.value)} - > - {tab.name} - - ))} - - ); -} -function MobileFollowPlatformTable({ - followType, - activePlatform, - platformCount, - onChangePlatform, -}: { - followType: FollowType; - activePlatform: SocialPlatform; - platformCount: { - [platform in SocialPlatform]: number; - }; - onChangePlatform: (platform: SocialPlatform) => void; -}) { - const followTypeText = - followType.slice(0, 1).toUpperCase() + followType.slice(1); - const options = [ - { - label: `Farcaster ${followTypeText}(${ - platformCount[SocialPlatform.Farcaster] || 0 - })`, - value: SocialPlatform.Farcaster, - }, - { - label: `Lens ${followTypeText}(${ - platformCount[SocialPlatform.Lens] || 0 - })`, - value: SocialPlatform.Lens, - }, - ]; - return ( - - ); -} -const SocialNavWrapper = styled.div` - width: 100%; - box-sizing: border-box; - display: flex; - align-items: center; - ${!isMobile && - ` height: 72px; - gap: 40px; - align-self: stretch; - border-bottom: 1px solid #39424c; - `} -`; -const SocialNavLeft = styled.div` - flex: 1; - display: flex; - align-items: center; - gap: 112px; -`; -const SocialNavCenter = styled.div` - width: 600px; - height: 100%; -`; -const SocialNavRight = styled.div` - flex: 1; -`; - -const GoBackBtn = styled(ButtonPrimaryLine)` - width: 40px; - height: 40px; - border-radius: 50%; - padding: 0px; -`; - -const FollowTypeTabsWrapper = styled.div` - height: 100%; - display: flex; - align-items: center; - gap: 40px; -`; -const FollowTypeTab = styled.div<{ active: boolean }>` - height: 100%; - color: ${({ active }) => (active ? '#FFF' : '#718096')}; - /* Bold-18 */ - font-family: Rubik; - font-size: 18px; - font-style: normal; - font-weight: 700; - line-height: 72px; - cursor: pointer; - border-bottom: ${({ active }) => (active ? '2px solid #fff' : 'none')}; - box-sizing: border-box; -`; diff --git a/apps/u3/src/components/profile/ProfilePageNav.tsx b/apps/u3/src/components/profile/ProfilePageNav.tsx deleted file mode 100644 index 0cd47f46..00000000 --- a/apps/u3/src/components/profile/ProfilePageNav.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { isMobile } from 'react-device-detect'; -import styled from 'styled-components'; -import MobilePageHeader from '../layout/mobile/MobilePageHeader'; - -export enum FeedsType { - POSTS = 'posts', - REPOSTS = 'reposts', - REPLIES = 'replies', - LIKES = 'likes', - ACTIVITIES = 'activities', -} -export default function ProfilePageNav({ - showFeedsTabs = true, - enabledFeedsTypes, - feedsType, - onChangeFeedsType, -}: { - showFeedsTabs?: boolean; - enabledFeedsTypes?: FeedsType[]; - feedsType: FeedsType; - onChangeFeedsType: (feedsType: FeedsType) => void; -}) { - return ( - - {!isMobile && ( - - Profile - - - )} - - - {showFeedsTabs && - (isMobile ? ( - - ) : ( - - ))} - - {!isMobile && } - - ); -} - -function PcFeedsTypeTable({ - feedsType, - enabledFeedsTypes, - onChangeFeedsType, -}: { - feedsType: FeedsType; - enabledFeedsTypes?: FeedsType[]; - onChangeFeedsType: (feedsType: FeedsType) => void; -}) { - const tabs = [ - { - name: 'Posts', - value: FeedsType.POSTS, - }, - { - name: 'Activities', - value: FeedsType.ACTIVITIES, - }, - { - name: 'Reposts', - value: FeedsType.REPOSTS, - }, - { - name: 'Replies', - value: FeedsType.REPLIES, - }, - // { - // name: 'Likes', - // value: FeedsType.LIKES, - // }, - ]; - const showTabs = enabledFeedsTypes - ? tabs.filter((tab) => enabledFeedsTypes.includes(tab.value)) - : tabs; - return ( - - {showTabs.map((tab) => ( - onChangeFeedsType(tab.value)} - > - {tab.name} - - ))} - - ); -} -function MobileFeedsTypeTable({ - feedsType, - enabledFeedsTypes, - onChangeFeedsType, -}: { - feedsType: FeedsType; - enabledFeedsTypes?: FeedsType[]; - onChangeFeedsType: (feedsType: FeedsType) => void; -}) { - const tabs = [ - FeedsType.POSTS, - FeedsType.ACTIVITIES, - FeedsType.REPOSTS, - FeedsType.REPLIES, - // FeedsType.LIKES, - ]; - const showTabs = enabledFeedsTypes - ? tabs.filter((tab) => enabledFeedsTypes.includes(tab)) - : tabs; - return ( - - ); -} -const SocialNavWrapper = styled.div` - width: 100%; - box-sizing: border-box; - display: flex; - align-items: center; - ${!isMobile && - ` height: 72px; - gap: 40px; - align-self: stretch; - border-bottom: 1px solid #39424c; - `} -`; -const SocialNavLeft = styled.div` - flex: 1; - display: flex; - align-items: center; - gap: 112px; -`; -const SocialNavCenter = styled.div` - width: 600px; - height: 100%; -`; -const SocialNavRight = styled.div` - flex: 1; -`; -const SocialNavTitle = styled.div` - color: #fff; - font-family: Rubik; - font-size: 24px; - font-style: italic; - font-weight: 700; - line-height: normal; -`; -const SocialNavDividingLine = styled.div` - width: 20px; - height: 1px; - transform: rotate(120deg); - background: #39424c; -`; - -const FeedsTypeTabsWrapper = styled.div` - height: 100%; - display: flex; - align-items: center; - gap: 40px; -`; -const FeedsTypeTab = styled.div<{ active: boolean }>` - height: 100%; - color: ${({ active }) => (active ? '#FFF' : '#718096')}; - /* Bold-18 */ - font-family: Rubik; - font-size: 18px; - font-style: normal; - font-weight: 700; - line-height: 72px; - cursor: pointer; - border-bottom: ${({ active }) => (active ? '2px solid #fff' : 'none')}; - box-sizing: border-box; -`; diff --git a/apps/u3/src/components/profile/ProfileSocial.tsx b/apps/u3/src/components/profile/ProfileSocial.tsx index c7b89e51..18b91cc5 100644 --- a/apps/u3/src/components/profile/ProfileSocial.tsx +++ b/apps/u3/src/components/profile/ProfileSocial.tsx @@ -8,8 +8,7 @@ import LensPostCard from '../social/lens/LensPostCard'; import FCast from '../social/farcaster/FCast'; import { useFarcasterCtx } from '../../contexts/social/FarcasterCtx'; import { ProfileFeedsGroups } from '../../services/social/api/feeds'; -import Rss3Content from '../fren/Rss3Content'; -import { NoActivity } from '../../container/Activity'; +import { EndMsgContainer } from '../social/CommonStyles'; export function ProfileSocialPosts({ lensProfileId, @@ -54,7 +53,7 @@ export function ProfileSocialPosts({ }, [activeLensProfileLoading, loadFirstSocialFeeds]); return ( - + {(() => { if (firstLoading) { return ( @@ -79,7 +78,9 @@ export function ProfileSocialPosts({ ) : null } - scrollableTarget="layout-main-wrapper" + endMessage={No more data} + scrollThreshold="5000px" + scrollableTarget="posts-warper" > {feeds.map(({ platform, data }) => { @@ -105,6 +106,7 @@ export function ProfileSocialPosts({ const key = Buffer.from(data.hash.data).toString('hex'); return ( ); } -export function ProfileSocialActivity({ address }: { address: string }) { - return ( - - - - - } - /> - - ); -} const MainCenter = styled.div` width: 100%; @@ -167,12 +155,3 @@ const PostList = styled.div` border-top: 1px solid #718096; } `; -const NoActivityWrapper = styled.div` - .no-item { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } -`; diff --git a/apps/u3/src/components/profile/UserInfoStyled.tsx b/apps/u3/src/components/profile/UserInfoStyled.tsx deleted file mode 100644 index 58944742..00000000 --- a/apps/u3/src/components/profile/UserInfoStyled.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { UserInfo, UserInfoEditForm } from '@us3r-network/profile'; -import styled from 'styled-components'; -import { useState } from 'react'; -import { - Dialog, - Heading, - Label, - Modal, - TextField, -} from 'react-aria-components'; -import EditSvg from '../common/assets/svgs/edit.svg'; -import { getAvatarUploadOpts } from '../../utils/profile/uploadAvatar'; -import { InputBaseCss } from '../common/input/InputBase'; -import { TextareaBaseCss } from '../common/input/TextareaBase'; -import { ButtonPrimaryLineCss } from '../common/button/ButtonBase'; - -export default function UserInfoStyled() { - const [isOpenEdit, setIsOpenEdit] = useState(false); - return ( - - { - setIsOpenEdit(true); - }} - /> - - - - - Edit Info - { - setIsOpenEdit(false); - }} - > - - - - - - - save - - - - - - - ); -} -const UserInfoWrapper = styled(UserInfo)` - display: flex; - flex-direction: column; - gap: 10px; - align-items: center; - padding: 20px; - width: 360px; - box-sizing: border-box; - background: #1b1e23; - border-radius: 20px; - - [data-us3r-component='UserAvatar'] { - display: inline-block; - margin: 0 auto; - position: relative; - width: 120px !important; - height: 120px !important; - overflow: hidden; - cursor: pointer; - &:hover { - &::after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 120px; - height: 120px; - background: rgba(0, 0, 0, 0.5); - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - background-image: url(${EditSvg}); - background-repeat: no-repeat; - background-position: center; - } - } - } - - [data-state-element='Name'] { - font-weight: 500; - font-size: 14px; - line-height: 17px; - font-style: italic; - font-weight: 700; - font-size: 24px; - line-height: 28px; - color: #fff; - } - - [data-state-element='Bio'] { - font-weight: 400; - font-size: 16px; - line-height: 24px; - color: #718096; - } -`; - -const UserInfoEditFormWrapper = styled(UserInfoEditForm)` - display: flex; - flex-direction: column; - justify-content: center; - gap: 10px; - width: 380px; - [data-state-element='AvatarField'] { - width: 120px; - height: 120px; - border-radius: 50%; - margin: 0 auto; - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - svg { - width: 50%; - height: 50%; - path { - fill: #ccc; - } - } - } - [data-state-element='AvatarUploadInput'] { - display: none; - } - [data-state-element='AvatarPreviewImg'] { - width: 100%; - height: 100%; - cursor: pointer; - } - [data-state-element='NameInput'] { - ${InputBaseCss} - } - [data-state-element='BioTextArea'] { - ${TextareaBaseCss} - resize: vertical; - } - [data-state-element='SubmitButton'] { - ${ButtonPrimaryLineCss} - } - [data-state-element='ErrorMessage'] { - color: red; - } -`; diff --git a/apps/u3/src/components/profile/UserTagsStyled.tsx b/apps/u3/src/components/profile/UserTagsStyled.tsx deleted file mode 100644 index 362aa168..00000000 --- a/apps/u3/src/components/profile/UserTagsStyled.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { UserTags, UserTagAddForm, UserTagsProps } from '@us3r-network/profile'; -import styled from 'styled-components'; -import { useState } from 'react'; -import { Dialog, Heading, Modal } from 'react-aria-components'; -import { InputBaseCss } from '../common/input/InputBase'; -import { ButtonPrimaryLineCss } from '../common/button/ButtonBase'; -import { Add } from '../common/icons/add'; - -export default function UserTagsStyled(props: UserTagsProps) { - const [isOpenEdit, setIsOpenEdit] = useState(false); - return ( - - {({ isLoginUser, tags }) => { - return ( - <> -
-

- Tags () -

- {isLoginUser && ( - { - setIsOpenEdit(true); - }} - > - - - )} -
- - {tags.map((item) => ( - - ))} - - {isLoginUser && ( - - - Add New Tag - { - setIsOpenEdit(false); - }} - > - - - - Save - - - - - - - )} - - ); - }} -
- ); -} -const UserTagsWrapper = styled(UserTags)` - display: flex; - flex-direction: column; - gap: 10px; - align-items: center; - padding: 20px; - width: 360px; - box-sizing: border-box; - background: #1b1e23; - border-radius: 20px; - border: 1px solid #39424c; - - .header { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - > h3 { - margin: 0; - font-style: italic; - font-weight: 700; - font-size: 24px; - line-height: 28px; - display: flex; - color: #ffffff; - } - - > span { - cursor: pointer; - } - } - - [data-state-element='List'] { - width: 100%; - margin-top: 20px; - display: flex; - gap: 10px; - flex-wrap: wrap; - } - [data-state-element='Item'] { - padding: 10px 20px; - background: #1a1e23; - border: 1px solid #39424c; - border-radius: 12px; - font-weight: 400; - font-size: 14px; - line-height: 17px; - text-align: center; - box-sizing: border-box; - height: 36px; - color: #718096; - &[data-focused] { - outline: none; - } - } -`; - -const UserTagAddFormWrapper = styled(UserTagAddForm)` - display: flex; - flex-direction: column; - justify-content: center; - gap: 10px; - width: 380px; - [data-state-element='TagInput'] { - ${InputBaseCss} - } - [data-state-element='SubmitButton'] { - ${ButtonPrimaryLineCss} - } - [data-state-element='ErrorMessage'] { - color: red; - } -`; diff --git a/apps/u3/src/components/profile/UserWalletsStyled.tsx b/apps/u3/src/components/profile/UserWalletsStyled.tsx deleted file mode 100644 index deaf1171..00000000 --- a/apps/u3/src/components/profile/UserWalletsStyled.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { - UserWallets, - UserWalletAddForm, - UserWalletsProps, -} from '@us3r-network/profile'; -import styled from 'styled-components'; -import { useState } from 'react'; -import { Dialog, Heading, Modal } from 'react-aria-components'; -import { toast } from 'react-toastify'; -import { InputBaseCss } from '../common/input/InputBase'; -import { ButtonPrimaryLineCss } from '../common/button/ButtonBase'; -import { Add } from '../common/icons/add'; -import { Copy } from '../common/icons/copy'; -import { Trash } from '../common/icons/trash'; - -export default function UserWalletsStyled(props: UserWalletsProps) { - const [isOpenEdit, setIsOpenEdit] = useState(false); - return ( - - {({ isLoginUser, wallets }) => { - return ( - <> -
-

- Wallets () -

- {isLoginUser && ( - { - setIsOpenEdit(true); - }} - > - - - )} -
- - {wallets.map((item) => ( - -
- - -
-
- {isLoginUser && ( - - - - )} - { - toast.success('Copied'); - }} - > - - -
-
- ))} -
- {isLoginUser && ( - - - Add New Wallet - { - setIsOpenEdit(false); - }} - > - - - - Save - - - - - - - )} - - ); - }} -
- ); -} -const UserWalletsWrapper = styled(UserWallets)` - display: flex; - flex-direction: column; - gap: 10px; - align-items: center; - padding: 20px; - width: 360px; - box-sizing: border-box; - background: #1b1e23; - border-radius: 20px; - border: 1px solid #39424c; - - .header { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - > h3 { - margin: 0; - font-style: italic; - font-weight: 700; - font-size: 24px; - line-height: 28px; - display: flex; - color: #ffffff; - } - - > span { - cursor: pointer; - } - } - - [data-state-element='List'] { - width: 100%; - margin-top: 20px; - display: flex; - flex-direction: column; - gap: 10px; - } - [data-state-element='Item'] { - display: flex; - align-items: center; - justify-content: space-between; - .text, - .btns { - color: #718096; - display: flex; - align-items: center; - gap: 10px; - } - &[data-focused] { - outline: none; - } - } - [data-state-element='Network'] { - padding: 2px 4px; - background: #718096; - border-radius: 4px; - font-weight: 400; - font-size: 12px; - line-height: 14px; - color: #14171a; - &:empty { - display: none; - } - } - - [data-state-element='Delete'] { - cursor: pointer; - } - [data-state-element='Copy'] { - cursor: pointer; - } -`; - -const UserWalletAddFormWrapper = styled(UserWalletAddForm)` - display: flex; - flex-direction: column; - justify-content: center; - gap: 10px; - width: 380px; - [data-state-element='AddressInput'] { - ${InputBaseCss} - } - [data-state-element='SubmitButton'] { - ${ButtonPrimaryLineCss} - } - [data-state-element='ErrorMessage'] { - color: red; - } -`; diff --git a/apps/u3/src/components/fren/Rss3Content.tsx b/apps/u3/src/components/profile/activity/Rss3Content.tsx similarity index 98% rename from apps/u3/src/components/fren/Rss3Content.tsx rename to apps/u3/src/components/profile/activity/Rss3Content.tsx index 1ce8fa1b..f8ec0acb 100644 --- a/apps/u3/src/components/fren/Rss3Content.tsx +++ b/apps/u3/src/components/profile/activity/Rss3Content.tsx @@ -27,12 +27,12 @@ import { setFollow, getReco, getRss3, -} from '../../features/frens/frensHandles'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { AsyncRequestStatus } from '../../services/shared/types'; -import Loading from '../common/loading/Loading'; -import ListScrollBox from '../common/box/ListScrollBox'; -import { messages } from '../../utils/shared/message'; +} from '../../../features/frens/frensHandles'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { AsyncRequestStatus } from '../../../services/shared/types'; +import Loading from '../../common/loading/Loading'; +import ListScrollBox from '../../common/box/ListScrollBox'; +import { messages } from '../../../utils/shared/message'; TimeAgo.addDefaultLocale(en); const timeAgo = new TimeAgo('en-US'); diff --git a/apps/u3/src/components/profile/OnChainInterest.tsx b/apps/u3/src/components/profile/asset/OnChainInterest.tsx similarity index 93% rename from apps/u3/src/components/profile/OnChainInterest.tsx rename to apps/u3/src/components/profile/asset/OnChainInterest.tsx index 556f7925..87e7e13c 100644 --- a/apps/u3/src/components/profile/OnChainInterest.tsx +++ b/apps/u3/src/components/profile/asset/OnChainInterest.tsx @@ -1,20 +1,17 @@ import { BigNumber, ethers } from 'ethers'; -import { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { MEDIA_BREAK_POINTS } from '../../constants'; +import { MEDIA_BREAK_POINTS } from '@/constants'; import { ERC20Balances, NFTData, NFTDataListItem, -} from '../../services/profile/types/profile'; -import Select from '../common/select/Select'; -import { NoItem } from '../common/icons/no-item'; - -import CrownImg from '../common/assets/imgs/crown.svg'; -import ethImage from '../common/assets/imgs/eth.png'; -import NFTShower from './Credential/NFTShower'; -import { ERC20Tokens } from '../airstack/ERC20Tokens'; -import { Tokens } from '../airstack/Tokens'; +} from '@/services/profile/types/profile'; +import { NoItem } from '../../common/icons/no-item'; +import CrownImg from '../../common/assets/imgs/crown.svg'; +import ethImage from '../../common/assets/imgs/eth.png'; +import NFTShower from '../gallery/NFTShower'; +import { ERC20Tokens } from './airstack/ERC20Tokens'; +import { Tokens } from './airstack/Tokens'; export default function OnChainInterest({ data, diff --git a/apps/u3/src/components/airstack/Asset.tsx b/apps/u3/src/components/profile/asset/airstack/Asset.tsx similarity index 100% rename from apps/u3/src/components/airstack/Asset.tsx rename to apps/u3/src/components/profile/asset/airstack/Asset.tsx diff --git a/apps/u3/src/components/airstack/ERC20Tokens.tsx b/apps/u3/src/components/profile/asset/airstack/ERC20Tokens.tsx similarity index 98% rename from apps/u3/src/components/airstack/ERC20Tokens.tsx rename to apps/u3/src/components/profile/asset/airstack/ERC20Tokens.tsx index 8899af40..8357d2a6 100644 --- a/apps/u3/src/components/airstack/ERC20Tokens.tsx +++ b/apps/u3/src/components/profile/asset/airstack/ERC20Tokens.tsx @@ -4,7 +4,7 @@ import InfiniteScroll from 'react-infinite-scroll-component'; import { useSession } from '@us3r-network/auth-with-rainbowkit'; import styled from 'styled-components'; -import { ERC20TokensQuery } from '../../services/shared/queries'; +import { ERC20TokensQuery } from '../../../../services/shared/queries'; import { TokenType } from './types'; function Token({ diff --git a/apps/u3/src/components/airstack/Tokens.tsx b/apps/u3/src/components/profile/asset/airstack/Tokens.tsx similarity index 97% rename from apps/u3/src/components/airstack/Tokens.tsx rename to apps/u3/src/components/profile/asset/airstack/Tokens.tsx index 7f285e46..47c4e500 100644 --- a/apps/u3/src/components/airstack/Tokens.tsx +++ b/apps/u3/src/components/profile/asset/airstack/Tokens.tsx @@ -6,10 +6,10 @@ import { useSession } from '@us3r-network/auth-with-rainbowkit'; import { MEDIA_BREAK_POINTS } from 'src/constants'; import styled from 'styled-components'; -import { POAPQuery, TokensQuery } from '../../services/shared/queries'; +import { POAPQuery, TokensQuery } from '../../../../services/shared/queries'; import { PoapType, TokenType } from './types'; import { Asset } from './Asset'; -import Loading from '../common/loading/Loading'; +import Loading from '../../../common/loading/Loading'; type TokenProps = { type: string; diff --git a/apps/u3/src/components/airstack/types.ts b/apps/u3/src/components/profile/asset/airstack/types.ts similarity index 100% rename from apps/u3/src/components/airstack/types.ts rename to apps/u3/src/components/profile/asset/airstack/types.ts diff --git a/apps/u3/src/components/profile/farcaster/FarcasterFollowProfileCard.tsx b/apps/u3/src/components/profile/contacts/FarcasterFollowProfileCard.tsx similarity index 96% rename from apps/u3/src/components/profile/farcaster/FarcasterFollowProfileCard.tsx rename to apps/u3/src/components/profile/contacts/FarcasterFollowProfileCard.tsx index 6a88929d..db7eb43d 100644 --- a/apps/u3/src/components/profile/farcaster/FarcasterFollowProfileCard.tsx +++ b/apps/u3/src/components/profile/contacts/FarcasterFollowProfileCard.tsx @@ -3,7 +3,7 @@ import useFarcasterUserData from 'src/hooks/social/farcaster/useFarcasterUserDat import { SocialPlatform } from 'src/services/social/types'; import useFarcasterFollowAction from 'src/hooks/social/farcaster/useFarcasterFollowAction'; -import FollowProfileCard from '../FollowProfileCard'; +import FollowProfileCard from './FollowProfileCard'; export default function FarcasterFollowProfileCard({ fid, diff --git a/apps/u3/src/components/profile/contacts/FarcasterFollowers.tsx b/apps/u3/src/components/profile/contacts/FarcasterFollowers.tsx new file mode 100644 index 00000000..5ba171f6 --- /dev/null +++ b/apps/u3/src/components/profile/contacts/FarcasterFollowers.tsx @@ -0,0 +1,73 @@ +import InfiniteScroll from 'react-infinite-scroll-component'; +import Loading from '@/components/common/loading/Loading'; +import { + FollowList, + FollowListWrapper, + LoadingMoreWrapper, + LoadingWrapper, +} from './FollowListWidgets'; +import FarcasterFollowProfileCard from './FarcasterFollowProfileCard'; +import useFarcasterLinks from '@/hooks/social/farcaster/useFarcasterLinks'; +import { FollowType } from '@/container/profile/Contacts'; +import { EndMsgContainer } from '@/components/social/CommonStyles'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; + +export default function FarcasterFollowers({ fid }: { fid: string | number }) { + const { following } = useFarcasterCtx(); + const { + links, + firstLoading, + moreLoading, + hasMore, + farcasterUserData, + loadMore, + } = useFarcasterLinks({ + fid, + pageSize: 20, + type: FollowType.FOLLOWER, + }); + + if (firstLoading) { + return ( + + + + + + ); + } + return ( + + { + if (moreLoading) return; + loadMore(); + }} + hasMore={!firstLoading && !moreLoading && hasMore} + loader={ + moreLoading ? ( + + + + ) : null + } + endMessage={No more data} + scrollThreshold="2000px" + scrollableTarget="follow-warper" + > + + {(links || []).map((item) => ( + + ))} + + + + ); +} diff --git a/apps/u3/src/components/profile/contacts/FarcasterFollowing.tsx b/apps/u3/src/components/profile/contacts/FarcasterFollowing.tsx new file mode 100644 index 00000000..0d845d12 --- /dev/null +++ b/apps/u3/src/components/profile/contacts/FarcasterFollowing.tsx @@ -0,0 +1,73 @@ +import InfiniteScroll from 'react-infinite-scroll-component'; +import Loading from 'src/components/common/loading/Loading'; +import { EndMsgContainer } from '@/components/social/CommonStyles'; +import { FollowType } from '@/container/profile/Contacts'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import useFarcasterLinks from '@/hooks/social/farcaster/useFarcasterLinks'; +import FarcasterFollowProfileCard from './FarcasterFollowProfileCard'; +import { + FollowList, + FollowListWrapper, + LoadingMoreWrapper, + LoadingWrapper, +} from './FollowListWidgets'; + +export default function FarcasterFollowing({ fid }: { fid: string | number }) { + const { following } = useFarcasterCtx(); + const { + links, + firstLoading, + moreLoading, + hasMore, + farcasterUserData, + loadMore, + } = useFarcasterLinks({ + fid, + pageSize: 20, + type: FollowType.FOLLOWING, + }); + + if (firstLoading) { + return ( + + + + + + ); + } + return ( + + { + if (moreLoading) return; + loadMore(); + }} + hasMore={!firstLoading && !moreLoading && hasMore} + loader={ + moreLoading ? ( + + + + ) : null + } + endMessage={No more data} + scrollThreshold="2000px" + scrollableTarget="follow-warper" + > + + {(links || []).map((item) => ( + + ))} + + + + ); +} diff --git a/apps/u3/src/components/profile/FollowListWidgets.tsx b/apps/u3/src/components/profile/contacts/FollowListWidgets.tsx similarity index 80% rename from apps/u3/src/components/profile/FollowListWidgets.tsx rename to apps/u3/src/components/profile/contacts/FollowListWidgets.tsx index 7d30a179..226395da 100644 --- a/apps/u3/src/components/profile/FollowListWidgets.tsx +++ b/apps/u3/src/components/profile/contacts/FollowListWidgets.tsx @@ -1,15 +1,10 @@ import styled from 'styled-components'; export const FollowListWrapper = styled.div` - width: 600px; + width: 100%; `; export const FollowList = styled.div` - width: 600px; - border-radius: 20px; - border: 1px solid #39424c; - box-sizing: border-box; - background: #1b1e23; overflow: hidden; & > * { border-bottom: 1px solid #39424c; diff --git a/apps/u3/src/components/profile/FollowProfileCard.tsx b/apps/u3/src/components/profile/contacts/FollowProfileCard.tsx similarity index 86% rename from apps/u3/src/components/profile/FollowProfileCard.tsx rename to apps/u3/src/components/profile/contacts/FollowProfileCard.tsx index b263c5d2..acaeb661 100644 --- a/apps/u3/src/components/profile/FollowProfileCard.tsx +++ b/apps/u3/src/components/profile/contacts/FollowProfileCard.tsx @@ -1,21 +1,20 @@ import styled, { StyledComponentPropsWithRef } from 'styled-components'; import { useEffect, useMemo } from 'react'; -import { SocialButtonPrimaryLine } from '../social/button/SocialButton'; -import { SocialMessageChatBtn } from '../message/MessageChatBtn'; -import { SocialPlatform } from '../../services/social/types'; -import LensIcon from '../common/icons/LensIcon'; -import FarcasterIcon from '../common/icons/FarcasterIcon'; -import useCanMessage from '../../hooks/message/xmtp/useCanMessage'; -import { useNav } from '../../contexts/NavCtx'; +import { SocialButtonPrimaryLine } from '../../social/button/SocialButton'; +import { SocialMessageChatBtn } from '../../message/MessageChatBtn'; +import { SocialPlatform } from '../../../services/social/types'; +import LensIcon from '../../common/icons/LensIcon'; +import FarcasterIcon from '../../common/icons/FarcasterIcon'; +import useCanMessage from '../../../hooks/message/xmtp/useCanMessage'; import { useXmtpClient, MessageRoute, -} from '../../contexts/message/XmtpClientCtx'; +} from '../../../contexts/message/XmtpClientCtx'; import { farcasterHandleToBioLinkHandle, lensHandleToBioLinkHandle, -} from '../../utils/profile/biolink'; -import NavigateToProfileLink from './NavigateToProfileLink'; +} from '../../../utils/profile/biolink'; +import NavigateToProfileLink from '../info/NavigateToProfileLink'; export type FollowProfileData = { handle: string; @@ -51,7 +50,6 @@ export default function FollowProfileCard({ const { handle, avatar, name, address, bio, platforms, isFollowed } = data; const { canMessage } = useCanMessage(address); const { setMessageRouteParams } = useXmtpClient(); - const { setOpenMessageModal } = useNav(); const profileIdentity = useMemo(() => { if (handle.endsWith('.eth')) return handle; @@ -128,9 +126,8 @@ export default function FollowProfileCard({ {canMessage && ( { - setOpenMessageModal(true); setMessageRouteParams({ - route: MessageRoute.DETAIL, + route: MessageRoute.PRIVATE_CHAT, peerAddress: address, }); }} @@ -146,7 +143,6 @@ export default function FollowProfileCard({ const Wrapper = styled.div` padding: 20px; box-sizing: border-box; - background: #1b1e23; `; const Top = styled.div` display: flex; diff --git a/apps/u3/src/components/profile/lens/LensFollowProfileCard.tsx b/apps/u3/src/components/profile/contacts/LensFollowProfileCard.tsx similarity index 97% rename from apps/u3/src/components/profile/lens/LensFollowProfileCard.tsx rename to apps/u3/src/components/profile/contacts/LensFollowProfileCard.tsx index c28f3a0a..b1520339 100644 --- a/apps/u3/src/components/profile/lens/LensFollowProfileCard.tsx +++ b/apps/u3/src/components/profile/contacts/LensFollowProfileCard.tsx @@ -1,7 +1,7 @@ import { Profile, useFollow, useUnfollow } from '@lens-protocol/react-web'; import { useMemo } from 'react'; import { StyledComponentPropsWithRef } from 'styled-components'; -import FollowProfileCard from '../FollowProfileCard'; +import FollowProfileCard from './FollowProfileCard'; import { useLensCtx } from '../../../contexts/social/AppLensCtx'; import { SocialPlatform } from '../../../services/social/types'; import getAvatar from '../../../utils/social/lens/getAvatar'; diff --git a/apps/u3/src/components/profile/lens/LensProfileFollowers.tsx b/apps/u3/src/components/profile/contacts/LensProfileFollowers.tsx similarity index 72% rename from apps/u3/src/components/profile/lens/LensProfileFollowers.tsx rename to apps/u3/src/components/profile/contacts/LensProfileFollowers.tsx index d4d5fa11..971bf202 100644 --- a/apps/u3/src/components/profile/lens/LensProfileFollowers.tsx +++ b/apps/u3/src/components/profile/contacts/LensProfileFollowers.tsx @@ -1,7 +1,7 @@ import { LimitType, + Profile, useProfileFollowers, - useProfiles, } from '@lens-protocol/react-web'; import { useCallback, useState } from 'react'; import InfiniteScroll from 'react-infinite-scroll-component'; @@ -12,15 +12,20 @@ import { FollowListWrapper, LoadingMoreWrapper, LoadingWrapper, -} from '../FollowListWidgets'; +} from './FollowListWidgets'; +import { EndMsgContainer } from '@/components/social/CommonStyles'; -export default function LensProfileFollowers({ address }: { address: string }) { - const { data: lensProfiles } = useProfiles({ - where: { - ownedBy: [address], - }, - }); - const lensProfile = lensProfiles?.[0]; +export default function LensProfileFollowers({ + lensProfile, +}: { + lensProfile: Profile; +}) { + // const { data: lensProfiles } = useProfiles({ + // where: { + // ownedBy: [address], + // }, + // }); + // const lensProfile = lensProfiles?.[0]; const { data: followersData, loading: firstLoading, @@ -45,7 +50,7 @@ export default function LensProfileFollowers({ address }: { address: string }) { } }, [hasMore, next, moreLoading]); return ( - + {(() => { if (firstLoading) { return ( @@ -69,11 +74,13 @@ export default function LensProfileFollowers({ address }: { address: string }) { ) : null } - scrollableTarget="profile-wrapper" + endMessage={No more data} + scrollThreshold="2000px" + scrollableTarget="follow-warper" > {(followersData || []).map((item) => ( - + ))} diff --git a/apps/u3/src/components/profile/lens/LensProfileFollowing.tsx b/apps/u3/src/components/profile/contacts/LensProfileFollowing.tsx similarity index 69% rename from apps/u3/src/components/profile/lens/LensProfileFollowing.tsx rename to apps/u3/src/components/profile/contacts/LensProfileFollowing.tsx index 20e23a01..5bf95de8 100644 --- a/apps/u3/src/components/profile/lens/LensProfileFollowing.tsx +++ b/apps/u3/src/components/profile/contacts/LensProfileFollowing.tsx @@ -1,7 +1,7 @@ import { LimitType, + Profile, useProfileFollowing, - useProfiles, } from '@lens-protocol/react-web'; import { useCallback, useState } from 'react'; import InfiniteScroll from 'react-infinite-scroll-component'; @@ -12,15 +12,20 @@ import { FollowListWrapper, LoadingMoreWrapper, LoadingWrapper, -} from '../FollowListWidgets'; +} from './FollowListWidgets'; +import { EndMsgContainer } from '@/components/social/CommonStyles'; -export default function LensProfileFollowing({ address }: { address: string }) { - const { data: lensProfiles } = useProfiles({ - where: { - ownedBy: [address], - }, - }); - const lensProfile = lensProfiles?.[0]; +export default function LensProfileFollowing({ + lensProfile, +}: { + lensProfile: Profile; +}) { + // const { data: lensProfiles } = useProfiles({ + // where: { + // ownedBy: [address], + // }, + // }); + // const lensProfile = lensProfiles?.[0]; const { data: followingData, loading: firstLoading, @@ -45,7 +50,7 @@ export default function LensProfileFollowing({ address }: { address: string }) { } }, [hasMore, next, moreLoading]); return ( - + {(() => { if (firstLoading) { return ( @@ -69,11 +74,17 @@ export default function LensProfileFollowing({ address }: { address: string }) { ) : null } - scrollableTarget="profile-wrapper" + endMessage={No more data} + scrollThreshold="2000px" + scrollableTarget="follow-warper" > {(followingData || []).map((item) => ( - + ))} diff --git a/apps/u3/src/components/profile/farcaster/FarcasterFollowers.tsx b/apps/u3/src/components/profile/farcaster/FarcasterFollowers.tsx deleted file mode 100644 index ce464135..00000000 --- a/apps/u3/src/components/profile/farcaster/FarcasterFollowers.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import useFarcasterFollowData from 'src/hooks/social/farcaster/useFarcasterFollowData'; -import Loading from 'src/components/common/loading/Loading'; - -import { - FollowList, - FollowListWrapper, - LoadingWrapper, -} from '../FollowListWidgets'; - -import FarcasterFollowProfileCard from './FarcasterFollowProfileCard'; - -export default function FarcasterFollowers({ fid }: { fid: string | number }) { - const { farcasterFollowData, loading } = useFarcasterFollowData({ - fid, - }); - const followers = farcasterFollowData.followerData; - const following = farcasterFollowData.followingData; - - if (loading) { - return ( - - - - - - ); - } - return ( - - - {(followers || []).map((item) => ( - - ))} - - - ); -} diff --git a/apps/u3/src/components/profile/farcaster/FarcasterFollowing.tsx b/apps/u3/src/components/profile/farcaster/FarcasterFollowing.tsx deleted file mode 100644 index 2760d81c..00000000 --- a/apps/u3/src/components/profile/farcaster/FarcasterFollowing.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import useFarcasterFollowData from 'src/hooks/social/farcaster/useFarcasterFollowData'; -import Loading from 'src/components/common/loading/Loading'; - -import { - FollowList, - FollowListWrapper, - LoadingWrapper, -} from '../FollowListWidgets'; -import FarcasterFollowProfileCard from './FarcasterFollowProfileCard'; - -export default function FarcasterFollowing({ fid }: { fid: string | number }) { - const { farcasterFollowData, loading } = useFarcasterFollowData({ - fid, - }); - const following = farcasterFollowData.followingData; - - if (loading) { - return ( - - - - - - ); - } - return ( - - - {(following || []).map((item) => ( - - ))} - - - ); -} diff --git a/apps/u3/src/components/profile/Credential/Card.tsx b/apps/u3/src/components/profile/gallery/Card.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/Card.tsx rename to apps/u3/src/components/profile/gallery/Card.tsx diff --git a/apps/u3/src/components/profile/Credential/Galxe.tsx b/apps/u3/src/components/profile/gallery/Galxe.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/Galxe.tsx rename to apps/u3/src/components/profile/gallery/Galxe.tsx diff --git a/apps/u3/src/components/profile/Credential/ItemContainer.tsx b/apps/u3/src/components/profile/gallery/ItemContainer.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/ItemContainer.tsx rename to apps/u3/src/components/profile/gallery/ItemContainer.tsx diff --git a/apps/u3/src/components/profile/Credential/NFTShower.tsx b/apps/u3/src/components/profile/gallery/NFTShower.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/NFTShower.tsx rename to apps/u3/src/components/profile/gallery/NFTShower.tsx diff --git a/apps/u3/src/components/profile/Credential/Noox.tsx b/apps/u3/src/components/profile/gallery/Noox.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/Noox.tsx rename to apps/u3/src/components/profile/gallery/Noox.tsx diff --git a/apps/u3/src/components/profile/Credential/Poap.tsx b/apps/u3/src/components/profile/gallery/Poap.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/Poap.tsx rename to apps/u3/src/components/profile/gallery/Poap.tsx diff --git a/apps/u3/src/components/profile/Credential/Title.tsx b/apps/u3/src/components/profile/gallery/Title.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/Title.tsx rename to apps/u3/src/components/profile/gallery/Title.tsx diff --git a/apps/u3/src/components/profile/Credential/index.tsx b/apps/u3/src/components/profile/gallery/index.tsx similarity index 100% rename from apps/u3/src/components/profile/Credential/index.tsx rename to apps/u3/src/components/profile/gallery/index.tsx diff --git a/apps/u3/src/components/profile/Credential/useInfoCalc.ts b/apps/u3/src/components/profile/gallery/useInfoCalc.ts similarity index 100% rename from apps/u3/src/components/profile/Credential/useInfoCalc.ts rename to apps/u3/src/components/profile/gallery/useInfoCalc.ts diff --git a/apps/u3/src/components/profile/NavigateToProfileLink.tsx b/apps/u3/src/components/profile/info/NavigateToProfileLink.tsx similarity index 100% rename from apps/u3/src/components/profile/NavigateToProfileLink.tsx rename to apps/u3/src/components/profile/info/NavigateToProfileLink.tsx diff --git a/apps/u3/src/components/profile/info/PlatformAccounts.tsx b/apps/u3/src/components/profile/info/PlatformAccounts.tsx new file mode 100644 index 00000000..1394885d --- /dev/null +++ b/apps/u3/src/components/profile/info/PlatformAccounts.tsx @@ -0,0 +1,120 @@ +import SettingIcon from 'src/components/common/icons/setting'; +import useUserData from 'src/hooks/social/farcaster/useUserData'; +import { useLensCtx } from '@/contexts/social/AppLensCtx'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { SocialPlatform } from '@/services/social/types'; +import getAvatar from '@/utils/social/lens/getAvatar'; +import { getHandle, getName } from '@/utils/social/lens/profile'; +import FarcasterIcon from '../../common/icons/FarcasterIcon'; +import LensIcon from '../../common/icons/LensIcon'; + +export function LensAccount() { + const { + isLogin: isLoginLens, + sessionProfile: lensProfile, + setOpenLensLoginModal, + } = useLensCtx(); + + if (!isLoginLens || !lensProfile) { + return ( +
setOpenLensLoginModal(true)} + > + +
Sign in with Lens
+
+ ); + } + return ( +
+ + +
+ ); +} + +export function FarcasterAccount() { + const { + isConnected, + currFid, + currUserInfo, + openFarcasterQR, + setSignerSelectModalOpen, + } = useFarcasterCtx(); + const userInfo = useUserData(currUserInfo?.[currFid]); + + if (!isConnected || !userInfo) { + return ( +
openFarcasterQR()} + > + +
Sign in with Farcaster
+
+ ); + } + return ( +
+ + +
+ ); +} + +function ProfileInfo({ + avatar, + name, + handle, + platform, +}: { + avatar: string; + name: string; + handle: string; + platform: SocialPlatform; +}) { + return ( +
+
+ {name} + {platform === SocialPlatform.Lens ? ( + + ) : platform === SocialPlatform.Farcaster ? ( + + ) : null} +
+
+ {name} + @{handle} +
+
+ ); +} diff --git a/apps/u3/src/components/profile/info/PlatformFollowCounts.tsx b/apps/u3/src/components/profile/info/PlatformFollowCounts.tsx new file mode 100644 index 00000000..69f6b329 --- /dev/null +++ b/apps/u3/src/components/profile/info/PlatformFollowCounts.tsx @@ -0,0 +1,59 @@ +import { ComponentPropsWithRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { cn } from '@/lib/utils'; + +interface PlatformFollowCountsProps { + identity?: string; + postCount: number; + followerCount: number; + followingCount: number; +} +export default function PlatformFollowCounts({ + identity, + postCount, + followerCount, + followingCount, +}: PlatformFollowCountsProps) { + const navigate = useNavigate(); + const pathSuffix = identity ? `/${identity}` : ''; + return ( +
+ navigate(`/u${pathSuffix}?type=following`)} + /> + navigate(`/u/contacts${pathSuffix}?type=follower`)} + /> + navigate(`/u/contacts${pathSuffix}?type=following`)} + /> +
+ ); +} + +function CountItem({ + label, + count, + onClick, + className, +}: ComponentPropsWithRef<'div'> & { + label: string; + count: number; + onClick: (path: string) => void; +}) { + return ( +
+

{label}

+

{count}

+
+ ); +} diff --git a/apps/u3/src/components/profile/profile-info/ProfileAvatar.tsx b/apps/u3/src/components/profile/info/ProfileAvatar.tsx similarity index 100% rename from apps/u3/src/components/profile/profile-info/ProfileAvatar.tsx rename to apps/u3/src/components/profile/info/ProfileAvatar.tsx diff --git a/apps/u3/src/components/profile/info/ProfileBasicInfo.tsx b/apps/u3/src/components/profile/info/ProfileBasicInfo.tsx new file mode 100644 index 00000000..40b34eff --- /dev/null +++ b/apps/u3/src/components/profile/info/ProfileBasicInfo.tsx @@ -0,0 +1,215 @@ +import { + UserAvatar, + UserInfo, + UserInfoEditForm, + UserName, +} from '@us3r-network/profile'; +import { useState } from 'react'; +import { Dialog, Heading, Modal } from 'react-aria-components'; +import styled from 'styled-components'; +import { getAvatarUploadOpts } from '../../../utils/profile/uploadAvatar'; +import { ButtonPrimaryLineCss } from '../../common/button/ButtonBase'; +import { InputBaseCss } from '../../common/input/InputBase'; +import { TextareaBaseCss } from '../../common/input/TextareaBase'; +import NavigateToProfileLink from './NavigateToProfileLink'; +import ProfileAvatar from './ProfileAvatar'; + +const extractErrorMessage = (message) => { + try { + const match = message.match(/{.*}/); + if (!match) return message; + const jsonStr = match[0]; + + const obj = JSON.parse(jsonStr); + return (obj.error as string) + .replace('Validation Error: ', '') + .replaceAll('data/', ''); + } catch (e) { + // 如果转换失败,返回原始消息 + return message; + } +}; + +export function U3ProfileBasicInfo({ + did, + navigateToProfileUrl, + onNavigateToProfileAfter, +}: { + did: string; + navigateToProfileUrl?: string; + onNavigateToProfileAfter?: () => void; +}) { + const [isOpenEdit, setIsOpenEdit] = useState(false); + return ( + + {({ isLoginUser }) => { + return ( + <> + + {({ avatarSrc }) => { + if (navigateToProfileUrl) { + return ( + + + + ); + } + return ( + { + if (!isLoginUser) return; + setIsOpenEdit(true); + }} + /> + ); + }} + + {navigateToProfileUrl ? ( + + + + ) : ( + + )} + + {isLoginUser && ( + + + Edit Info + { + setIsOpenEdit(false); + }} + > + {({ errMsg }) => { + return ( + <> + + + + + + + + save + + + {extractErrorMessage(errMsg)} + + + ); + }} + + + + )} + + ); + }} + + ); +} + +export function PlatformProfileBasicInfo({ + data, + navigateToProfileUrl, + onNavigateToProfileAfter, +}: { + data: { + avatar: string; + name: string; + bio?: string; + identity?: string | number; + }; + navigateToProfileUrl?: string; + onNavigateToProfileAfter?: () => void; +}) { + const { avatar, name, bio } = data; + return ( +
+ {navigateToProfileUrl ? ( + + + + ) : ( + + )} + {navigateToProfileUrl ? ( + +

{name}

+
+ ) : ( +

{name}

+ )} +

{bio}

+
+ ); +} + +const UserInfoEditFormWrapper = styled(UserInfoEditForm)` + display: flex; + flex-direction: column; + justify-content: center; + gap: 10px; + width: 380px; + [data-state-element='AvatarField'] { + width: 120px; + height: 120px; + border-radius: 50%; + margin: 0 auto; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + svg { + width: 50%; + height: 50%; + path { + fill: #ccc; + } + } + } + [data-state-element='AvatarUploadInput'] { + display: none; + } + [data-state-element='AvatarPreviewImg'] { + width: 100%; + height: 100%; + cursor: pointer; + } + [data-state-element='NameInput'] { + ${InputBaseCss} + } + [data-state-element='BioTextArea'] { + ${TextareaBaseCss} + resize: vertical; + } + [data-state-element='SubmitButton'] { + ${ButtonPrimaryLineCss} + } + .errorMessage { + color: #e63734; + } +`; diff --git a/apps/u3/src/components/profile/profile-info/ProfileBasicInfo.tsx b/apps/u3/src/components/profile/info/ProfileBasicInfo_old.tsx similarity index 99% rename from apps/u3/src/components/profile/profile-info/ProfileBasicInfo.tsx rename to apps/u3/src/components/profile/info/ProfileBasicInfo_old.tsx index b38cae63..174adffb 100644 --- a/apps/u3/src/components/profile/profile-info/ProfileBasicInfo.tsx +++ b/apps/u3/src/components/profile/info/ProfileBasicInfo_old.tsx @@ -16,7 +16,7 @@ import { getAvatarUploadOpts } from '../../../utils/profile/uploadAvatar'; import { shortPubKey } from '../../../utils/shared/shortPubKey'; import { Copy } from '../../common/icons/copy'; import ProfileAvatar from './ProfileAvatar'; -import NavigateToProfileLink from '../NavigateToProfileLink'; +import NavigateToProfileLink from './NavigateToProfileLink'; const extractErrorMessage = (message) => { try { diff --git a/apps/u3/src/components/profile/profile-info/ProfileBtns.tsx b/apps/u3/src/components/profile/info/ProfileBtns.tsx similarity index 88% rename from apps/u3/src/components/profile/profile-info/ProfileBtns.tsx rename to apps/u3/src/components/profile/info/ProfileBtns.tsx index 9e21063d..0aa7b02c 100644 --- a/apps/u3/src/components/profile/profile-info/ProfileBtns.tsx +++ b/apps/u3/src/components/profile/info/ProfileBtns.tsx @@ -2,14 +2,13 @@ import styled, { StyledComponentPropsWithRef } from 'styled-components'; import { Profile } from '@lens-protocol/react-web'; import { useEffect } from 'react'; import { SocialButtonPrimary } from '../../social/button/SocialButton'; -import { useNav } from '../../../contexts/NavCtx'; import { ReactComponent as MessageChatSquareSvg } from '../../common/assets/svgs/message-chat-square.svg'; import useCanMessage from '../../../hooks/message/xmtp/useCanMessage'; import { useXmtpClient, MessageRoute, } from '../../../contexts/message/XmtpClientCtx'; -import ProfileFollowBtn from '../ProfileFollowBtn'; +import ProfileFollowBtn from './ProfileFollowBtn'; interface ProfileBtnsProps extends StyledComponentPropsWithRef<'div'> { showFollowBtn: boolean; @@ -30,8 +29,6 @@ export default function ProfileBtns({ useEffect(() => { setCanEnableXmtp(true); }, []); - - const { setOpenMessageModal } = useNav(); const { canMessage } = useCanMessage(address); return ( @@ -40,9 +37,8 @@ export default function ProfileBtns({ {showMessageBtn && canMessage && ( { - setOpenMessageModal(true); setMessageRouteParams({ - route: MessageRoute.DETAIL, + route: MessageRoute.PRIVATE_CHAT, peerAddress: address, }); }} @@ -58,7 +54,6 @@ const BtnsWrapper = styled.div` justify-content: space-between; align-items: center; gap: 20px; - margin-top: 30px; `; const FollowBtn = styled(ProfileFollowBtn)` diff --git a/apps/u3/src/components/profile/ProfileFollowBtn.tsx b/apps/u3/src/components/profile/info/ProfileFollowBtn.tsx similarity index 93% rename from apps/u3/src/components/profile/ProfileFollowBtn.tsx rename to apps/u3/src/components/profile/info/ProfileFollowBtn.tsx index 8f9b0d6f..d861f566 100644 --- a/apps/u3/src/components/profile/ProfileFollowBtn.tsx +++ b/apps/u3/src/components/profile/info/ProfileFollowBtn.tsx @@ -2,16 +2,16 @@ import styled, { StyledComponentPropsWithRef } from 'styled-components'; import { Profile, useFollow, useUnfollow } from '@lens-protocol/react-web'; import { useCallback, useMemo } from 'react'; import { toast } from 'react-toastify'; -import { SocialButtonPrimary } from '../social/button/SocialButton'; -import useFarcasterFollowAction from '../../hooks/social/farcaster/useFarcasterFollowAction'; -import { useFarcasterCtx } from '../../contexts/social/FarcasterCtx'; +import { SocialButtonPrimary } from '../../social/button/SocialButton'; +import useFarcasterFollowAction from '../../../hooks/social/farcaster/useFarcasterFollowAction'; +import { useFarcasterCtx } from '../../../contexts/social/FarcasterCtx'; import { canFollow, canUnfollow, getHandle, isFollowedByMe, -} from '../../utils/social/lens/profile'; -import { useLensCtx } from '../../contexts/social/AppLensCtx'; +} from '../../../utils/social/lens/profile'; +import { useLensCtx } from '../../../contexts/social/AppLensCtx'; interface ProfileFollowBtnProps extends StyledComponentPropsWithRef<'button'> { lensProfiles?: Profile[]; diff --git a/apps/u3/src/components/profile/info/ProfileInfoCard.tsx b/apps/u3/src/components/profile/info/ProfileInfoCard.tsx new file mode 100644 index 00000000..a6bee652 --- /dev/null +++ b/apps/u3/src/components/profile/info/ProfileInfoCard.tsx @@ -0,0 +1,200 @@ +import { Profile as U3Profile } from '@us3r-network/data-model'; +import { ComponentPropsWithRef, useMemo } from 'react'; +import useDid from '@/hooks/profile/useDid'; +// import Loading from '../../common/loading/Loading'; +import { MultiPlatformShareMenuBtn } from '@/components/shared/share/MultiPlatformShareMenuBtn'; +import usePlatformProfileInfoData from '@/hooks/profile/usePlatformProfileInfoData'; +import useU3ProfileInfoData from '@/hooks/profile/useU3ProfileInfoData'; +import { getProfileShareUrl } from '@/utils/shared/share'; +import ProfileInfoCardLayout from './ProfileInfoCardLayout'; +import useHasU3ProfileWithDid from '@/hooks/profile/useHasU3ProfileWithDid'; + +interface ProfileInfoCardProps extends ComponentPropsWithRef<'div'> { + isSelf?: boolean; + identity: string; + canNavigateToProfile?: boolean; + onNavigateToProfileAfter?: () => void; +} +export default function ProfileInfoCard({ + identity, + isSelf, + canNavigateToProfile, + onNavigateToProfileAfter, + ...wrapperProps +}: ProfileInfoCardProps) { + const { did, loading: didLoading } = useDid(identity); + const { u3Profile, hasU3ProfileLoading } = useHasU3ProfileWithDid(did); + + const shareLink = useMemo(() => getProfileShareUrl(identity), [identity]); + const navigateToProfileUrl = useMemo( + () => (canNavigateToProfile ? `/u/${identity}` : undefined), + [canNavigateToProfile, identity] + ); + if (didLoading || hasU3ProfileLoading) { + return null; + // return ( + // + // + // + // ); + } + if (did && u3Profile) { + return ( + + ); + } + if (identity) { + return ( + + ); + } + return null; +} + +interface PlatformProfileInfoCardContainerProps + extends ComponentPropsWithRef<'div'> { + identity: string; + navigateToProfileUrl?: string; + onNavigateToProfileAfter?: () => void; + isSelf: boolean; + shareLink: string; +} +function PlatformProfileInfoCardContainer({ + identity, + isSelf, + navigateToProfileUrl, + onNavigateToProfileAfter, + shareLink, + ...wrapperProps +}: PlatformProfileInfoCardContainerProps) { + const { + fid, + lensProfiles, + recommendAddress, + platformAccounts, + postCount, + followerCount, + followingCount, + bioLinkLoading, + } = usePlatformProfileInfoData({ identity }); + return ( +
+ + {platformAccounts.length > 0 && ( +
+ +
+ )} +
+ ); +} + +interface U3ProfileInfoCardContainerProps extends ComponentPropsWithRef<'div'> { + did: string; + u3Profile: U3Profile; + isSelf?: boolean; + navigateToProfileUrl?: string; + onNavigateToProfileAfter?: () => void; + shareLink: string; +} +function U3ProfileInfoCardContainer({ + did, + u3Profile, + isSelf, + navigateToProfileUrl, + onNavigateToProfileAfter, + shareLink, + ...wrapperProps +}: U3ProfileInfoCardContainerProps) { + const { + fid, + address, + lensProfiles, + platformAccounts, + postCount, + followerCount, + followingCount, + bioLinkLoading, + } = useU3ProfileInfoData({ did, isSelf: !!isSelf }); + + return ( +
+ {' '} + {platformAccounts.length > 0 && ( +
+ +
+ )} +
+ ); +} + +const MY_PROFILE_SHARE_TITLE = 'View my profile in U3!'; +const getShareNewFriendProfileTitle = (name) => `New friend ${name} in U3!`; diff --git a/apps/u3/src/components/profile/info/ProfileInfoCardLayout.tsx b/apps/u3/src/components/profile/info/ProfileInfoCardLayout.tsx new file mode 100644 index 00000000..1fb349a5 --- /dev/null +++ b/apps/u3/src/components/profile/info/ProfileInfoCardLayout.tsx @@ -0,0 +1,208 @@ +import { Profile as LensProfile } from '@lens-protocol/react-web'; +import { Profile as U3Profile } from '@us3r-network/data-model'; +import { useSession } from '@us3r-network/auth-with-rainbowkit'; +import { toast } from 'react-toastify'; +import { ComponentPropsWithRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import PlatformFollowCounts from './PlatformFollowCounts'; +import ProfileBtns from './ProfileBtns'; +import { + PlatformProfileBasicInfo, + U3ProfileBasicInfo, +} from './ProfileBasicInfo'; +import { shortPubKey } from '@/utils/shared/shortPubKey'; +import { getDefaultAvatarWithIdentity } from '@/utils/profile/avatar'; +import { PlatformAccountData, SocialPlatform } from '@/services/social/types'; +import { Copy } from '../../common/icons/copy'; +import { cn } from '@/lib/utils'; +import { getLensProfileExternalLinkWithHandle } from '@/utils/social/lens/getLensExternalLink'; +import LensIcon from '@/components/common/icons/LensIcon'; +import { getFarcasterProfileExternalLinkWithHandle } from '@/utils/social/farcaster/getFarcasterExternalLink'; +import FarcasterIcon from '@/components/common/icons/FarcasterIcon'; + +interface ProfileInfoCardLayoutProps extends ComponentPropsWithRef<'div'> { + navigateToProfileUrl?: string; + onNavigateToProfileAfter?: () => void; + u3Profile?: U3Profile; + address: string; + identity?: string; + platformAccounts: PlatformAccountData[]; + postCount: number; + followerCount: number; + followingCount: number; + lensProfiles: LensProfile[]; + fid?: number; + loading?: boolean; + isSelf: boolean; +} +export default function ProfileInfoCardLayout({ + navigateToProfileUrl, + onNavigateToProfileAfter, + u3Profile, + identity, + address, + platformAccounts, + postCount, + followerCount, + followingCount, + lensProfiles, + fid, + loading, + isSelf, + ...wrapperProps +}: ProfileInfoCardLayoutProps) { + const session = useSession(); + + const findLensAccount = platformAccounts?.find( + (item) => item.platform === SocialPlatform.Lens + ); + const findFarcasterAccount = platformAccounts?.find( + (item) => item.platform === SocialPlatform.Farcaster + ); + const showFollowBtn = !!findLensAccount || !!findFarcasterAccount; + const showMessageBtn = !!address; + const navigate = useNavigate(); + const pathSuffix = identity ? `/${identity}` : ''; + + if (!address) { + return null; + } + + return ( +
{ + e.stopPropagation(); + }} + {...wrapperProps} + > + + {session?.id && !isSelf && ( + + )} + + {address && ( + navigate(`/u${pathSuffix}?type=following`)} + /> + )} + {platformAccounts?.length > 0 && ( + + )} +
+ ); +} + +function SocialAccounts({ + accounts, + onClick, + className, +}: ComponentPropsWithRef<'div'> & { + accounts: PlatformAccountData[]; + onClick?: (path: string) => void; +}) { + return ( +
+

Social Accounts

+
+ {accounts.map((account) => { + if (account.platform === SocialPlatform.Lens) { + return ( + + + + ); + } + if (account.platform === SocialPlatform.Farcaster) { + return ( + + + + ); + } + return null; + })} +
+
+ ); +} + +function Wallets({ + address, + onClick, + className, +}: ComponentPropsWithRef<'div'> & { + address: string; + onClick: (path: string) => void; +}) { + return ( +
+

Wallet Address

+
{ + navigator.clipboard.writeText(address).then(() => { + toast.success('Copied!'); + }); + }} + > + {shortPubKey(address)} + +
+
+ ); +} diff --git a/apps/u3/src/components/profile/info/ProfileInfoHeadless.tsx b/apps/u3/src/components/profile/info/ProfileInfoHeadless.tsx new file mode 100644 index 00000000..9233dd87 --- /dev/null +++ b/apps/u3/src/components/profile/info/ProfileInfoHeadless.tsx @@ -0,0 +1,188 @@ +import { PropsWithChildren, useEffect, useMemo } from 'react'; +import { Profile as U3Profile } from '@us3r-network/data-model'; +import useDid from '@/hooks/profile/useDid'; +import useHasU3ProfileWithDid from '@/hooks/profile/useHasU3ProfileWithDid'; +import { ChildrenRenderProps, childrenRender } from '@/utils/shared/props'; +import useU3ProfileInfoData from '@/hooks/profile/useU3ProfileInfoData'; +import usePlatformProfileInfoData from '@/hooks/profile/usePlatformProfileInfoData'; +import { getDefaultAvatarWithIdentity } from '@/utils/profile/avatar'; +import { shortPubKey } from '@/utils/shared/shortPubKey'; +import { PlatformAccountData } from '@/services/social/types'; +import { useProfileInfoCtx } from '@/contexts/profile/ProfileInfoCtx'; + +export type U3ProfileInfoRenderProps = ReturnType; +export type PlatformProfileInfoRenderProps = ReturnType< + typeof usePlatformProfileInfoData +>; + +type ProfileInfoRenderCommonProps = { + loading: boolean; + did?: string; + u3Profile?: U3Profile | null; + displayAvatar: string; + displayName: string; + displayBio: string; +}; +export type ProfileInfoRenderProps = + | (U3ProfileInfoRenderProps & ProfileInfoRenderCommonProps) + | (PlatformProfileInfoRenderProps & ProfileInfoRenderCommonProps); + +export interface ProfileInfoProps + extends ChildrenRenderProps { + identity: string; + isSelf?: boolean; +} +export default function ProfileInfoHeadless(props: ProfileInfoProps) { + const { identity, children } = props; + const { isCachedProfileInfo, getCachedProfileInfoWithIdentity } = + useProfileInfoCtx(); + + const isCached = isCachedProfileInfo(identity); + const cachedData = getCachedProfileInfoWithIdentity(identity); + const { address, recommendAddress, u3Profile, platformAccounts } = + cachedData || {}; + const displayInfo = getDisplayInfo({ + address: address || recommendAddress, + u3Profile, + platformAccounts, + }); + + if (isCached) { + return childrenRender(children, { ...cachedData, ...displayInfo }, null); + } + return ; +} + +function LoadProfileInfoHeadless({ + identity, + isSelf, + ...props +}: ProfileInfoProps) { + const { did, loading: didLoading } = useDid(identity); + const { u3Profile, hasU3ProfileLoading } = useHasU3ProfileWithDid(did); + if (didLoading || hasU3ProfileLoading) { + return childrenRender( + props?.children, + { did, u3Profile, loading: true }, + null + ); + } + if (did && u3Profile) { + return ( + + ); + } + return ; +} + +const getDisplayInfo = ({ + address, + u3Profile, + platformAccounts, +}: { + address: string; + platformAccounts: PlatformAccountData[] | null; + u3Profile?: U3Profile; +}) => { + const displayAvatar = + u3Profile?.avatar || + platformAccounts?.[0]?.avatar || + getDefaultAvatarWithIdentity( + address || String(platformAccounts?.[0]?.id) || '' + ); + const displayName = + u3Profile?.name || platformAccounts?.[0]?.name || shortPubKey(address); + const displayBio = + u3Profile?.bio || platformAccounts?.[0]?.bio || 'There is nothing here'; + return { + displayAvatar, + displayName, + displayBio, + }; +}; +export interface U3ProfileInfoProps + extends ChildrenRenderProps< + PropsWithChildren, + U3ProfileInfoRenderProps & ProfileInfoRenderCommonProps + > { + identity: string; + did: string; + u3Profile: U3Profile; + isSelf?: boolean; +} +function U3ProfileInfoHeadless({ + identity, + did, + u3Profile, + isSelf, + children, +}: U3ProfileInfoProps) { + const profileInfoData = useU3ProfileInfoData({ + did, + isSelf: !!isSelf, + }); + const { loading } = profileInfoData; + const { address, platformAccounts } = profileInfoData; + const displayInfo = getDisplayInfo({ + address, + u3Profile, + platformAccounts, + }); + const data = useMemo( + () => ({ did, u3Profile, ...profileInfoData }), + [did, u3Profile, profileInfoData] + ); + useCacheProfileInfo(identity, loading, data); + return childrenRender(children, { ...data, ...displayInfo }, null); +} + +export interface PlatformProfileInfoProps + extends ChildrenRenderProps< + PropsWithChildren, + PlatformProfileInfoRenderProps & ProfileInfoRenderCommonProps + > { + identity: string; +} +function PlatformProfileInfoHeadless({ + identity, + children, +}: PlatformProfileInfoProps) { + const profileInfoData = usePlatformProfileInfoData({ identity }); + const { loading } = profileInfoData; + const { recommendAddress: address, platformAccounts } = profileInfoData; + const displayInfo = getDisplayInfo({ + address, + platformAccounts, + }); + useCacheProfileInfo(identity, loading, profileInfoData); + + return childrenRender(children, { ...profileInfoData, ...displayInfo }, null); +} + +function useCacheProfileInfo(identity: string, loading: boolean, data: any) { + const { + addLoadingIdentity, + removeLoadingIdentity, + upsertCachedProfileInfoWithIdentity, + } = useProfileInfoCtx(); + + useEffect(() => { + if (loading) { + addLoadingIdentity(identity); + } else { + removeLoadingIdentity(identity); + } + }, [identity, loading, addLoadingIdentity, removeLoadingIdentity]); + + useEffect(() => { + if (!loading && data) { + upsertCachedProfileInfoWithIdentity(identity, data); + } + }, [identity, loading, data, upsertCachedProfileInfoWithIdentity]); +} diff --git a/apps/u3/src/components/profile/profile-info/TooltipProfileNavigateLink.tsx b/apps/u3/src/components/profile/info/TooltipProfileNavigateLink.tsx similarity index 79% rename from apps/u3/src/components/profile/profile-info/TooltipProfileNavigateLink.tsx rename to apps/u3/src/components/profile/info/TooltipProfileNavigateLink.tsx index 6926f4e0..9d22aabe 100644 --- a/apps/u3/src/components/profile/profile-info/TooltipProfileNavigateLink.tsx +++ b/apps/u3/src/components/profile/info/TooltipProfileNavigateLink.tsx @@ -5,7 +5,6 @@ import styled from 'styled-components'; import { isMobile } from 'react-device-detect'; import TooltipBase from '../../common/tooltip/TooltipBase'; import ProfileInfoCard from './ProfileInfoCard'; -import { FollowType } from '../ProfilePageFollowNav'; interface TooltipProfileNavigateLinkProps extends Omit { children: React.ReactNode; @@ -31,7 +30,7 @@ export default function TooltipProfileNavigateLink({ if (!linkRef.current || !isOpen) return; const el = linkRef.current; - const handleMouseWheel = (e: WheelEvent) => { + const handleMouseWheel = () => { setIsOpen(false); }; el.addEventListener('wheel', handleMouseWheel); @@ -66,23 +65,14 @@ export default function TooltipProfileNavigateLink({ onOpenChange={setIsOpen} > {linkEl} - + setIsOpen(false)} - clickFollowers={() => { - if (profileUrl) { - navigate(`${profileUrl}?followType=${FollowType.FOLLOWERS}`); - setIsOpen(false); - } - }} - clickFollowing={() => { - if (profileUrl) { - navigate(`${profileUrl}?followType=${FollowType.FOLLOWING}`); - setIsOpen(false); - } - }} /> diff --git a/apps/u3/src/components/profile/profile-info/PlatformAccounts.tsx b/apps/u3/src/components/profile/profile-info/PlatformAccounts.tsx deleted file mode 100644 index ba3646c7..00000000 --- a/apps/u3/src/components/profile/profile-info/PlatformAccounts.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import useUserData from 'src/hooks/social/farcaster/useUserData'; -import SettingIcon from 'src/components/common/icons/setting'; - -import { SocialPlatform } from '../../../services/social/types'; -import LensIcon from '../../common/icons/LensIcon'; -import FarcasterIcon from '../../common/icons/FarcasterIcon'; -import { SocialButtonPrimary } from '../../social/button/SocialButton'; -import { useLensCtx } from '../../../contexts/social/AppLensCtx'; -import { useFarcasterCtx } from '../../../contexts/social/FarcasterCtx'; -import { getFarcasterProfileExternalLinkWithHandle } from '../../../utils/social/farcaster/getFarcasterExternalLink'; -import { getLensProfileExternalLinkWithHandle } from '../../../utils/social/lens/getLensExternalLink'; - -export type PlatformAccountsData = Array<{ - platform: SocialPlatform; - avatar: string; - name: string; - handle: string; - id: string | number; - bio: string; - address?: string; -}>; -interface PlatformAccountsProps extends StyledComponentPropsWithRef<'div'> { - data: PlatformAccountsData; - isLoginUser?: boolean; - isSelf: boolean; -} -export default function PlatformAccounts({ - isSelf, - data, - isLoginUser, - ...wrapperProps -}: PlatformAccountsProps) { - const { isLogin: isLoginLens, setOpenLensLoginModal } = useLensCtx(); - const { - isConnected: isLoginFarcaster, - openFarcasterQR, - currFid, - currUserInfo, - setSignerSelectModalOpen, - } = useFarcasterCtx(); - const findLensAccount = data?.find( - (item) => item.platform === SocialPlatform.Lens - ); - const findFarcasterAccount = data?.find( - (item) => item.platform === SocialPlatform.Farcaster - ); - - const userInfo = useUserData(currUserInfo?.[currFid]); - const selfFarcasterAccountExtLink = - isSelf && currFid && currUserInfo - ? getFarcasterProfileExternalLinkWithHandle( - userInfo.handle || userInfo.name - ) - : ''; - - return ( - - {isLoginUser && !findLensAccount && ( - - - -
- setOpenLensLoginModal(true)}> - {isLoginLens ? 'bind' : 'login'} - -
-
- )} - {isLoginUser && !findFarcasterAccount && ( - - - -
- openFarcasterQR()}> - {isLoginFarcaster ? 'bind' : 'login'} - -
-
- )} - - {data.map((item) => { - if (isSelf && item.platform === SocialPlatform.Farcaster) { - return null; - } - let accountExternalLink = ''; - switch (item.platform) { - case SocialPlatform.Farcaster: - accountExternalLink = getFarcasterProfileExternalLinkWithHandle( - item.handle || item.name - ); - break; - case SocialPlatform.Lens: - accountExternalLink = getLensProfileExternalLinkWithHandle( - item.handle || item.name - ); - break; - default: - break; - } - return ( - - - -
- {!!item.name && {item.name}} - {!!item.handle && @{item.handle}} -
- - {(() => { - switch (item.platform) { - case SocialPlatform.Lens: - return ; - case SocialPlatform.Farcaster: - return ; - default: - return null; - } - })()} -
- ); - })} - {isSelf && currFid && currUserInfo && ( - - -
- - -
-
- {!!userInfo.name && {userInfo.name}} - {!!userInfo.handle && @{userInfo.handle}} -
- -
- )} -
- ); -} -const Wrapper = styled.div` - width: 100%; -`; -const Row = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - gap: 20px; - padding-top: 20px; - position: relative; - - .avatar { - position: relative; - > .platform { - position: absolute; - bottom: 0px; - left: 0px; - } - } - - button.setting { - background: transparent; - border: none; - cursor: pointer; - outline: none; - } -`; -const Line = styled.div` - width: 0px; - height: 20px; - border-left: 1px dashed #718096; - position: absolute; - top: 0px; - left: 20px; -`; -const Avatar = styled.img` - width: 40px; - height: 40px; - flex-shrink: 0; - border-radius: 50%; - object-fit: cover; -`; -const Center = styled.a` - display: flex; - align-items: end; - flex: 1; - gap: 5px; - color: #718096; - text-decoration: none; - &:hover { - text-decoration: underline; - } -`; -const Name = styled.span` - color: #fff; - - /* Text/Body 16pt · 1rem */ - font-family: Rubik; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: 24px; /* 150% */ -`; -const Handle = styled.span` - width: 0; - flex: 1; - color: #718096; - - /* Text/Body 16pt · 1rem */ - font-family: Rubik; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: 24px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const LoginButton = styled(SocialButtonPrimary)` - width: 60px; - height: 24px; - font-size: 12px; - font-weight: 400; -`; diff --git a/apps/u3/src/components/profile/profile-info/PlatformFollowCounts.tsx b/apps/u3/src/components/profile/profile-info/PlatformFollowCounts.tsx deleted file mode 100644 index 5c4f8810..00000000 --- a/apps/u3/src/components/profile/profile-info/PlatformFollowCounts.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import styled, { StyledComponentPropsWithRef } from 'styled-components'; - -interface PlatformFollowCountsProps extends StyledComponentPropsWithRef<'div'> { - followersCount: number; - followingCount: number; - clickFollowing?: () => void; - clickFollowers?: () => void; -} -export default function PlatformFollowCounts({ - followersCount, - followingCount, - clickFollowing, - clickFollowers, - ...wrapperProps -}: PlatformFollowCountsProps) { - return ( - - - {followersCount} - Followers - - - {followingCount} - Following - - - ); -} - -const CountsWrapper = styled.div` - margin-top: 20px; - display: flex; - justify-content: space-between; - align-items: center; -`; -const CountItem = styled.div<{ onClick: () => void }>` - display: flex; - align-items: center; - gap: 5px; - ${(props) => !!props.onClick && `cursor: pointer;`} -`; -const Count = styled.span` - color: #fff; - font-family: Rubik; - font-size: 16px; - font-style: normal; - font-weight: 700; - line-height: normal; -`; -const CountText = styled.span` - color: #718096; - - /* Regular-16 */ - font-family: Rubik; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: normal; -`; diff --git a/apps/u3/src/components/profile/profile-info/PlatformProfileInfoCardContainer.tsx b/apps/u3/src/components/profile/profile-info/PlatformProfileInfoCardContainer.tsx deleted file mode 100644 index 3d47840c..00000000 --- a/apps/u3/src/components/profile/profile-info/PlatformProfileInfoCardContainer.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { StyledComponentPropsWithRef } from 'styled-components'; -import ProfileInfoCardLayout from './ProfileInfoCardLayout'; -import usePlatformProfileInfoData from '../../../hooks/profile/usePlatformProfileInfoData'; - -interface PlatformProfileInfoCardContainerProps - extends StyledComponentPropsWithRef<'div'> { - identity: string; - navigateToProfileUrl?: string; - onNavigateToProfileAfter?: () => void; - clickFollowing?: () => void; - clickFollowers?: () => void; - isSelf: boolean; - shareLink: string; -} -export default function PlatformProfileInfoCardContainer({ - identity, - isSelf, - navigateToProfileUrl, - onNavigateToProfileAfter, - clickFollowing, - clickFollowers, - shareLink, - ...wrapperProps -}: PlatformProfileInfoCardContainerProps) { - const { - fid, - lensProfiles, - recommendAddress, - platformAccounts, - followersCount, - followingCount, - bioLinkLoading, - } = usePlatformProfileInfoData({ identity }); - - return ( - - ); -} diff --git a/apps/u3/src/components/profile/profile-info/ProfileInfoCard.tsx b/apps/u3/src/components/profile/profile-info/ProfileInfoCard.tsx deleted file mode 100644 index 2f475f63..00000000 --- a/apps/u3/src/components/profile/profile-info/ProfileInfoCard.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useProfileState } from '@us3r-network/profile'; -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import { useEffect, useMemo, useState } from 'react'; -import useDid from '../../../hooks/profile/useDid'; -import Loading from '../../common/loading/Loading'; -import U3ProfileInfoCardContainer from './U3ProfileInfoCardContainer'; -import PlatformProfileInfoCardContainer from './PlatformProfileInfoCardContainer'; -import { isDidPkh } from '../../../utils/shared/did'; -import { getProfileShareUrl } from '../../../utils/shared/share'; - -interface ProfileInfoCardProps extends StyledComponentPropsWithRef<'div'> { - isSelf?: boolean; - identity: string; - canNavigateToProfile?: boolean; - onNavigateToProfileAfter?: () => void; - clickFollowing?: () => void; - clickFollowers?: () => void; -} -export default function ProfileInfoCard({ - identity, - isSelf, - canNavigateToProfile, - onNavigateToProfileAfter, - clickFollowing, - clickFollowers, - ...wrapperProps -}: ProfileInfoCardProps) { - const { did, loading: didLoading } = useDid(identity); - const { getProfileWithDid } = useProfileState(); - const [hasProfile, setHasProfile] = useState(false); - const [hasProfileLoading, setHasProfileLoading] = useState(false); - useEffect(() => { - (async () => { - if (isDidPkh(did)) { - setHasProfileLoading(true); - const profile = await getProfileWithDid(did); - if (profile) { - setHasProfile(true); - } - setHasProfileLoading(false); - } else { - setHasProfile(false); - } - })(); - }, [did]); - - const shareLink = useMemo(() => getProfileShareUrl(identity), [identity]); - const navigateToProfileUrl = useMemo( - () => (canNavigateToProfile ? `/u/${identity}` : undefined), - [canNavigateToProfile, identity] - ); - if (didLoading || hasProfileLoading) { - return null; - // return ( - // - // - // - // ); - } - if (did && hasProfile) { - return ( - - ); - } - if (identity) { - return ( - - ); - } - return null; -} - -// const LoadingWrapper = styled.div` -// padding: 20px; -// width: 360px; -// box-sizing: border-box; -// background: #1b1e23; -// border-radius: 20px; -// display: flex; -// justify-content: center; -// align-items: center; -// `; diff --git a/apps/u3/src/components/profile/profile-info/ProfileInfoCardLayout.tsx b/apps/u3/src/components/profile/profile-info/ProfileInfoCardLayout.tsx deleted file mode 100644 index 5f1723b7..00000000 --- a/apps/u3/src/components/profile/profile-info/ProfileInfoCardLayout.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { UserInfo } from '@us3r-network/profile'; -import styled, { StyledComponentPropsWithRef, css } from 'styled-components'; -import { Profile } from '@lens-protocol/react-web'; - -import { useSession } from '@us3r-network/auth-with-rainbowkit'; -import PlatformAccounts, { PlatformAccountsData } from './PlatformAccounts'; -import Loading from '../../common/loading/Loading'; -import PlatformFollowCounts from './PlatformFollowCounts'; -import ProfileBtns from './ProfileBtns'; -import { - PlatformProfileBasicInfo, - U3ProfileBasicInfo, -} from './ProfileBasicInfo'; -import { shortPubKey } from '../../../utils/shared/shortPubKey'; -import { getDefaultAvatarWithIdentity } from '../../../utils/profile/avatar'; -import { SocialPlatform } from '../../../services/social/types'; -import { MultiPlatformShareMenuBtn } from '../../shared/share/MultiPlatformShareMenuBtn'; - -const MY_PROFILE_SHARE_TITLE = 'View my profile in U3!'; -const getShareNewFriendProfileTitle = (name) => `New friend ${name} in U3!`; - -interface ProfileInfoCardLayoutProps - extends StyledComponentPropsWithRef<'div'> { - isU3Profile: boolean; - navigateToProfileUrl?: string; - onNavigateToProfileAfter?: () => void; - did?: string; - address: string; - platformAccounts: PlatformAccountsData; - followersCount: number; - followingCount: number; - lensProfiles: Profile[]; - fid?: number; - clickFollowing?: () => void; - clickFollowers?: () => void; - loading?: boolean; - isSelf: boolean; - shareLink: string; -} -export default function ProfileInfoCardLayout({ - isU3Profile, - navigateToProfileUrl, - onNavigateToProfileAfter, - did, - address, - platformAccounts, - followersCount, - followingCount, - lensProfiles, - fid, - clickFollowing, - clickFollowers, - loading, - isSelf, - shareLink, - ...wrapperProps -}: ProfileInfoCardLayoutProps) { - const session = useSession(); - - const findLensAccount = platformAccounts?.find( - (item) => item.platform === SocialPlatform.Lens - ); - const findFarcasterAccount = platformAccounts?.find( - (item) => item.platform === SocialPlatform.Farcaster - ); - const showFollowBtn = !!findLensAccount || !!findFarcasterAccount; - const showMessageBtn = !!address; - - if (loading) { - return null; - // return ( - // - // - // - // ); - } - - if (isU3Profile) { - return ( - { - e.stopPropagation(); - }} - {...wrapperProps} - > - {({ isLoginUser, info }) => { - return ( - <> - - - - - - - - - - - - {session?.id && !isLoginUser && ( - - )} - - ); - }} - - ); - } - - if (!address) { - return null; - } - - return ( - { - e.stopPropagation(); - }} - {...wrapperProps} - > - - - {platformAccounts.length > 0 && ( - - )} - - - - - - {platformAccounts.length > 0 - ? platformAccounts?.[0]?.bio - : 'There is nothing here'} - - - - - {session?.id && ( - - )} - - ); -} -const HeaderWrapper = styled.div` - display: flex; - gap: 10px; - justify-content: space-between; -`; -export const PostShareMenuBtn = styled(MultiPlatformShareMenuBtn)` - border: none; - padding: 0px; - width: 20px; - height: 20px; - border-radius: 50%; - background: none; - &:not(:disabled):hover { - border: none; - background-color: #14171a; - } - & > svg { - width: 12px; - height: 12px; - cursor: pointer; - path { - stroke: #ffffff; - } - } -`; -const BaseWrapperCss = css` - padding: 20px; - width: 360px; - box-sizing: border-box; - background: #1b1e23; - border-radius: 20px; - border: 1px solid #39424c; -`; -const LoadingWrapper = styled.div` - ${BaseWrapperCss} - height: 230px; - display: flex; - justify-content: center; - align-items: center; -`; - -const BioCss = css` - color: var(--718096, #718096); - - /* Text/Body 16pt · 1rem */ - font-family: Rubik; - font-size: 16px; - font-style: normal; - font-weight: 400; - line-height: 24px; /* 150% */ - opacity: 0.8; - - display: block; - margin-top: 20px; -`; - -const U3ProfileCardWrapper = styled(UserInfo)` - ${BaseWrapperCss} - - [data-state-element='Bio'] { - ${BioCss} - } -`; -const PlatformProfileCardWrapper = styled.div` - ${BaseWrapperCss} -`; -const PlatformBio = styled.span` - ${BioCss} -`; diff --git a/apps/u3/src/components/profile/profile-info/U3ProfileInfoCardContainer.tsx b/apps/u3/src/components/profile/profile-info/U3ProfileInfoCardContainer.tsx deleted file mode 100644 index ac454bce..00000000 --- a/apps/u3/src/components/profile/profile-info/U3ProfileInfoCardContainer.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { StyledComponentPropsWithRef } from 'styled-components'; -import ProfileInfoCardLayout from './ProfileInfoCardLayout'; -import useU3ProfileInfoData from '../../../hooks/profile/useU3ProfileInfoData'; - -interface U3ProfileInfoCardContainerProps - extends StyledComponentPropsWithRef<'div'> { - did: string; - isSelf?: boolean; - navigateToProfileUrl?: string; - onNavigateToProfileAfter?: () => void; - clickFollowing?: () => void; - clickFollowers?: () => void; - shareLink: string; -} -export default function U3ProfileInfoCardContainer({ - did, - isSelf, - navigateToProfileUrl, - onNavigateToProfileAfter, - clickFollowing, - clickFollowers, - shareLink, - ...wrapperProps -}: U3ProfileInfoCardContainerProps) { - const { - fid, - address, - lensProfiles, - platformAccounts, - followersCount, - followingCount, - bioLinkLoading, - } = useU3ProfileInfoData({ did, isSelf: !!isSelf }); - - return ( - - ); -} diff --git a/apps/u3/src/components/profile/save/FavList.tsx b/apps/u3/src/components/profile/save/FavList.tsx new file mode 100644 index 00000000..1cd0b91c --- /dev/null +++ b/apps/u3/src/components/profile/save/FavList.tsx @@ -0,0 +1,46 @@ +import { + FavListItemData, + FavListLinkItem, + FavListPostItem, +} from './FavListItem'; + +export type FavListProps = { + data: FavListItemData[]; + onItemClick?: (item: FavListItemData) => void; +}; + +export default function FavList({ data, onItemClick }: FavListProps) { + if (!data || data.length === 0) { + return ( +
+ Nothing to see here! Explore and favorite what you like! +
+ ); + } + return ( +
+ {data.map((item) => { + switch (item.type) { + case 'link': + return ( + onItemClick && onItemClick(item)} + /> + ); + case 'post': + return ( + onItemClick && onItemClick(item)} + /> + ); + default: + return null; + } + })} +
+ ); +} diff --git a/apps/u3/src/components/profile/save/FavListItem.tsx b/apps/u3/src/components/profile/save/FavListItem.tsx new file mode 100644 index 00000000..f730efa2 --- /dev/null +++ b/apps/u3/src/components/profile/save/FavListItem.tsx @@ -0,0 +1,103 @@ +import styled, { StyledComponentPropsWithRef } from 'styled-components'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { defaultFormatFromNow } from '../../../utils/shared/time'; +import LinkBox from '../../news/contents/LinkBox'; +import FCast from '@/components/social/farcaster/FCast'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { getFarcasterCastInfo } from '@/services/social/api/farcaster'; +import { FarCast } from '@/services/social/types'; +import { userDataObjFromArr } from '@/utils/social/farcaster/user-data'; +import { getExploreFcPostDetailPath } from '@/route/path'; + +export type FavListItemData = { + id: string; + url?: string; + title?: string; + type: string; + data: string; + logo?: string; + createAt?: string; +}; + +export type FavListItemProps = StyledComponentPropsWithRef<'div'> & { + data: FavListItemData; +}; + +export function FavListLinkItem({ data, ...props }: FavListItemProps) { + return ( +
+ +

+ {data.title || data.url} +

+ {!!data?.createAt && ( +

{defaultFormatFromNow(data.createAt)}

+ )} +
+ ); +} + +export function FavListPostItem({ data, ...props }: FavListItemProps) { + const navigate = useNavigate(); + // console.log('FavListPostItem', JSON.parse(data.data || '')); + const { openFarcasterQR } = useFarcasterCtx(); + const [cast, setCast] = useState(); + const [farcasterUserDataObj, setFarcasterUserDataObj] = useState({}); + const castData = JSON.parse(data.data); + const castHex = Buffer.from(castData.hash).toString('hex'); + useEffect(() => { + if (castHex) + getFarcasterCastInfo(castHex, {}).then((res) => { + // console.log('getFarcasterCastInfo', res); + if (res.data.code !== 0) { + throw new Error(res.data.msg); + } + const { farcasterUserData: farcasterUserDataTemp, cast: castTemp } = + res.data.data; + setCast(castTemp); + setFarcasterUserDataObj(userDataObjFromArr(farcasterUserDataTemp)); + }); + }, [castHex]); + if (cast) { + return ( + { + console.log('castClickAction'); + navigate(getExploreFcPostDetailPath(castHex)); + }} + /> + ); + } + return null; +} + +const IconLink = styled(LinkBox)` + display: flex; + flex-direction: row; + align-items: center; + padding: 0; + box-sizing: border-box; + gap: 6px; + background: #14171a; + border-radius: 100px; + + img { + width: 20px; + height: 20px; + } + span { + font-weight: 400; + font-size: 14px; + line-height: 18px; + color: #718096; + } +`; diff --git a/apps/u3/src/components/save/SyncingBotSaves.tsx b/apps/u3/src/components/profile/save/SyncingBotSaves.tsx similarity index 95% rename from apps/u3/src/components/save/SyncingBotSaves.tsx rename to apps/u3/src/components/profile/save/SyncingBotSaves.tsx index e8c74f3d..e2de4e70 100644 --- a/apps/u3/src/components/save/SyncingBotSaves.tsx +++ b/apps/u3/src/components/profile/save/SyncingBotSaves.tsx @@ -6,7 +6,7 @@ import { getSavedCasts, setSavedCastsSynced, } from '@/services/social/api/farcaster'; -import { getAddressWithDidPkh } from '../../utils/shared/did'; +import { getAddressWithDidPkh } from '../../../utils/shared/did'; export default function SyncingBotSaves({ onComplete, @@ -29,8 +29,8 @@ export default function SyncingBotSaves({ title: text ? text.slice(0, 200) : 'Saved Farcaster Cast using U3 Bot', - type: 'link', - data: JSON.stringify(item), + type: 'post', + data: JSON.stringify({ hash: item.castHash, text }), }; }); setSaves(links); diff --git a/apps/u3/src/components/save/SaveExploreList.tsx b/apps/u3/src/components/save/SaveExploreList.tsx deleted file mode 100644 index 776ef708..00000000 --- a/apps/u3/src/components/save/SaveExploreList.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * @Author: bufan bufan@hotmail.com - * @Date: 2023-11-24 18:31:36 - * @LastEditors: bufan bufan@hotmail.com - * @LastEditTime: 2023-12-14 10:19:49 - * @FilePath: /u3/apps/u3/src/components/save/SaveExploreList.tsx - * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - */ -import styled from 'styled-components'; -import AnimatedListItem, { - useAnimatedListTransition, -} from '../common/animation/AnimatedListItem'; -import SaveExploreListItem from './SaveExploreListItem'; -import { MEDIA_BREAK_POINTS } from '@/constants'; - -export type SaveExploreListItemData = { - id: string; - url?: string; - title?: string; - logo?: string; - createAt?: string; -}; - -export type SaveExploreListProps = { - data: SaveExploreListItemData[]; - onItemClick?: (item: SaveExploreListItemData) => void; -}; - -export default function SaveExploreList({ - data, - onItemClick, -}: SaveExploreListProps) { - const transitions = useAnimatedListTransition(data); - return ( - - {transitions((styles, item) => { - return ( - - onItemClick && onItemClick(item)} - /> - - ); - })} - - ); -} -const SaveExploreListWrapper = styled.div` - width: 100%; - display: grid; - grid-gap: 20px; - grid-template-columns: repeat(4, minmax(calc((100% - 20px * 3) / 4), 1fr)); - - @media (min-width: ${MEDIA_BREAK_POINTS.md}px) and (max-width: ${MEDIA_BREAK_POINTS.xxl}px) { - grid-template-columns: repeat(6, minmax(calc((100% - 20px * 2) / 3), 1fr)); - } -`; diff --git a/apps/u3/src/components/save/SaveExploreListItem.tsx b/apps/u3/src/components/save/SaveExploreListItem.tsx deleted file mode 100644 index 81445dd4..00000000 --- a/apps/u3/src/components/save/SaveExploreListItem.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import { defaultFormatFromNow } from '../../utils/shared/time'; -import LinkBox from '../news/contents/LinkBox'; -import type { SaveExploreListItemData } from './SaveExploreList'; - -export type SaveExploreListItemProps = StyledComponentPropsWithRef<'div'> & { - data: SaveExploreListItemData; -}; - -export default function SaveExploreListItem({ - data, - ...props -}: SaveExploreListItemProps) { - // console.log('save item', data); - return ( - - - -
-
- {data.title || data.url} -
- {!!data?.createAt && ( - - {defaultFormatFromNow(data.createAt)} - - )} -
-
-
- ); -} - -const Wrapper = styled.div` - width: 100%; - height: 200px; - padding: 20px; - box-sizing: border-box; - background: #1b1e23; - border: 1px solid #39424c; - border-radius: 20px; - position: relative; - cursor: pointer; - color: #ffffff; - &:hover { - background: rgba(20, 23, 26, 0.3); - } -`; -const ListItemInner = styled.div` - width: 100%; - height: 100%; - transition: all 0.3s; - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 20px; -`; - -const TimeText = styled.span` - font-weight: 400; - font-size: 14px; - line-height: 18px; - - color: #718096; -`; - -const IconLink = styled(LinkBox)` - display: flex; - flex-direction: row; - align-items: center; - padding: 0; - box-sizing: border-box; - gap: 6px; - background: #14171a; - border-radius: 100px; - - img { - width: 20px; - height: 20px; - } - span { - font-weight: 400; - font-size: 14px; - line-height: 18px; - color: #718096; - } -`; - -export const SaveExploreListItemMobile = styled(SaveExploreListItem)` - padding: 10px; - height: auto; -`; diff --git a/apps/u3/src/components/save/SaveExploreListMobile.tsx b/apps/u3/src/components/save/SaveExploreListMobile.tsx deleted file mode 100644 index 2ce9e5c4..00000000 --- a/apps/u3/src/components/save/SaveExploreListMobile.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import styled from 'styled-components'; -import { SaveExploreListItemMobile } from './SaveExploreListItem'; -import AnimatedListItem, { - useAnimatedListTransition, -} from '../common/animation/AnimatedListItem'; -import CardBase from '../common/card/CardBase'; -import type { SaveExploreListItemData } from './SaveExploreList'; - -export type SaveExploreListProps = { - data: SaveExploreListItemData[]; - onItemClick?: (item: SaveExploreListItemData) => void; -}; - -export default function SaveExploreListMobile({ - data, - onItemClick, -}: SaveExploreListProps) { - const transitions = useAnimatedListTransition(data); - return ( - - {transitions((styles, item) => { - return ( - - onItemClick && onItemClick(item)} - /> - - ); - })} - - ); -} -const SaveExploreListWrapper = styled(CardBase)` - padding: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background: transparent; - border: none; - overflow: visible; - & > div { - & { - border: 1px solid rgba(57, 66, 76, 0.5); - border-radius: 10px; - margin-bottom: 10px; - } - &:last-child:not(:first-child) { - margin-bottom: none; - } - } -`; diff --git a/apps/u3/src/components/shared/select/FeedsFilter.tsx b/apps/u3/src/components/shared/select/FeedsFilter.tsx new file mode 100644 index 00000000..456fd6dd --- /dev/null +++ b/apps/u3/src/components/shared/select/FeedsFilter.tsx @@ -0,0 +1,75 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ProfileFeedsGroups } from '@/services/social/api/feeds'; + +export default function FeedsFilter({ + defaultValue, + onChange, +}: { + defaultValue: ProfileFeedsGroups; + onChange: (type: ProfileFeedsGroups) => void; +}) { + return ( + + ); +} diff --git a/apps/u3/src/components/shared/select/PlatformFilter.tsx b/apps/u3/src/components/shared/select/PlatformFilter.tsx new file mode 100644 index 00000000..11639ba2 --- /dev/null +++ b/apps/u3/src/components/shared/select/PlatformFilter.tsx @@ -0,0 +1,60 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { SocialPlatform } from '@/services/social/types'; + +export default function PlatformFilter({ + defaultValue, + onChange, +}: { + defaultValue: SocialPlatform; + onChange: (platform: SocialPlatform) => void; +}) { + return ( + + ); +} diff --git a/apps/u3/src/components/shared/share/MultiPlatformShareModal.tsx b/apps/u3/src/components/shared/share/MultiPlatformShareModal.tsx index b89e81ea..a713fd33 100644 --- a/apps/u3/src/components/shared/share/MultiPlatformShareModal.tsx +++ b/apps/u3/src/components/shared/share/MultiPlatformShareModal.tsx @@ -1,10 +1,10 @@ import styled from 'styled-components'; -import { isMobile } from 'react-device-detect'; import ModalContainer from '../../common/modal/ModalContainer'; import { ModalCloseBtn } from '../../common/modal/ModalWidgets'; import AddPostForm from '../../social/AddPostForm'; import { useGlobalModalsCtx } from '../../../contexts/shared/GlobalModalsCtx'; +import { cn } from '@/lib/utils'; export default function MultiPlatformShareModal({ open, @@ -32,8 +32,16 @@ export default function MultiPlatformShareModal({ contentTop="60px" contentTransform="translateX(-50%)" > - - +
+ { closeModal(); @@ -50,28 +58,7 @@ export default function MultiPlatformShareModal({ previewDomain: shareLinkDomain, }} /> - +
); } - -const ModalBody = styled.div<{ isMobile?: boolean }>` - width: ${(props) => (props.isMobile ? 'fit-content' : '600px')}; - /* min-height: 194px; */ - flex-shrink: 0; - - padding: 20px; - box-sizing: border-box; - - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 20px; - - position: relative; -`; -const CloseBtn = styled(ModalCloseBtn)` - position: absolute; - top: 20px; - right: 20px; -`; diff --git a/apps/u3/src/components/social/AddPost.tsx b/apps/u3/src/components/social/AddPost.tsx index 2ae41b2b..c35c7927 100644 --- a/apps/u3/src/components/social/AddPost.tsx +++ b/apps/u3/src/components/social/AddPost.tsx @@ -27,6 +27,7 @@ export default function AddPost({ } setOpenPostModal(true); }} + {...props} > Command + P to Post diff --git a/apps/u3/src/components/social/AddPostMobileBtn.tsx b/apps/u3/src/components/social/AddPostMobileBtn.tsx new file mode 100644 index 00000000..7a42ce50 --- /dev/null +++ b/apps/u3/src/components/social/AddPostMobileBtn.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import AddPostModal from './AddPostModal'; +import useLogin from '../../hooks/shared/useLogin'; +import ColorButton from '../common/button/ColorButton'; + +export default function AddPostMobileBtn() { + const [open, setOpen] = useState(false); + const { isLogin, login } = useLogin(); + return ( + <> + { + if (!isLogin) { + login(); + return; + } + setOpen(true); + }} + > + + + + { + setOpen(false); + }} + /> + + ); +} diff --git a/apps/u3/src/components/social/AddPostModal.tsx b/apps/u3/src/components/social/AddPostModal.tsx index aa932524..4b818997 100644 --- a/apps/u3/src/components/social/AddPostModal.tsx +++ b/apps/u3/src/components/social/AddPostModal.tsx @@ -13,6 +13,7 @@ import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; import ModalContainer from '../common/modal/ModalContainer'; import { ModalCloseBtn } from '../common/modal/ModalWidgets'; import AddPostForm from './AddPostForm'; +import { cn } from '@/lib/utils'; export default function AddPostModal({ open, @@ -35,31 +36,18 @@ export default function AddPostModal({ zIndex={40} contentTop="30%" > - - +
+ - +
); } - -const ModalBody = styled.div<{ isMobile?: boolean }>` - width: ${(props) => (props.isMobile ? '96vw' : '600px')}; - /* min-height: 194px; */ - flex-shrink: 0; - - padding: 20px; - box-sizing: border-box; - - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 20px; - - position: relative; -`; -const CloseBtn = styled(ModalCloseBtn)` - position: absolute; - top: 20px; - right: 20px; -`; diff --git a/apps/u3/src/container/community/CommonStyles.tsx b/apps/u3/src/components/social/CommonStyles.tsx similarity index 100% rename from apps/u3/src/container/community/CommonStyles.tsx rename to apps/u3/src/components/social/CommonStyles.tsx diff --git a/apps/u3/src/components/social/Embed.tsx b/apps/u3/src/components/social/Embed.tsx index 0ee10e69..e7bcd564 100644 --- a/apps/u3/src/components/social/Embed.tsx +++ b/apps/u3/src/components/social/Embed.tsx @@ -20,6 +20,7 @@ import ModalImg from './ModalImg'; import U3ZoraMinter from './farcaster/U3ZoraMinter'; import LinkModal from '../news/links/LinkModal'; import EmbedFrameWebsite from './EmbedFrameWebsite'; +import EmbedVideo from './EmbedVideo'; const ValidFrameButtonValue = [ [0, 0, 0, 0].join(''), @@ -119,7 +120,9 @@ export default function Embed({ /> )} - {/* {embedVideos */} + {embedVideos.map((video) => { + return ; + })}
{[...metadataCasts].map((item) => { if (item.cast === undefined) return null; diff --git a/apps/u3/src/components/social/EmbedFrame.tsx b/apps/u3/src/components/social/EmbedFrame.tsx index 9862b963..60512e03 100644 --- a/apps/u3/src/components/social/EmbedFrame.tsx +++ b/apps/u3/src/components/social/EmbedFrame.tsx @@ -4,15 +4,14 @@ import { useCallback, useMemo, useState } from 'react'; import { Frame } from 'frames.js'; import { sendTransaction, - simulateContract, switchChain, waitForTransactionReceipt, - writeContract, } from '@wagmi/core'; import { useAccount, useConfig, useChains } from 'wagmi'; +import { useConnectModal } from '@rainbow-me/rainbowkit'; import { CastId, Message, makeFrameAction } from '@farcaster/hub-web'; -import { formatEther, fromHex, parseEther, toHex } from 'viem'; +import { fromHex, toHex } from 'viem'; import { toast } from 'react-toastify'; import { Cross2Icon, CaretLeftIcon } from '@radix-ui/react-icons'; import { FarCast } from '../../services/social/types'; @@ -45,10 +44,11 @@ export default function EmbedCastFrame({ const [frameText, setFrameText] = useState(''); const [frameRedirect, setFrameRedirect] = useState(''); const [frameData, setFrameData] = useState(data); - + const [isLoading, setIsLoading] = useState(false); const [txSimulate, setTxSimulate] = useState([]); const [txData, setTxData] = useState(); const [txBtnIdx, setTxBtnIdx] = useState(0); + const { openConnectModal } = useConnectModal(); const config = useConfig(); @@ -198,6 +198,10 @@ export default function EmbedCastFrame({ }; if (action === 'tx') { + if (!address) { + openConnectModal(); + return; + } console.log('tx', target); postData.actionUrl = target; postData.fromAddress = address; @@ -241,7 +245,16 @@ export default function EmbedCastFrame({ e.stopPropagation(); }} > -
+
{(frameData.inputText && ( @@ -267,7 +280,7 @@ export default function EmbedCastFrame({ key={idx} type="button" className="flex-grow p-2 m-2 mt-1 rounded-xl" - onClick={(e) => { + onClick={async (e) => { e.stopPropagation(); console.log( 'postFrameAction', @@ -275,10 +288,22 @@ export default function EmbedCastFrame({ item.action, item.target ); - postFrameAction(idx + 1, item.action, item.target); + try { + setIsLoading(true); + await postFrameAction(idx + 1, item.action, item.target); + } catch (err) { + console.error(err); + } finally { + setIsLoading(false); + } }} > + {item.action === 'mint' ? `⬗ ` : ''} {item.label} + {item.action === 'tx' ? : ''} + {item.action === 'post_redirect' || item.action === 'link' + ? ` ↗` + : ''} ); })} @@ -321,6 +346,23 @@ export default function EmbedCastFrame({ ); } +function TxIcon() { + return ( + + ); +} + function EmbedCastFrameTxSimulate({ walletAddress, txBtnIdx, diff --git a/apps/u3/src/components/social/EmbedVideo.tsx b/apps/u3/src/components/social/EmbedVideo.tsx new file mode 100644 index 00000000..ed56f2a6 --- /dev/null +++ b/apps/u3/src/components/social/EmbedVideo.tsx @@ -0,0 +1,9 @@ +import VideoRenderer from './VideoRender'; + +export default function EmbedVideo({ videoUrl }: { videoUrl: string }) { + return ( +
+ ; +
+ ); +} diff --git a/apps/u3/src/components/social/PostCard.tsx b/apps/u3/src/components/social/PostCard.tsx index 8ff73cc1..f0b9fa5d 100644 --- a/apps/u3/src/components/social/PostCard.tsx +++ b/apps/u3/src/components/social/PostCard.tsx @@ -22,7 +22,7 @@ import { farcasterHandleToBioLinkHandle, lensHandleToBioLinkHandle, } from '../../utils/profile/biolink'; -import TooltipProfileNavigateLink from '../profile/profile-info/TooltipProfileNavigateLink'; +import TooltipProfileNavigateLink from '../profile/info/TooltipProfileNavigateLink'; import { MultiPlatformShareMenuBtn } from '../shared/share/MultiPlatformShareMenuBtn'; import { SOCIAL_SHARE_TITLE } from '../../constants'; import { SaveButton } from '../shared/button/SaveButton'; @@ -98,10 +98,11 @@ export default function PostCard({ if (isDetail) setLinkParam({ url: getOfficialPublicationUrl(id), - type: 'link', + type: 'post', title: data?.content.slice(0, 200), // todo: expand this limit at model + data, }); - }, [id, isDetail]); + }, [id, isDetail, data?.content]); return ( @@ -216,6 +217,7 @@ export function PostCardWrapperV2({
` } } `; -export const PostCardActionsWrapper = styled.div` - display: flex; - align-items: center; - gap: 15px; -`; +export function PostCardActionsWrapper(props: ComponentPropsWithRef<'div'>) { + return ( +
+ ); +} export const PostCardImgWrapper = styled.div<{ len: number }>` display: flex; diff --git a/apps/u3/src/components/social/SocialWhoToFollow.tsx b/apps/u3/src/components/social/SocialWhoToFollow.tsx index 5c2babbc..ebc4d668 100644 --- a/apps/u3/src/components/social/SocialWhoToFollow.tsx +++ b/apps/u3/src/components/social/SocialWhoToFollow.tsx @@ -21,7 +21,7 @@ import { farcasterHandleToBioLinkHandle, lensHandleToBioLinkHandle, } from '../../utils/profile/biolink'; -import TooltipProfileNavigateLink from '../profile/profile-info/TooltipProfileNavigateLink'; +import TooltipProfileNavigateLink from '../profile/info/TooltipProfileNavigateLink'; import { getHandle, getName } from '../../utils/social/lens/profile'; const SUGGEST_NUM = 3; diff --git a/apps/u3/src/components/social/VideoRender.tsx b/apps/u3/src/components/social/VideoRender.tsx new file mode 100644 index 00000000..31bdf9d2 --- /dev/null +++ b/apps/u3/src/components/social/VideoRender.tsx @@ -0,0 +1,92 @@ +/* eslint-disable react/destructuring-assignment */ + +import React, { useMemo, useEffect } from 'react'; +import videojs from 'video.js'; + +import 'video.js/dist/video-js.css'; + +interface PlayerProps { + videoSrc: string; +} + +const videoJSoptions: { + techOrder?: string[]; + autoplay?: boolean; + fill?: boolean; + controls?: boolean; + responsive?: true; + fluid?: true; +} = { + fluid: true, + controls: true, + fill: true, + responsive: true, +}; + +function handleClick(e: any) { + e.stopPropagation(); + e.preventDefault(); +} + +export default function VideoRenderer(props: PlayerProps) { + const videoRef = React.useRef(null); + const playerRef = React.useRef(null); + + const options = useMemo( + () => ({ + ...videoJSoptions, + // video is not necessarily rewritten yet + sources: [ + { + src: props.videoSrc ?? '', + type: props.videoSrc?.endsWith('.m3u8') + ? 'application/x-mpegURL' + : props.videoSrc?.endsWith('.mp4') + ? 'video/mp4' + : '', + }, + ], + }), + [props.videoSrc] + ); + + useEffect(() => { + // Make sure Video.js player is only initialized once + if (!playerRef.current) { + // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode. + const videoElement = document.createElement('video-js'); + + videoElement.classList.add('vjs-big-play-centered'); + videoRef.current?.appendChild(videoElement); + + playerRef.current = videojs(videoElement, options); + } else { + const player = playerRef.current; + + player.autoplay(options.autoplay); + player.src(options.sources); + } + }, [options, videoRef, props]); + + // Dispose the Video.js player when the functional component unmounts + useEffect(() => { + const player = playerRef.current; + + return () => { + if (player && !player.isDisposed()) { + player.dispose(); + playerRef.current = null; + } + }; + }, [playerRef]); + + return ( +
+
+
+ ); +} diff --git a/apps/u3/src/components/social/farcaster/ChannelItem.tsx b/apps/u3/src/components/social/farcaster/ChannelItem.tsx index 5e068357..3ad15ed1 100644 --- a/apps/u3/src/components/social/farcaster/ChannelItem.tsx +++ b/apps/u3/src/components/social/farcaster/ChannelItem.tsx @@ -10,7 +10,7 @@ export default function ChannelItem({ data }: { data: FarcasterChannel }) { # - + {data.name} {`${data.count || 0} posts today`} diff --git a/apps/u3/src/components/social/farcaster/FCast.tsx b/apps/u3/src/components/social/farcaster/FCast.tsx index 587e05a2..9307f881 100644 --- a/apps/u3/src/components/social/farcaster/FCast.tsx +++ b/apps/u3/src/components/social/farcaster/FCast.tsx @@ -162,15 +162,18 @@ export default function FCast({ const [linkParam, setLinkParam] = useState(null); useEffect(() => { + if (!castId.hash || !cast) return; + if (!userData.userName) return; setLinkParam({ url: getOfficialCastUrl( userData.userName, Buffer.from(castId.hash).toString('hex') ), - type: 'link', + type: 'post', title: cast.text.slice(0, 200), // todo: expand this limit at model + data: JSON.stringify(cast), }); - }, [castId.hash, isDetail]); + }, [castId.hash, userData.userName, cast]); const [updatedCast, setUpdatedCast] = useState(cast); const changeCastLikesWithCurrFid = (liked: boolean) => { @@ -228,7 +231,12 @@ export default function FCast({ {...wrapperProps} > {!simpleLayout && ( -
+
{(cast.parent_url || cast.rootParentUrl) && ( @@ -403,6 +411,24 @@ export default function FCast({ changeCastRecastsWithCurrFid(false); }} /> +
+
+ { + setLinkId(newLinkId); + }} + onLikeSuccess={() => { + changeCastLikesWithCurrFid(true); + }} + onRecastSuccess={() => { + changeCastRecastsWithCurrFid(true); + }} + /> +
diff --git a/apps/u3/src/components/social/farcaster/TrendChannel.tsx b/apps/u3/src/components/social/farcaster/TrendChannel.tsx index d02afe21..7d863684 100644 --- a/apps/u3/src/components/social/farcaster/TrendChannel.tsx +++ b/apps/u3/src/components/social/farcaster/TrendChannel.tsx @@ -25,7 +25,7 @@ export default function TrendChannel() { Topics { - navigate(`/social/trends`); + navigate(`/communities`); }} > View All diff --git a/apps/u3/src/components/social/farcaster/signupv2/AddAccountKey.tsx b/apps/u3/src/components/social/farcaster/signupv2/AddAccountKey.tsx index b7111ac7..3052fd0d 100644 --- a/apps/u3/src/components/social/farcaster/signupv2/AddAccountKey.tsx +++ b/apps/u3/src/components/social/farcaster/signupv2/AddAccountKey.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import { useCallback } from 'react'; +import { ComponentPropsWithRef, useCallback } from 'react'; import { createWalletClient, custom, fromHex, toHex } from 'viem'; import { optimism } from 'viem/chains'; import { toast } from 'react-toastify'; @@ -28,7 +28,9 @@ export default function AddAccountKey({ fid, signer, setSigner, -}: { + className, + ...props +}: ComponentPropsWithRef<'div'> & { fid: number; signer: NobleEd25519Signer | null; setSigner: (s: NobleEd25519Signer | null) => void; @@ -129,8 +131,10 @@ export default function AddAccountKey({
<div className="italic py-5 text-xl font-bold border-b border-[#39424c]"> diff --git a/apps/u3/src/components/social/farcaster/signupv2/FnameRegister.tsx b/apps/u3/src/components/social/farcaster/signupv2/FnameRegister.tsx index 970ffb86..35b54100 100644 --- a/apps/u3/src/components/social/farcaster/signupv2/FnameRegister.tsx +++ b/apps/u3/src/components/social/farcaster/signupv2/FnameRegister.tsx @@ -3,7 +3,7 @@ import { toHex } from 'viem'; import axios from 'axios'; import { toast } from 'react-toastify'; import { useConnectModal } from '@rainbow-me/rainbowkit'; -import { useCallback, useState } from 'react'; +import { ComponentPropsWithRef, useCallback, useState } from 'react'; import { NobleEd25519Signer, ViemWalletEip712Signer } from '@farcaster/hub-web'; import { switchChain } from '@wagmi/core'; import { mainnet } from 'viem/chains'; @@ -20,7 +20,9 @@ export default function AddAccountKey({ signer, setFname, makePrimaryName, -}: { + className, + ...props +}: ComponentPropsWithRef<'div'> & { fid: number; fname: string; signer?: NobleEd25519Signer; @@ -97,8 +99,10 @@ export default function AddAccountKey({ <div className={cn( 'text-white flex flex-col border border-[#39424c] rounded-2xl', - 'p-5 h-[350px] w-[320px]' + 'p-5 h-[350px] w-[320px]', + className )} + {...props} > <Title checked={!!fid && !!fname} text={'Step 3'} /> <div className="italic py-5 text-xl font-bold border-b border-[#39424c]"> diff --git a/apps/u3/src/components/social/farcaster/signupv2/RegisterAndPay.tsx b/apps/u3/src/components/social/farcaster/signupv2/RegisterAndPay.tsx index 9fe4101d..8a60de97 100644 --- a/apps/u3/src/components/social/farcaster/signupv2/RegisterAndPay.tsx +++ b/apps/u3/src/components/social/farcaster/signupv2/RegisterAndPay.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { ComponentPropsWithRef, useCallback } from 'react'; import { toast } from 'react-toastify'; import { idRegistryABI } from '@farcaster/hub-web'; import { useConnectModal } from '@rainbow-me/rainbowkit'; @@ -20,7 +20,9 @@ import { shortAddress } from '@/utils/message/xmtp'; export default function RegisterAndPay({ fid, setFid, -}: { + className, + ...props +}: ComponentPropsWithRef<'div'> & { fid: number; setFid: (fid: number) => void; }) { @@ -90,8 +92,10 @@ export default function RegisterAndPay({ <div className={cn( 'text-white flex flex-col border border-[#39424c] rounded-2xl', - 'p-5 h-[350px] w-[320px]' + 'p-5 h-[350px] w-[320px]', + className )} + {...props} > <Title checked={!!fid} text={'Step 1'} /> <div className="italic py-5 text-xl font-bold border-b border-[#39424c]"> diff --git a/apps/u3/src/components/social/farcaster/signupv2/RentStorage.tsx b/apps/u3/src/components/social/farcaster/signupv2/RentStorage.tsx index 45fa7493..dc7bc636 100644 --- a/apps/u3/src/components/social/farcaster/signupv2/RentStorage.tsx +++ b/apps/u3/src/components/social/farcaster/signupv2/RentStorage.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { ComponentPropsWithRef, useCallback } from 'react'; import { toast } from 'react-toastify'; import { useConnectModal } from '@rainbow-me/rainbowkit'; import { @@ -19,7 +19,9 @@ export default function RentStorage({ fid, hasStorage, setHasStorage, -}: { + className, + ...props +}: ComponentPropsWithRef<'div'> & { fid: number; hasStorage: boolean; setHasStorage: (h: boolean) => void; @@ -74,8 +76,10 @@ export default function RentStorage({ <div className={cn( 'text-white flex flex-col border border-[#39424c] rounded-2xl', - 'p-5 h-[350px] w-[320px]' + 'p-5 h-[350px] w-[320px]', + className )} + {...props} > <Title checked={!!fid && hasStorage} text={'Step 4'} /> <div className="italic py-5 text-xl font-bold border-b border-[#39424c]"> diff --git a/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx b/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx index a0045463..1dcf0ff9 100644 --- a/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx +++ b/apps/u3/src/components/social/frames/red-envelope/RedEnvelopeFloatingWindow.tsx @@ -198,6 +198,7 @@ function FloatingWindowWrapper({ 'fixed bottom-[20px] right-[54px] z-[100]', 'w-[280px] h-auto flex flex-col items-start gap-[20px] rounded-[20px] bg-[#F41F4C] p-[20px] box-border', `transition-[height] duration-500`, + 'max-sm:hidden', className )} {...props} @@ -217,7 +218,8 @@ function FloatingWindowWrapper({ ) : ( <div className={cn( - `fixed bottom-[20px] right-[280px] z-[100] text-[50px] cursor-pointer flex flex-col` + `fixed bottom-[20px] right-[280px] z-[100] text-[50px] cursor-pointer flex flex-col`, + 'max-sm:hidden' )} onClick={toggleExpand} > diff --git a/apps/u3/src/components/ui/collapsible.tsx b/apps/u3/src/components/ui/collapsible.tsx new file mode 100644 index 00000000..83c1e67d --- /dev/null +++ b/apps/u3/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +'use client'; + +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const { CollapsibleTrigger } = CollapsiblePrimitive; + +const { CollapsibleContent } = CollapsiblePrimitive; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/apps/u3/src/components/ui/drawer.tsx b/apps/u3/src/components/ui/drawer.tsx new file mode 100644 index 00000000..29b2d1c5 --- /dev/null +++ b/apps/u3/src/components/ui/drawer.tsx @@ -0,0 +1,126 @@ +/* eslint-disable react/prop-types */ + +'use client'; + +import * as React from 'react'; +import { Drawer as DrawerPrimitive } from 'vaul'; + +import { cn } from '@/lib/utils'; + +function Drawer({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Root>) { + return ( + <DrawerPrimitive.Root + shouldScaleBackground={shouldScaleBackground} + {...props} + /> + ); +} +Drawer.displayName = 'Drawer'; + +const DrawerTrigger = DrawerPrimitive.Trigger; + +const DrawerPortal = DrawerPrimitive.Portal; + +const DrawerClose = DrawerPrimitive.Close; + +const DrawerOverlay = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Overlay + ref={ref} + className={cn('fixed inset-0 z-50 bg-black/80', className)} + {...props} + /> +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; + +const DrawerContent = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> +>(({ className, children, ...props }, ref) => ( + <DrawerPortal> + <DrawerOverlay /> + <DrawerPrimitive.Content + ref={ref} + className={cn( + 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background', + className + )} + {...props} + > + {/* <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> */} + {children} + </DrawerPrimitive.Content> + </DrawerPortal> +)); +DrawerContent.displayName = 'DrawerContent'; + +function DrawerHeader({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) { + return ( + <div + className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)} + {...props} + /> + ); +} +DrawerHeader.displayName = 'DrawerHeader'; + +function DrawerFooter({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) { + return ( + <div + className={cn('mt-auto flex flex-col gap-2 p-4', className)} + {...props} + /> + ); +} +DrawerFooter.displayName = 'DrawerFooter'; + +const DrawerTitle = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Title + ref={ref} + className={cn( + 'text-lg font-semibold leading-none tracking-tight', + className + )} + {...props} + /> +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; + +const DrawerDescription = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Description + ref={ref} + className={cn('text-sm text-muted-foreground', className)} + {...props} + /> +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/apps/u3/src/components/ui/hover-card.tsx b/apps/u3/src/components/ui/hover-card.tsx new file mode 100644 index 00000000..dd6d80f8 --- /dev/null +++ b/apps/u3/src/components/ui/hover-card.tsx @@ -0,0 +1,33 @@ +/* eslint-disable react/prop-types */ + +'use client'; + +import * as React from 'react'; +import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; + +import { cn } from '@/lib/utils'; + +const HoverCard = HoverCardPrimitive.Root; + +const HoverCardTrigger = HoverCardPrimitive.Trigger; + +const HoverCardArrow = HoverCardPrimitive.Arrow; + +const HoverCardContent = React.forwardRef< + React.ElementRef<typeof HoverCardPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + <HoverCardPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + className={cn( + 'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + className + )} + {...props} + /> +)); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; + +export { HoverCard, HoverCardTrigger, HoverCardArrow, HoverCardContent }; diff --git a/apps/u3/src/components/ui/select.tsx b/apps/u3/src/components/ui/select.tsx new file mode 100644 index 00000000..128ea864 --- /dev/null +++ b/apps/u3/src/components/ui/select.tsx @@ -0,0 +1,193 @@ +/* eslint-disable react/prop-types */ + +'use client'; + +import * as React from 'react'; +import { + CaretSortIcon, + CheckIcon, + ChevronDownIcon, + ChevronUpIcon, +} from '@radix-ui/react-icons'; +import * as SelectPrimitive from '@radix-ui/react-select'; + +import { cn } from '@/lib/utils'; + +// TODO see issue: https://github.com/radix-ui/primitives/issues/1658 + +// const Select = SelectPrimitive.Root; +const Select = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> +>(({ open: openSelect, onOpenChange, ...props }) => { + const [open, setOpen] = React.useState(openSelect); + return ( + <SelectPrimitive.Root + open={openSelect !== undefined ? openSelect : open} + onOpenChange={(o) => { + setTimeout(() => { + const selection = document.getSelection(); + if (selection) { + selection.removeAllRanges(); + } + if (openSelect !== undefined && onOpenChange) { + onOpenChange(o); + } else { + setOpen(o); + } + }, 10); + }} + {...props} + /> + ); +}); + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> +>(({ className, children, ...props }, ref) => ( + <SelectPrimitive.Trigger + ref={ref} + className={cn( + 'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', + className + )} + {...props} + > + {children} + <SelectPrimitive.Icon asChild> + <CaretSortIcon className="h-4 w-4 opacity-50" /> + </SelectPrimitive.Icon> + </SelectPrimitive.Trigger> +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.ScrollUpButton + ref={ref} + className={cn( + 'flex cursor-default items-center justify-center py-1', + className + )} + {...props} + > + <ChevronUpIcon /> + </SelectPrimitive.ScrollUpButton> +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.ScrollDownButton + ref={ref} + className={cn( + 'flex cursor-default items-center justify-center py-1', + className + )} + {...props} + > + <ChevronDownIcon /> + </SelectPrimitive.ScrollDownButton> +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> +>(({ className, children, position = 'popper', ...props }, ref) => ( + <SelectPrimitive.Portal> + <SelectPrimitive.Content + ref={ref} + className={cn( + 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + position === 'popper' && + 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', + className + )} + position={position} + {...props} + > + <SelectScrollUpButton /> + <SelectPrimitive.Viewport + className={cn( + 'p-1', + position === 'popper' && + 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]' + )} + > + {children} + </SelectPrimitive.Viewport> + <SelectScrollDownButton /> + </SelectPrimitive.Content> + </SelectPrimitive.Portal> +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.Label + ref={ref} + className={cn('px-2 py-1.5 text-sm font-semibold', className)} + {...props} + /> +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> +>(({ className, children, ...props }, ref) => ( + <SelectPrimitive.Item + ref={ref} + className={cn( + 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', + className + )} + {...props} + > + <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> + <SelectPrimitive.ItemIndicator> + <CheckIcon className="h-4 w-4" /> + </SelectPrimitive.ItemIndicator> + </span> + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> + </SelectPrimitive.Item> +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef<typeof SelectPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <SelectPrimitive.Separator + ref={ref} + className={cn('-mx-1 my-1 h-px bg-muted', className)} + {...props} + /> +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/apps/u3/src/constants/index.ts b/apps/u3/src/constants/index.ts index fb415767..ae0cc989 100644 --- a/apps/u3/src/constants/index.ts +++ b/apps/u3/src/constants/index.ts @@ -62,3 +62,5 @@ export const CONTACT_US_LINKS = { export const RED_ENVELOPE_PLEDGE_ADDRESS = process.env.REACT_APP_RED_ENVELOPE_PLEDGE_ADDRESS || '0xCFd3527F4334Ebb2E3b53b01f70B7BD5C3170cD5'; + +export const POSTER_IMG_URL = `${API_BASE_URL}/static-assets/poster/poster.webp`; diff --git a/apps/u3/src/container/Activity.tsx b/apps/u3/src/container/Activity.tsx deleted file mode 100644 index fcebd190..00000000 --- a/apps/u3/src/container/Activity.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import styled from 'styled-components'; -import { useState } from 'react'; - -import { isMobile } from 'react-device-detect'; -import CardBase from '../components/common/card/CardBase'; -import PageTitle from '../components/layout/PageTitle'; -import MobilePageHeader from '../components/layout/mobile/MobilePageHeader'; -import Rss3Content from '../components/fren/Rss3Content'; -import { CurrencyETH } from '../components/common/icons/currency-eth'; -import { MainWrapper } from '../components/layout/Index'; - -const tabs = ['Feeds']; -function Activity() { - const [tab, setTab] = useState<string>('Feeds'); - return ( - <Wrapper> - {isMobile ? ( - <MobilePageHeader tabs={tabs} curTab={tab} setTab={setTab} /> - ) : ( - <PageHeader tab={tab}> - <PageTitle>Activity</PageTitle> - <i>{' / '}</i> - - {tabs?.map((key) => ( - <div - key={key} - className={tab === key ? 'tab active' : 'tab'} - onClick={() => setTab(key)} - > - {key} - </div> - ))} - </PageHeader> - )} - - {tab === 'Feeds' && ( - <ContentWrapper> - <Rss3Content empty={<NoActivity />} /> - </ContentWrapper> - )} - </Wrapper> - ); -} -export default Activity; -export function NoActivity() { - return ( - <div className="no-item"> - <CurrencyETH /> - <p>No transactions found on Ethereum.</p> - </div> - ); -} -const Wrapper = styled(MainWrapper)` - display: flex; - flex-direction: column; - gap: 20px; -`; - -const ContentWrapper = styled(CardBase)` - flex: 1; - & .no-item { - box-sizing: border-box; - text-align: center; - height: fit-content; - background: #1b1e23; - border-radius: 20px; - padding: 40px 0 40px 0; - flex-grow: 1; - & p { - font-weight: 400; - font-size: 16px; - line-height: 19px; - - color: #748094; - } - } - - & .activity { - &:last-child { - border-bottom: none; - } - - & .info { - display: flex; - gap: 10px; - flex-direction: column; - > div { - display: flex; - gap: 10px; - } - & p { - margin: 0; - } - - & p.quote { - padding: 10px 20px; - gap: 10px; - background: #14171a; - border-radius: 10px; - } - & .header { - display: flex; - align-items: center; - justify-content: space-between; - > div { - display: flex; - gap: 10px; - align-items: center; - & span { - font-weight: 400; - font-size: 14px; - line-height: 17px; - color: #718096; - } - & .nickname { - font-weight: 500; - font-size: 16px; - line-height: 19px; - } - } - } - - & .intro { - display: flex; - align-items: center; - - font-weight: 400; - font-size: 14px; - line-height: 17px; - - color: #718096; - } - - & .source { - display: flex; - padding: 8px 20px 8px 16px; - box-sizing: border-box; - gap: 8px; - height: 40px; - width: fit-content; - background: #1a1e23; - border: 1px solid #39424c; - border-radius: 100px; - > img { - width: 20px; - height: 20px; - border-radius: 50%; - } - } - } - } -`; - -const PageHeader = styled.div<{ tab: string }>` - display: flex; - padding-bottom: 8px; - border-bottom: 1px solid #39424c; - font-weight: 700; - font-size: 16px; - line-height: 28px; - color: #ffffff; - white-space: pre; - column-gap: 40px; - - i { - color: #39424c; - } - - .tab { - cursor: pointer; - color: #39424c; - } - - .active { - color: white; - position: relative; - &:after { - content: ''; - position: absolute; - left: 0; - bottom: -10px; - width: 100%; - height: 2px; - background: white; - } - } -`; diff --git a/apps/u3/src/container/Message.tsx b/apps/u3/src/container/Message.tsx deleted file mode 100644 index 62b73ae2..00000000 --- a/apps/u3/src/container/Message.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ComingSoonPage from '../components/layout/ComingSoonPage'; - -export default function Message() { - return <ComingSoonPage />; -} diff --git a/apps/u3/src/container/Save.tsx b/apps/u3/src/container/Save.tsx deleted file mode 100644 index 347a8ab1..00000000 --- a/apps/u3/src/container/Save.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { usePersonalFavors } from '@us3r-network/link'; -import { isMobile } from 'react-device-detect'; -import { uniqBy } from 'lodash'; -import { MainWrapper } from '../components/layout/Index'; -import Loading from '../components/common/loading/Loading'; -import PageTitle from '../components/layout/PageTitle'; -import SaveExploreList from '../components/save/SaveExploreList'; -import SaveExploreListMobile from '../components/save/SaveExploreListMobile'; -import { - getContentLinkDataWithJsonValue, - getContentPlatformLogoWithJsonValue, -} from '../utils/news/content'; -import { getDappLinkDataWithJsonValue } from '../utils/dapp/dapp'; -import { getEventLinkDataWithJsonValue } from '../utils/news/event'; -import SyncingBotSaves from '@/components/save/SyncingBotSaves'; -// import { DappLinkData } from '../services/dapp/types/dapp'; -// import { ContentLinkData } from '../services/news/types/contents'; -// import { EventLinkData } from '../services/news/types/event'; - -function EmptyList() { - return ( - <EmptyBox> - <EmptyDesc> - Nothing to see here! Explore and favorite what you like! - </EmptyDesc> - </EmptyBox> - ); -} - -const EmptyBox = styled.div` - width: 100%; - height: 100%; - padding: 20px; - box-sizing: border-box; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - gap: 20px; - background-color: #1b1e23; -`; - -const EmptyDesc = styled.span` - font-weight: 400; - font-size: 16px; - line-height: 19px; - text-align: center; - color: #748094; -`; - -export default function Save() { - const { isFetching, personalFavors } = usePersonalFavors(); - const [savedLinks, setSavedLinks] = useState([]); - // console.log('personalFavors', personalFavors); - const list = useMemo( - () => [ - ...savedLinks.map((item) => { - const createAt = item.createAt || new Date().getTime(); - return { ...item, createAt }; - }), - ...uniqBy( - personalFavors - .filter((item) => !!item?.link && item.link.type !== 'test') - .map((item) => { - const { link, createAt } = item; - let linkData; - let title = ''; - let logo = ''; - switch (link.type) { - case 'dapp': - linkData = getDappLinkDataWithJsonValue(link?.data); - title = linkData?.name || link.title; - logo = linkData?.image || ''; - break; - case 'content': - linkData = getContentLinkDataWithJsonValue(link?.data); - title = linkData?.title || link.title; - logo = - getContentPlatformLogoWithJsonValue(linkData?.value) || - linkData?.platform?.logo || - ''; - break; - case 'event': - linkData = getEventLinkDataWithJsonValue(link?.data); - title = linkData?.name || link.title; - logo = linkData?.image || linkData?.platform?.logo || ''; - break; - default: - linkData = JSON.parse(link?.data); - title = linkData?.title || link.title; - logo = linkData?.image || ''; - break; - } - return { ...link, id: link.id, title, logo, createAt }; - }), - 'id' - ), - ], - [personalFavors, savedLinks] - ); - const isEmpty = useMemo(() => list.length === 0, [list]); - return ( - <Wrapper> - {isMobile ? null : <PageTitle>Saves</PageTitle>} - <SyncingBotSaves - onComplete={(saves) => { - console.log('onComplete SyncingBotSaves'); - setSavedLinks(saves); - }} - /> - <ContentWrapper> - {isFetching ? ( - <Loading /> - ) : isEmpty ? ( - <EmptyList /> - ) : isMobile ? ( - <SaveExploreListMobile - data={list} - onItemClick={(item) => { - window.open(item.url, '_blank'); - }} - /> - ) : ( - <div className="w-full h-full"> - <SaveExploreList - data={list} - onItemClick={(item) => { - window.open(item.url, '_blank'); - }} - /> - </div> - )} - </ContentWrapper> - </Wrapper> - ); -} - -const Wrapper = styled(MainWrapper)` - display: flex; - flex-direction: column; - gap: 20px; -`; - -const ContentWrapper = styled.div` - flex: 1; - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/apps/u3/src/container/community/Communities.tsx b/apps/u3/src/container/community/Communities.tsx new file mode 100644 index 00000000..68687985 --- /dev/null +++ b/apps/u3/src/container/community/Communities.tsx @@ -0,0 +1,219 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'; +import { getDefaultTrendingCommunitiesCachedData } from '@/hooks/community/useLoadTrendingCommunities'; +import { getDefaultNewestCommunitiesCachedData } from '@/hooks/community/useLoadNewestCommunities'; +import { cn } from '@/lib/utils'; +import useRoute from '@/route/useRoute'; +import NavLinkItem from '@/components/layout/NavLinkItem'; +import { RouteKey } from '@/route/routes'; +import { getDefaultJoinedCommunitiesCachedData } from '@/hooks/community/useLoadJoinedCommunities'; +import { CommunityTypesData } from '@/services/community/api/community'; +import useLoadCommunityTypes from '@/hooks/community/useLoadCommunityTypes'; +import Select2 from '@/components/common/select/Select2'; +import CommunitiesGrowing from './CommunitiesGrowing'; + +export enum FeedsType { + TRENDING = 'trending', + JOINED = 'joined', + NEWEST = 'newest', +} +const defaultCommunitiesCachedData = { + trending: getDefaultTrendingCommunitiesCachedData(), + newest: getDefaultNewestCommunitiesCachedData(), + joined: getDefaultJoinedCommunitiesCachedData(), +}; +export default function Communities() { + const { lastRouteMeta } = useRoute(); + const lastRouteKey = lastRouteMeta.key; + const { communityTypes, loadCommunityTypes } = useLoadCommunityTypes(); + useEffect(() => { + loadCommunityTypes(); + }, []); + + const communitiesCachedData = useRef({ + ...defaultCommunitiesCachedData, + }).current; + + const feedsType = useMemo(() => { + switch (lastRouteKey) { + case RouteKey.newestCommunities: + return FeedsType.NEWEST; + case RouteKey.joinedCommunities: + return FeedsType.JOINED; + default: + return FeedsType.TRENDING; + } + }, [lastRouteKey]); + + const [searchParams, setSearchParams] = useSearchParams(); + const communityTypeFilter = useMemo( + () => searchParams.get('type'), + [searchParams] + ); + + useEffect(() => { + document.getElementById('communities-scroll-wrapper')?.scrollTo(0, 0); + }, [lastRouteKey]); + + return ( + <div className={cn(`w-full h-full flex bg-[#20262F]`)}> + <div + id="communities-scroll-wrapper" + className="flex-1 h-full overflow-auto" + > + <Header + feedsType={feedsType} + communityTypes={communityTypes} + communityTypeFilter={communityTypeFilter} + /> + <Outlet + context={{ + feedsType, + communityTypeFilter, + + communitiesCachedData, + }} + /> + </div> + <div + className={cn( + 'w-[320px] h-full overflow-auto bg-[#1B1E23]', + 'max-sm:hidden' + )} + > + <CommunitiesGrowing /> + </div> + </div> + ); +} + +function Header({ + feedsType, + communityTypes, + communityTypeFilter, +}: { + feedsType: FeedsType; + communityTypes: CommunityTypesData; + communityTypeFilter: string; +}) { + const navigate = useNavigate(); + const getNavUrl = (type: FeedsType, communityType?: string) => { + return `/communities/${type}${ + communityType ? `?type=${communityType}` : '' + }`; + }; + const validateIsActive = (type: FeedsType) => { + return type === feedsType; + }; + const communitTypeOptions = [ + { + value: null, + label: 'All Types', + }, + ...communityTypes.map((item) => ({ + label: item.type, + value: item.type, + })), + ]; + return ( + <div + className={cn( + 'w-full flex p-[20px] justify-between items-center self-stretch sticky top-0 bg-[#20262F] border-b border-[#39424c] z-10', + 'max-sm:px-[10px] max-sm:py-[14px]' + )} + > + <div + className={cn( + 'w-full flex items-center gap-[20px]', + 'max-sm:gap-[10px]' + )} + > + <NavLinkItem + href={getNavUrl(FeedsType.TRENDING)} + active={validateIsActive(FeedsType.TRENDING)} + className="w-auto max-sm:p-0" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <path + d="M14.3945 8.56641C14.207 8.86914 13.9824 9.16602 13.7227 9.45703C13.5966 9.59862 13.4436 9.71369 13.2726 9.79551C13.1015 9.87733 12.9159 9.92427 12.7266 9.93359C12.5371 9.94453 12.3473 9.9177 12.1682 9.85466C11.9892 9.79163 11.8245 9.69364 11.6836 9.56641C11.5211 9.4204 11.3937 9.23945 11.3112 9.03715C11.2287 8.83485 11.193 8.61648 11.207 8.39844C11.2656 7.47266 10.9649 6.38477 10.3125 5.16211C9.98244 4.54883 9.58791 4.02539 9.1172 3.5918C9.06901 3.90167 8.98983 4.20593 8.88087 4.5C8.61349 5.21581 8.22939 5.88238 7.74416 6.47266C7.40726 6.88586 7.02314 7.25819 6.59962 7.58203C5.93556 8.0918 5.38869 8.75391 5.0215 9.49414C4.64579 10.2461 4.45115 11.0754 4.45314 11.916C4.45314 13.3789 5.02931 14.7539 6.07423 15.791C7.12306 16.8301 8.51564 17.4004 10 17.4004C11.4844 17.4004 12.877 16.8301 13.9258 15.791C14.9707 14.7559 15.5469 13.3789 15.5469 11.916C15.5469 11.1504 15.3887 10.4062 15.0781 9.70703C14.8965 9.29688 14.668 8.91602 14.3945 8.56641Z" + fill={validateIsActive(FeedsType.TRENDING) ? '#FFF' : '#718096'} + /> + <path + d="M16.291 9.16404C15.9118 8.31063 15.3606 7.54466 14.6719 6.91404L14.1035 6.39255C14.0842 6.37533 14.061 6.36311 14.0359 6.35697C14.0107 6.35082 13.9845 6.35093 13.9594 6.35729C13.9344 6.36366 13.9112 6.37608 13.8921 6.39346C13.8729 6.41085 13.8584 6.43267 13.8496 6.45701L13.5957 7.18552C13.4375 7.64255 13.1465 8.10935 12.7344 8.56834C12.707 8.59763 12.6758 8.60545 12.6543 8.6074C12.6328 8.60935 12.5996 8.60545 12.5703 8.5781C12.543 8.55466 12.5293 8.51951 12.5312 8.48435C12.6035 7.30857 12.252 5.9824 11.4824 4.53904C10.8457 3.33982 9.96094 2.40427 8.85547 1.75193L8.04883 1.27732C7.94336 1.21482 7.80859 1.29685 7.81445 1.4199L7.85742 2.3574C7.88672 2.99802 7.8125 3.56443 7.63672 4.03513C7.42187 4.6113 7.11328 5.14646 6.71875 5.62693C6.44418 5.96084 6.13299 6.26287 5.79102 6.52732C4.96739 7.16046 4.29768 7.97172 3.83203 8.90037C3.36753 9.83711 3.12557 10.8685 3.125 11.914C3.125 12.8359 3.30664 13.7285 3.66602 14.5703C4.01302 15.3808 4.51379 16.1163 5.14062 16.7363C5.77344 17.3613 6.50781 17.8535 7.32617 18.1953C8.17383 18.5508 9.07227 18.7304 10 18.7304C10.9277 18.7304 11.8262 18.5508 12.6738 18.1972C13.4902 17.8574 14.2325 17.3619 14.8594 16.7383C15.4922 16.1133 15.9883 15.3828 16.334 14.5722C16.6928 13.7327 16.8769 12.829 16.875 11.916C16.875 10.9629 16.6797 10.0371 16.291 9.16404ZM13.9258 15.791C12.877 16.8301 11.4844 17.4004 10 17.4004C8.51562 17.4004 7.12305 16.8301 6.07422 15.791C5.0293 14.7539 4.45312 13.3789 4.45312 11.916C4.45312 11.0664 4.64453 10.2519 5.02148 9.49412C5.38867 8.75388 5.93555 8.09177 6.59961 7.58201C7.02312 7.25817 7.40725 6.88584 7.74414 6.47263C8.23242 5.87693 8.61523 5.21287 8.88086 4.49998C8.98982 4.20591 9.06899 3.90165 9.11719 3.59177C9.58789 4.02537 9.98242 4.5488 10.3125 5.16209C10.9648 6.38474 11.2656 7.47263 11.207 8.39841C11.193 8.61645 11.2286 8.83483 11.3112 9.03713C11.3937 9.23942 11.5211 9.42038 11.6836 9.56638C11.8245 9.69362 11.9892 9.7916 12.1682 9.85464C12.3473 9.91767 12.5371 9.9445 12.7266 9.93357C13.1113 9.91404 13.4648 9.74412 13.7227 9.45701C13.9824 9.16599 14.207 8.86912 14.3945 8.56638C14.668 8.91599 14.8965 9.29685 15.0781 9.70701C15.3887 10.4062 15.5469 11.1504 15.5469 11.916C15.5469 13.3789 14.9707 14.7558 13.9258 15.791Z" + fill={validateIsActive(FeedsType.TRENDING) ? '#FFF' : '#718096'} + /> + </svg> + Trending + </NavLinkItem> + <div + className={cn('w-px h-[16px] bg-[#39424C] hidden', 'max-sm:block')} + /> + <NavLinkItem + href={getNavUrl(FeedsType.JOINED)} + active={validateIsActive(FeedsType.JOINED)} + className="w-auto max-sm:p-0" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <path + d="M16.3642 3.63604C15.5302 2.79765 14.5381 2.13298 13.4453 1.68051C12.3526 1.22804 11.181 0.996748 9.99826 1.00003C8.81642 0.997536 7.64581 1.2292 6.55405 1.68165C5.46229 2.1341 4.47103 2.79835 3.63755 3.63604C2.79863 4.4695 2.13352 5.46114 1.68077 6.55349C1.22803 7.64584 0.996642 8.81716 1.00004 9.99957C0.997075 11.1819 1.22866 12.3531 1.68138 13.4454C2.1341 14.5377 2.79898 15.5294 3.63755 16.3631C4.47102 17.2009 5.46225 17.8652 6.554 18.3178C7.64575 18.7704 8.81638 19.0023 9.99826 19C11.181 19.0029 12.3526 18.7713 13.4453 18.3187C14.538 17.8661 15.5301 17.2014 16.3642 16.3631C17.2023 15.5291 17.8667 14.5373 18.3191 13.4451C18.7715 12.3528 19.0029 11.1818 19 9.99957C19.0031 8.81736 18.7717 7.64627 18.3193 6.554C17.8669 5.46173 17.2024 4.46996 16.3642 3.63604ZM6.82052 12.7027C6.97055 12.8993 7.04895 13.1413 7.04277 13.3885C7.03659 13.6358 6.9462 13.8735 6.78654 14.0625C6.738 14.1204 6.6837 14.1732 6.62447 14.2201C6.3969 14.3991 6.10756 14.4805 5.82002 14.4464C5.53247 14.4122 5.27024 14.2654 5.09093 14.0381C4.17352 12.8853 3.67463 11.4553 3.67589 9.98215C3.67582 8.50766 4.17624 7.0768 5.09529 5.9236C5.14204 5.86558 5.19427 5.8122 5.25126 5.76419C5.47354 5.57876 5.76034 5.48911 6.04869 5.51492C6.33703 5.54073 6.60334 5.67988 6.78915 5.90182C6.94809 6.09109 7.03773 6.32888 7.04328 6.57594C7.04882 6.823 6.96994 7.06457 6.81965 7.26077C6.23704 8.04809 5.92267 9.00162 5.92282 9.98098C5.92298 10.9603 6.23766 11.9138 6.82052 12.7009V12.7027ZM11.9422 10.1433C11.9032 10.6037 11.7025 11.0355 11.3755 11.362C11.0486 11.6886 10.6165 11.8889 10.156 11.9274C9.64053 11.969 9.12965 11.8043 8.73569 11.4694C8.34173 11.1345 8.09695 10.6569 8.05519 10.1416C8.04742 10.0372 8.04742 9.93235 8.05519 9.82796C8.09367 9.36765 8.29387 8.93581 8.62033 8.60894C8.94679 8.28208 9.37844 8.0813 9.83881 8.04216C10.3536 7.99949 10.8643 8.16286 11.2587 8.4964C11.6531 8.82994 11.8989 9.30635 11.9422 9.82099C11.9508 9.92825 11.9508 10.036 11.9422 10.1433ZM14.8995 14.0747C14.8536 14.1331 14.8016 14.1866 14.7444 14.2341C14.5218 14.4191 14.2349 14.5083 13.9466 14.4822C13.6583 14.456 13.3922 14.3167 13.2065 14.0947C13.0472 13.9054 12.9574 13.6673 12.952 13.42C12.9466 13.1727 13.0259 12.9309 13.1769 12.7349C13.7594 11.9474 14.0738 10.9938 14.0738 10.0144C14.0738 9.03493 13.7594 8.08134 13.1769 7.29387C13.0267 7.0973 12.9482 6.8554 12.9542 6.60814C12.9602 6.36088 13.0504 6.12308 13.21 5.93405C13.2583 5.87592 13.3126 5.82306 13.372 5.77638C13.5996 5.59701 13.889 5.51534 14.1768 5.54931C14.4645 5.58329 14.727 5.73015 14.9065 5.95757C15.8238 7.11069 16.3224 8.54101 16.3206 10.0144C16.3214 11.4898 15.8202 12.9216 14.8995 14.0747Z" + fill={validateIsActive(FeedsType.JOINED) ? '#FFF' : '#718096'} + /> + </svg> + Joined + </NavLinkItem> + <div + className={cn('w-px h-[16px] bg-[#39424C] hidden', 'max-sm:block')} + /> + <NavLinkItem + href={getNavUrl(FeedsType.NEWEST)} + active={validateIsActive(FeedsType.NEWEST)} + className="w-auto max-sm:p-0" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <g clipPath="url(#clip0_3631_4450)"> + <path + d="M6.58093 9.02782L6.78968 9.80657C6.92031 10.2934 7.06218 10.7459 7.22687 11.1659L7.21249 11.17C6.96281 10.8244 6.65562 10.4369 6.38812 10.1594L5.59749 9.29126L4.55999 9.56938L5.43937 12.8506L6.24249 12.635L6.02624 11.8269C5.89729 11.3377 5.75494 10.8521 5.59937 10.3706L5.61906 10.3653C5.88031 10.7181 6.21343 11.1034 6.48656 11.4009L7.33843 12.3416L8.26343 12.0938L7.38406 8.81251L6.58093 9.02782ZM19.1641 7.54126L16.8459 6.04313L16.7075 3.28688L13.9509 3.14845L12.4528 0.830322L9.99656 2.08876L7.53999 0.830322L6.04187 3.14845L3.28499 3.28688L3.14687 6.04313L0.82843 7.54126L2.08718 9.99751L0.82843 12.4541L3.14687 13.9522L3.28499 16.7084L6.04187 16.8469L7.53999 19.165L9.99624 17.9066L12.4525 19.165L13.9509 16.8469L16.7075 16.7084L16.8459 13.9522L19.1641 12.4541L17.9056 9.99782L19.1641 7.54126ZM17.1291 11.9088L15.3253 13.0744L15.2178 15.2191L13.0731 15.3266L11.9075 17.13L9.99624 16.1509L8.08499 17.13L6.91937 15.3266L4.77499 15.2191L4.66718 13.0744L2.86343 11.9088L3.84249 9.99751L2.86343 8.08657L4.66718 6.92095L4.77499 4.77626L6.91937 4.66876L8.08499 2.86501L9.99624 3.84438L11.9075 2.86501L13.0731 4.66876L15.2175 4.77626L15.3253 6.92095L17.1291 8.08657L16.15 9.99751L17.1291 11.9088ZM9.56031 10.9738L9.39874 10.37L10.5769 10.0544L10.3866 9.34376L9.20843 9.65938L9.06781 9.13376L10.3191 8.79813L10.1256 8.07782L7.99343 8.64907L8.87281 11.9303L11.0728 11.3409L10.88 10.6203L9.56062 10.9738H9.56031ZM13.8556 7.07845L13.9831 8.33314C14.0291 8.73813 14.0737 9.13845 14.1241 9.52157L14.1147 9.52407C13.9753 9.17244 13.8295 8.82342 13.6772 8.4772L13.135 7.27157L12.1956 7.52313L12.3081 8.78157C12.3437 9.20532 12.3809 9.61782 12.4269 10.0025L12.4172 10.005C12.2769 9.67688 12.1025 9.25939 11.9422 8.89563L11.4266 7.72907L10.4819 7.9822L12.1159 11.0613L13.0894 10.8003L12.9931 9.44876C12.9747 9.14595 12.9353 8.82251 12.8853 8.42345L12.8953 8.42095C13.0194 8.74038 13.1528 9.05614 13.2953 9.36782L13.8584 10.5944L14.8175 10.3375L14.7462 6.8397L13.8556 7.07845Z" + fill={validateIsActive(FeedsType.NEWEST) ? '#FFF' : '#718096'} + /> + </g> + <defs> + <clipPath id="clip0_3631_4450"> + <rect width="20" height="20" fill="white" /> + </clipPath> + </defs> + </svg> + Newest + </NavLinkItem> + </div> + <Select2 + options={communitTypeOptions} + defaultValue={communitTypeOptions[0]?.value} + value={communityTypeFilter} + onValueChange={(value) => { + navigate(getNavUrl(feedsType, value)); + }} + /> + </div> + ); +} diff --git a/apps/u3/src/container/community/CommunitiesGrowing.tsx b/apps/u3/src/container/community/CommunitiesGrowing.tsx new file mode 100644 index 00000000..1bfdaa48 --- /dev/null +++ b/apps/u3/src/container/community/CommunitiesGrowing.tsx @@ -0,0 +1,63 @@ +import { useEffect, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import Loading from 'src/components/common/loading/Loading'; +import { + EndMsgContainer, + LoadingMoreWrapper, +} from '@/components/social/CommonStyles'; +import useLoadGrowingCommunities from '@/hooks/community/useLoadGrowingCommunities'; +import { CommunityList } from '@/components/community/CommonStyled'; +import GrowingCommunityItem from '@/components/community/GrowingCommunityItem'; + +export default function CommunitiesGrowing() { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + const { loading, growingCommunities, loadGrowingCommunities, pageInfo } = + useLoadGrowingCommunities(); + + useEffect(() => { + if (!mounted) return; + loadGrowingCommunities(); + }, [mounted]); + + return ( + <div + className="w-full h-full overflow-auto p-[20px] box-border max-sm:p-[10px]" + id="growing-communities-scroll-wrapper" + > + <h3 className="text-[#718096] text-[14px] font-medium mb-[20px] box-border"> + Growing Communities + </h3> + <InfiniteScroll + style={{ overflow: 'hidden' }} + dataLength={growingCommunities.length} + next={() => { + if (loading) return; + loadGrowingCommunities(); + }} + hasMore={pageInfo.hasNextPage} + loader={ + <LoadingMoreWrapper> + <Loading /> + </LoadingMoreWrapper> + } + endMessage={<EndMsgContainer>No more data</EndMsgContainer>} + scrollableTarget="growing-communities-scroll-wrapper" + > + <div className="flex flex-col gap-[20px] max-sm:gap-[10px]"> + {growingCommunities.map((data, idx) => ( + <GrowingCommunityItem + key={data.id} + ranking={idx + 1} + communityInfo={data} + /> + ))} + </div> + </InfiniteScroll> + </div> + ); +} diff --git a/apps/u3/src/container/community/CommunitiesJoined.tsx b/apps/u3/src/container/community/CommunitiesJoined.tsx new file mode 100644 index 00000000..d5cb7900 --- /dev/null +++ b/apps/u3/src/container/community/CommunitiesJoined.tsx @@ -0,0 +1,69 @@ +import { useEffect, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import Loading from 'src/components/common/loading/Loading'; +import { + EndMsgContainer, + LoadingMoreWrapper, +} from '@/components/social/CommonStyles'; +import { CommunityList } from '@/components/community/CommonStyled'; +import CommunityItem from '@/components/community/CommunityItem'; +import useLoadJoinedCommunities from '@/hooks/community/useLoadJoinedCommunities'; +import useAllJoinedCommunities from '@/hooks/community/useAllJoinedCommunities'; + +export default function CommunitiesJoined() { + // const [mounted, setMounted] = useState(false); + // useEffect(() => { + // setMounted(true); + // }, []); + + const { communitiesCachedData, communityTypeFilter } = + useOutletContext<any>(); + // const joinedCachedData = communitiesCachedData?.joined; + + // const { loading, joinedCommunities, loadJoinedCommunities, pageInfo } = + // useLoadJoinedCommunities({ + // cachedDataRefValue: joinedCachedData, + // }); + + // useEffect(() => { + // if (!mounted) return; + // loadJoinedCommunities({ type: communityTypeFilter }); + // }, [mounted, communityTypeFilter]); + + const { + joinedCommunities: allJoinedCommunities, + joinedCommunitiesPending: loading, + } = useAllJoinedCommunities(); + const pageInfo = { hasNextPage: false }; + const joinedCommunities = allJoinedCommunities.filter( + (item) => !communityTypeFilter || item.types.includes(communityTypeFilter) + ); + + return ( + <div className="w-full p-[20px] box-border max-sm:p-[10px]"> + <InfiniteScroll + style={{ overflow: 'hidden' }} + dataLength={joinedCommunities.length} + next={() => { + // if (loading) return; + // loadJoinedCommunities({ type: communityTypeFilter }); + }} + hasMore={pageInfo.hasNextPage} + loader={ + <LoadingMoreWrapper> + <Loading /> + </LoadingMoreWrapper> + } + endMessage={<EndMsgContainer>No more data</EndMsgContainer>} + scrollableTarget="communities-scroll-wrapper" + > + <CommunityList> + {joinedCommunities.map((data) => ( + <CommunityItem key={data.id} communityInfo={data} /> + ))} + </CommunityList> + </InfiniteScroll> + </div> + ); +} diff --git a/apps/u3/src/container/community/CommunitiesNewest.tsx b/apps/u3/src/container/community/CommunitiesNewest.tsx new file mode 100644 index 00000000..eec5d286 --- /dev/null +++ b/apps/u3/src/container/community/CommunitiesNewest.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import Loading from 'src/components/common/loading/Loading'; +import { + EndMsgContainer, + LoadingMoreWrapper, +} from '@/components/social/CommonStyles'; +import { CommunityList } from '@/components/community/CommonStyled'; +import CommunityItem from '@/components/community/CommunityItem'; +import useLoadNewestCommunities from '@/hooks/community/useLoadNewestCommunities'; + +export default function CommunitiesNewest() { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + const { communitiesCachedData, communityTypeFilter } = + useOutletContext<any>(); + const newestCachedData = communitiesCachedData?.newest; + + const { loading, newestCommunities, loadNewestCommunities, pageInfo } = + useLoadNewestCommunities({ + cachedDataRefValue: newestCachedData, + }); + + useEffect(() => { + if (!mounted) return; + loadNewestCommunities({ type: communityTypeFilter }); + }, [mounted, communityTypeFilter]); + + return ( + <div className="w-full p-[20px] box-border max-sm:p-[10px]"> + <InfiniteScroll + style={{ overflow: 'hidden' }} + dataLength={newestCommunities.length} + next={() => { + if (loading) return; + loadNewestCommunities({ type: communityTypeFilter }); + }} + hasMore={pageInfo.hasNextPage} + loader={ + <LoadingMoreWrapper> + <Loading /> + </LoadingMoreWrapper> + } + endMessage={<EndMsgContainer>No more data</EndMsgContainer>} + scrollableTarget="communities-scroll-wrapper" + > + <CommunityList> + {newestCommunities.map((data) => ( + <CommunityItem key={data.id} communityInfo={data} /> + ))} + </CommunityList> + </InfiniteScroll> + </div> + ); +} diff --git a/apps/u3/src/container/community/CommunitiesTrending.tsx b/apps/u3/src/container/community/CommunitiesTrending.tsx new file mode 100644 index 00000000..900aad7a --- /dev/null +++ b/apps/u3/src/container/community/CommunitiesTrending.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import Loading from 'src/components/common/loading/Loading'; +import { + EndMsgContainer, + LoadingMoreWrapper, +} from '@/components/social/CommonStyles'; +import useLoadTrendingCommunities from '@/hooks/community/useLoadTrendingCommunities'; +import { CommunityList } from '@/components/community/CommonStyled'; +import CommunityItem from '@/components/community/CommunityItem'; + +export default function CommunitiesTrending() { + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + const { communitiesCachedData, communityTypeFilter } = + useOutletContext<any>(); + const trendingCachedData = communitiesCachedData?.trending; + + const { loading, trendingCommunities, loadTrendingCommunities, pageInfo } = + useLoadTrendingCommunities({ + cachedDataRefValue: trendingCachedData, + }); + + useEffect(() => { + if (!mounted) return; + loadTrendingCommunities({ type: communityTypeFilter }); + }, [mounted, communityTypeFilter]); + + return ( + <div className="w-full p-[20px] box-border max-sm:p-[10px]"> + <InfiniteScroll + style={{ overflow: 'hidden' }} + dataLength={trendingCommunities.length} + next={() => { + if (loading) return; + loadTrendingCommunities({ type: communityTypeFilter }); + }} + hasMore={pageInfo.hasNextPage} + loader={ + <LoadingMoreWrapper> + <Loading /> + </LoadingMoreWrapper> + } + endMessage={<EndMsgContainer>No more data</EndMsgContainer>} + scrollableTarget="communities-scroll-wrapper" + > + <CommunityList> + {trendingCommunities.map((data) => ( + <CommunityItem key={data.id} communityInfo={data} /> + ))} + </CommunityList> + </InfiniteScroll> + </div> + ); +} diff --git a/apps/u3/src/container/community/CommunityLayout.tsx b/apps/u3/src/container/community/CommunityLayout.tsx index 476b9f5e..660f2e59 100644 --- a/apps/u3/src/container/community/CommunityLayout.tsx +++ b/apps/u3/src/container/community/CommunityLayout.tsx @@ -1,7 +1,5 @@ -import { ComponentPropsWithRef, useEffect, useMemo, useState } from 'react'; +import { ComponentPropsWithRef, useEffect, useState } from 'react'; import { Outlet, useParams } from 'react-router-dom'; - -import { toast } from 'react-toastify'; import { cn } from '@/lib/utils'; import CommunityMenu from './CommunityMenu'; import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; @@ -10,14 +8,16 @@ import useLoadCommunityMembers from '@/hooks/community/useLoadCommunityMembers'; import useLoadCommunityTopMembers from '@/hooks/community/useLoadCommunityTopMembers'; import { CommunityInfo } from '@/services/community/types/community'; import { fetchCommunity } from '@/services/community/api/community'; -import useLogin from '@/hooks/shared/useLogin'; +import CommunityMobileHeader from './CommunityMobileHeader'; +import useJoinCommunityAction from '@/hooks/community/useJoinCommunityAction'; +import useBrowsingCommunity from '@/hooks/community/useBrowsingCommunity'; export default function CommunityLayout() { - const { isLogin, login } = useLogin(); const { channelId } = useParams(); const [communityLoading, setCommunityLoading] = useState(true); const [communityInfo, setCommunityInfo] = useState<CommunityInfo | null>(); + const communityId = communityInfo?.id; useEffect(() => { (async () => { setCommunityInfo(null); @@ -36,14 +36,7 @@ export default function CommunityLayout() { })(); }, [channelId]); - const { - farcasterChannels, - setDefaultPostChannelId, - userChannels, - joinChannel, - openFarcasterQR, - isConnected: isLoginFarcaster, - } = useFarcasterCtx(); + const { farcasterChannels, setDefaultPostChannelId } = useFarcasterCtx(); useEffect(() => { setDefaultPostChannelId(channelId); @@ -53,12 +46,15 @@ export default function CommunityLayout() { }, [channelId, setDefaultPostChannelId]); const channel = farcasterChannels.find((c) => c?.channel_id === channelId); - const [joining, setJoining] = useState(false); - const parentUrl = channel?.parent_url; - const joined = useMemo(() => { - const joinItem = userChannels.find((c) => c.parent_url === parentUrl); - return !!joinItem; - }, [userChannels, parentUrl]); + + const { setBrowsingCommunity, clearBrowsingCommunity } = + useBrowsingCommunity(); + useEffect(() => { + setBrowsingCommunity(communityInfo); + return () => { + clearBrowsingCommunity(); + }; + }, [communityInfo, setBrowsingCommunity, clearBrowsingCommunity]); // members state const { @@ -76,49 +72,43 @@ export default function CommunityLayout() { load: loadTopMembers, } = useLoadCommunityTopMembers(channelId); + const { joined } = useJoinCommunityAction(communityInfo); + if (communityLoading) { - <CommunityLayoutWrapper className="flex justify-center items-center"> + <div className="w-full h-full flex justify-center items-center"> <Loading /> - </CommunityLayoutWrapper>; + </div>; } - if (!communityInfo?.id) { + if (!communityId) { return null; } return ( - <CommunityLayoutWrapper> + <div className="w-full h-full flex flex-col"> {!joined && ( <GuestModeHeader - joining={joining} - joinAction={async () => { - if (!isLogin) { - login(); - return; - } - if (!isLoginFarcaster) { - openFarcasterQR(); - return; - } - setJoining(true); - await joinChannel(parentUrl); - await new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - toast.success('Join success'); - setJoining(false); - }} + className="max-sm:hidden" + communityInfo={communityInfo} /> )} - <div className="w-full h-0 flex-1 flex"> - <div className="w-[280px] h-full"> - <CommunityMenu - communityInfo={communityInfo} - channelId={channel?.channel_id} - joined={joined} - /> - </div> - <div className="flex-1 h-full overflow-auto"> + <div className={cn('w-full h-0 flex-1 flex', 'max-sm:flex-col')}> + <CommunityMenu + className="max-sm:hidden" + communityInfo={communityInfo} + channelId={channel?.channel_id} + /> + <CommunityMobileHeader + className="min-sm:hidden" + communityInfo={communityInfo} + channelId={channel?.channel_id} + /> + <div + className={cn( + 'flex-1 h-full overflow-auto', + 'max-sm:w-full max-sm:h-auto' + )} + > <Outlet context={{ channelId, @@ -141,51 +131,27 @@ export default function CommunityLayout() { /> </div> </div> - </CommunityLayoutWrapper> - ); -} - -function CommunityLayoutWrapper({ - className, - ...props -}: ComponentPropsWithRef<'div'>) { - return ( - <div - className={cn( - `w-full h-screen bg-[#20262F] flex flex-col overflow-hidden`, - className - )} - // 特殊处理,宽度 = 屏幕宽度 - 左侧浮动栏宽度 - 右侧浮动栏宽度 - style={{ - width: 'calc(100vw - 60px - 30px)', - position: 'fixed', - top: 0, - left: '60px', - }} - {...props} - /> + </div> ); } function GuestModeHeader({ className, - joinAction, - joining, + communityInfo, ...props }: ComponentPropsWithRef<'div'> & { - joined?: boolean; - joining?: boolean; - joinAction?: () => void; - unjoinAction?: () => void; + communityInfo: CommunityInfo; }) { + const { joined, isPending, isDisabled, joinChangeAction } = + useJoinCommunityAction(communityInfo); return ( <div className={cn( ` - w-full h-[42px] bg-[#F41F4C] - rounded-t-[20px] rounded-tr-[20px] - flex justify-center items-center gap-[20px] self-stretch - text-[#FFF] text-[16px] font-medium leading-[20px]`, + w-full h-[42px] bg-[#F41F4C] + rounded-t-[20px] rounded-tr-[20px] + flex justify-center items-center gap-[20px] self-stretch + text-[#FFF] text-[16px] font-medium leading-[20px]`, className )} {...props} @@ -195,16 +161,31 @@ function GuestModeHeader({ </span> <button type="button" - className="px-[12px] h-[22px] box-border rounded-[4px] border-[1px] border-solid border-[#FFF] text-[#FFF] text-[12px] font-normal" - onClick={() => { - joinAction?.(); + className={cn( + 'px-[12px] h-[22px] box-border rounded-[4px] border-[1px] border-solid border-[#FFF] text-[#FFF] text-[12px] font-normal' + )} + disabled={isDisabled} + onClick={(e) => { + e.stopPropagation(); + joinChangeAction(); }} > - {joining ? 'Joining...' : 'Join the community'} + {(() => { + if (joined) { + if (isPending) { + return 'Leaving ...'; + } + return 'Leave Community'; + } + if (isPending) { + return 'Joining ...'; + } + return 'Join the community'; + })()} </button> {/* <Button className="px-[12px] h-[22px] box-border rounded-[4px] border-[1px] border-solid border-[#FFF] text-[#FFF] text-[12px] font-normal"> - Mint NFT & Add to Shortcut - </Button> */} + Mint NFT & Add to Shortcut + </Button> */} </div> ); } diff --git a/apps/u3/src/container/community/CommunityMenu.tsx b/apps/u3/src/container/community/CommunityMenu.tsx index 3486191e..0bcb70b1 100644 --- a/apps/u3/src/container/community/CommunityMenu.tsx +++ b/apps/u3/src/container/community/CommunityMenu.tsx @@ -1,110 +1,33 @@ import { ComponentPropsWithRef } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { cn } from '@/lib/utils'; import { CommunityInfo } from '@/services/community/types/community'; import AddPost from '@/components/social/AddPost'; -import LoginButtonV2 from '@/components/layout/LoginButtonV2'; -import { - getCommunityAppPath, - getCommunityNftPath, - getCommunityPointPath, - getCommunityPostsPath, - getCommunityTokenPath, -} from '@/route/path'; +import NavLinkItem from '@/components/layout/NavLinkItem'; +import CommunityBaseInfo from '@/components/community/CommunityBaseInfo'; +import getCommunityNavs from '@/utils/community/getCommunityNavs'; +import { isCommunityLinksPath } from '@/route/path'; +import useJoinCommunityAction from '@/hooks/community/useJoinCommunityAction'; export default function CommunityMenu({ className, communityInfo, channelId, - joined, ...props }: ComponentPropsWithRef<'div'> & { communityInfo: CommunityInfo; channelId: string; - joined: boolean; }) { - const { logo, name, description, memberInfo, nfts, tokens, points, apps } = - communityInfo || {}; const { pathname } = useLocation(); - const mainNavs = [ - { title: 'Posts', href: getCommunityPostsPath(channelId) }, - // { title: 'Members', href: `/community/${channelId}/members` }, - ]; - const nft = nfts?.length > 0 ? nfts[0] : null; - if (nft) { - mainNavs.push({ - title: 'NFT', - href: getCommunityNftPath(channelId, nft?.contract), - }); - } - - const token = tokens?.length > 0 ? tokens[0] : null; - if (token) { - mainNavs.push({ - title: 'Token', - href: getCommunityTokenPath(channelId, token?.contract), - }); - } - - const point = points?.length > 0 ? points[0] : null; - if (point) { - mainNavs.push({ - title: 'Points', - href: getCommunityPointPath(channelId), - }); - } - - const dappNavs = apps?.map((dapp) => { - return { - title: dapp.name, - href: getCommunityAppPath(channelId, dapp.name), - icon: dapp.logo, - }; - }); + const { mainNavs, dappNavs } = getCommunityNavs(channelId, communityInfo); + const { joined } = useJoinCommunityAction(communityInfo); return ( <div - className={cn( - ` - w-full h-full flex flex-col bg-[#1B1E23]`, - className - )} + className={cn(`w-[280px] h-full flex flex-col bg-[#1B1E23]`, className)} {...props} > <div className="flex-1 w-full p-[20px] box-border overflow-auto"> - <div className="flex gap-[10px] items-center "> - <img src={logo} alt="" className="w-[50px] h-[50px] rounded-[4px]" /> - <div className="flex flex-col gap-[5px]"> - {communityInfo?.types?.length > 0 && ( - <div className="text-[#718096] text-[12px] font-normal line-clamp-1"> - {communityInfo?.types.reduce((acc, cur) => { - return `${acc}, ${cur}`; - })} - </div> - )} - - <div className="text-[#FFF] text-[16px] font-medium">{name}</div> - </div> - </div> - <div className="text-[#FFF] text-[14px] font-normal leading-[20px] mt-[20px]"> - {description} - </div> - <div className="flex gap-[10px] items-center mt-[20px]"> - {memberInfo?.newPostNumber > 0 && ( - <div className="text-[#718096] text-[12px] font-normal leading-[15px]"> - {memberInfo?.newPostNumber} new posts - </div> - )} - {memberInfo?.totalNumber > 0 && ( - <div className="text-[#718096] text-[12px] font-normal leading-[15px]"> - {memberInfo?.totalNumber} members - </div> - )} - </div> - {memberInfo?.friendMemberNumber > 0 && ( - <div className="text-[#718096] text-[12px] font-normal leading-[15px] mt-[20px]"> - {memberInfo?.friendMemberNumber} of your friends are members - </div> - )} + <CommunityBaseInfo communityInfo={communityInfo} /> <AddPost className={cn( @@ -112,9 +35,13 @@ export default function CommunityMenu({ !joined && `bg-[#F41F4C] hover:bg-[#F41F4C]` )} /> + <div className="w-full h-[1px] bg-[#39424C] mt-[20px] mb-[20px]" /> <div className="w-full flex flex-col gap-[5px]"> {mainNavs.map((nav) => { + if (isCommunityLinksPath(nav.href)) { + return null; + } return ( <NavLinkItem key={nav.href} @@ -147,40 +74,6 @@ export default function CommunityMenu({ })} </div> </div> - <div className="flex justify-center items-center h-[76px] w-full p-[20px] box-border bg-[#14171A] text-[#FFF] text-[16px] font-normal"> - <LoginButtonV2 /> - </div> </div> ); } - -function NavLinkItem({ - active, - href, - className, - children, - ...props -}: ComponentPropsWithRef<'a'> & { - active?: boolean; -}) { - const navigate = useNavigate(); - return ( - <a - href={href} - onClick={(e) => { - e.preventDefault(); - navigate(href); - }} - className={cn( - `block w-full h-[40px] p-[10px] box-border select-none rounded-[10px] leading-none no-underline outline-none transition-colors - text-[#718096] text-[16px] font-medium`, - `hover:bg-[#20262F] focus:bg-[#20262F] active:bg-[#20262F]`, - active && 'bg-[#20262F] text-[#FFF]', - className - )} - {...props} - > - {children} - </a> - ); -} diff --git a/apps/u3/src/container/community/CommunityMobileHeader.tsx b/apps/u3/src/container/community/CommunityMobileHeader.tsx new file mode 100644 index 00000000..3cd97ecd --- /dev/null +++ b/apps/u3/src/container/community/CommunityMobileHeader.tsx @@ -0,0 +1,94 @@ +import { ComponentPropsWithRef } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import { CommunityInfo } from '@/services/community/types/community'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { MobileHeaderWrapper } from '@/components/layout/mobile/MobileHeaderCommon'; +import SearchIconBtn from '@/components/layout/SearchIconBtn'; +import AddPostMobileBtn from '@/components/social/AddPostMobileBtn'; +import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'; +import getCommunityNavs from '@/utils/community/getCommunityNavs'; +import CommunityInfoAndAction from '@/components/community/CommunityInfoAndAction'; +import LoginButtonV2Mobile from '@/components/layout/LoginButtonV2Mobile'; + +export default function CommunityMobileHeader({ + className, + communityInfo, + channelId, + ...props +}: ComponentPropsWithRef<'div'> & { + communityInfo: CommunityInfo; + channelId: string; +}) { + const navigate = useNavigate(); + const { mainNavs } = getCommunityNavs(channelId, communityInfo); + const { pathname } = useLocation(); + const findHref = mainNavs?.find((nav) => pathname.includes(nav.href))?.href; + return ( + <MobileHeaderWrapper + className={cn('bg-[#20262F] border-b border-[#39424C]', className)} + {...props} + > + <Select + onValueChange={(href) => { + navigate(href); + }} + value={findHref} + > + <SelectTrigger className="w-auto border-none rounded-[10px] bg-[#1B1E23] text-[#FFF] text-[14px] font-medium outline-none focus:outline-none focus:border-none"> + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="mr-[10px]" + > + <path + d="M1.95692 1.97998H18.2685C18.6975 1.97998 19.0453 2.32776 19.0453 2.75672C19.0453 3.18566 18.6975 3.53344 18.2685 3.53344H1.95692C1.52796 3.53346 1.18018 3.18566 1.18018 2.75672C1.18018 2.32776 1.52796 1.97998 1.95692 1.97998ZM1.95692 8.19392H18.2685C18.6975 8.19392 19.0453 8.54172 19.0453 8.97066C19.0453 9.39962 18.6975 9.7474 18.2685 9.7474H1.95692C1.52796 9.7474 1.18018 9.3996 1.18018 8.97066C1.18018 8.54172 1.52796 8.19392 1.95692 8.19392ZM1.95692 14.4079H18.2685C18.6975 14.4079 19.0453 14.7556 19.0453 15.1846C19.0453 15.6135 18.6975 15.9613 18.2685 15.9613H1.95692C1.52796 15.9613 1.18018 15.6135 1.18018 15.1846C1.18018 14.7556 1.52796 14.4079 1.95692 14.4079Z" + fill="white" + /> + </svg> + <SelectValue /> + </SelectTrigger> + <SelectContent className="rounded-[10px] bg-[#1B1E23] text-[#FFF] text-[14px] font-medium border-none"> + {mainNavs.map((nav) => { + return ( + <SelectItem + key={nav.href} + value={nav.href} + className="hover:bg-[#20262F]" + > + {nav.title} + </SelectItem> + ); + })} + <Drawer> + <DrawerTrigger asChild> + <button + type="button" + className="hover:bg-[#20262F] w-full text-start py-1.5 pl-2 pr-8" + > + Info + </button> + </DrawerTrigger> + <DrawerContent className="rounded-tl-[20px] rounded-br-none rounded-tr-[20px] rounded-bl-none bg-[#20262F] border-none outline-none p-[20px]"> + <CommunityInfoAndAction communityInfo={communityInfo} /> + </DrawerContent> + </Drawer> + </SelectContent> + </Select> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + <AddPostMobileBtn /> + </div> + </MobileHeaderWrapper> + ); +} diff --git a/apps/u3/src/container/community/FarcasterPostDetail.tsx b/apps/u3/src/container/community/FarcasterPostDetail.tsx index 37b78e6a..67ab5b1e 100644 --- a/apps/u3/src/container/community/FarcasterPostDetail.tsx +++ b/apps/u3/src/container/community/FarcasterPostDetail.tsx @@ -1,7 +1,6 @@ /* eslint-disable no-underscore-dangle */ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { isMobile } from 'react-device-detect'; import { Channelv1 } from '@mod-protocol/farcaster'; import { CastAddBody, makeCastAdd } from '@farcaster/hub-web'; import { toast } from 'react-toastify'; @@ -10,10 +9,7 @@ import { getFarcasterCastInfo } from '../../services/social/api/farcaster'; import { FarCast } from '../../services/social/types'; import FCast from '../../components/social/farcaster/FCast'; import { useFarcasterCtx } from '../../contexts/social/FarcasterCtx'; -import { - PostDetailCommentsWrapper, - PostDetailWrapper, -} from '../../components/social/PostDetail'; +import { PostDetailCommentsWrapper } from '../../components/social/PostDetail'; import Loading from '../../components/common/loading/Loading'; import { scrollToAnchor } from '../../utils/shared/scrollToAnchor'; @@ -22,9 +18,9 @@ import useLogin from '@/hooks/shared/useLogin'; import { FARCASTER_NETWORK, FARCASTER_WEB_CLIENT } from '@/constants/farcaster'; import { ReplyCast } from '@/components/social/farcaster/FCastReply'; -import { LoadingWrapper } from '../social/CommonStyles'; import { getCommunityPostDetailShareUrlWithFarcaster } from '@/utils/shared/share'; import { getCommunityFcPostDetailPath } from '@/route/path'; +import { LoadingWrapper } from '@/components/social/CommonStyles'; export default function FarcasterPostDetail() { const navigate = useNavigate(); @@ -168,6 +164,10 @@ export default function FarcasterPostDetail() { farcasterUserData={{}} farcasterUserDataObj={farcasterUserDataObj} isV2Layout + shareLink={getCommunityPostDetailShareUrlWithFarcaster( + channelId, + key + )} castClickAction={(e, castHex) => { navigate(getCommunityFcPostDetailPath(channelId, castHex)); }} diff --git a/apps/u3/src/container/community/PostsFcMentionedLinks.tsx b/apps/u3/src/container/community/PostsFcMentionedLinks.tsx index ed9d9776..152e43d9 100644 --- a/apps/u3/src/container/community/PostsFcMentionedLinks.tsx +++ b/apps/u3/src/container/community/PostsFcMentionedLinks.tsx @@ -46,36 +46,34 @@ export default function PostsFcMentionedLinks() { }, [hasMore, loadMore, moreLoading, currentSearchParams, channel]); return ( - <div className="w-full h-full overflow-auto"> - <div className="mt-[20px]"> - <h3 className="text-[#718096] text-[14px] font-medium px-[20px] box-border"> - 🔗 Mentioned Links - </h3> - {links && links.length > 0 && ( - <CommunityLinks - loading={loading} - hasMore={hasMore} - links={links} - getMore={getMore} - quickView={(link) => { - console.log('quickView', link); - setSelectLink(link); - }} - /> - )} - <LinkModal - show={selectLink} - closeModal={() => { - setSelectLink(null); - }} - data={selectLink} - isV2Layout - castClickAction={(e, castHex) => { - setSelectLink(null); - navigate(getCommunityFcPostDetailPath(channelId, castHex)); + <div className="w-full h-full overflow-auto mt-[20px] max-sm:mt-0"> + <h3 className="text-[#718096] text-[14px] font-medium px-[20px] box-border max-sm:hidden"> + 🔗 Mentioned Links + </h3> + {links && links.length > 0 && ( + <CommunityLinks + loading={loading} + hasMore={hasMore} + links={links} + getMore={getMore} + quickView={(link) => { + console.log('quickView', link); + setSelectLink(link); }} /> - </div> + )} + <LinkModal + show={selectLink} + closeModal={() => { + setSelectLink(null); + }} + data={selectLink} + isV2Layout + castClickAction={(e, castHex) => { + setSelectLink(null); + navigate(getCommunityFcPostDetailPath(channelId, castHex)); + }} + /> </div> ); } diff --git a/apps/u3/src/container/community/PostsFcNewest.tsx b/apps/u3/src/container/community/PostsFcNewest.tsx index 69e23954..16cef28a 100644 --- a/apps/u3/src/container/community/PostsFcNewest.tsx +++ b/apps/u3/src/container/community/PostsFcNewest.tsx @@ -1,94 +1,94 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate, useOutletContext } from 'react-router-dom'; import InfiniteScroll from 'react-infinite-scroll-component'; import FCast from 'src/components/social/farcaster/FCast'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import Loading from 'src/components/common/loading/Loading'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { - LoadingMoreWrapper, - LoadingWrapper, -} from '@/components/profile/FollowListWidgets'; import useListScroll from '@/hooks/social/useListScroll'; -import { PostList } from './CommonStyles'; import { getCommunityPostDetailShareUrlWithFarcaster } from '@/utils/shared/share'; import { getCommunityFcPostDetailPath } from '@/route/path'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; +import useChannelFeeds from '@/hooks/social/useChannelFeeds'; export default function PostsFcNewest() { - const [parentId] = useState('posts-fc-newest'); + const [parentId] = useState('community-posts-fc-newest'); const { mounted } = useListScroll(parentId); + const navigate = useNavigate(); + const { openFarcasterQR } = useFarcasterCtx(); + const { channelId, setPostScroll, postsCachedData } = useOutletContext<any>(); + const whatsnewCachedData = postsCachedData?.fc?.whatsnew; + const { + loading, + loadFarcasterWhatsnew, + farcasterWhatsnew, + farcasterWhatsnewUserDataObj, + pageInfo, + } = useChannelFeeds({ channelId, + cachedDataRefValue: whatsnewCachedData, + }); - fcNewestFeeds, - fcNewestPageInfo, - fcNewestFirstLoading, - fcNewestMoreLoading, - loadFcNewestMoreFeeds, - fcUserDataObj, - - setPostScroll, - } = useOutletContext<any>(); - const navigate = useNavigate(); + useEffect(() => { + if (mounted && !whatsnewCachedData?.data?.length) { + loadFarcasterWhatsnew(); + } + }, [mounted]); return ( - <div className="w-full"> - {(fcNewestFirstLoading && ( - <LoadingWrapper> + <InfiniteScroll + style={{ overflow: 'hidden' }} + dataLength={farcasterWhatsnew.length} + next={() => { + if (loading) return; + loadFarcasterWhatsnew(); + }} + hasMore={pageInfo.hasNextPage} + loader={ + <LoadingMoreWrapper> <Loading /> - </LoadingWrapper> - )) || ( - <InfiniteScroll - style={{ overflow: 'hidden' }} - dataLength={fcNewestFeeds.length} - next={() => { - if (fcNewestMoreLoading) return; - loadFcNewestMoreFeeds(); - }} - hasMore={fcNewestPageInfo?.hasNextPage || false} - scrollThreshold={FEEDS_SCROLL_THRESHOLD} - loader={ - <LoadingMoreWrapper> - <Loading /> - </LoadingMoreWrapper> + </LoadingMoreWrapper> + } + endMessage={<EndMsgContainer>No more data</EndMsgContainer>} + scrollThreshold={FEEDS_SCROLL_THRESHOLD} + scrollableTarget="community-posts-scroll-wrapper" + > + <PostList> + {farcasterWhatsnew.map(({ platform, data }) => { + if (platform === 'farcaster') { + const key = Buffer.from(data.hash.data).toString('hex'); + return ( + <FCast + isV2Layout + key={key} + cast={data} + openFarcasterQR={openFarcasterQR} + farcasterUserData={{}} + farcasterUserDataObj={farcasterWhatsnewUserDataObj} + shareLink={getCommunityPostDetailShareUrlWithFarcaster( + channelId, + key + )} + castClickAction={(e, castHex) => { + setPostScroll({ + currentParent: parentId, + id: key, + top: (e.target as HTMLDivElement).offsetTop, + }); + navigate(getCommunityFcPostDetailPath(channelId, castHex)); + }} + /> + ); } - scrollableTarget="posts-scroll-wrapper" - > - <PostList> - {fcNewestFeeds.map(({ platform, data }) => { - if (platform === 'farcaster') { - const key = Buffer.from(data.hash.data).toString('hex'); - return ( - <FCast - isV2Layout - key={key} - cast={data} - openFarcasterQR={openFarcasterQR} - farcasterUserData={{}} - farcasterUserDataObj={fcUserDataObj} - shareLink={getCommunityPostDetailShareUrlWithFarcaster( - channelId, - key - )} - castClickAction={(e, castHex) => { - setPostScroll({ - currentParent: parentId, - id: key, - top: (e.target as HTMLDivElement).offsetTop, - }); - navigate( - getCommunityFcPostDetailPath(channelId, castHex) - ); - }} - /> - ); - } - return null; - })} - </PostList> - </InfiniteScroll> - )} - </div> + return null; + })} + </PostList> + </InfiniteScroll> ); } diff --git a/apps/u3/src/container/community/PostsFcTrending.tsx b/apps/u3/src/container/community/PostsFcTrending.tsx index 5d4f17f4..7513ef0f 100644 --- a/apps/u3/src/container/community/PostsFcTrending.tsx +++ b/apps/u3/src/container/community/PostsFcTrending.tsx @@ -5,18 +5,23 @@ import FCast from 'src/components/social/farcaster/FCast'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import Loading from 'src/components/common/loading/Loading'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { LoadingMoreWrapper } from '@/components/profile/FollowListWidgets'; import useListScroll from '@/hooks/social/useListScroll'; import useFarcasterTrending from '@/hooks/social/farcaster/useFarcasterTrending'; -import { EndMsgContainer, PostList } from './CommonStyles'; import { getCommunityPostDetailShareUrlWithFarcaster } from '@/utils/shared/share'; import { getCommunityFcPostDetailPath } from '@/route/path'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; export default function PostsFcTrending() { - const [parentId] = useState('posts-fc-trending'); + const [parentId] = useState('community-posts-fc-trending'); const { mounted } = useListScroll(parentId); const { openFarcasterQR } = useFarcasterCtx(); - const { channelId, setPostScroll } = useOutletContext<any>(); + const { channelId, setPostScroll, postsCachedData } = useOutletContext<any>(); + const trendingCachedData = postsCachedData?.fc?.trending; + const navigate = useNavigate(); const { @@ -25,10 +30,13 @@ export default function PostsFcTrending() { farcasterTrendingUserDataObj, loadFarcasterTrending, pageInfo: farcasterTrendingPageInfo, - } = useFarcasterTrending({ channelId }); + } = useFarcasterTrending({ + channelId, + cachedDataRefValue: trendingCachedData, + }); useEffect(() => { - if (mounted) { + if (mounted && !trendingCachedData?.data?.length) { loadFarcasterTrending(); } }, [mounted]); @@ -50,7 +58,7 @@ export default function PostsFcTrending() { } endMessage={<EndMsgContainer>No more data</EndMsgContainer>} scrollThreshold={FEEDS_SCROLL_THRESHOLD} - scrollableTarget="posts-scroll-wrapper" + scrollableTarget="community-posts-scroll-wrapper" > <PostList> {farcasterTrending.map(({ platform, data }) => { diff --git a/apps/u3/src/container/community/PostsLayout.tsx b/apps/u3/src/container/community/PostsLayout.tsx index 4be3fee7..b5079304 100644 --- a/apps/u3/src/container/community/PostsLayout.tsx +++ b/apps/u3/src/container/community/PostsLayout.tsx @@ -1,4 +1,4 @@ -import { ComponentPropsWithRef, useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Outlet, useNavigate, useOutletContext } from 'react-router-dom'; import { cn } from '@/lib/utils'; import PostsFcMentionedLinks from './PostsFcMentionedLinks'; @@ -6,7 +6,18 @@ import { ArrowLeft } from '@/components/common/icons/ArrowLeft'; import useRoute from '@/route/useRoute'; import { RouteKey } from '@/route/routes'; import { SocialPlatform } from '@/services/social/types'; -import useChannelFeeds from '@/hooks/social/useChannelFeeds'; +import { getDefaultFarcasterWhatsnewCachedData } from '@/hooks/social/useChannelFeeds'; +import NavLinkItem from '@/components/layout/NavLinkItem'; +import { getDefaultFarcasterTrendingCachedData } from '@/hooks/social/farcaster/useFarcasterTrending'; + +const getDefaultPostsCachedData = () => { + return { + fc: { + trending: getDefaultFarcasterTrendingCachedData(), + whatsnew: getDefaultFarcasterWhatsnewCachedData(), + }, + }; +}; export enum FeedsSort { TRENDING = 'trending', @@ -25,15 +36,8 @@ export default function PostsLayout() { id: '', top: 0, }); - const { - feeds: fcNewestFeeds, - firstLoading: fcNewestFirstLoading, - moreLoading: fcNewestMoreLoading, - loadFirstFeeds: loadFcNewestFirstFeeds, - loadMoreFeeds: loadFcNewestMoreFeeds, - pageInfo: fcNewestPageInfo, - farcasterUserDataObj: fcUserDataObj, - } = useChannelFeeds(); + + const postsCachedData = useRef({ ...getDefaultPostsCachedData() }).current; useEffect(() => { if (routeKey === RouteKey.communityPostsFcTrending) { @@ -47,7 +51,10 @@ export default function PostsLayout() { return ( <div className={cn(`w-full h-full flex bg-[#20262F]`)}> - <div id="posts-scroll-wrapper" className="flex-1 h-full overflow-auto"> + <div + id="community-posts-scroll-wrapper" + className="flex-1 h-full overflow-auto" + > {isDetail ? ( <PostDetailHeader /> ) : ( @@ -62,20 +69,19 @@ export default function PostsLayout() { socialPlatform, setSocialPlatform, - fcNewestFeeds, - fcNewestPageInfo, - fcNewestFirstLoading, - fcNewestMoreLoading, - loadFcNewestFirstFeeds, - loadFcNewestMoreFeeds, - fcUserDataObj, - postScroll, setPostScroll, + + postsCachedData, }} /> </div> - <div className="w-[320px] h-full overflow-auto bg-[#1B1E23]"> + <div + className={cn( + 'w-[320px] h-full overflow-auto bg-[#1B1E23]', + 'max-sm:hidden' + )} + > <PostsFcMentionedLinks /> </div> </div> @@ -92,12 +98,22 @@ function PostListHeader({ feedsSort }: { feedsSort: FeedsSort }) { return sort === feedsSort; }; return ( - <div className="w-full flex p-[20px] justify-between items-start self-stretch sticky top-0 bg-[#20262F] border-b border-[#39424c]"> - <div className="w-full flex items-center gap-[20px]"> + <div + className={cn( + 'w-full flex p-[20px] justify-between items-center self-stretch sticky top-0 bg-[#20262F] border-b border-[#39424c] z-10', + 'max-sm:px-[10px] max-sm:py-[14px]' + )} + > + <div + className={cn( + 'w-full flex items-center gap-[20px]', + 'max-sm:gap-[10px]' + )} + > <NavLinkItem href={getNavUrlWithSort(FeedsSort.TRENDING)} active={sortIsActive(FeedsSort.TRENDING)} - className="flex items-center gap-[10px]" + className="w-auto max-sm:p-0" > <svg xmlns="http://www.w3.org/2000/svg" @@ -105,6 +121,7 @@ function PostListHeader({ feedsSort }: { feedsSort: FeedsSort }) { height="20" viewBox="0 0 20 20" fill="none" + className="max-sm:hidden" > <path d="M14.3945 8.56641C14.207 8.86914 13.9824 9.16602 13.7227 9.45703C13.5966 9.59862 13.4436 9.71369 13.2726 9.79551C13.1015 9.87733 12.9159 9.92427 12.7266 9.93359C12.5371 9.94453 12.3473 9.9177 12.1682 9.85466C11.9892 9.79163 11.8245 9.69364 11.6836 9.56641C11.5211 9.4204 11.3937 9.23945 11.3112 9.03715C11.2287 8.83485 11.193 8.61648 11.207 8.39844C11.2656 7.47266 10.9649 6.38477 10.3125 5.16211C9.98244 4.54883 9.58791 4.02539 9.1172 3.5918C9.06901 3.90167 8.98983 4.20593 8.88087 4.5C8.61349 5.21581 8.22939 5.88238 7.74416 6.47266C7.40726 6.88586 7.02314 7.25819 6.59962 7.58203C5.93556 8.0918 5.38869 8.75391 5.0215 9.49414C4.64579 10.2461 4.45115 11.0754 4.45314 11.916C4.45314 13.3789 5.02931 14.7539 6.07423 15.791C7.12306 16.8301 8.51564 17.4004 10 17.4004C11.4844 17.4004 12.877 16.8301 13.9258 15.791C14.9707 14.7559 15.5469 13.3789 15.5469 11.916C15.5469 11.1504 15.3887 10.4062 15.0781 9.70703C14.8965 9.29688 14.668 8.91602 14.3945 8.56641Z" @@ -117,10 +134,13 @@ function PostListHeader({ feedsSort }: { feedsSort: FeedsSort }) { </svg> Trending </NavLinkItem> + <div + className={cn('w-px h-[16px] bg-[#39424C] hidden', 'max-sm:block')} + /> <NavLinkItem href={getNavUrlWithSort(FeedsSort.NEWEST)} active={sortIsActive(FeedsSort.NEWEST)} - className="flex items-center gap-[10px]" + className="w-auto max-sm:p-0" > <svg xmlns="http://www.w3.org/2000/svg" @@ -128,6 +148,7 @@ function PostListHeader({ feedsSort }: { feedsSort: FeedsSort }) { height="20" viewBox="0 0 20 20" fill="none" + className="max-sm:hidden" > <g clipPath="url(#clip0_3631_4450)"> <path @@ -148,44 +169,15 @@ function PostListHeader({ feedsSort }: { feedsSort: FeedsSort }) { ); } -function NavLinkItem({ - active, - href, - className, - children, - clickAfter, - ...props -}: ComponentPropsWithRef<'a'> & { - active?: boolean; - clickAfter?: () => void; -}) { +function PostDetailHeader() { const navigate = useNavigate(); return ( - <a - href={href} - onClick={(e) => { - e.preventDefault(); - navigate(href); - clickAfter?.(); - }} + <div className={cn( - `block w-auto h-[40px] p-[10px] box-border select-none rounded-[10px] leading-none no-underline outline-none - text-[#718096] text-[16px] font-medium`, - `hover:bg-[#20262F] focus:bg-[#1B1E23] active:bg-[#1B1E23]`, - active && 'bg-[#1B1E23] text-[#FFF]', - className + 'w-full z-10 flex p-[20px] justify-between items-start self-stretch bg-[#20262F] border-b border-[#39424c]', + 'max-sm:p-[10px]' )} - {...props} > - {children} - </a> - ); -} - -function PostDetailHeader() { - const navigate = useNavigate(); - return ( - <div className="w-full z-10 flex p-[20px] justify-between items-start self-stretch bg-[#20262F] border-b border-[#39424c]"> <div> <button type="button" diff --git a/apps/u3/src/container/explore/ExploreLayout.tsx b/apps/u3/src/container/explore/ExploreLayout.tsx new file mode 100644 index 00000000..f6707ab6 --- /dev/null +++ b/apps/u3/src/container/explore/ExploreLayout.tsx @@ -0,0 +1,33 @@ +import { Outlet } from 'react-router-dom'; +import { useState } from 'react'; +import ExploreMenu from './ExploreMenu'; +import { DailyPosterLayoutData } from '@/components/poster/layout/DailyPosterLayout'; +import { cn } from '@/lib/utils'; +import ExploreMobileHeader from './ExploreMobileHeader'; + +export type ExploreLayoutCtx = { + setDailyPosterLayoutData: (data: DailyPosterLayoutData) => void; +}; + +export default function ExploreLayout() { + const [dailyPosterLayoutData, setDailyPosterLayoutData] = + useState<DailyPosterLayoutData>({ + posts: [], + farcasterUserData: {}, + topics: [], + dapps: [], + links: [], + }); + return ( + <div className={cn('w-full h-full flex', 'max-sm:flex-col')}> + <ExploreMenu + className="max-sm:hidden" + dailyPosterLayoutData={dailyPosterLayoutData} + /> + <ExploreMobileHeader /> + <div className="flex-1 h-full overflow-auto"> + <Outlet context={{ setDailyPosterLayoutData } as ExploreLayoutCtx} /> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/explore/ExploreMenu.tsx b/apps/u3/src/container/explore/ExploreMenu.tsx new file mode 100644 index 00000000..095724f0 --- /dev/null +++ b/apps/u3/src/container/explore/ExploreMenu.tsx @@ -0,0 +1,54 @@ +import { ComponentPropsWithRef } from 'react'; +import { useLocation } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import NavLinkItem from '@/components/layout/NavLinkItem'; +import AddPost from '@/components/social/AddPost'; +import DailyPosterBtn from '@/components/poster/DailyPosterBtn'; +import { DailyPosterLayoutData } from '@/components/poster/layout/DailyPosterLayout'; + +export default function ExploreMenu({ + className, + dailyPosterLayoutData, + ...props +}: ComponentPropsWithRef<'div'> & { + dailyPosterLayoutData: DailyPosterLayoutData; +}) { + const { pathname } = useLocation(); + const isHomePath = pathname === '/'; + const isCommunitiesPath = pathname.startsWith('/communities'); + const isPosterGalleryPath = pathname === '/poster-gallery'; + const isPostsPath = pathname.startsWith('/social'); + + return ( + <div + className={cn( + ` + w-[280px] h-full flex flex-col bg-[#1B1E23]`, + className + )} + {...props} + > + <div className="flex-1 w-full p-[20px] box-border overflow-auto flex flex-col"> + <h1 className="text-[#FFF] text-[24px] font-medium leading-[20px] mb-[20px]"> + Explore + </h1> + <div className="flex-1 w-full flex flex-col gap-[5px]"> + <NavLinkItem href="/" active={isHomePath}> + Home + </NavLinkItem> + <NavLinkItem href="/social" active={isPostsPath}> + Feed + </NavLinkItem> + <NavLinkItem href="/communities" active={isCommunitiesPath}> + Communities + </NavLinkItem> + <NavLinkItem href="/poster-gallery" active={isPosterGalleryPath}> + Poster Gallery + </NavLinkItem> + </div> + {isPostsPath && <AddPost />} + {isHomePath && <DailyPosterBtn {...dailyPosterLayoutData} />} + </div> + </div> + ); +} diff --git a/apps/u3/src/container/explore/ExploreMobileHeader.tsx b/apps/u3/src/container/explore/ExploreMobileHeader.tsx new file mode 100644 index 00000000..246ef07b --- /dev/null +++ b/apps/u3/src/container/explore/ExploreMobileHeader.tsx @@ -0,0 +1,82 @@ +/* + * @Author: shixuewen friendlysxw@163.com + * @Date: 2022-12-29 18:44:14 + * @LastEditors: shixuewen friendlysxw@163.com + * @LastEditTime: 2023-02-28 23:32:58 + * @Description: file description + */ +import { ComponentPropsWithRef } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import AddPostMobileBtn from '@/components/social/AddPostMobileBtn'; +import { + MobileHeaderBackBtn, + MobileHeaderWrapper, +} from '@/components/layout/mobile/MobileHeaderCommon'; +import SearchIconBtn from '@/components/layout/SearchIconBtn'; +import LoginButtonV2Mobile from '@/components/layout/LoginButtonV2Mobile'; + +export default function ExploreMobileHeader( + props: ComponentPropsWithRef<'div'> +) { + const { pathname } = useLocation(); + const isHomePath = pathname === '/'; + const isCommunitiesPath = pathname.startsWith('/communities'); + const isPosterGalleryPath = pathname === '/poster-gallery'; + const isCasterDailyPath = pathname === '/caster-daily'; + const isPostsPath = pathname.startsWith('/social'); + const isPostDetailPath = pathname.startsWith('/social/post-detail'); + + return ( + <MobileHeaderWrapper {...props}> + {(() => { + if (isHomePath) { + return ( + <> + <div className="text-[#FFF] text-[16px] font-medium">Explore</div> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + </> + ); + } + if (isCommunitiesPath) { + return ( + <> + <div className="text-[#FFF] text-[16px] font-medium"> + Communities + </div> + {/* <MobileHeaderBackBtn title="Communities" backToPath="/" /> */} + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + </> + ); + } + if (isPosterGalleryPath) { + return <MobileHeaderBackBtn title="Poster Gallery" />; + } + if (isCasterDailyPath) { + return <MobileHeaderBackBtn title="Caster Daily" />; + } + if (isPostsPath) { + return ( + <> + <MobileHeaderBackBtn + title="Posts" + backToPath={isPostDetailPath ? undefined : '/'} + /> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + <AddPostMobileBtn /> + </> + ); + } + return null; + })()} + </MobileHeaderWrapper> + ); +} diff --git a/apps/u3/src/container/Explore.tsx b/apps/u3/src/container/explore/Home.tsx similarity index 66% rename from apps/u3/src/container/Explore.tsx rename to apps/u3/src/container/explore/Home.tsx index a901f34a..5b7267ee 100644 --- a/apps/u3/src/container/Explore.tsx +++ b/apps/u3/src/container/explore/Home.tsx @@ -1,16 +1,20 @@ import { useEffect, useState } from 'react'; -import { HotPostsData } from '../components/explore/posts/HotPosts'; -import { TopLinksData } from '../components/explore/links/TopLinks'; -import { HighScoreDappsData } from '../components/explore/dapps/HighScoreDapps'; +import { useOutletContext } from 'react-router-dom'; +import { HotPostsData } from '../../components/explore/posts/HotPosts'; +import { TopLinksData } from '../../components/explore/links/TopLinks'; +import { HighScoreDappsData } from '../../components/explore/dapps/HighScoreDapps'; import { getHotPosts, getTopLinks, getHighScoreDapps, -} from '../services/shared/api/explore'; -import { processMetadata } from '../utils/news/link'; -import ExploreLayout from '../components/explore/ExploreLayout'; +} from '../../services/shared/api/explore'; +import { processMetadata } from '../../utils/news/link'; +import HomeLayout from '../../components/explore/HomeLayout'; import { TopChannelsData } from '@/components/explore/channels/TopChannels'; import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import type { ExploreLayoutCtx } from './ExploreLayout'; +import { HotCommunitiesData } from '@/components/explore/community/HotCommunities'; +import { fetchTrendingCommunities } from '@/services/community/api/community'; type FarcasterUserData = { [key: string]: { type: number; value: string }[] }; type HotPostsState = { @@ -30,14 +34,20 @@ type HighScoreDappsState = { dapps: HighScoreDappsData; isLoading: boolean; }; -export type ExploreState = { +type HotCommunitiesState = { + communities: HotCommunitiesData; + isLoading: boolean; +}; +export type ExploreHomeState = { hotPosts: HotPostsState; topLinks: TopLinksState; topChannels: TopChannelsState; highScoreDapps: HighScoreDappsState; + hotCommunities: HotCommunitiesState; }; -export default function Explore() { +export default function Home() { + const { setDailyPosterLayoutData } = useOutletContext<ExploreLayoutCtx>(); const [hotPosts, setHotPosts] = useState<HotPostsState>({ posts: [], farcasterUserData: {}, @@ -59,6 +69,11 @@ export default function Explore() { isLoading: true, }); + const [hotCommunities, setHotCommunities] = useState<HotCommunitiesState>({ + communities: [], + isLoading: true, + }); + useEffect(() => { getHotPosts() .then((res) => { @@ -124,6 +139,25 @@ export default function Explore() { .catch(() => { setHighScoreDapps((pre) => ({ ...pre, dapps: [], isLoading: false })); }); + + fetchTrendingCommunities({ + pageSize: 3, + pageNumber: 1, + }) + .then((res) => { + const { data: communities } = res.data; + setHotCommunities({ + communities, + isLoading: false, + }); + }) + .catch(() => { + setHotCommunities((pre) => ({ + ...pre, + communities: [], + isLoading: false, + })); + }); }, []); const { trendChannels, trendChannelsLoading } = useFarcasterCtx(); @@ -142,12 +176,29 @@ export default function Explore() { }); }, [trendChannels, trendChannelsLoading]); + useEffect(() => { + setDailyPosterLayoutData({ + posts: hotPosts.posts, + farcasterUserData: hotPosts.farcasterUserData, + topics: topChannels.channels, + dapps: highScoreDapps.dapps, + links: topLinks.links, + }); + }, [ + setDailyPosterLayoutData, + hotPosts, + topLinks, + highScoreDapps, + topChannels, + ]); + return ( - <ExploreLayout + <HomeLayout hotPosts={hotPosts} topLinks={topLinks} topChannels={topChannels} highScoreDapps={highScoreDapps} + hotCommunities={hotCommunities} /> ); } diff --git a/apps/u3/src/container/fav/Fav.tsx b/apps/u3/src/container/fav/Fav.tsx new file mode 100644 index 00000000..5dc72531 --- /dev/null +++ b/apps/u3/src/container/fav/Fav.tsx @@ -0,0 +1,104 @@ +import { usePersonalFavors } from '@us3r-network/link'; +import { uniqBy } from 'lodash'; +import { useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import Loading from '@/components/common/loading/Loading'; +import { MainWrapper } from '@/components/layout/Index'; +import SaveExploreList from '@/components/profile/save/FavList'; +import SyncingBotSaves from '@/components/profile/save/SyncingBotSaves'; +import { getDappLinkDataWithJsonValue } from '@/utils/dapp/dapp'; +import { + getContentLinkDataWithJsonValue, + getContentPlatformLogoWithJsonValue, +} from '@/utils/news/content'; +import { getEventLinkDataWithJsonValue } from '@/utils/news/event'; + +enum FavType { + Link = 'link', + Dapp = 'dapp', + Content = 'content', + Event = 'event', + Post = 'post', +} +export default function Fav() { + const { pathname } = useLocation(); + + const { isFetching, personalFavors } = usePersonalFavors(); + const [savedLinks, setSavedLinks] = useState([]); + const type = useMemo(() => { + if (pathname.includes('dapps')) return FavType.Dapp; + if (pathname.includes('contents')) return FavType.Content; + if (pathname.includes('events')) return FavType.Event; + if (pathname.includes('posts')) return FavType.Post; + if (pathname.includes('links')) return FavType.Link; + return ''; + }, [pathname]); + const list = useMemo(() => { + // console.log('personalFavors', personalFavors, savedLinks); + return [ + ...savedLinks.map((item) => { + const createAt = item.createAt || new Date().getTime(); + return { ...item, createAt }; + }), + ...uniqBy( + personalFavors + .filter((item) => !!item?.link && item.link.type !== 'test') + .map((item) => { + const { link, createAt } = item; + let linkData; + let title = ''; + let logo = ''; + switch (link.type) { + case 'dapp': + linkData = getDappLinkDataWithJsonValue(link?.data); + title = linkData?.name || link.title; + logo = linkData?.image || ''; + break; + case 'content': + linkData = getContentLinkDataWithJsonValue(link?.data); + title = linkData?.title || link.title; + logo = + getContentPlatformLogoWithJsonValue(linkData?.value) || + linkData?.platform?.logo || + ''; + break; + case 'event': + linkData = getEventLinkDataWithJsonValue(link?.data); + title = linkData?.name || link.title; + logo = linkData?.image || linkData?.platform?.logo || ''; + break; + default: + linkData = JSON.parse(link?.data); + title = linkData?.title || link.title; + logo = linkData?.image || ''; + break; + } + return { ...link, id: link.id, title, logo, createAt }; + }), + 'id' + ), + ]; + }, [personalFavors, savedLinks]); + return ( + <MainWrapper className="flex flex-col gap-4"> + <SyncingBotSaves + onComplete={(saves) => { + console.log('onComplete SyncingBotSaves'); + setSavedLinks(saves); + }} + /> + {isFetching ? ( + <div className="flex items-center justify-center"> + <Loading /> + </div> + ) : ( + <SaveExploreList + data={list.filter((item) => (type ? item.type === type : true))} + onItemClick={(item) => { + window.open(item.url, '_blank'); + }} + /> + )} + </MainWrapper> + ); +} diff --git a/apps/u3/src/container/fav/FavLayout.tsx b/apps/u3/src/container/fav/FavLayout.tsx new file mode 100644 index 00000000..1014aaa2 --- /dev/null +++ b/apps/u3/src/container/fav/FavLayout.tsx @@ -0,0 +1,28 @@ +import { Outlet } from 'react-router-dom'; +import FavMenu from './FavMenu'; +import FavMobileHeader from './FavMobileHeader'; +import FavMobileMenu from './FavMobileMenu'; + +export default function FavLayout() { + return ( + <> + {/* Desktop */} + <div className="w-full h-full flex max-sm:hidden"> + <div className="bg-[#1B1E23] w-[280px] h-full"> + <FavMenu /> + </div> + <div className="flex-1 h-full overflow-auto" id="profile-warper"> + <Outlet /> + </div> + </div> + {/* Mobile */} + <div className="w-full h-full flex-col sm:hidden"> + <FavMobileHeader /> + <FavMobileMenu /> + <div className="flex-1 h-full overflow-auto" id="profile-warper"> + <Outlet /> + </div> + </div> + </> + ); +} diff --git a/apps/u3/src/container/fav/FavMenu.tsx b/apps/u3/src/container/fav/FavMenu.tsx new file mode 100644 index 00000000..2914ff83 --- /dev/null +++ b/apps/u3/src/container/fav/FavMenu.tsx @@ -0,0 +1,55 @@ +import { ComponentPropsWithRef } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import NavLinkItem from '@/components/layout/NavLinkItem'; + +export default function FavMenu({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { user } = useParams(); + const { pathname } = useLocation(); + const pathSuffix = user ? `/${user}` : ''; + return ( + <div + className={cn( + ` + w-full h-full flex flex-col`, + className + )} + {...props} + > + <div className="flex-1 w-full p-[20px] box-border overflow-auto"> + <h1 className="text-[#FFF] text-[24px] font-medium leading-[20px] mb-[20px]"> + {user || 'My'} Favorites + </h1> + <div className="flex-1 w-full flex flex-col gap-[5px]"> + <NavLinkItem + href={`/fav/posts${pathSuffix}`} + active={pathname === `/fav/posts${pathSuffix}`} + > + Posts + </NavLinkItem> + <NavLinkItem + href={`/fav/links${pathSuffix}`} + active={pathname === `/fav/links${pathSuffix}`} + > + Links + </NavLinkItem> + {/* <NavLinkItem + href={`/fav/frame${pathSuffix}`} + active={pathname === `/fav/frame${pathSuffix}`} + > + Frame + </NavLinkItem> + <NavLinkItem + href={`/fav/apps${pathSuffix}`} + active={pathname === `/fav/apps${pathSuffix}`} + > + Apps + </NavLinkItem> */} + </div> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/fav/FavMobileHeader.tsx b/apps/u3/src/container/fav/FavMobileHeader.tsx new file mode 100644 index 00000000..8318598f --- /dev/null +++ b/apps/u3/src/container/fav/FavMobileHeader.tsx @@ -0,0 +1,23 @@ +/* + * @Author: shixuewen friendlysxw@163.com + * @Date: 2022-12-29 18:44:14 + * @LastEditors: shixuewen friendlysxw@163.com + * @LastEditTime: 2023-02-28 23:32:58 + * @Description: file description + */ +import { ComponentPropsWithRef } from 'react'; +import { MobileHeaderWrapper } from '@/components/layout/mobile/MobileHeaderCommon'; +import LoginButtonV2Mobile from '@/components/layout/LoginButtonV2Mobile'; +import SearchIconBtn from '@/components/layout/SearchIconBtn'; + +export default function FavMobileHeader(props: ComponentPropsWithRef<'div'>) { + return ( + <MobileHeaderWrapper {...props}> + <div className="text-[#FFF] text-[16px] font-medium">My Favorites</div> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + </MobileHeaderWrapper> + ); +} diff --git a/apps/u3/src/container/fav/FavMobileMenu.tsx b/apps/u3/src/container/fav/FavMobileMenu.tsx new file mode 100644 index 00000000..b7fce1a2 --- /dev/null +++ b/apps/u3/src/container/fav/FavMobileMenu.tsx @@ -0,0 +1,52 @@ +import { ComponentPropsWithRef } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import NavLinkItem from '@/components/layout/NavLinkItem'; + +export default function FavMobileMenu({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { user } = useParams(); + const { pathname } = useLocation(); + const pathSuffix = user ? `/${user}` : ''; + return ( + <div + className={cn( + ` + w-full flex p-4`, + className + )} + {...props} + > + <div className="flex divide-x-2"> + <NavLinkItem + className="rounded-none py-0" + href={`/fav/posts${pathSuffix}`} + active={pathname === `/fav/posts${pathSuffix}`} + > + Posts + </NavLinkItem> + <NavLinkItem + className="rounded-none py-0" + href={`/fav/links${pathSuffix}`} + active={pathname === `/fav/links${pathSuffix}`} + > + Links + </NavLinkItem> + {/* <NavLinkItem + href={`/fav/frame${pathSuffix}`} + active={pathname === `/fav/frame${pathSuffix}`} + > + Frame + </NavLinkItem> + <NavLinkItem + href={`/fav/apps${pathSuffix}`} + active={pathname === `/fav/apps${pathSuffix}`} + > + Apps + </NavLinkItem> */} + </div> + </div> + ); +} diff --git a/apps/u3/src/container/message/MessageDetail.tsx b/apps/u3/src/container/message/MessageDetail.tsx new file mode 100644 index 00000000..79e518f8 --- /dev/null +++ b/apps/u3/src/container/message/MessageDetail.tsx @@ -0,0 +1,24 @@ +import { useXmtpClient } from '@/contexts/message/XmtpClientCtx'; +import ProfileInfoCard from '@/components/profile/info/ProfileInfoCard'; +import MessageList from '@/components/message/MessageList'; +import SendMessageForm from '@/components/message/SendMessageForm'; + +export default function MessageDetail() { + const { messageRouteParams } = useXmtpClient(); + const { peerAddress } = messageRouteParams; + return ( + <div className="w-full h-full flex"> + <div className="flex-1 h-full px-[20px] pb-[20px] box-border flex flex-col"> + <div className="w-full h-[0] flex-[1] overflow-y-scroll"> + <MessageList /> + </div> + <SendMessageForm /> + </div> + {peerAddress && ( + <div className="w-[320px] h-full bg-[#1B1E23] max-sm:hidden"> + <ProfileInfoCard identity={peerAddress} /> + </div> + )} + </div> + ); +} diff --git a/apps/u3/src/container/message/MessageLayout.tsx b/apps/u3/src/container/message/MessageLayout.tsx new file mode 100644 index 00000000..0805c4c6 --- /dev/null +++ b/apps/u3/src/container/message/MessageLayout.tsx @@ -0,0 +1,52 @@ +import { useEffect } from 'react'; +import { MessageRoute, useXmtpClient } from '@/contexts/message/XmtpClientCtx'; +import MessageMenu from './MessageMenu'; +import { XmtpStoreProvider } from '@/contexts/message/XmtpStoreCtx'; +import MessageDetail from './MessageDetail'; +import { cn } from '@/lib/utils'; +import MessageMobileHeader from './MessageMobileHeader'; +import Conversations from '@/components/message/Conversations'; +import NoEnableXmtp from '@/components/message/NoEnableXmtp'; +import StartNewConversation from '@/components/message/StartNewConversation'; + +export default function MessageLayout() { + const { messageRouteParams, setCanEnableXmtp, xmtpClient } = useXmtpClient(); + const { route, peerAddress } = messageRouteParams; + useEffect(() => { + setCanEnableXmtp(true); + }, [setCanEnableXmtp]); + return ( + <XmtpStoreProvider> + <div className={cn('w-full h-full flex', 'max-sm:flex-col')}> + <div className="w-[280px] h-full max-sm:hidden "> + <MessageMenu /> + </div> + + <MessageMobileHeader /> + <div className="flex-1 h-full overflow-auto max-sm:hidden"> + {peerAddress && <MessageDetail />} + </div> + <div className="flex-1 h-full overflow-auto hidden max-sm:block "> + {(() => { + if (!xmtpClient) { + return <NoEnableXmtp />; + } + return ( + <> + {route === MessageRoute.HOME && ( + <div className="w-full h-full flex flex-col gap-[10px] p-[10px] box-border"> + <StartNewConversation /> + <Conversations className="flex-1 overflow-auto" /> + </div> + )} + {route === MessageRoute.PRIVATE_CHAT && peerAddress && ( + <MessageDetail /> + )} + </> + ); + })()} + </div> + </div> + </XmtpStoreProvider> + ); +} diff --git a/apps/u3/src/container/message/MessageMenu.tsx b/apps/u3/src/container/message/MessageMenu.tsx new file mode 100644 index 00000000..25bc7f45 --- /dev/null +++ b/apps/u3/src/container/message/MessageMenu.tsx @@ -0,0 +1,53 @@ +import { ComponentPropsWithRef, useEffect } from 'react'; +import { cn } from '@/lib/utils'; +import StartNewConversation from '@/components/message/StartNewConversation'; +import Conversations from '@/components/message/Conversations'; +import { MessageRoute, useXmtpClient } from '@/contexts/message/XmtpClientCtx'; +import NoEnableXmtp from '@/components/message/NoEnableXmtp'; +import useConversationList from '@/hooks/message/xmtp/useConversationList'; + +export default function MessageMenu({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { xmtpClient, messageRouteParams, setMessageRouteParams } = + useXmtpClient(); + const { peerAddress } = messageRouteParams; + const { isLoading, conversationList } = useConversationList(); + useEffect(() => { + if (isLoading) return; + if (conversationList.length === 0) return; + if (peerAddress) return; + setMessageRouteParams({ + route: MessageRoute.HOME, + peerAddress: conversationList[0]?.conversation?.peerAddress, + }); + }, [isLoading, conversationList, peerAddress]); + + return ( + <div + className={cn(`w-[280px] h-full flex flex-col bg-[#1B1E23]`, className)} + {...props} + > + <div className="flex-1 w-full p-[20px] box-border overflow-auto"> + <h1 className="text-[#FFF] text-[24px] font-medium leading-[20px] mb-[20px]"> + Message + </h1> + <div className="flex-1 w-full flex flex-col gap-[5px]"> + {(() => { + if (!xmtpClient) { + return <NoEnableXmtp />; + } + return ( + <> + {' '} + <StartNewConversation /> + <Conversations className="flex-1 overflow-auto" /> + </> + ); + })()} + </div> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/message/MessageMobileHeader.tsx b/apps/u3/src/container/message/MessageMobileHeader.tsx new file mode 100644 index 00000000..2ec95131 --- /dev/null +++ b/apps/u3/src/container/message/MessageMobileHeader.tsx @@ -0,0 +1,60 @@ +/* + * @Author: shixuewen friendlysxw@163.com + * @Date: 2022-12-29 18:44:14 + * @LastEditors: shixuewen friendlysxw@163.com + * @LastEditTime: 2023-02-28 23:32:58 + * @Description: file description + */ +import { ComponentPropsWithRef } from 'react'; +import { + MobileHeaderBackBtn, + MobileHeaderWrapper, +} from '@/components/layout/mobile/MobileHeaderCommon'; +import { MessageRoute, useXmtpClient } from '@/contexts/message/XmtpClientCtx'; +import ProfileInfoHeadless from '@/components/profile/info/ProfileInfoHeadless'; +import LoginButtonV2Mobile from '@/components/layout/LoginButtonV2Mobile'; +import SearchIconBtn from '@/components/layout/SearchIconBtn'; + +export default function MessageMobileHeader( + props: ComponentPropsWithRef<'div'> +) { + const { messageRouteParams, setMessageRouteParams } = useXmtpClient(); + const { route, peerAddress } = messageRouteParams; + const isPrivateChat = route === MessageRoute.PRIVATE_CHAT; + + return ( + <MobileHeaderWrapper {...props}> + {(() => { + if (isPrivateChat) { + return ( + <ProfileInfoHeadless identity={peerAddress}> + {({ displayName }) => { + return ( + <MobileHeaderBackBtn + title={displayName} + onBackClick={() => { + setMessageRouteParams({ + route: MessageRoute.HOME, + peerAddress: null, + }); + }} + /> + ); + }} + </ProfileInfoHeadless> + ); + } + return ( + <> + <div className="text-[#FFF] text-[16px] font-medium">Messages</div> + {/* <MobileHeaderBackBtn title="Messages" /> */} + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + </> + ); + })()} + </MobileHeaderWrapper> + ); +} diff --git a/apps/u3/src/container/Project.tsx b/apps/u3/src/container/news/Project.tsx similarity index 84% rename from apps/u3/src/container/Project.tsx rename to apps/u3/src/container/news/Project.tsx index 63a37353..8354d276 100644 --- a/apps/u3/src/container/Project.tsx +++ b/apps/u3/src/container/news/Project.tsx @@ -9,22 +9,22 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import styled from 'styled-components'; -import { MainWrapper } from '../components/layout/Index'; -import Loading from '../components/common/loading/Loading'; +import { MainWrapper } from '@/components/layout/Index'; +import Loading from '@/components/common/loading/Loading'; import { UpdateProjectData, ProjectExploreListItemResponse, -} from '../services/shared/types/project'; -import { fetchOneProject, updateProject } from '../services/shared/api/project'; -import { ApiRespCode } from '../services/shared/types'; -import Header from '../components/project/detail/Header'; -import Events from '../components/project/detail/Events'; -import Conents from '../components/project/detail/Conents'; -import Team from '../components/project/detail/Team'; -import QA from '../components/project/detail/QA'; -import ProjectEditModal from '../components/project/ProjectEditModal'; -import { messages } from '../utils/shared/message'; -import Dapps from '../components/project/detail/Dapps'; +} from '@/services/shared/types/project'; +import { fetchOneProject, updateProject } from '@/services/shared/api/project'; +import { ApiRespCode } from '@/services/shared/types'; +import Header from '@/components/project/detail/Header'; +import Events from '@/components/project/detail/Events'; +import Conents from '@/components/project/detail/Conents'; +import Team from '@/components/project/detail/Team'; +import QA from '@/components/project/detail/QA'; +import ProjectEditModal from '@/components/project/ProjectEditModal'; +import { messages } from '@/utils/shared/message'; +import Dapps from '@/components/project/detail/Dapps'; export default function Project() { const navigate = useNavigate(); diff --git a/apps/u3/src/container/ProjectCreate.tsx b/apps/u3/src/container/news/ProjectCreate.tsx similarity index 83% rename from apps/u3/src/container/ProjectCreate.tsx rename to apps/u3/src/container/news/ProjectCreate.tsx index c06d45cf..37b145f6 100644 --- a/apps/u3/src/container/ProjectCreate.tsx +++ b/apps/u3/src/container/news/ProjectCreate.tsx @@ -10,16 +10,16 @@ import { useCallback, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import styled from 'styled-components'; -import CardBase from '../components/common/card/CardBase'; -import ProjectForm from '../components/project/ProjectForm'; -import { MainWrapper } from '../components/layout/Index'; -import { createProject } from '../services/shared/api/project'; +import CardBase from '@/components/common/card/CardBase'; +import ProjectForm from '@/components/project/ProjectForm'; +import { MainWrapper } from '@/components/layout/Index'; +import { createProject } from '@/services/shared/api/project'; import { UniprojectStatus, UpdateProjectData, -} from '../services/shared/types/project'; -import { messages } from '../utils/shared/message'; -import useLinkSubmit from '../hooks/shared/useLinkSubmit'; +} from '@/services/shared/types/project'; +import { messages } from '@/utils/shared/message'; +import useLinkSubmit from '@/hooks/shared/useLinkSubmit'; function ProjectCreate() { const { createProjectLink } = useLinkSubmit(); diff --git a/apps/u3/src/container/Projects.tsx b/apps/u3/src/container/news/Projects.tsx similarity index 80% rename from apps/u3/src/container/Projects.tsx rename to apps/u3/src/container/news/Projects.tsx index 6cd57e44..7bf309b2 100644 --- a/apps/u3/src/container/Projects.tsx +++ b/apps/u3/src/container/news/Projects.tsx @@ -8,24 +8,24 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; -import { MainWrapper } from '../components/layout/Index'; -import ListScrollBox from '../components/common/box/ListScrollBox'; +import { MainWrapper } from '@/components/layout/Index'; +import ListScrollBox from '@/components/common/box/ListScrollBox'; import { fetchMoreProjectExploreList, fetchProjectExploreList, selectAll, selectState, -} from '../features/project/projectExploreList'; -import { AsyncRequestStatus } from '../services/shared/types'; -import { useAppDispatch, useAppSelector } from '../store/hooks'; -import Loading from '../components/common/loading/Loading'; -import useProjectHandles from '../hooks/shared/useProjectHandles'; -import NoResult from '../components/layout/NoResult'; +} from '@/features/project/projectExploreList'; +import { AsyncRequestStatus } from '@/services/shared/types'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import Loading from '@/components/common/loading/Loading'; +import useProjectHandles from '@/hooks/shared/useProjectHandles'; +import NoResult from '@/components/layout/NoResult'; import ProjectExploreListFilter, { defaultProjectExploreListFilterValues, -} from '../components/project/ProjectExploreListFilter'; -import ProjectExploreList from '../components/project/ProjectExploreList'; -import useProjectWebsite from '../hooks/shared/useProjectWebsite'; +} from '@/components/project/ProjectExploreListFilter'; +import ProjectExploreList from '@/components/project/ProjectExploreList'; +import useProjectWebsite from '@/hooks/shared/useProjectWebsite'; export default function Projects() { const navigate = useNavigate(); diff --git a/apps/u3/src/container/Notification.tsx b/apps/u3/src/container/notification/Notification.tsx similarity index 62% rename from apps/u3/src/container/Notification.tsx rename to apps/u3/src/container/notification/Notification.tsx index 717a6469..8af0f3c5 100644 --- a/apps/u3/src/container/Notification.tsx +++ b/apps/u3/src/container/notification/Notification.tsx @@ -1,22 +1,24 @@ -import { useIsAuthenticated } from '@us3r-network/auth-with-rainbowkit'; -import useFarcasterCurrFid from '@/hooks/social/farcaster/useFarcasterCurrFid'; +import { useOutletContext } from 'react-router-dom'; +import { isMobile } from 'react-device-detect'; import { NotificationStoreProvider, useNotificationStore, } from '@/contexts/notification/NotificationStoreCtx'; import NotificationList from '@/components/notification/ui/NotificationList'; import { NotificationSettingsGroup } from '@/components/notification/PushNotificationsToogleBtn'; +import { NotificationType } from '@/services/notification/types/notifications'; // import isInstalledPwa from '@/utils/shared/isInstalledPwa'; - export default function Notification() { - const isAuthenticated = useIsAuthenticated(); - const fid = Number(useFarcasterCurrFid()); - if (!isAuthenticated) return null; + const { fid, type } = useOutletContext<{ + fid: number; + type?: NotificationType[]; + }>(); return ( <div className="w-full h-[calc(100vh-56px-40px)] px-4"> <NotificationStoreProvider config={{ fid, + type, }} > <NotificationPage /> @@ -31,11 +33,16 @@ function NotificationPage() { // const isPwa = isInstalledPwa(); return ( <> - {/* {isPwa && ( */} - <div className="text-[white] flex gap-2 pt-[10px]"> - <NotificationSettingsGroup /> - </div> - {/* )} */} + {isMobile ? ( + <div className="w-full pt-2 text-right"> + <NotificationSettingsGroup /> + </div> + ) : ( + <div className="flex items-center justify-between pt-4"> + <h2 className="text-white font-bold">Notifications</h2> + <NotificationSettingsGroup /> + </div> + )} <NotificationList notifications={notifications} diff --git a/apps/u3/src/container/notification/NotificationLayout.tsx b/apps/u3/src/container/notification/NotificationLayout.tsx new file mode 100644 index 00000000..09139cee --- /dev/null +++ b/apps/u3/src/container/notification/NotificationLayout.tsx @@ -0,0 +1,46 @@ +import { Outlet, useLocation } from 'react-router-dom'; +import { useMemo } from 'react'; +import NotificationMenu from './NotificationMenu'; +import useFarcasterCurrFid from '@/hooks/social/farcaster/useFarcasterCurrFid'; +import { NotificationType } from '@/services/notification/types/notifications'; +import NotificationMobileHeader from './NotificationMobileHeader'; + +export default function NotificationLayout() { + const fid = Number(useFarcasterCurrFid()); + const { pathname } = useLocation(); + const type = useMemo(() => { + switch (pathname) { + case '/notification/activity': + return [ + NotificationType.REACTION, + NotificationType.FOLLOW, + NotificationType.REPLY, + ]; + case '/notification/mention': + return [NotificationType.MENTION]; + default: + return [ + NotificationType.REACTION, + NotificationType.FOLLOW, + NotificationType.REPLY, + NotificationType.MENTION, + ]; + } + }, [pathname]); + return ( + <div className="w-full h-full flex"> + <div className="w-[280px] h-full max-sm:hidden"> + <NotificationMenu /> + </div> + <div className="flex-1 h-full overflow-auto"> + <NotificationMobileHeader className="w-full sm:hidden" /> + <Outlet + context={{ + fid, + type, + }} + /> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/notification/NotificationMenu.tsx b/apps/u3/src/container/notification/NotificationMenu.tsx new file mode 100644 index 00000000..e3d3770f --- /dev/null +++ b/apps/u3/src/container/notification/NotificationMenu.tsx @@ -0,0 +1,44 @@ +import { ComponentPropsWithRef } from 'react'; +import { useLocation } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import NavLinkItem from '@/components/layout/NavLinkItem'; + +export default function NotificationMenu({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { pathname } = useLocation(); + return ( + <div + className={cn( + ` + w-full h-full flex flex-col bg-[#1B1E23]`, + className + )} + {...props} + > + <div className="flex-1 w-full p-[20px] box-border overflow-auto"> + <h1 className="text-[#FFF] text-[24px] font-medium leading-[20px] mb-[20px]"> + Notifications + </h1> + <div className="flex-1 w-full flex flex-col gap-[5px]"> + <NavLinkItem + href="/notification/activity" + active={pathname === '/notification/activity'} + > + Activity + </NavLinkItem> + <NavLinkItem + href="/notification/mention" + active={pathname === '/notification/mention'} + > + Mention + </NavLinkItem> + {/* <NavLinkItem > + Setting + </NavLinkItem> */} + </div> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/notification/NotificationMobileHeader.tsx b/apps/u3/src/container/notification/NotificationMobileHeader.tsx new file mode 100644 index 00000000..693001e0 --- /dev/null +++ b/apps/u3/src/container/notification/NotificationMobileHeader.tsx @@ -0,0 +1,23 @@ +/* + * @Author: shixuewen friendlysxw@163.com + * @Date: 2022-12-29 18:44:14 + * @LastEditors: shixuewen friendlysxw@163.com + * @LastEditTime: 2023-02-28 23:32:58 + * @Description: file description + */ +import { ComponentPropsWithRef } from 'react'; +import { MobileHeaderWrapper } from '@/components/layout/mobile/MobileHeaderCommon'; +import LoginButtonV2Mobile from '@/components/layout/LoginButtonV2Mobile'; +import SearchIconBtn from '@/components/layout/SearchIconBtn'; + +export default function FavMobileHeader(props: ComponentPropsWithRef<'div'>) { + return ( + <MobileHeaderWrapper {...props}> + <div className="text-[#FFF] text-[16px] font-medium">Notifications</div> + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + </MobileHeaderWrapper> + ); +} diff --git a/apps/u3/src/container/poster/CasterDaily.tsx b/apps/u3/src/container/poster/CasterDaily.tsx new file mode 100644 index 00000000..958580d6 --- /dev/null +++ b/apps/u3/src/container/poster/CasterDaily.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import MintSuccessModalBody from '@/components/poster/mint/MintSuccessModalBody'; +import { POSTER_IMG_URL } from '@/constants'; +import PosterShare from '@/components/poster/PosterShare'; +import PosterMint from '@/components/poster/mint/PosterMint'; +import { MainWrapper } from '@/components/layout/Index'; +import { cn } from '@/lib/utils'; + +const posterImg = POSTER_IMG_URL; +export default function CasterDaily() { + const [showMinted, setShowMinted] = useState(false); + const [mintedTokenId, setMintedTokenId] = useState(0); + const [mintedWalletAddress, setMintedWalletAddress] = useState(''); + return ( + <MainWrapper className={cn('flex flex-row gap-[20px]', 'max-sm:flex-col')}> + {showMinted ? ( + <MintSuccessModalBody + img={posterImg} + tokenId={mintedTokenId} + referrerAddress={mintedWalletAddress} + className="max-sm:w-full" + /> + ) : ( + <> + {' '} + <img + src={posterImg} + alt="" + className={cn('w-[560px] h-fit object-cover', 'max-sm:w-full')} + /> + <div + className={cn( + 'w-[310px] flex flex-col gap-[20px]', + 'max-sm:w-full' + )} + > + <PosterShare posterImg={posterImg} /> + <div className="w-full h-[1px] bg-[#39424C]" /> + <PosterMint + img={posterImg} + onFirstMintSuccess={(tokenId, walletAddress) => { + setShowMinted(true); + setMintedTokenId(tokenId); + setMintedWalletAddress(walletAddress); + }} + onFreeMintSuccess={(tokenId, walletAddress) => { + setShowMinted(true); + setMintedTokenId(tokenId); + setMintedWalletAddress(walletAddress); + }} + /> + </div> + </> + )} + </MainWrapper> + ); +} diff --git a/apps/u3/src/container/PosterGallery.tsx b/apps/u3/src/container/poster/PosterGallery.tsx similarity index 84% rename from apps/u3/src/container/PosterGallery.tsx rename to apps/u3/src/container/poster/PosterGallery.tsx index 90dd1de8..992ef3cf 100644 --- a/apps/u3/src/container/PosterGallery.tsx +++ b/apps/u3/src/container/poster/PosterGallery.tsx @@ -50,22 +50,10 @@ export default function PosterGallery() { }, [loadFirst]); return ( - <div className="w-full"> - <div - className=" - w-full - h-[72px] - leading-[72px] - mb-[20px] - text-white - text-[24px] - italic - font-bold - [border-bottom:1px_solid_#39424C] - " - > - Poster Gallery - </div> + <div + className="w-full h-full p-[20px] box-border overflow-y-auto" + id="poster-gallery-scroll" + > {posters.length === 0 && loading ? ( <div className="w-full h-[calc(100vh-72px)] flex justify-center items-center"> <Loading /> @@ -85,9 +73,9 @@ export default function PosterGallery() { <Loading /> </div> } - scrollableTarget="layout-main-wrapper" + scrollableTarget="poster-gallery-scroll" > - <div className="grid grid-cols-4 gap-[30px]"> + <div className="grid grid-cols-4 gap-[30px] max-lg:grid-cols-2 max-md:grid-cols-1"> {posters.map((item) => { return <GalleryItem key={item.tokenId} data={item} />; })} diff --git a/apps/u3/src/container/profile/Activity.tsx b/apps/u3/src/container/profile/Activity.tsx new file mode 100644 index 00000000..e790a196 --- /dev/null +++ b/apps/u3/src/container/profile/Activity.tsx @@ -0,0 +1,41 @@ +import { useOutletContext } from 'react-router-dom'; +import { CurrencyETH } from '@/components/common/icons/currency-eth'; +import Rss3Content from '@/components/profile/activity/Rss3Content'; +import { ProfileOutletContext } from './ProfileLayout'; +import { shortPubKey } from '@/utils/shared/shortPubKey'; + +function Activity() { + const { address: wallets } = useOutletContext<ProfileOutletContext>(); + return ( + <div className="flex flex-col g-4 p-6"> + <div className="flex items-center justify-between"> + <h3 className="text-white font-bold">Activity</h3> + <div className="flex items-center bg-black p-2 rounded-sm gap-2"> + <svg + width="18" + height="18" + viewBox="0 0 18 18" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M14.3825 2.20586C14.402 2.30847 14.4117 2.41268 14.4117 2.51711V2.78378C14.9953 2.88138 15.5253 3.18273 15.9077 3.63429C16.2901 4.08584 16.4999 4.65834 16.5 5.25003V6.9692C17.2192 7.15461 17.75 7.8067 17.75 8.58336V11.5C17.75 11.8696 17.6272 12.2286 17.4009 12.5208C17.1747 12.8129 16.8578 13.0217 16.5 13.1142V14.8334C16.5 15.4964 16.2366 16.1323 15.7678 16.6011C15.2989 17.07 14.663 17.3334 14 17.3334H2.75C2.08696 17.3334 1.45107 17.07 0.982233 16.6011C0.513392 16.1323 0.25 15.4964 0.25 14.8334V5.25003C0.249999 4.58785 0.512704 3.95273 0.980464 3.48403C1.44822 3.01533 2.08283 2.75136 2.745 2.75003V2.72211L12.4342 0.879197C12.6492 0.838303 12.8702 0.840169 13.0845 0.88469C13.2988 0.929211 13.5023 1.01551 13.6833 1.13867C13.8642 1.26183 14.0191 1.41943 14.1392 1.60247C14.2592 1.7855 14.3421 1.9904 14.3829 2.20545L14.3825 2.20586ZM14 4.00003H2.75C2.43116 4.00001 2.12437 4.12183 1.89239 4.34057C1.66041 4.5593 1.52079 4.85841 1.50208 5.1767L1.5 5.25003V14.8334C1.49998 15.1522 1.6218 15.459 1.84053 15.691C2.05927 15.9229 2.35838 16.0626 2.67667 16.0813L2.75 16.0834H14C14.3188 16.0834 14.6256 15.9616 14.8576 15.7428C15.0896 15.5241 15.2292 15.225 15.2479 14.9067L15.25 14.8334V13.1667H11.7083C10.8795 13.1667 10.0847 12.8375 9.49862 12.2514C8.91257 11.6654 8.58333 10.8705 8.58333 10.0417C8.58333 9.2129 8.91257 8.41804 9.49862 7.83199C10.0847 7.24594 10.8795 6.9167 11.7083 6.9167H15.25V5.25003C15.25 4.93119 15.1282 4.6244 14.9095 4.39242C14.6907 4.16044 14.3916 4.02082 14.0733 4.00211L14 4.00003ZM16.0833 8.1667H11.7083C11.2187 8.16671 10.7485 8.35824 10.3982 8.70035C10.048 9.04247 9.84542 9.50803 9.83388 9.99752C9.82235 10.487 10.0027 10.9616 10.3365 11.3198C10.6703 11.6781 11.1309 11.8915 11.62 11.9146L11.7083 11.9167H16.0833C16.1854 11.9167 16.2839 11.8792 16.3602 11.8114C16.4364 11.7436 16.4851 11.6501 16.4971 11.5488L16.5 11.5V8.58336C16.5 8.48131 16.4625 8.38281 16.3947 8.30654C16.3269 8.23028 16.2334 8.18155 16.1321 8.16961L16.0833 8.1667ZM11.7083 9.4167C11.8741 9.4167 12.0331 9.48255 12.1503 9.59976C12.2675 9.71697 12.3333 9.87594 12.3333 10.0417C12.3333 10.2075 12.2675 10.3664 12.1503 10.4836C12.0331 10.6008 11.8741 10.6667 11.7083 10.6667C11.5426 10.6667 11.3836 10.6008 11.2664 10.4836C11.1492 10.3664 11.0833 10.2075 11.0833 10.0417C11.0833 9.87594 11.1492 9.71697 11.2664 9.59976C11.3836 9.48255 11.5426 9.4167 11.7083 9.4167ZM12.7158 2.10128L12.6675 2.10795L9.28958 2.74961H13.1613V2.49753L13.1546 2.4392C13.1355 2.33898 13.0802 2.24927 12.9994 2.18706C12.9185 2.12485 12.8176 2.09448 12.7158 2.1017V2.10128Z" + fill="white" + /> + </svg> + <p className="text-sm text-gray-400">{shortPubKey(wallets[0])}</p> + </div> + </div> + <Rss3Content address={wallets} empty={<NoActivity />} /> + </div> + ); +} +export default Activity; +export function NoActivity() { + return ( + <div className="no-item"> + <CurrencyETH /> + <p>No transactions found on Ethereum.</p> + </div> + ); +} diff --git a/apps/u3/src/container/Asset.tsx b/apps/u3/src/container/profile/Asset.tsx similarity index 61% rename from apps/u3/src/container/Asset.tsx rename to apps/u3/src/container/profile/Asset.tsx index 325bd39b..a548a5c1 100644 --- a/apps/u3/src/container/Asset.tsx +++ b/apps/u3/src/container/profile/Asset.tsx @@ -1,31 +1,23 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { toast } from 'react-toastify'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useState } from 'react'; import { isMobile } from 'react-device-detect'; - -import { useSession } from '@us3r-network/auth-with-rainbowkit'; -import { useProfileState } from '@us3r-network/profile'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import styled from 'styled-components'; +import Loading from '@/components/common/loading/Loading'; +import { MainWrapper } from '@/components/layout/Index'; +import PageTitle from '@/components/layout/PageTitle'; +import MobilePageHeader from '@/components/layout/mobile/MobilePageHeader'; import OnChainInterest, { OnChainInterestMobile, -} from '../components/profile/OnChainInterest'; -import { fetchU3Assets, ProfileDefault } from '../services/profile/api/profile'; -import { ProfileEntity } from '../services/profile/types/profile'; -import Loading from '../components/common/loading/Loading'; -import { mergeProfilesData } from '../utils/profile/mergeProfilesData'; -import { MainWrapper } from '../components/layout/Index'; -import PageTitle from '../components/layout/PageTitle'; -import MobilePageHeader from '../components/layout/mobile/MobilePageHeader'; +} from '@/components/profile/asset/OnChainInterest'; +import { ProfileDefault, fetchU3Assets } from '@/services/profile/api/profile'; +import { ProfileEntity } from '@/services/profile/types/profile'; +import { mergeProfilesData } from '@/utils/profile/mergeProfilesData'; +import { ProfileOutletContext } from './ProfileLayout'; export default function Asset() { - const { profile } = useProfileState(); - const { wallet } = useParams(); - const session = useSession(); - const sessId = session?.id || ''; + const { address: wallets } = useOutletContext<ProfileOutletContext>(); const navigate = useNavigate(); - - const sessWallet = useMemo(() => sessId.split(':').pop() || '', [sessId]); - const [loading, setLoading] = useState(true); const [profileData, setProfileData] = useState<ProfileEntity>(); @@ -49,16 +41,10 @@ export default function Asset() { }, []); useEffect(() => { - if (wallet) { - fetchData([wallet]); - return; + if (wallets) { + fetchData(wallets); } - const profileWallets = profile?.wallets?.map( - ({ address: walletAddress }) => walletAddress - ); - const wallets = [...new Set([sessWallet, ...(profileWallets || [])])]; - fetchData(wallets); - }, [fetchData, sessWallet, wallet, profile]); + }, [fetchData, wallets]); return ( <Wrapper id="top-wrapper"> diff --git a/apps/u3/src/container/profile/Contacts.tsx b/apps/u3/src/container/profile/Contacts.tsx new file mode 100644 index 00000000..e687af3c --- /dev/null +++ b/apps/u3/src/container/profile/Contacts.tsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from 'react'; +import { useOutletContext, useSearchParams } from 'react-router-dom'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/lib/utils'; +import FarcasterFollowers from '@/components/profile/contacts/FarcasterFollowers'; +import FarcasterFollowing from '@/components/profile/contacts/FarcasterFollowing'; +import LensProfileFollowers from '@/components/profile/contacts/LensProfileFollowers'; +import LensProfileFollowing from '@/components/profile/contacts/LensProfileFollowing'; +import FollowingDefault from '@/components/social/FollowingDefault'; +import useFarcasterUserStats from '@/hooks/social/farcaster/useFarcasterUserStats'; +import { SocialPlatform } from '@/services/social/types'; +import { ProfileOutletContext } from './ProfileLayout'; +import PlatformFilter from '@/components/shared/select/PlatformFilter'; + +export enum FollowType { + FOLLOWING = 'following', + FOLLOWER = 'follower', +} + +export default function Contacts() { + const { fid, lensProfileFirst } = useOutletContext<ProfileOutletContext>(); + const [searchParams] = useSearchParams(); + const [followNavData, setFollowNavData] = useState({ + type: searchParams.get('type') || FollowType.FOLLOWING, + platform: SocialPlatform.Farcaster, + followingPlatformCount: { + [SocialPlatform.Lens]: 0, + [SocialPlatform.Farcaster]: 0, + }, + followersPlatformCount: { + [SocialPlatform.Lens]: 0, + [SocialPlatform.Farcaster]: 0, + }, + }); + const { type, platform, followingPlatformCount, followersPlatformCount } = + followNavData; + + const { farcasterUserStats } = useFarcasterUserStats(fid); + + useEffect(() => { + setFollowNavData((prevData) => ({ + ...prevData, + followingPlatformCount: { + [SocialPlatform.Lens]: lensProfileFirst?.stats.following || 0, + [SocialPlatform.Farcaster]: farcasterUserStats.followingCount, + }, + followersPlatformCount: { + [SocialPlatform.Lens]: lensProfileFirst?.stats.followers || 0, + [SocialPlatform.Farcaster]: farcasterUserStats.followerCount, + }, + })); + }, [lensProfileFirst, farcasterUserStats]); + const [tab, setTab] = useState(type); + return ( + <div + className=" + w-full + h-full + overflow-scroll + box-border + p-[24px]" + id="profile-wrapper" + > + <Tabs + className="h-full" + value={tab} + onValueChange={(v) => { + setTab(v); + }} + > + <div className="flex items-center justify-between"> + <TabsList className="flex gap-5 justify-start bg-inherit"> + <TabsTrigger + value={FollowType.FOLLOWING} + className={cn( + 'border-[#1B1E23] border-b-2 px-0 pb-2 text-base rounded-none data-[state=active]:bg-inherit data-[state=active]:text-white data-[state=active]:border-white' + )} + > + {`Following(${followingPlatformCount[platform]})`} + </TabsTrigger> + <TabsTrigger + value={FollowType.FOLLOWER} + className={cn( + 'border-[#1B1E23] border-b-2 px-0 pb-2 text-base rounded-none data-[state=active]:bg-inherit data-[state=active]:text-white data-[state=active]:border-white' + )} + > + {`Followers(${followersPlatformCount[platform]})`} + </TabsTrigger> + </TabsList> + <div> + <PlatformFilter + defaultValue={platform} + onChange={(v) => { + setFollowNavData((prevData) => ({ + ...prevData, + platform: v, + })); + }} + /> + </div> + </div> + <TabsContent + id="profile-contacts-following-warper" + value={FollowType.FOLLOWING} + className="h-full" + > + {(() => { + if (platform === SocialPlatform.Lens) { + return <LensProfileFollowing lensProfile={lensProfileFirst} />; + } + if (platform === SocialPlatform.Farcaster) { + return <FarcasterFollowing fid={fid} />; + } + if (!lensProfileFirst?.id && !fid) { + return <FollowingDefault />; + } + return null; + })()} + </TabsContent> + <TabsContent + id="profile-contacts-follower-warper" + value={FollowType.FOLLOWER} + className="h-full" + > + {(() => { + if (platform === SocialPlatform.Lens) { + return <LensProfileFollowers lensProfile={lensProfileFirst} />; + } + if (platform === SocialPlatform.Farcaster) { + return <FarcasterFollowers fid={fid} />; + } + if (!lensProfileFirst?.id && !fid) { + return <FollowingDefault />; + } + return null; + })()} + </TabsContent> + </Tabs> + </div> + ); +} diff --git a/apps/u3/src/container/Frens.tsx b/apps/u3/src/container/profile/Frens.tsx similarity index 97% rename from apps/u3/src/container/Frens.tsx rename to apps/u3/src/container/profile/Frens.tsx index 191d1399..47142077 100644 --- a/apps/u3/src/container/Frens.tsx +++ b/apps/u3/src/container/profile/Frens.tsx @@ -17,15 +17,15 @@ import styled from 'styled-components'; import { toast } from 'react-toastify'; import TimeAgo from 'javascript-time-ago'; import en from 'javascript-time-ago/locale/en'; -import { TagType } from '../services/shared/types/common'; -import { AsyncRequestStatus } from '../services/shared/types'; +import { TagType } from '@/services/shared/types/common'; +import { AsyncRequestStatus } from '@/services/shared/types'; -import { useAppDispatch, useAppSelector } from '../store/hooks'; -import SearchInput from '../components/common/input/SearchInput'; -import Loading from '../components/common/loading/Loading'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import SearchInput from '@/components/common/input/SearchInput'; +import Loading from '@/components/common/loading/Loading'; -import Select, { SelectOption } from '../components/common/select/Select'; -import ProjectTypeSvg from '../components/common/assets/svgs/grid.svg'; +import Select, { SelectOption } from '@/components/common/select/Select'; +import ProjectTypeSvg from '@/components/common/assets/svgs/grid.svg'; import { getFeed, @@ -34,12 +34,12 @@ import { getFollowing, setFollow, getReco, -} from '../features/frens/frensHandles'; -import { MainWrapper } from '../components/layout/Index'; -import FeedsMenu from '../components/news/header/NewsMenu'; -import ListScrollBox from '../components/common/box/ListScrollBox'; -import { messages } from '../utils/shared/message'; -import useLogin from '../hooks/shared/useLogin'; +} from '@/features/frens/frensHandles'; +import { MainWrapper } from '@/components/layout/Index'; +import FeedsMenu from '@/components/news/header/NewsMenu'; +import ListScrollBox from '@/components/common/box/ListScrollBox'; +import { messages } from '@/utils/shared/message'; +import useLogin from '@/hooks/shared/useLogin'; TimeAgo.addDefaultLocale(en); const timeAgo = new TimeAgo('en-US'); diff --git a/apps/u3/src/container/Gallery.tsx b/apps/u3/src/container/profile/Gallery.tsx similarity index 59% rename from apps/u3/src/container/Gallery.tsx rename to apps/u3/src/container/profile/Gallery.tsx index b6d97f86..f0fc5cd7 100644 --- a/apps/u3/src/container/Gallery.tsx +++ b/apps/u3/src/container/profile/Gallery.tsx @@ -1,29 +1,21 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { toast } from 'react-toastify'; -import { useParams, useNavigate } from 'react-router-dom'; import { isMobile } from 'react-device-detect'; - -import { useSession } from '@us3r-network/auth-with-rainbowkit'; -import { useProfileState } from '@us3r-network/profile'; -import Credential, { CredentialMobile } from '../components/profile/Credential'; -import { fetchU3Assets, ProfileDefault } from '../services/profile/api/profile'; -import { ProfileEntity } from '../services/profile/types/profile'; -import Loading from '../components/common/loading/Loading'; -import { mergeProfilesData } from '../utils/profile/mergeProfilesData'; -import { MainWrapper } from '../components/layout/Index'; -import PageTitle from '../components/layout/PageTitle'; -import MobilePageHeader from '../components/layout/mobile/MobilePageHeader'; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useOutletContext } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import styled from 'styled-components'; +import Loading from '@/components/common/loading/Loading'; +import { MainWrapper } from '@/components/layout/Index'; +import PageTitle from '@/components/layout/PageTitle'; +import MobilePageHeader from '@/components/layout/mobile/MobilePageHeader'; +import Credential, { CredentialMobile } from '@/components/profile/gallery'; +import { ProfileDefault, fetchU3Assets } from '@/services/profile/api/profile'; +import { ProfileEntity } from '@/services/profile/types/profile'; +import { mergeProfilesData } from '@/utils/profile/mergeProfilesData'; +import { ProfileOutletContext } from './ProfileLayout'; export default function Gallery() { - const { profile } = useProfileState(); - const { wallet } = useParams(); - const session = useSession(); - const sessId = session?.id; + const { address: wallets } = useOutletContext<ProfileOutletContext>(); const navigate = useNavigate(); - - const sessWallet = useMemo(() => sessId.split(':').pop() || '', [sessId]); - const [loading, setLoading] = useState(true); const [profileData, setProfileData] = useState<ProfileEntity>(); @@ -43,16 +35,10 @@ export default function Gallery() { }, []); useEffect(() => { - if (wallet) { - fetchData([wallet]); - return; + if (wallets) { + fetchData(wallets); } - const profileWallets = profile?.wallets?.map( - ({ address: walletAddress }) => walletAddress - ); - const wallets = [...new Set([sessWallet, ...(profileWallets || [])])]; - fetchData(wallets); - }, [fetchData, sessWallet, wallet, profile]); + }, [fetchData, wallets]); return ( <Wrapper> diff --git a/apps/u3/src/container/profile/Posts.tsx b/apps/u3/src/container/profile/Posts.tsx new file mode 100644 index 00000000..f043ea58 --- /dev/null +++ b/apps/u3/src/container/profile/Posts.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import FeedsFilter from '@/components/shared/select/FeedsFilter'; +import PlatformFilter from '@/components/shared/select/PlatformFilter'; +import { SocialPlatform } from '@/services/social/types'; +import { ProfileSocialPosts } from '../../components/profile/ProfileSocial'; +import { LivepeerProvider } from '../../contexts/social/LivepeerCtx'; +import { ProfileFeedsGroups } from '../../services/social/api/feeds'; +import { ProfileOutletContext } from './ProfileLayout'; + +export default function Posts() { + const { fid, lensProfileFirst } = useOutletContext<ProfileOutletContext>(); + const [feedsGroup, setFeedsGroup] = useState<ProfileFeedsGroups>( + ProfileFeedsGroups.POSTS + ); + const [platform, setPlatform] = useState<SocialPlatform>( + SocialPlatform.Farcaster + ); + return ( + <div + className=" + w-full + h-full + overflow-scroll + box-border + p-[24px]" + > + <div className="flex items-center justify-between"> + <h2 className="text-white font-bold">Posts</h2> + <div className="flex gap-2"> + <FeedsFilter + defaultValue={ProfileFeedsGroups.POSTS} + onChange={(v) => { + setFeedsGroup(v); + }} + /> + <PlatformFilter + defaultValue={SocialPlatform.Farcaster} + onChange={(v) => { + setPlatform(v); + }} + /> + </div> + </div> + <LivepeerProvider> + <ProfileSocialPosts + lensProfileId={ + platform === SocialPlatform.Lens ? lensProfileFirst?.id : '' + } + fid={platform === SocialPlatform.Farcaster ? fid : ''} + group={feedsGroup as unknown as ProfileFeedsGroups} + /> + </LivepeerProvider> + </div> + ); +} diff --git a/apps/u3/src/container/profile/Profile.tsx b/apps/u3/src/container/profile/Profile.tsx deleted file mode 100644 index f1f3f7b1..00000000 --- a/apps/u3/src/container/profile/Profile.tsx +++ /dev/null @@ -1,544 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { isMobile } from 'react-device-detect'; -import styled, { StyledComponentPropsWithRef } from 'styled-components'; -import { Profile as LensProfile } from '@lens-protocol/react-web'; -import { useParams, useSearchParams } from 'react-router-dom'; -import { useSession } from '@us3r-network/auth-with-rainbowkit'; -import { useProfileState } from '@us3r-network/profile'; -import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; -import ProfilePageNav, { - FeedsType, -} from '../../components/profile/ProfilePageNav'; -import UserWalletsStyled from '../../components/profile/UserWalletsStyled'; -import UserTagsStyled from '../../components/profile/UserTagsStyled'; -import { LogoutButton } from '../../components/layout/LoginButton'; -import useLogin from '../../hooks/shared/useLogin'; -import LogoutConfirmModal from '../../components/layout/LogoutConfirmModal'; -import ProfileInfoCard from '../../components/profile/profile-info/ProfileInfoCard'; -import { - selectWebsite, - setProfilePageFeedsType, -} from '../../features/shared/websiteSlice'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import ProfilePageFollowNav, { - FollowType, -} from '../../components/profile/ProfilePageFollowNav'; -import LensProfileFollowing from '../../components/profile/lens/LensProfileFollowing'; -import LensProfileFollowers from '../../components/profile/lens/LensProfileFollowers'; -import { SocialPlatform } from '../../services/social/types'; -import useFarcasterFollowNum from '../../hooks/social/farcaster/useFarcasterFollowNum'; -import FarcasterFollowing from '../../components/profile/farcaster/FarcasterFollowing'; -import FarcasterFollowers from '../../components/profile/farcaster/FarcasterFollowers'; -import { isDidPkh } from '../../utils/shared/did'; -import { - ProfileSocialActivity, - ProfileSocialPosts, -} from '../../components/profile/ProfileSocial'; -import Loading from '../../components/common/loading/Loading'; -import useDid from '../../hooks/profile/useDid'; -import FollowingDefault from '../../components/social/FollowingDefault'; -import { ProfileFeedsGroups } from '../../services/social/api/feeds'; -import useU3ProfileInfoData from '../../hooks/profile/useU3ProfileInfoData'; -import usePlatformProfileInfoData from '../../hooks/profile/usePlatformProfileInfoData'; -import { LivepeerProvider } from '../../contexts/social/LivepeerCtx'; - -export default function ProfileContainer() { - return ( - <LivepeerProvider> - <Profile /> - </LivepeerProvider> - ); -} -function Profile() { - const { user: identity } = useParams(); - const { did, loading: didLoading } = useDid(identity); - const { getProfileWithDid } = useProfileState(); - const [hasProfile, setHasProfile] = useState(false); - const [hasProfileLoading, setHasProfileLoading] = useState(false); - useEffect(() => { - (async () => { - if (isDidPkh(did)) { - setHasProfileLoading(true); - const profile = await getProfileWithDid(did); - if (profile) { - setHasProfile(true); - } - setHasProfileLoading(false); - } else { - setHasProfile(false); - } - })(); - }, [did]); - - const session = useSession(); - const isSelf = useMemo(() => { - return !identity; - }, [identity]); - - if (!identity) { - return <U3ProfileContainer did={session?.id} isSelf={isSelf} />; - } - if (didLoading || hasProfileLoading) { - return null; - } - if (isDidPkh(did) && hasProfile) { - return <U3ProfileContainer did={did} isSelf={isSelf} />; - } - if (identity) { - return <PlatformProfileContainer identity={identity} isSelf={isSelf} />; - } - return null; -} - -function U3ProfileContainer({ did, isSelf }: { did: string; isSelf: boolean }) { - const session = useSession(); - const { - fid: u3ProfileFid, - address, - lensProfileFirst, - loading, - } = useU3ProfileInfoData({ - did, - isSelf, - }); - const { currFid } = useFarcasterCtx(); - const fid = useMemo(() => { - if (isSelf) { - return `${currFid || ''}`; - } - return `${u3ProfileFid || ''}`; - }, [currFid, isSelf, u3ProfileFid]); - - if (loading) { - return ( - <LoadingWrapper> - <Loading /> - </LoadingWrapper> - ); - } - - return ( - <ProfileView - fid={fid} - isSelf={isSelf} - lensProfileFirst={lensProfileFirst} - address={address} - did={did} - identity="" - isLoginUser={did === session?.id} - /> - ); -} -function PlatformProfileContainer({ - identity, - isSelf, -}: { - identity: string; - isSelf: boolean; -}) { - const { fid, recommendAddress, lensProfileFirst, loading } = - usePlatformProfileInfoData({ identity }); - - if (loading) { - return ( - <LoadingWrapper> - <Loading /> - </LoadingWrapper> - ); - } - return ( - <ProfileView - isSelf={isSelf} - fid={fid} - lensProfileFirst={lensProfileFirst} - address={recommendAddress} - did="" - identity={identity} - isLoginUser={false} - /> - ); -} - -type ProfileViewProps = { - fid: string; - lensProfileFirst: LensProfile; - address: string; - did: string; - identity: string; - isLoginUser: boolean; - isSelf: boolean; -}; -function ProfileView({ - fid, - lensProfileFirst, - address, - did, - identity, - isLoginUser, - isSelf, -}: ProfileViewProps) { - const [searchParams] = useSearchParams(); - const currSearchParams = useMemo( - () => ({ - followType: searchParams.get('followType') || '', - }), - [searchParams] - ); - const { followType } = currSearchParams; - - const { logout } = useLogin(); - const [openLogoutConfirm, setOpenLogoutConfirm] = useState(false); - - const { profilePageFeedsType } = useAppSelector(selectWebsite); - const dispatch = useAppDispatch(); - - const [followNavData, setFollowNavData] = useState({ - showFollowNav: false, - followNavType: FollowType.FOLLOWING, - followingActivePlatform: SocialPlatform.Farcaster, - followersActivePlatform: SocialPlatform.Farcaster, - followingPlatformCount: { - [SocialPlatform.Lens]: 0, - [SocialPlatform.Farcaster]: 0, - }, - followersPlatformCount: { - [SocialPlatform.Lens]: 0, - [SocialPlatform.Farcaster]: 0, - }, - }); - const { - showFollowNav, - followNavType, - followingActivePlatform, - followersActivePlatform, - followingPlatformCount, - followersPlatformCount, - } = followNavData; - - const { farcasterFollowData } = useFarcasterFollowNum(fid); - - useEffect(() => { - setFollowNavData((prevData) => ({ - ...prevData, - followingPlatformCount: { - [SocialPlatform.Lens]: lensProfileFirst?.stats.following || 0, - [SocialPlatform.Farcaster]: farcasterFollowData.following, - }, - followersPlatformCount: { - [SocialPlatform.Lens]: lensProfileFirst?.stats.followers || 0, - [SocialPlatform.Farcaster]: farcasterFollowData.followers, - }, - })); - }, [lensProfileFirst, farcasterFollowData]); - - useEffect(() => { - if (followType) { - setFollowNavData((prevData) => ({ - ...prevData, - showFollowNav: true, - followNavType: followType as FollowType, - })); - } - }, [followType]); - - useEffect(() => { - setFollowNavData((prevData) => ({ - ...prevData, - showFollowNav: false, - })); - }, [identity]); - - const onlyShowActivities = useMemo( - () => !isLoginUser && !lensProfileFirst?.id && !fid, - [isLoginUser, lensProfileFirst, fid] - ); - - return ( - <ProfileWrapper id="profile-wrapper"> - {isLoginUser && ( - <LogoutConfirmModal - isOpen={openLogoutConfirm} - onClose={() => { - setOpenLogoutConfirm(false); - }} - onConfirm={() => { - logout(); - setOpenLogoutConfirm(false); - }} - /> - )} - {isMobile && ( - <ProfileInfoMobileWrapper> - <ProfileInfoMobile did={did} identity={identity} isSelf={isSelf} /> - <LogoutButton - className="logout-button" - onClick={() => { - setOpenLogoutConfirm(true); - }} - /> - </ProfileInfoMobileWrapper> - )} - {(() => { - if (showFollowNav) { - if (followNavType === FollowType.FOLLOWING) { - return ( - <ProfilePageFollowNav - followType={FollowType.FOLLOWING} - activePlatform={followingActivePlatform} - platformCount={followingPlatformCount} - onChangePlatform={(platform) => { - setFollowNavData((prevData) => ({ - ...prevData, - followingActivePlatform: platform, - })); - }} - goBack={() => { - setFollowNavData((prevData) => ({ - ...prevData, - showFollowNav: false, - })); - }} - /> - ); - } - if (followNavType === FollowType.FOLLOWERS) { - return ( - <ProfilePageFollowNav - followType={FollowType.FOLLOWERS} - activePlatform={followersActivePlatform} - platformCount={followersPlatformCount} - onChangePlatform={(platform) => { - setFollowNavData((prevData) => ({ - ...prevData, - followersActivePlatform: platform, - })); - }} - goBack={() => { - setFollowNavData((prevData) => ({ - ...prevData, - showFollowNav: false, - })); - }} - /> - ); - } - return null; - } - - return ( - <ProfilePageNav - feedsType={ - onlyShowActivities && address - ? FeedsType.ACTIVITIES - : profilePageFeedsType - } - enabledFeedsTypes={ - onlyShowActivities - ? address - ? [FeedsType.ACTIVITIES] - : [] - : undefined - } - onChangeFeedsType={(type) => { - dispatch(setProfilePageFeedsType(type)); - }} - /> - ); - })()} - - <MainWrapper> - {!isMobile && ( - <MainLeft> - <ProfileInfo - isSelf={isSelf} - did={did} - identity={identity} - clickFollowing={() => { - setFollowNavData((prevData) => ({ - ...prevData, - showFollowNav: true, - followNavType: FollowType.FOLLOWING, - })); - }} - clickFollowers={() => { - setFollowNavData((prevData) => ({ - ...prevData, - showFollowNav: true, - followNavType: FollowType.FOLLOWERS, - })); - }} - /> - {isLoginUser && ( - <LogoutButton - className="logout-button" - onClick={() => { - setOpenLogoutConfirm(true); - }} - /> - )} - </MainLeft> - )} - - {(() => { - if (showFollowNav) { - if ( - followNavType === FollowType.FOLLOWING && - followingActivePlatform === SocialPlatform.Lens - ) { - return <LensProfileFollowing address={address} />; - } - if ( - followNavType === FollowType.FOLLOWERS && - followersActivePlatform === SocialPlatform.Lens - ) { - return <LensProfileFollowers address={address} />; - } - if ( - followNavType === FollowType.FOLLOWING && - followingActivePlatform === SocialPlatform.Farcaster - ) { - return <FarcasterFollowing fid={fid} />; - } - if ( - followNavType === FollowType.FOLLOWERS && - followersActivePlatform === SocialPlatform.Farcaster - ) { - return <FarcasterFollowers fid={fid} />; - } - return null; - } - if ( - address && - (onlyShowActivities || - profilePageFeedsType === FeedsType.ACTIVITIES) - ) { - return ( - <MainCenter> - <ProfileSocialActivity address={address} /> - </MainCenter> - ); - } - - if (isLoginUser && !lensProfileFirst?.id && !fid) { - return ( - <MainCenter> - <FollowingDefault /> - </MainCenter> - ); - } - - return ( - <MainCenter> - <ProfileSocialPosts - lensProfileId={lensProfileFirst?.id} - fid={fid} - group={profilePageFeedsType as unknown as ProfileFeedsGroups} - /> - </MainCenter> - ); - })()} - - {!isMobile && <MainRight />} - </MainWrapper> - </ProfileWrapper> - ); -} - -function ProfileInfo({ - clickFollowing, - clickFollowers, - did, - identity, - isSelf, - ...props -}: StyledComponentPropsWithRef<'div'> & { - clickFollowing?: () => void; - clickFollowers?: () => void; - did: string; - identity: string; - isSelf: boolean; -}) { - return ( - <ProfileInfoWrap {...props}> - <ProfileInfoCard - isSelf={isSelf} - identity={identity || did} - clickFollowing={clickFollowing} - clickFollowers={clickFollowers} - /> - <UserWalletsStyled did={did} /> - <UserTagsStyled did={did} /> - </ProfileInfoWrap> - ); -} -const ProfileInfoWrap = styled.div` - width: 320px; - display: flex; - flex-direction: column; - gap: 20px; - & > div { - width: 100%; - } -`; - -const ProfileWrapper = styled.div` - width: 100%; - height: 100%; - overflow: scroll; - box-sizing: border-box; - padding: 24px; - margin-bottom: 20px; - ${isMobile && - ` - height: 100vh; - padding: 10px; - padding-bottom: 60px; - `} -`; -const ProfileInfoMobileWrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - gap: 20px; - margin-bottom: 20px; -`; -const ProfileInfoMobile = styled(ProfileInfo)` - width: 100%; -`; -const MainWrapper = styled.div` - margin-top: 20px; - display: flex; - gap: 40px; -`; -const MainLeft = styled.div` - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; - position: sticky; - top: 0; - height: fit-content; - max-height: calc(100vh - 40px); - overflow-y: auto; -`; -const MainRight = styled.div` - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; - position: sticky; - top: 0; - height: fit-content; -`; -const MainCenter = styled.div` - width: 600px; -`; - -const LoadingWrapper = styled.div` - width: 100%; - height: 80vh; - display: flex; - justify-content: center; - align-items: center; -`; diff --git a/apps/u3/src/container/profile/ProfileLayout.tsx b/apps/u3/src/container/profile/ProfileLayout.tsx new file mode 100644 index 00000000..c3683ec1 --- /dev/null +++ b/apps/u3/src/container/profile/ProfileLayout.tsx @@ -0,0 +1,143 @@ +import { Profile as LensProfile } from '@lens-protocol/react-web'; +import { useSession } from '@us3r-network/auth-with-rainbowkit'; +import { useMemo } from 'react'; +import { Outlet, useLocation, useParams } from 'react-router-dom'; +import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; +import useU3ProfileInfoData from '@/hooks/profile/useU3ProfileInfoData'; +import usePlatformProfileInfoData from '@/hooks/profile/usePlatformProfileInfoData'; +import useDid from '@/hooks/profile/useDid'; +import ProfileInfoCard from '@/components/profile/info/ProfileInfoCard'; +import Loading from '@/components/common/loading/Loading'; +import ProfileMenu from './ProfileMenu'; +import ProfileMobileHeader from './ProfileMobileHeader'; + +export type ProfileOutletContext = { + did: string; + fid: string; + lensProfileFirst: LensProfile; // TODO: change to LensProfile array + address: string[]; +}; + +export default function ProfileLayout() { + const { user: identity } = useParams(); + const { pathname } = useLocation(); + const isFav = useMemo(() => pathname.includes('fav'), [pathname]); + const { + fid: identityFid, + recommendAddress: identityAddress, + lensProfileFirst: identityLensProfileFirst, + loading: identityLoading, + } = usePlatformProfileInfoData({ identity }); + const { did: identityDid, loading: identityDidLoading } = useDid(identity); + // console.log( + // 'identity', + // identity, + // 'identityDid', + // identityDid, + // 'identityFid', + // identityFid, + // 'identityAddress', + // identityAddress, + // 'identityLensProfileFirst', + // identityLensProfileFirst, + // 'identityLoading', + // identityLoading, + // 'identityDidLoading', + // identityDidLoading + // ); + const session = useSession(); + const isSelf = useMemo(() => { + return identity ? session && identityDid === session.id : !!session; + }, [identity, session]); + const { + fid: u3ProfileFid, + address: u3ProfileAddress, + lensProfileFirst: u3ProfileLensProfileFirst, + loading: u3ProfileLoading, + } = useU3ProfileInfoData({ + did: identity ? identityDid : session?.id, + isSelf, + }); + const { currFid } = useFarcasterCtx(); + // console.log( + // 'u3ProfileFid', + // u3ProfileFid, + // 'u3ProfileAddress', + // u3ProfileAddress, + // 'u3ProfileLensProfileFirst', + // u3ProfileLensProfileFirst, + // 'u3ProfileLoading', + // u3ProfileLoading, + // 'currFid', + // currFid + // ); + if (identityLoading || identityDidLoading || u3ProfileLoading) { + return ( + <div className="w-full h-full flex flex-col justify-center items-center text-[white] gap-4"> + <Loading /> + </div> + ); + } + + return ( + <> + {/* Desktop */} + <div className="w-full h-full flex max-sm:hidden"> + <div className="bg-[#1B1E23] w-[280px] h-full"> + <ProfileMenu /> + </div> + <div className="flex-1 h-full overflow-auto" id="profile-warper"> + <Outlet + context={{ + did: identity ? identityDid : session?.id, + fid: identity ? identityFid : u3ProfileFid || currFid || '', + lensProfileFirst: identity + ? identityLensProfileFirst + : u3ProfileLensProfileFirst, + address: identity + ? [identityAddress].filter((address) => + address.startsWith('0x') + ) + : [u3ProfileAddress].filter((address) => + address.startsWith('0x') + ), + }} + /> + </div> + <div className="bg-[#1B1E23] w-[320px] h-full overflow-auto"> + <ProfileInfoCard isSelf={isSelf} identity={identity || session?.id} /> + </div> + </div> + {/* Mobile */} + <div className="w-full h-full flex-col sm:hidden"> + <ProfileMobileHeader /> + {!isFav && ( + <div className="bg-[#1B1E23] overflow-auto"> + <ProfileInfoCard + isSelf={isSelf} + identity={identity || session?.id} + /> + </div> + )} + <div className="flex-1 h-full overflow-auto" id="profile-warper"> + <Outlet + context={{ + did: identity ? identityDid : session?.id, + fid: identity ? identityFid : u3ProfileFid || currFid || '', + lensProfileFirst: identity + ? identityLensProfileFirst + : u3ProfileLensProfileFirst, + address: identity + ? [identityAddress].filter((address) => + address.startsWith('0x') + ) + : [u3ProfileAddress].filter((address) => + address.startsWith('0x') + ), + }} + /> + </div> + </div> + </> + ); +} diff --git a/apps/u3/src/container/profile/ProfileMenu.tsx b/apps/u3/src/container/profile/ProfileMenu.tsx new file mode 100644 index 00000000..1045b6e2 --- /dev/null +++ b/apps/u3/src/container/profile/ProfileMenu.tsx @@ -0,0 +1,61 @@ +import { ComponentPropsWithRef } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { cn } from '@/lib/utils'; +import NavLinkItem from '@/components/layout/NavLinkItem'; + +export default function ProfileMenu({ + className, + ...props +}: ComponentPropsWithRef<'div'>) { + const { user } = useParams(); + const { pathname } = useLocation(); + const pathSuffix = user ? `/${user}` : ''; + return ( + <div + className={cn( + ` + w-full h-full flex flex-col`, + className + )} + {...props} + > + <div className="flex-1 w-full p-[20px] box-border overflow-auto"> + <h1 className="text-[#FFF] text-[24px] font-medium leading-[20px] mb-[20px]"> + {user || 'My'} Profile + </h1> + <div className="flex-1 w-full flex flex-col gap-[5px]"> + <NavLinkItem + href={`/u${pathSuffix}`} + active={pathname === `/u${pathSuffix}`} + > + Posts + </NavLinkItem> + <NavLinkItem + href={`/u/contacts${pathSuffix}`} + active={pathname === `/u/contacts${pathSuffix}`} + > + Contacts + </NavLinkItem> + <NavLinkItem + href={`/u/activity${pathSuffix}`} + active={pathname === `/u/activity${pathSuffix}`} + > + Activity + </NavLinkItem> + {/* <NavLinkItem + href={`/u/asset${pathSuffix}`} + active={pathname === `/u/asset${pathSuffix}`} + > + Assets + </NavLinkItem> + <NavLinkItem + href={`/u/gallery${pathSuffix}`} + active={pathname === `/u/gallery${pathSuffix}`} + > + Gallery + </NavLinkItem> */} + </div> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/profile/ProfileMobileHeader.tsx b/apps/u3/src/container/profile/ProfileMobileHeader.tsx new file mode 100644 index 00000000..8baecbae --- /dev/null +++ b/apps/u3/src/container/profile/ProfileMobileHeader.tsx @@ -0,0 +1,28 @@ +/* + * @Author: shixuewen friendlysxw@163.com + * @Date: 2022-12-29 18:44:14 + * @LastEditors: shixuewen friendlysxw@163.com + * @LastEditTime: 2023-02-28 23:32:58 + * @Description: file description + */ +import { ComponentPropsWithRef } from 'react'; +import { + MobileHeaderBackBtn, + MobileHeaderWrapper, +} from '@/components/layout/mobile/MobileHeaderCommon'; +import LoginButtonV2Mobile from '@/components/layout/LoginButtonV2Mobile'; +import SearchIconBtn from '@/components/layout/SearchIconBtn'; + +export default function ProfileMobileHeader( + props: ComponentPropsWithRef<'div'> +) { + return ( + <MobileHeaderWrapper {...props}> + <MobileHeaderBackBtn title="My Profile" />; + <div className="flex items-center gap-[20px]"> + <SearchIconBtn /> + <LoginButtonV2Mobile /> + </div> + </MobileHeaderWrapper> + ); +} diff --git a/apps/u3/src/container/profile/ProfileRe.tsx b/apps/u3/src/container/profile/ProfileRe.tsx deleted file mode 100644 index f390fbd7..00000000 --- a/apps/u3/src/container/profile/ProfileRe.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import styled from 'styled-components'; -import { useState } from 'react'; -import { isMobile } from 'react-device-detect'; - -import LogoutConfirmModal from '../../components/layout/LogoutConfirmModal'; -import useLogin from '../../hooks/shared/useLogin'; -import { LogoutButton } from '../../components/layout/LoginButton'; -import Reviews from '../../components/profile/review/Reviews'; -import ReviewsMobile from '../../components/profile/review/ReviewsMobile'; -import UserInfoStyled from '../../components/profile/UserInfoStyled'; -import UserTagsStyled from '../../components/profile/UserTagsStyled'; -import UserWalletsStyled from '../../components/profile/UserWalletsStyled'; - -function ProfileInfo() { - return ( - <ProfileInfoWrap> - <UserInfoStyled /> - <UserWalletsStyled /> - <UserTagsStyled /> - </ProfileInfoWrap> - ); -} -const ProfileInfoWrap = styled.div` - display: flex; - flex-direction: column; - gap: 20px; -`; - -export default function ProfileRe() { - const { logout } = useLogin(); - const [openLogoutConfirm, setOpenLogoutConfirm] = useState(false); - - return ( - <ProfileWrapper> - {isMobile ? ( - <div className="profile-wrap_mobile"> - <ProfileInfo /> - <ReviewsMobile /> - <LogoutButton - className="logout-button" - onClick={() => { - setOpenLogoutConfirm(true); - }} - /> - </div> - ) : ( - <> - <div className="profile-wrap"> - <ProfileInfo /> - <LogoutButton - className="logout-button" - onClick={() => { - setOpenLogoutConfirm(true); - }} - /> - </div> - <div className="reviews-warp"> - <Reviews /> - </div> - </> - )} - - <LogoutConfirmModal - isOpen={openLogoutConfirm} - onClose={() => { - setOpenLogoutConfirm(false); - }} - onConfirm={() => { - logout(); - setOpenLogoutConfirm(false); - }} - /> - </ProfileWrapper> - ); -} - -const ProfileWrapper = styled.div` - height: 100%; - overflow: scroll; - display: flex; - gap: 40px; - .profile-wrap { - padding-top: 40px; - margin: 0 auto; - .logout-button { - margin-top: 20px; - } - } - - .reviews-warp { - padding-top: 40px; - flex-grow: 1; - } - - .profile-wrap_mobile { - width: 100vw; - padding: 10px; - & > div:first-of-type { - width: auto; - & > div { - width: auto; - } - & > div:nth-of-type(2) { - background: transparent; - padding: 0; - .wallet-item { - background: #1b1e23; - border-radius: 20px; - padding: 11.2px; - } - } - & > div:last-of-type { - padding: 0; - background: transparent; - } - & > div:first-of-type { - /* background: red; */ - & > div { - & > div { - padding: 10px; - height: 100px; - display: block; - border: 1px solid #39424c; - border-radius: 10px; - width: auto; - /* margin-top: 8px; */ - /* flex-direction: row; */ - .name-box { - text-align: left; - margin-top: 15px; - } - .avatar-box { - float: left; - width: 80px; - height: 80px; - margin-right: 10px; - } - } - } - } - } - } -`; diff --git a/apps/u3/src/container/social/CommonStyles.tsx b/apps/u3/src/container/social/CommonStyles.tsx index 6561220d..b3eecc1e 100644 --- a/apps/u3/src/container/social/CommonStyles.tsx +++ b/apps/u3/src/container/social/CommonStyles.tsx @@ -6,10 +6,8 @@ * @FilePath: /u3/apps/u3/src/container/social/CommonStyles.tsx * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ -import { ComponentPropsWithRef } from 'react'; import NoLogin from 'src/components/layout/NoLogin'; import styled from 'styled-components'; -import { cn } from '@/lib/utils'; export const MainCenter = styled.div` width: 100%; @@ -32,57 +30,3 @@ export const AddPostFormWrapper = styled.div` width: 100%; box-sizing: border-box; `; - -export const PostList = styled.div` - display: flex; - flex-direction: column; - gap: 1px; - - border-radius: 20px; - background: #212228; - overflow: hidden; - & > :not(:first-child) { - border-top: 1px solid #39424c; - } -`; - -export function PostListV2({ - className, - ...props -}: ComponentPropsWithRef<'div'>) { - return ( - <div - className={cn( - `flex flex-col gap-[1px] rounded-[20px] bg-[#212228] overflow-hidden`, - className - )} - {...props} - /> - ); -} - -export const LoadingWrapper = styled.div` - width: 100%; - height: 80vh; - display: flex; - justify-content: center; - align-items: center; -`; - -export const LoadingMoreWrapper = styled.div` - width: 100%; - display: flex; - justify-content: center; - align-items: center; - margin-top: 20px; -`; - -export const EndMsgContainer = styled.div` - width: 100%; - display: flex; - justify-content: center; - align-items: center; - padding: 20px 0; - font-size: 14px; - color: #718096; -`; diff --git a/apps/u3/src/container/social/FarcasterPostDetail.tsx b/apps/u3/src/container/social/FarcasterPostDetail.tsx index 74384449..19340c50 100644 --- a/apps/u3/src/container/social/FarcasterPostDetail.tsx +++ b/apps/u3/src/container/social/FarcasterPostDetail.tsx @@ -1,7 +1,6 @@ /* eslint-disable no-underscore-dangle */ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { isMobile } from 'react-device-detect'; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { Channelv1 } from '@mod-protocol/farcaster'; import { CastAddBody, makeCastAdd } from '@farcaster/hub-web'; import { toast } from 'react-toastify'; @@ -10,21 +9,22 @@ import { getFarcasterCastInfo } from '../../services/social/api/farcaster'; import { FarCast } from '../../services/social/types'; import FCast from '../../components/social/farcaster/FCast'; import { useFarcasterCtx } from '../../contexts/social/FarcasterCtx'; -import { - PostDetailCommentsWrapper, - PostDetailWrapper, -} from '../../components/social/PostDetail'; +import { PostDetailCommentsWrapper } from '../../components/social/PostDetail'; import Loading from '../../components/common/loading/Loading'; import { scrollToAnchor } from '../../utils/shared/scrollToAnchor'; -import { LoadingWrapper } from './CommonStyles'; import { userDataObjFromArr } from '@/utils/social/farcaster/user-data'; import useLogin from '@/hooks/shared/useLogin'; import { FARCASTER_NETWORK, FARCASTER_WEB_CLIENT } from '@/constants/farcaster'; import { ReplyCast } from '@/components/social/farcaster/FCastReply'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; +import { LoadingWrapper } from '@/components/social/CommonStyles'; +import { cn } from '@/lib/utils'; export default function FarcasterPostDetail() { + const navigate = useNavigate(); const { castId } = useParams(); const [loading, setLoading] = useState(true); const [cast, setCast] = useState<FarCast>(); @@ -129,16 +129,25 @@ export default function FarcasterPostDetail() { if (cast) { scrollToAnchor(window.location.hash.split('#')[1]); return ( - <PostDetailWrapper isMobile={isMobile}> + <div className="w-full"> <FCast + isV2Layout cast={cast} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={farcasterUserDataObj} isDetail - showMenuBtn + shareLink={getSocialDetailShareUrlWithFarcaster(castId)} + castClickAction={(e, castHex) => { + navigate(getExploreFcPostDetailPath(castHex)); + }} /> - <div className="flex gap-3 w-full mb-2 p-5 border-t border-[#39424c]"> + <div + className={cn( + 'flex gap-3 w-full mb-2 p-5 border-t border-[#39424c]', + 'max-sm:p-2' + )} + > <ReplyCast replyAction={async (data) => { await replyCastAction(data); @@ -150,16 +159,21 @@ export default function FarcasterPostDetail() { const key = Buffer.from(item.data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={item.data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={farcasterUserDataObj} + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { + navigate(getExploreFcPostDetailPath(castHex)); + }} /> ); })} </PostDetailCommentsWrapper> - </PostDetailWrapper> + </div> ); } return <LoadingWrapper />; diff --git a/apps/u3/src/container/social/FarcasterSignupV2.tsx b/apps/u3/src/container/social/FarcasterSignupV2.tsx index 6dc3d8a5..4352d79f 100644 --- a/apps/u3/src/container/social/FarcasterSignupV2.tsx +++ b/apps/u3/src/container/social/FarcasterSignupV2.tsx @@ -10,6 +10,11 @@ import { FARCASTER_NETWORK, FARCASTER_WEB_CLIENT } from '@/constants/farcaster'; import RentStorage from '@/components/social/farcaster/signupv2/RentStorage'; import { ChevronRightDouble } from '@/components/common/icons/chevon-right-double'; import ColorButton from '@/components/common/button/ColorButton'; +import { cn } from '@/lib/utils'; +import { + MobileHeaderBackBtn, + MobileHeaderWrapper, +} from '@/components/layout/mobile/MobileHeaderCommon'; export default function FarcasterSignupV2() { const { @@ -52,39 +57,52 @@ export default function FarcasterSignupV2() { [fid, signer, hasStorage] ); return ( - <div className="h-screen flex flex-col items-center justify-center"> - <h3 className="text-white font-bold text-4xl italic"> + <div className={cn('w-full h-full flex-col')}> + <h3 className="text-white font-bold text-4xl italic text-center pt-[20px] max-sm:hidden"> Sign up for Farcaster </h3> - <div className="steps flex flex-wrap items-center justify-between gap-5 w-full my-auto mt-[50px] mb-[80px]"> - <RegisterAndPay fid={fid} setFid={setFid} /> - <AddAccountKey fid={fid} signer={signer} setSigner={setSigner} /> - <FnameRegister - fid={fid} - fname={fname} - signer={signer} - setFname={setFname} - makePrimaryName={makePrimaryName} - /> - <RentStorage - fid={fid} - hasStorage={hasStorage} - setHasStorage={setHasStorage} - /> - </div> - <div className="w-full text-white flex justify-end"> - {(fid && fname && signer && hasStorage && ( - <ColorButton - type="button" - onClick={() => { - navigate('/farcaster/profile'); - }} - > - Setup your profile - <ChevronRightDouble /> - </ColorButton> - )) || - null} + <MobileHeaderWrapper> + <MobileHeaderBackBtn title="Sign up for Farcaster" /> + </MobileHeaderWrapper> + + <div className="flex-1 h-full overflow-auto p-[20px] box-border max-sm:h-[calc(100vh-56px-80px)]"> + <div className="steps flex flex-wrap items-center justify-between gap-5 w-full max-sm:flex-col"> + <RegisterAndPay className="max-sm:w-full" fid={fid} setFid={setFid} /> + <AddAccountKey + className="max-sm:w-full" + fid={fid} + signer={signer} + setSigner={setSigner} + /> + <FnameRegister + className="max-sm:w-full" + fid={fid} + fname={fname} + signer={signer} + setFname={setFname} + makePrimaryName={makePrimaryName} + /> + <RentStorage + className="max-sm:w-full" + fid={fid} + hasStorage={hasStorage} + setHasStorage={setHasStorage} + /> + </div> + <div className="w-full text-white flex justify-end"> + {(fid && fname && signer && hasStorage && ( + <ColorButton + type="button" + onClick={() => { + navigate('/farcaster/profile'); + }} + > + Setup your profile + <ChevronRightDouble /> + </ColorButton> + )) || + null} + </div> </div> </div> ); diff --git a/apps/u3/src/container/social/PostsMentionedLinks.tsx b/apps/u3/src/container/social/PostsMentionedLinks.tsx new file mode 100644 index 00000000..a6287727 --- /dev/null +++ b/apps/u3/src/container/social/PostsMentionedLinks.tsx @@ -0,0 +1,69 @@ +import { useEffect, useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useFeedLinks from '@/hooks/news/useFeedLinks'; +import useLinksSearchParams from '@/hooks/news/useLinksSearchParams'; +import LinkModal from '@/components/news/links/LinkModal'; +import CommunityLinks from '@/components/news/links/community/CommunityLinks'; +import { getExploreFcPostDetailPath } from '@/route/path'; + +export default function PostsMentionedLinks() { + const navigate = useNavigate(); + const domains = []; // todo: 目前暂定为全部,以后从参数获取 + const { currentSearchParams } = useLinksSearchParams(); + const { loading, moreLoading, hasMore, links, load, loadMore } = + useFeedLinks(); + const [selectLink, setSelectLink] = useState(null); + useEffect(() => { + const groupDomain = { + includeDomains: domains, + }; + load(groupDomain, [], currentSearchParams); + // debounce( + // () => load(groupDomain, [channel.parent_url], currentSearchParams, link), + // 200 + // ); + }, [currentSearchParams]); + + const getMore = useCallback(() => { + if (moreLoading) return; + if (!hasMore) return; + const groupDomain = { + includeDomains: domains, + }; + loadMore(groupDomain, [], currentSearchParams); + }, [hasMore, loadMore, moreLoading, currentSearchParams]); + + return ( + <div className="w-full h-full overflow-auto"> + <div className="mt-[20px]"> + <h3 className="text-[#718096] text-[14px] font-medium px-[20px] box-border"> + 🔗 Mentioned Links + </h3> + {links && links.length > 0 && ( + <CommunityLinks + loading={loading} + hasMore={hasMore} + links={links} + getMore={getMore} + quickView={(link) => { + console.log('quickView', link); + setSelectLink(link); + }} + /> + )} + <LinkModal + show={selectLink} + closeModal={() => { + setSelectLink(null); + }} + data={selectLink} + isV2Layout + castClickAction={(e, castHex) => { + setSelectLink(null); + navigate(getExploreFcPostDetailPath(castHex)); + }} + /> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/social/SocialAll.tsx b/apps/u3/src/container/social/SocialAll.tsx index 8cdab79e..5cd4be41 100644 --- a/apps/u3/src/container/social/SocialAll.tsx +++ b/apps/u3/src/container/social/SocialAll.tsx @@ -14,7 +14,12 @@ import { FeedsType } from '../../components/social/SocialPageNav'; export default function SocialAll() { const currentFeedType = useRef<FeedsType>(); - const { feedsType, postScroll, setPostScroll } = useOutletContext<any>(); // TODO: any type + const { feedsType, postScroll, setPostScroll, postsCachedData } = + useOutletContext<any>(); // TODO: any type + + const trendingCachedData = postsCachedData?.all?.trending; + const whatsnewCachedData = postsCachedData?.all?.whatsnew; + const followingCachedData = postsCachedData?.all?.following; useEffect(() => { if (feedsType === currentFeedType.current) return; @@ -29,6 +34,10 @@ export default function SocialAll() { feedsType, postScroll, setPostScroll, + + trendingCachedData, + whatsnewCachedData, + followingCachedData, }} /> </MainCenter> diff --git a/apps/u3/src/container/social/SocialAllFollowing.tsx b/apps/u3/src/container/social/SocialAllFollowing.tsx index 600fdcd8..75a2946c 100644 --- a/apps/u3/src/container/social/SocialAllFollowing.tsx +++ b/apps/u3/src/container/social/SocialAllFollowing.tsx @@ -7,7 +7,7 @@ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ import InfiniteScroll from 'react-infinite-scroll-component'; -import { useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; @@ -20,19 +20,16 @@ import useAllFollowing from 'src/hooks/social/useAllFollowing'; import LensPostCard from 'src/components/social/lens/LensPostCard'; import { useLensCtx } from 'src/contexts/social/AppLensCtx'; +import { MainCenter, NoLoginStyled } from './CommonStyles'; +import useLogin from '../../hooks/shared/useLogin'; +import FollowingDefault from '../../components/social/FollowingDefault'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; import { EndMsgContainer, LoadingMoreWrapper, - MainCenter, - NoLoginStyled, PostList, -} from './CommonStyles'; -import useLogin from '../../hooks/shared/useLogin'; -import FollowingDefault from '../../components/social/FollowingDefault'; - -export const AllFirst = { - done: false, -}; +} from '@/components/social/CommonStyles'; export default function SocialAllFollowing() { const [parentId] = useState('social-all-following'); @@ -41,21 +38,22 @@ export default function SocialAllFollowing() { const { id: lensSessionProfileId } = lensSessionProfile || {}; const { currFid } = useFarcasterCtx(); const { openFarcasterQR } = useFarcasterCtx(); - const { setPostScroll } = useOutletContext<any>(); // TODO: any + const { followingCachedData, setPostScroll } = useOutletContext<any>(); // TODO: any const { mounted } = useListScroll(parentId); const { allFollowing, loadAllFollowing, loading, pageInfo, allUserDataObj } = - useAllFollowing(); + useAllFollowing({ + cachedDataRefValue: followingCachedData, + }); + const navigate = useNavigate(); useEffect(() => { - if (AllFirst.done) return; if (!mounted) return; if (!isLogin) return; if (!currFid && !lensSessionProfileId) return; + if (followingCachedData?.data?.length > 0) return; - loadAllFollowing().finally(() => { - AllFirst.done = true; - }); + loadAllFollowing(); }, [mounted, isLogin, currFid, lensSessionProfileId]); if (!isLogin) { @@ -108,18 +106,20 @@ export default function SocialAllFollowing() { const key = Buffer.from(data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={allUserDataObj} - showMenuBtn - cardClickAction={(e) => { + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { setPostScroll({ currentParent: parentId, id: key, top: (e.target as HTMLDivElement).offsetTop, }); + navigate(getExploreFcPostDetailPath(castHex)); }} /> ); diff --git a/apps/u3/src/container/social/SocialAllTrending.tsx b/apps/u3/src/container/social/SocialAllTrending.tsx index 80ad19d2..fef5ae38 100644 --- a/apps/u3/src/container/social/SocialAllTrending.tsx +++ b/apps/u3/src/container/social/SocialAllTrending.tsx @@ -1,5 +1,5 @@ import InfiniteScroll from 'react-infinite-scroll-component'; -import { useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import FCast from 'src/components/social/farcaster/FCast'; @@ -7,12 +7,19 @@ import Loading from 'src/components/common/loading/Loading'; import useListScroll from 'src/hooks/social/useListScroll'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; import useFarcasterTrending from 'src/hooks/social/farcaster/useFarcasterTrending'; -import { EndMsgContainer, LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; export default function SocialAllTrending() { const [parentId] = useState('social-all-trending'); const { openFarcasterQR } = useFarcasterCtx(); - const { setPostScroll } = useOutletContext<any>(); + const { trendingCachedData, setPostScroll } = useOutletContext<any>(); + const navigate = useNavigate(); // use farcaster trending temp. const { @@ -21,12 +28,15 @@ export default function SocialAllTrending() { farcasterTrendingUserDataObj, loadFarcasterTrending, pageInfo: farcasterTrendingPageInfo, - } = useFarcasterTrending(); + } = useFarcasterTrending({ + cachedDataRefValue: trendingCachedData, + }); + console.log('farcasterTrending', farcasterTrending); const { mounted } = useListScroll(parentId); useEffect(() => { - if (mounted) { + if (mounted && !trendingCachedData?.data?.length) { loadFarcasterTrending(); } }, [mounted]); @@ -55,18 +65,20 @@ export default function SocialAllTrending() { const key = Buffer.from(data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={farcasterTrendingUserDataObj} - showMenuBtn - cardClickAction={(e) => { + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { setPostScroll({ currentParent: parentId, id: key, top: (e.target as HTMLDivElement).offsetTop, }); + navigate(getExploreFcPostDetailPath(castHex)); }} /> ); diff --git a/apps/u3/src/container/social/SocialAllWhatsnew.tsx b/apps/u3/src/container/social/SocialAllWhatsnew.tsx index 26e38e21..f6e0a85c 100644 --- a/apps/u3/src/container/social/SocialAllWhatsnew.tsx +++ b/apps/u3/src/container/social/SocialAllWhatsnew.tsx @@ -8,7 +8,7 @@ */ import { useEffect, useState } from 'react'; import InfiniteScroll from 'react-infinite-scroll-component'; -import { useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext } from 'react-router-dom'; import Loading from 'src/components/common/loading/Loading'; import FCast from 'src/components/social/farcaster/FCast'; import LensPostCard from 'src/components/social/lens/LensPostCard'; @@ -16,19 +16,28 @@ import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import useAllWhatsnew from 'src/hooks/social/useAllWhatsnew'; import useListScroll from 'src/hooks/social/useListScroll'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { EndMsgContainer, LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; export default function SocialAllWhatsnew() { const [parentId] = useState('social-all-whatsnew'); const { openFarcasterQR } = useFarcasterCtx(); - const { setPostScroll } = useOutletContext<any>(); + const { whatsnewCachedData, setPostScroll } = useOutletContext<any>(); + const navigate = useNavigate(); const { mounted } = useListScroll(parentId); const { loading, loadAllWhatsnew, allWhatsnew, allUserDataObj, pageInfo } = - useAllWhatsnew(); + useAllWhatsnew({ + cachedDataRefValue: whatsnewCachedData, + }); useEffect(() => { - if (mounted) { + if (mounted && !whatsnewCachedData?.data?.length) { loadAllWhatsnew(); } }, [mounted]); @@ -72,18 +81,20 @@ export default function SocialAllWhatsnew() { const key = Buffer.from(data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={allUserDataObj} - showMenuBtn - cardClickAction={(e) => { + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { setPostScroll({ currentParent: parentId, id: key, top: (e.target as HTMLDivElement).offsetTop, }); + navigate(getExploreFcPostDetailPath(castHex)); }} /> ); diff --git a/apps/u3/src/container/social/SocialChannel.tsx b/apps/u3/src/container/social/SocialChannel.tsx index 8bc71f70..35e5454c 100644 --- a/apps/u3/src/container/social/SocialChannel.tsx +++ b/apps/u3/src/container/social/SocialChannel.tsx @@ -5,7 +5,11 @@ import FCast from 'src/components/social/farcaster/FCast'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import Loading from 'src/components/common/loading/Loading'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { LoadingWrapper, LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + LoadingMoreWrapper, + LoadingWrapper, + PostList, +} from '@/components/social/CommonStyles'; export default function SocialChannel() { const { openFarcasterQR } = useFarcasterCtx(); diff --git a/apps/u3/src/container/social/SocialFarcaster.tsx b/apps/u3/src/container/social/SocialFarcaster.tsx index e11a74e6..55f6c784 100644 --- a/apps/u3/src/container/social/SocialFarcaster.tsx +++ b/apps/u3/src/container/social/SocialFarcaster.tsx @@ -4,9 +4,13 @@ import { useEffect, useRef } from 'react'; import { FeedsType } from 'src/components/social/SocialPageNav'; export default function SocialFarcaster() { - const { feedsType, postScroll, setPostScroll } = useOutletContext<any>(); // TODO: any + const { feedsType, postScroll, setPostScroll, postsCachedData } = + useOutletContext<any>(); // TODO: any const currentFeedType = useRef<FeedsType>(); + const trendingCachedData = postsCachedData?.fc?.trending; + const whatsnewCachedData = postsCachedData?.fc?.whatsnew; + const followingCachedData = postsCachedData?.fc?.following; useEffect(() => { if (feedsType === currentFeedType.current) return; @@ -21,6 +25,10 @@ export default function SocialFarcaster() { feedsType, postScroll, setPostScroll, + + trendingCachedData, + whatsnewCachedData, + followingCachedData, }} /> </FarcasterListBox> diff --git a/apps/u3/src/container/social/SocialFarcasterFollowing.tsx b/apps/u3/src/container/social/SocialFarcasterFollowing.tsx index 2ba2808d..aefc0d48 100644 --- a/apps/u3/src/container/social/SocialFarcasterFollowing.tsx +++ b/apps/u3/src/container/social/SocialFarcasterFollowing.tsx @@ -1,5 +1,5 @@ import InfiniteScroll from 'react-infinite-scroll-component'; -import { useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; @@ -11,21 +11,23 @@ import useListScroll from 'src/hooks/social/useListScroll'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; import useFarcasterFollowing from 'src/hooks/social/farcaster/useFarcasterFollowing'; import useLogin from 'src/hooks/shared/useLogin'; +import { MainCenter, NoLoginStyled } from './CommonStyles'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; import { EndMsgContainer, LoadingMoreWrapper, - MainCenter, - NoLoginStyled, PostList, -} from './CommonStyles'; +} from '@/components/social/CommonStyles'; -export default function SocialFarcaster() { +export default function SocialFarcasterFollowing() { const [parentId] = useState('social-farcaster-following'); const { openFarcasterQR } = useFarcasterCtx(); - const { setPostScroll } = useOutletContext<any>(); // TODO: any + const { followingCachedData, setPostScroll } = useOutletContext<any>(); // TODO: any const { mounted } = useListScroll(parentId); const { isConnected: isConnectedFarcaster } = useFarcasterCtx(); const { isLogin } = useLogin(); + const navigate = useNavigate(); const { farcasterFollowing, @@ -33,12 +35,15 @@ export default function SocialFarcaster() { loading: farcasterFollowingLoading, pageInfo: farcasterFollowingPageInfo, farcasterFollowingUserDataObj, - } = useFarcasterFollowing(); + } = useFarcasterFollowing({ + cachedDataRefValue: followingCachedData, + }); useEffect(() => { if (!mounted) return; if (!isLogin) return; if (!isConnectedFarcaster) return; + if (followingCachedData?.data?.length > 0) return; loadFarcasterFollowing(); }, [mounted, isLogin, isConnectedFarcaster]); @@ -77,18 +82,20 @@ export default function SocialFarcaster() { const key = Buffer.from(data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={farcasterFollowingUserDataObj} - showMenuBtn - cardClickAction={(e) => { + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { setPostScroll({ currentParent: parentId, id: key, top: (e.target as HTMLDivElement).offsetTop, }); + navigate(getExploreFcPostDetailPath(castHex)); }} /> ); diff --git a/apps/u3/src/container/social/SocialFarcasterTrending.tsx b/apps/u3/src/container/social/SocialFarcasterTrending.tsx index 92ba8ae9..53ce7884 100644 --- a/apps/u3/src/container/social/SocialFarcasterTrending.tsx +++ b/apps/u3/src/container/social/SocialFarcasterTrending.tsx @@ -7,7 +7,7 @@ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE */ import InfiniteScroll from 'react-infinite-scroll-component'; -import { useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import FCast from 'src/components/social/farcaster/FCast'; @@ -15,13 +15,20 @@ import Loading from 'src/components/common/loading/Loading'; import useListScroll from 'src/hooks/social/useListScroll'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; import useFarcasterTrending from 'src/hooks/social/farcaster/useFarcasterTrending'; -import { EndMsgContainer, LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; export default function SocialFarcasterTrending() { const [parentId] = useState('social-farcaster-trending'); const { openFarcasterQR } = useFarcasterCtx(); - const { setPostScroll } = useOutletContext<any>(); + const { trendingCachedData, setPostScroll } = useOutletContext<any>(); const { mounted } = useListScroll(parentId); + const navigate = useNavigate(); const { loading: farcasterTrendingLoading, @@ -29,10 +36,12 @@ export default function SocialFarcasterTrending() { farcasterTrendingUserDataObj, loadFarcasterTrending, pageInfo: farcasterTrendingPageInfo, - } = useFarcasterTrending(); + } = useFarcasterTrending({ + cachedDataRefValue: trendingCachedData, + }); useEffect(() => { - if (mounted) { + if (mounted && !trendingCachedData?.data?.length) { loadFarcasterTrending(); } }, [mounted]); @@ -61,18 +70,20 @@ export default function SocialFarcasterTrending() { const key = Buffer.from(data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={farcasterTrendingUserDataObj} - showMenuBtn - cardClickAction={(e) => { + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { setPostScroll({ currentParent: parentId, id: key, top: (e.target as HTMLDivElement).offsetTop, }); + navigate(getExploreFcPostDetailPath(castHex)); }} /> ); diff --git a/apps/u3/src/container/social/SocialFarcasterWhatsnew.tsx b/apps/u3/src/container/social/SocialFarcasterWhatsnew.tsx index e68c4069..3bfd77e0 100644 --- a/apps/u3/src/container/social/SocialFarcasterWhatsnew.tsx +++ b/apps/u3/src/container/social/SocialFarcasterWhatsnew.tsx @@ -8,30 +8,40 @@ */ import { useEffect, useState } from 'react'; import InfiniteScroll from 'react-infinite-scroll-component'; -import { useOutletContext } from 'react-router-dom'; +import { useNavigate, useOutletContext } from 'react-router-dom'; import Loading from 'src/components/common/loading/Loading'; import FCast from 'src/components/social/farcaster/FCast'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import useFarcasterWhatsnew from 'src/hooks/social/farcaster/useFarcasterWhatsnew'; import useListScroll from 'src/hooks/social/useListScroll'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { EndMsgContainer, LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; +import { getSocialDetailShareUrlWithFarcaster } from '@/utils/shared/share'; +import { getExploreFcPostDetailPath } from '@/route/path'; export default function SocialFarcasterWhatsnew() { const [parentId] = useState('social-farcaster-whatsnew'); const { openFarcasterQR } = useFarcasterCtx(); - const { setPostScroll } = useOutletContext<any>(); // TODO: any + const { whatsnewCachedData, setPostScroll } = useOutletContext<any>(); // TODO: any const { mounted } = useListScroll(parentId); + const navigate = useNavigate(); + const { loading, pageInfo, loadFarcasterWhatsnew, farcasterWhatsnew, farcasterWhatsnewUserDataObj, - } = useFarcasterWhatsnew(); + } = useFarcasterWhatsnew({ + cachedDataRefValue: whatsnewCachedData, + }); useEffect(() => { - if (mounted) { + if (mounted && !whatsnewCachedData?.data?.length) { loadFarcasterWhatsnew(); } }, [mounted]); @@ -60,18 +70,20 @@ export default function SocialFarcasterWhatsnew() { const key = Buffer.from(data.hash.data).toString('hex'); return ( <FCast + isV2Layout key={key} cast={data} openFarcasterQR={openFarcasterQR} farcasterUserData={{}} farcasterUserDataObj={farcasterWhatsnewUserDataObj} - showMenuBtn - cardClickAction={(e) => { + shareLink={getSocialDetailShareUrlWithFarcaster(key)} + castClickAction={(e, castHex) => { setPostScroll({ currentParent: parentId, id: key, top: (e.target as HTMLDivElement).offsetTop, }); + navigate(getExploreFcPostDetailPath(castHex)); }} /> ); diff --git a/apps/u3/src/container/social/SocialLayout.tsx b/apps/u3/src/container/social/SocialLayout.tsx index b5e52d1d..bed1b754 100644 --- a/apps/u3/src/container/social/SocialLayout.tsx +++ b/apps/u3/src/container/social/SocialLayout.tsx @@ -1,32 +1,29 @@ -import styled from 'styled-components'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { - Outlet, - useLocation, - useParams, - useSearchParams, -} from 'react-router-dom'; -import { isMobile } from 'react-device-detect'; - -import useChannelFeeds from 'src/hooks/social/useChannelFeeds'; -import { resetFarcasterFollowingData } from 'src/hooks/social/farcaster/useFarcasterFollowing'; -import { resetAllFollowingData } from 'src/hooks/social/useAllFollowing'; -import { resetLensFollowingData } from 'src/hooks/social/lens/useLensFollowing'; +import { useEffect, useRef, useState } from 'react'; +import { Outlet, useNavigate } from 'react-router-dom'; -import { MEDIA_BREAK_POINTS } from 'src/constants'; -import AddPostMobile from 'src/components/social/AddPostMobile'; -import SocialPageNav, { - FeedsType, - SocialBackNav, -} from '../../components/social/SocialPageNav'; +import { FeedsType } from '../../components/social/SocialPageNav'; import { SocialPlatform } from '../../services/social/types'; -import SocialPlatformChoice from '../../components/social/SocialPlatformChoice'; -import AddPost from '../../components/social/AddPost'; -import SocialWhoToFollow from '../../components/social/SocialWhoToFollow'; -import TrendChannel from '../../components/social/farcaster/TrendChannel'; import { LivepeerProvider } from '../../contexts/social/LivepeerCtx'; -import { AllFirst } from './SocialAllFollowing'; import { cn } from '@/lib/utils'; +import { ArrowLeft } from '@/components/common/icons/ArrowLeft'; +import NavLinkItem from '@/components/layout/NavLinkItem'; +import useRoute from '@/route/useRoute'; +import { RouteKey } from '@/route/routes'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import PostsMentionedLinks from './PostsMentionedLinks'; +import { getDefaultFarcasterTrendingCachedData } from '@/hooks/social/farcaster/useFarcasterTrending'; +import { getDefaultAllWhatsnewCachedData } from '@/hooks/social/useAllWhatsnew'; +import { getDefaultAllFollowingCachedData } from '@/hooks/social/useAllFollowing'; +import { getDefaultFarcasterWhatsnewCachedData } from '@/hooks/social/farcaster/useFarcasterWhatsnew'; +import { getDefaultFarcasterFollowingCachedData } from '@/hooks/social/farcaster/useFarcasterFollowing'; +import { getDefaultLensTrendingCachedData } from '@/hooks/social/lens/useLensTrending'; +import { getDefaultLensFollowingCachedData } from '@/hooks/social/lens/useLensFollowing'; export default function SocialLayoutContainer() { return ( @@ -35,199 +32,315 @@ export default function SocialLayoutContainer() { </LivepeerProvider> ); } + +const getDefaultPostsCachedData = () => { + return { + all: { + trending: getDefaultFarcasterTrendingCachedData(), + whatsnew: getDefaultAllWhatsnewCachedData(), + following: getDefaultAllFollowingCachedData(), + }, + fc: { + trending: getDefaultFarcasterTrendingCachedData(), + whatsnew: getDefaultFarcasterWhatsnewCachedData(), + following: getDefaultFarcasterFollowingCachedData(), + }, + lens: { + trending: getDefaultLensTrendingCachedData(), + whatsnew: getDefaultLensTrendingCachedData(), + following: getDefaultLensFollowingCachedData(), + }, + }; +}; +const socialPlatformDefault = 'all' as SocialPlatform; function SocialLayout() { - const location = useLocation(); + const { lastRouteMeta } = useRoute(); + const routeKey = lastRouteMeta.key; const [postScroll, setPostScroll] = useState({ currentParent: '', id: '', top: 0, }); - const { channelId } = useParams(); - - const { - feeds: channelFeeds, - channel: currentChannel, - firstLoading: channelFirstLoading, - moreLoading: channelMoreLoading, - loadMoreFeeds: loadChannelMoreFeeds, - pageInfo: channelPageInfo, - farcasterUserDataObj: channelFarcasterUserDataObj, - } = useChannelFeeds(); const [feedsType, setFeedsType] = useState(FeedsType.TRENDING); - const [socialPlatform, setSocialPlatform] = useState<SocialPlatform | ''>(''); + const [socialPlatform, setSocialPlatform] = useState<SocialPlatform>( + socialPlatformDefault + ); - useEffect(() => { - return () => { - resetFarcasterFollowingData(); - resetAllFollowingData(); - AllFirst.done = false; - resetLensFollowingData(); - }; - }, []); + const postsCachedData = useRef({ ...getDefaultPostsCachedData() }).current; - const titleElem = useMemo(() => { - if (location.pathname.includes('social/trends')) { - return <SocialBackNav title="Topics" />; + useEffect(() => { + if ( + [ + RouteKey.socialAllWhatsnew, + RouteKey.socialFarcasterWhatsnew, + RouteKey.socialLensWhatsnew, + ].includes(routeKey) + ) { + setFeedsType(FeedsType.WHATSNEW); + return; } - if (location.pathname.includes('social/channel') && channelId) { - return <SocialBackNav isChannel channelId={channelId} />; + if ( + [ + RouteKey.socialAllFollowing, + RouteKey.socialFarcasterFollowing, + RouteKey.socialLensFollowing, + ].includes(routeKey) + ) { + setFeedsType(FeedsType.FOLLOWING); + return; } - if (location.pathname.includes('post-detail')) { - return <SocialBackNav />; + setFeedsType(FeedsType.TRENDING); + }, [routeKey]); + + useEffect(() => { + if ( + [ + RouteKey.socialFarcasterTrending, + RouteKey.socialFarcasterWhatsnew, + RouteKey.socialFarcasterFollowing, + ].includes(routeKey) + ) { + setSocialPlatform(SocialPlatform.Farcaster); + return; } - if (location.pathname.includes('suggest-follow')) { - return <SocialBackNav title="Suggest Profiles" />; + if ( + [ + RouteKey.socialLensTrending, + RouteKey.socialLensWhatsnew, + RouteKey.socialLensFollowing, + ].includes(routeKey) + ) { + setSocialPlatform(SocialPlatform.Lens); + return; } - return ( - <SocialPageNav - showFeedsTabs - feedsType={feedsType} - onChangeFeedsType={(type) => { - setFeedsType(type); - }} - /> - ); - }, [location.pathname, feedsType, socialPlatform, channelId]); + setSocialPlatform(socialPlatformDefault); + }, [routeKey]); + + const isDetail = + routeKey === RouteKey.socialPostDetailFcast || + routeKey === RouteKey.socialPostDetailLens; return ( - <HomeWrapper id="social-wrapper"> - <HeaderWrapper>{titleElem}</HeaderWrapper> - <MainWrapper id="social-scroll-wrapper"> - {!isMobile && ( - <LeftWrapper> - <SocialPlatformChoice /> - <AddPost /> - </LeftWrapper> + <div className={cn(`w-full h-full flex bg-[#20262F]`)}> + <div id="social-scroll-wrapper" className="flex-1 h-full overflow-auto"> + {isDetail ? ( + <PostDetailHeader /> + ) : ( + <PostListHeader + socialPlatform={socialPlatform} + feedsSort={feedsType} + /> )} + <Outlet + context={{ + socialPlatform, + feedsType, + + postScroll, + setPostScroll, + + postsCachedData, + }} + /> + </div> + <div className="w-[320px] h-full overflow-auto bg-[#1B1E23] max-sm:hidden"> + <PostsMentionedLinks /> + </div> + </div> + ); +} +function PostListHeader({ + feedsSort, + socialPlatform, +}: { + feedsSort: FeedsType; + socialPlatform: SocialPlatform; +}) { + const navigate = useNavigate(); + const getNavUrl = (sort: FeedsType, platform: SocialPlatform) => { + const sortPath = sort === FeedsType.TRENDING ? '' : sort; + switch (platform) { + case SocialPlatform.Farcaster: + return `/social/farcaster/${sortPath}`; + case SocialPlatform.Lens: + return `/social/lens/${sortPath}`; + default: + return `/social/all/${sortPath}`; + } + }; + const sortIsActive = (sort: FeedsType) => { + return sort === feedsSort; + }; + return ( + <div + className={cn( + 'w-full flex p-[20px] justify-between items-center self-stretch sticky top-0 bg-[#20262F] border-b border-[#39424c] z-10', + 'max-sm:px-[10px] max-sm:py-[14px]' + )} + > + <div + className={cn( + 'w-full flex items-center gap-[20px]', + 'max-sm:gap-[10px]' + )} + > + <NavLinkItem + href={getNavUrl(FeedsType.TRENDING, socialPlatform)} + active={sortIsActive(FeedsType.TRENDING)} + className="w-auto max-sm:p-0" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <path + d="M14.3945 8.56641C14.207 8.86914 13.9824 9.16602 13.7227 9.45703C13.5966 9.59862 13.4436 9.71369 13.2726 9.79551C13.1015 9.87733 12.9159 9.92427 12.7266 9.93359C12.5371 9.94453 12.3473 9.9177 12.1682 9.85466C11.9892 9.79163 11.8245 9.69364 11.6836 9.56641C11.5211 9.4204 11.3937 9.23945 11.3112 9.03715C11.2287 8.83485 11.193 8.61648 11.207 8.39844C11.2656 7.47266 10.9649 6.38477 10.3125 5.16211C9.98244 4.54883 9.58791 4.02539 9.1172 3.5918C9.06901 3.90167 8.98983 4.20593 8.88087 4.5C8.61349 5.21581 8.22939 5.88238 7.74416 6.47266C7.40726 6.88586 7.02314 7.25819 6.59962 7.58203C5.93556 8.0918 5.38869 8.75391 5.0215 9.49414C4.64579 10.2461 4.45115 11.0754 4.45314 11.916C4.45314 13.3789 5.02931 14.7539 6.07423 15.791C7.12306 16.8301 8.51564 17.4004 10 17.4004C11.4844 17.4004 12.877 16.8301 13.9258 15.791C14.9707 14.7559 15.5469 13.3789 15.5469 11.916C15.5469 11.1504 15.3887 10.4062 15.0781 9.70703C14.8965 9.29688 14.668 8.91602 14.3945 8.56641Z" + fill={sortIsActive(FeedsType.TRENDING) ? '#FFF' : '#718096'} + /> + <path + d="M16.291 9.16404C15.9118 8.31063 15.3606 7.54466 14.6719 6.91404L14.1035 6.39255C14.0842 6.37533 14.061 6.36311 14.0359 6.35697C14.0107 6.35082 13.9845 6.35093 13.9594 6.35729C13.9344 6.36366 13.9112 6.37608 13.8921 6.39346C13.8729 6.41085 13.8584 6.43267 13.8496 6.45701L13.5957 7.18552C13.4375 7.64255 13.1465 8.10935 12.7344 8.56834C12.707 8.59763 12.6758 8.60545 12.6543 8.6074C12.6328 8.60935 12.5996 8.60545 12.5703 8.5781C12.543 8.55466 12.5293 8.51951 12.5312 8.48435C12.6035 7.30857 12.252 5.9824 11.4824 4.53904C10.8457 3.33982 9.96094 2.40427 8.85547 1.75193L8.04883 1.27732C7.94336 1.21482 7.80859 1.29685 7.81445 1.4199L7.85742 2.3574C7.88672 2.99802 7.8125 3.56443 7.63672 4.03513C7.42187 4.6113 7.11328 5.14646 6.71875 5.62693C6.44418 5.96084 6.13299 6.26287 5.79102 6.52732C4.96739 7.16046 4.29768 7.97172 3.83203 8.90037C3.36753 9.83711 3.12557 10.8685 3.125 11.914C3.125 12.8359 3.30664 13.7285 3.66602 14.5703C4.01302 15.3808 4.51379 16.1163 5.14062 16.7363C5.77344 17.3613 6.50781 17.8535 7.32617 18.1953C8.17383 18.5508 9.07227 18.7304 10 18.7304C10.9277 18.7304 11.8262 18.5508 12.6738 18.1972C13.4902 17.8574 14.2325 17.3619 14.8594 16.7383C15.4922 16.1133 15.9883 15.3828 16.334 14.5722C16.6928 13.7327 16.8769 12.829 16.875 11.916C16.875 10.9629 16.6797 10.0371 16.291 9.16404ZM13.9258 15.791C12.877 16.8301 11.4844 17.4004 10 17.4004C8.51562 17.4004 7.12305 16.8301 6.07422 15.791C5.0293 14.7539 4.45312 13.3789 4.45312 11.916C4.45312 11.0664 4.64453 10.2519 5.02148 9.49412C5.38867 8.75388 5.93555 8.09177 6.59961 7.58201C7.02312 7.25817 7.40725 6.88584 7.74414 6.47263C8.23242 5.87693 8.61523 5.21287 8.88086 4.49998C8.98982 4.20591 9.06899 3.90165 9.11719 3.59177C9.58789 4.02537 9.98242 4.5488 10.3125 5.16209C10.9648 6.38474 11.2656 7.47263 11.207 8.39841C11.193 8.61645 11.2286 8.83483 11.3112 9.03713C11.3937 9.23942 11.5211 9.42038 11.6836 9.56638C11.8245 9.69362 11.9892 9.7916 12.1682 9.85464C12.3473 9.91767 12.5371 9.9445 12.7266 9.93357C13.1113 9.91404 13.4648 9.74412 13.7227 9.45701C13.9824 9.16599 14.207 8.86912 14.3945 8.56638C14.668 8.91599 14.8965 9.29685 15.0781 9.70701C15.3887 10.4062 15.5469 11.1504 15.5469 11.916C15.5469 13.3789 14.9707 14.7558 13.9258 15.791Z" + fill={sortIsActive(FeedsType.TRENDING) ? '#FFF' : '#718096'} + /> + </svg> + Trending + </NavLinkItem> + <div + className={cn('w-px h-[16px] bg-[#39424C] hidden', 'max-sm:block')} + /> + <NavLinkItem + href={getNavUrl(FeedsType.FOLLOWING, socialPlatform)} + active={sortIsActive(FeedsType.FOLLOWING)} + className="w-auto max-sm:p-0" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <path + d="M11.6667 11.8766V13.6183C10.9125 13.3517 10.1053 13.2699 9.31288 13.3798C8.52048 13.4897 7.76604 13.7882 7.1129 14.2502C6.45976 14.7121 5.92699 15.324 5.55934 16.0345C5.19169 16.745 4.99989 17.5333 5.00004 18.3333L3.33337 18.3325C3.33312 17.3149 3.56581 16.3107 4.01362 15.397C4.46144 14.4832 5.11249 13.6841 5.91689 13.0608C6.72129 12.4376 7.65769 12.0068 8.65434 11.8014C9.65098 11.5959 10.6814 11.6214 11.6667 11.8758V11.8766ZM10 10.8333C7.23754 10.8333 5.00004 8.59581 5.00004 5.83331C5.00004 3.07081 7.23754 0.833313 10 0.833313C12.7625 0.833313 15 3.07081 15 5.83331C15 8.59581 12.7625 10.8333 10 10.8333ZM10 9.16665C11.8417 9.16665 13.3334 7.67498 13.3334 5.83331C13.3334 3.99165 11.8417 2.49998 10 2.49998C8.15837 2.49998 6.66671 3.99165 6.66671 5.83331C6.66671 7.67498 8.15837 9.16665 10 9.16665ZM14.8275 16.595L17.7734 13.6491L18.9525 14.8275L14.8275 18.9525L11.8809 16.0058L13.06 14.8275L14.8267 16.595H14.8275Z" + fill={sortIsActive(FeedsType.FOLLOWING) ? '#FFF' : '#718096'} + /> + </svg> + Following + </NavLinkItem> <div - id="main-center" + className={cn('w-px h-[16px] bg-[#39424C] hidden', 'max-sm:block')} + /> + <NavLinkItem + href={getNavUrl(FeedsType.WHATSNEW, socialPlatform)} + active={sortIsActive(FeedsType.WHATSNEW)} + className="w-auto max-sm:p-0" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <g clipPath="url(#clip0_3631_4450)"> + <path + d="M6.58093 9.02782L6.78968 9.80657C6.92031 10.2934 7.06218 10.7459 7.22687 11.1659L7.21249 11.17C6.96281 10.8244 6.65562 10.4369 6.38812 10.1594L5.59749 9.29126L4.55999 9.56938L5.43937 12.8506L6.24249 12.635L6.02624 11.8269C5.89729 11.3377 5.75494 10.8521 5.59937 10.3706L5.61906 10.3653C5.88031 10.7181 6.21343 11.1034 6.48656 11.4009L7.33843 12.3416L8.26343 12.0938L7.38406 8.81251L6.58093 9.02782ZM19.1641 7.54126L16.8459 6.04313L16.7075 3.28688L13.9509 3.14845L12.4528 0.830322L9.99656 2.08876L7.53999 0.830322L6.04187 3.14845L3.28499 3.28688L3.14687 6.04313L0.82843 7.54126L2.08718 9.99751L0.82843 12.4541L3.14687 13.9522L3.28499 16.7084L6.04187 16.8469L7.53999 19.165L9.99624 17.9066L12.4525 19.165L13.9509 16.8469L16.7075 16.7084L16.8459 13.9522L19.1641 12.4541L17.9056 9.99782L19.1641 7.54126ZM17.1291 11.9088L15.3253 13.0744L15.2178 15.2191L13.0731 15.3266L11.9075 17.13L9.99624 16.1509L8.08499 17.13L6.91937 15.3266L4.77499 15.2191L4.66718 13.0744L2.86343 11.9088L3.84249 9.99751L2.86343 8.08657L4.66718 6.92095L4.77499 4.77626L6.91937 4.66876L8.08499 2.86501L9.99624 3.84438L11.9075 2.86501L13.0731 4.66876L15.2175 4.77626L15.3253 6.92095L17.1291 8.08657L16.15 9.99751L17.1291 11.9088ZM9.56031 10.9738L9.39874 10.37L10.5769 10.0544L10.3866 9.34376L9.20843 9.65938L9.06781 9.13376L10.3191 8.79813L10.1256 8.07782L7.99343 8.64907L8.87281 11.9303L11.0728 11.3409L10.88 10.6203L9.56062 10.9738H9.56031ZM13.8556 7.07845L13.9831 8.33314C14.0291 8.73813 14.0737 9.13845 14.1241 9.52157L14.1147 9.52407C13.9753 9.17244 13.8295 8.82342 13.6772 8.4772L13.135 7.27157L12.1956 7.52313L12.3081 8.78157C12.3437 9.20532 12.3809 9.61782 12.4269 10.0025L12.4172 10.005C12.2769 9.67688 12.1025 9.25939 11.9422 8.89563L11.4266 7.72907L10.4819 7.9822L12.1159 11.0613L13.0894 10.8003L12.9931 9.44876C12.9747 9.14595 12.9353 8.82251 12.8853 8.42345L12.8953 8.42095C13.0194 8.74038 13.1528 9.05614 13.2953 9.36782L13.8584 10.5944L14.8175 10.3375L14.7462 6.8397L13.8556 7.07845Z" + fill={sortIsActive(FeedsType.WHATSNEW) ? '#FFF' : '#718096'} + /> + </g> + <defs> + <clipPath id="clip0_3631_4450"> + <rect width="20" height="20" fill="white" /> + </clipPath> + </defs> + </svg> + Newest + </NavLinkItem> + </div> + <Select + onValueChange={(platform) => { + navigate(getNavUrl(feedsSort, platform as SocialPlatform)); + }} + defaultValue={socialPlatform} + > + <SelectTrigger className={cn( - 'w-[600px] mt-[20px] box-border h-[fit-content]', - 'max-sm:w-full' + 'w-[180px] border-none rounded-[10px] bg-[#1B1E23] text-[#FFF] text-[14px] font-medium outline-none focus:outline-none focus:border-none', + 'max-sm:w-auto max-sm:h-auto max-sm:p-0 max-sm:m-0 max-sm:bg-transparent' )} > - <MainOutletWrapper> - <Outlet - context={{ - socialPlatform, - feedsType, - - postScroll, - setPostScroll, - - currentChannel, - channelFeeds, - channelPageInfo, - channelFirstLoading, - channelMoreLoading, - loadChannelMoreFeeds, - channelFarcasterUserDataObj, - }} + <svg + xmlns="http://www.w3.org/2000/svg" + width="20" + height="20" + viewBox="0 0 20 20" + fill="none" + className="max-sm:hidden" + > + <path + d="M5.625 1.875C4.63044 1.875 3.67661 2.27009 2.97335 2.97335C2.27009 3.67661 1.875 4.63044 1.875 5.625C1.875 6.61956 2.27009 7.57339 2.97335 8.27665C3.67661 8.97991 4.63044 9.375 5.625 9.375H8.75C8.91576 9.375 9.07473 9.30915 9.19194 9.19194C9.30915 9.07473 9.375 8.91576 9.375 8.75V5.625C9.375 4.63044 8.97991 3.67661 8.27665 2.97335C7.57339 2.27009 6.61956 1.875 5.625 1.875ZM8.75 10.625H5.625C4.88332 10.625 4.1583 10.8449 3.54161 11.257C2.92493 11.669 2.44428 12.2547 2.16045 12.9399C1.87662 13.6252 1.80236 14.3792 1.94706 15.1066C2.09175 15.834 2.4489 16.5022 2.97335 17.0267C3.4978 17.5511 4.16598 17.9083 4.89341 18.0529C5.62084 18.1976 6.37484 18.1234 7.06006 17.8396C7.74529 17.5557 8.33096 17.0751 8.74301 16.4584C9.15507 15.8417 9.375 15.1167 9.375 14.375V11.25C9.375 11.0842 9.30915 10.9253 9.19194 10.8081C9.07473 10.6908 8.91576 10.625 8.75 10.625ZM14.375 1.875C13.3804 1.875 12.4266 2.27009 11.7234 2.97335C11.0201 3.67661 10.625 4.63044 10.625 5.625V8.75C10.625 8.91576 10.6908 9.07473 10.8081 9.19194C10.9253 9.30915 11.0842 9.375 11.25 9.375H14.375C15.3696 9.375 16.3234 8.97991 17.0267 8.27665C17.7299 7.57339 18.125 6.61956 18.125 5.625C18.125 4.63044 17.7299 3.67661 17.0267 2.97335C16.3234 2.27009 15.3696 1.875 14.375 1.875Z" + fill="#EEEFF7" /> - </MainOutletWrapper> - </div> - - {!isMobile && ( - <RightWrapper> - {/* <SearchInput - placeholder="Search" - onlyOnKeyDown - onSearch={onSearch} - /> */} - <div className="recommend"> - <SocialWhoToFollow /> - <TrendChannel /> - </div> - </RightWrapper> - )} - </MainWrapper> - </HomeWrapper> + <path + opacity="0.3" + d="M14.3751 10.6249H11.2501C11.0843 10.6249 10.9253 10.6907 10.8081 10.8079C10.6909 10.9251 10.6251 11.0841 10.6251 11.2499V14.3749C10.6251 15.1166 10.845 15.8416 11.2571 16.4583C11.6691 17.075 12.2548 17.5556 12.94 17.8394C13.6252 18.1233 14.3792 18.1975 15.1067 18.0528C15.8341 17.9081 16.5023 17.551 17.0267 17.0265C17.5512 16.5021 17.9083 15.8339 18.053 15.1065C18.1977 14.379 18.1235 13.625 17.8396 12.9398C17.5558 12.2546 17.0751 11.6689 16.4585 11.2569C15.8418 10.8448 15.1168 10.6249 14.3751 10.6249Z" + fill="#EEEFF7" + /> + </svg> + <SelectValue /> + </SelectTrigger> + <SelectContent className="rounded-[10px] bg-[#1B1E23] text-[#FFF] text-[14px] font-medium border-none"> + <SelectItem + value={socialPlatformDefault} + className="hover:bg-[#20262F]" + > + All Platform + </SelectItem> + <SelectItem + value={SocialPlatform.Farcaster} + className="hover:bg-[#20262F]" + > + Farcaster + </SelectItem> + <SelectItem + value={SocialPlatform.Lens} + className="hover:bg-[#20262F]" + > + Lens + </SelectItem> + </SelectContent> + </Select> + </div> ); } -const HomeWrapper = styled.div` - width: 100%; - height: 100%; - padding: 0px 24px; - box-sizing: border-box; - position: relative; - ${isMobile && - ` - height: 100vh; - padding: 10px; - padding-bottom: 60px; - `} - - .ka-wrapper { - width: 100%; - height: calc(100% - 96px); - } - .ka-content { - width: 100%; - height: 100%; - } -`; -const MainWrapper = styled.div` - width: 100%; - height: calc(100vh - 70px); - overflow: scroll; - width: 100%; - display: flex; - justify-content: center; - gap: 40px; - padding-bottom: 24px; - box-sizing: border-box; -`; -export const HeaderWrapper = styled.div` - @media (max-width: ${MEDIA_BREAK_POINTS.xxxl}px) { - width: 100%; - } - @media (min-width: ${MEDIA_BREAK_POINTS.xxxl}px) { - width: calc(${MEDIA_BREAK_POINTS.xxxl}px - 60px - 40px); - } - /* height: 100%; */ - margin: 0 auto; -`; - -const MainLeft = styled.div` - width: 302px; -`; -const MainRight = styled.div` - width: 350px; -`; -const MainOutletWrapper = styled.div` - width: 100%; - max-width: 600px; - margin: 0 auto; -`; - -const LeftWrapper = styled(MainLeft)` - display: flex; - flex-direction: column; - gap: 20px; - position: sticky; - top: 0px; - padding: 24px 0; - height: 100%; - overflow: scroll; - box-sizing: border-box; -`; - -const RightWrapper = styled(MainRight)` - display: flex; - flex-direction: column; - gap: 20px; - position: sticky; - top: 0px; - padding: 24px 0; - overflow: scroll; - height: 100%; - box-sizing: border-box; - > .recommend { - display: flex; - flex-direction: column; - gap: 20px; - } -`; +function PostDetailHeader() { + const navigate = useNavigate(); + return ( + <div + className={cn( + 'w-full z-10 flex p-[20px] justify-between items-start self-stretch bg-[#20262F] border-b border-[#39424c]', + 'max-sm:p-[10px] max-sm:hidden' + )} + > + <div> + <button + type="button" + className="rounded-[50%] w-[40px] h-[40px] flex justify-center items-center flex-shrink-0 border-[1px] border-solid border-[#39424c] bg-[var(--neutral-100,_#1a1e23)] cursor-pointer" + onClick={() => { + navigate(-1); + }} + > + <ArrowLeft /> + </button> + </div> + </div> + ); +} diff --git a/apps/u3/src/container/social/SocialLens.tsx b/apps/u3/src/container/social/SocialLens.tsx index 7ccecf01..9cb52707 100644 --- a/apps/u3/src/container/social/SocialLens.tsx +++ b/apps/u3/src/container/social/SocialLens.tsx @@ -14,7 +14,12 @@ import { LensListBox } from './CommonStyles'; export default function SocialFarcaster() { const currentFeedType = useRef<FeedsType>(); - const { feedsType, postScroll, setPostScroll } = useOutletContext<any>(); // TODO: any + const { feedsType, postScroll, setPostScroll, postsCachedData } = + useOutletContext<any>(); // TODO: any + + const trendingCachedData = postsCachedData?.lens?.trending; + const whatsnewCachedData = postsCachedData?.lens?.whatsnew; + const followingCachedData = postsCachedData?.lens?.following; useEffect(() => { if (feedsType === currentFeedType.current) return; @@ -29,6 +34,10 @@ export default function SocialFarcaster() { postScroll, setPostScroll, feedsType, + + trendingCachedData, + whatsnewCachedData, + followingCachedData, }} /> </LensListBox> diff --git a/apps/u3/src/container/social/SocialLensFollowing.tsx b/apps/u3/src/container/social/SocialLensFollowing.tsx index 9c95f015..3195ec5e 100644 --- a/apps/u3/src/container/social/SocialLensFollowing.tsx +++ b/apps/u3/src/container/social/SocialLensFollowing.tsx @@ -17,29 +17,31 @@ import LensPostCard from 'src/components/social/lens/LensPostCard'; import useLensFollowing from 'src/hooks/social/lens/useLensFollowing'; import FollowingDefault from 'src/components/social/FollowingDefault'; import { useLensCtx } from '../../contexts/social/AppLensCtx'; +import { MainCenter, NoLoginStyled } from './CommonStyles'; import { - MainCenter, - NoLoginStyled, + EndMsgContainer, LoadingMoreWrapper, PostList, - EndMsgContainer, -} from './CommonStyles'; +} from '@/components/social/CommonStyles'; export default function SocialLensFollowing() { const [parentId] = useState('social-lens-following'); - const { setPostScroll } = useOutletContext<any>(); // TODO: any + const { followingCachedData, setPostScroll } = useOutletContext<any>(); // TODO: any const { mounted } = useListScroll(parentId); const { isLogin } = useLogin(); const { sessionProfile } = useLensCtx(); const { id: lensSessionProfileId } = sessionProfile || {}; const { lensFollowing, loadLensFollowing, loading, pageInfo } = - useLensFollowing(); + useLensFollowing({ + cachedDataRefValue: followingCachedData, + }); useEffect(() => { if (!mounted) return; if (!isLogin) return; if (!lensSessionProfileId) return; + if (followingCachedData?.data?.length > 0) return; loadLensFollowing(); }, [mounted, isLogin, lensSessionProfileId]); diff --git a/apps/u3/src/container/social/SocialLensTrending.tsx b/apps/u3/src/container/social/SocialLensTrending.tsx index a1c15020..9e9016dc 100644 --- a/apps/u3/src/container/social/SocialLensTrending.tsx +++ b/apps/u3/src/container/social/SocialLensTrending.tsx @@ -14,19 +14,26 @@ import useListScroll from 'src/hooks/social/useListScroll'; import LensPostCard from 'src/components/social/lens/LensPostCard'; import Loading from 'src/components/common/loading/Loading'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; export default function SocialLensTrending() { const [parentId] = useState('social-lens-trending'); - const { setPostScroll } = useOutletContext<any>(); // TODO: any + const { trendingCachedData, setPostScroll } = useOutletContext<any>(); // TODO: any const { mounted } = useListScroll(parentId); - const { loadLensTrending, loading, lensTrending, pageInfo } = - useLensTrending(); + const { loadLensTrending, loading, lensTrending, pageInfo } = useLensTrending( + { + cachedDataRefValue: trendingCachedData, + } + ); useEffect(() => { - if (mounted) { + if (mounted && !trendingCachedData?.data?.length) { loadLensTrending(); } }, [mounted]); diff --git a/apps/u3/src/container/social/SocialLensWhatsnew.tsx b/apps/u3/src/container/social/SocialLensWhatsnew.tsx index 0cd4469b..f686a712 100644 --- a/apps/u3/src/container/social/SocialLensWhatsnew.tsx +++ b/apps/u3/src/container/social/SocialLensWhatsnew.tsx @@ -6,19 +6,26 @@ import useListScroll from 'src/hooks/social/useListScroll'; import LensPostCard from 'src/components/social/lens/LensPostCard'; import Loading from 'src/components/common/loading/Loading'; import { FEEDS_SCROLL_THRESHOLD } from 'src/services/social/api/feeds'; -import { EndMsgContainer, LoadingMoreWrapper, PostList } from './CommonStyles'; +import { + EndMsgContainer, + LoadingMoreWrapper, + PostList, +} from '@/components/social/CommonStyles'; export default function SocialLensWhatsnew() { const [parentId] = useState('social-lens-whatsnew'); - const { setPostScroll } = useOutletContext<any>(); // TODO: any + const { whatsnewCachedData, setPostScroll } = useOutletContext<any>(); // TODO: any const { mounted } = useListScroll(parentId); - const { loadLensTrending, loading, lensTrending, pageInfo } = - useLensTrending(); + const { loadLensTrending, loading, lensTrending, pageInfo } = useLensTrending( + { + cachedDataRefValue: whatsnewCachedData, + } + ); useEffect(() => { - if (mounted) { + if (mounted && !whatsnewCachedData?.data?.length) { loadLensTrending(); } }, [mounted]); diff --git a/apps/u3/src/container/social/SocialSuggestFollow.tsx b/apps/u3/src/container/social/SocialSuggestFollow.tsx index d855fcc0..e94d4272 100644 --- a/apps/u3/src/container/social/SocialSuggestFollow.tsx +++ b/apps/u3/src/container/social/SocialSuggestFollow.tsx @@ -22,7 +22,7 @@ import { farcasterHandleToBioLinkHandle, lensHandleToBioLinkHandle, } from '../../utils/profile/biolink'; -import TooltipProfileNavigateLink from '../../components/profile/profile-info/TooltipProfileNavigateLink'; +import TooltipProfileNavigateLink from '../../components/profile/info/TooltipProfileNavigateLink'; import { getBio, getHandle, getName } from '../../utils/social/lens/profile'; const SUGGEST_NUM = 20; diff --git a/apps/u3/src/contexts/NavCtx.tsx b/apps/u3/src/contexts/NavCtx.tsx deleted file mode 100644 index 701cdebd..00000000 --- a/apps/u3/src/contexts/NavCtx.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-disable no-plusplus */ -import { - createContext, - useContext, - useMemo, - useState, - ReactNode, - useRef, - useCallback, - useEffect, -} from 'react'; -import styled from 'styled-components'; -import { useXmtpClient } from './message/XmtpClientCtx'; - -export enum NavModalName { - Notification = 'Notification', - Message = 'Message', - ContactUs = 'ContactUs', -} -interface NavCtxValue { - openNotificationModal: boolean; - setOpenNotificationModal: React.Dispatch<React.SetStateAction<boolean>>; - openMessageModal: boolean; - setOpenMessageModal: React.Dispatch<React.SetStateAction<boolean>>; - openContactUsModal: boolean; - setOpenContactUsModal: React.Dispatch<React.SetStateAction<boolean>>; - isOpen: boolean; - setIsOpen: React.Dispatch<React.SetStateAction<boolean>>; - renderNavItemText; - switchNavModal: (name: string) => void; -} - -const defaultContextValue: NavCtxValue = { - openNotificationModal: false, - setOpenNotificationModal: () => {}, - openMessageModal: false, - setOpenMessageModal: () => {}, - openContactUsModal: false, - setOpenContactUsModal: () => {}, - isOpen: false, - setIsOpen: () => {}, - renderNavItemText: () => {}, - switchNavModal: () => {}, -}; - -export interface NavCtxProviderProps { - children: ReactNode; -} -export const NavCtx = createContext(defaultContextValue); -export function NavProvider({ children }: NavCtxProviderProps) { - const [openNotificationModal, setOpenNotificationModal] = useState( - defaultContextValue.openNotificationModal - ); - const [openMessageModal, setOpenMessageModal] = useState( - defaultContextValue.openMessageModal - ); - - const [openContactUsModal, setOpenContactUsModal] = useState( - defaultContextValue.openContactUsModal - ); - - const switchNavModal = useCallback( - (name: string) => { - if (name !== NavModalName.Notification) setOpenNotificationModal(false); - if (name !== NavModalName.Message) setOpenMessageModal(false); - if (name !== NavModalName.ContactUs) setOpenContactUsModal(false); - switch (name) { - case NavModalName.Notification: - setOpenNotificationModal((pre) => !pre); - break; - case NavModalName.Message: - setOpenMessageModal((pre) => !pre); - break; - case NavModalName.ContactUs: - setOpenContactUsModal((pre) => !pre); - break; - default: - break; - } - }, - [setOpenNotificationModal, setOpenMessageModal, setOpenContactUsModal] - ); - - const { setCanEnableXmtp } = useXmtpClient(); - useEffect(() => { - if (openMessageModal) { - setCanEnableXmtp(true); - } - }, [openMessageModal]); - const [isOpen, setIsOpen] = useState(defaultContextValue.isOpen); - - const navItemTextInnerEls = useRef(new Map()); - const renderNavItemText = useCallback( - (text: string) => { - if (navItemTextInnerEls.current.has(text)) { - const innerEl = navItemTextInnerEls.current.get(text); - innerEl.parentElement.style.width = `${innerEl.scrollWidth}px`; - } - return ( - <PcNavItemTextBox> - <PcNavItemTextInner - ref={(el) => { - if (el) { - navItemTextInnerEls.current.set(text, el); - } - }} - > - {text} - </PcNavItemTextInner> - </PcNavItemTextBox> - ); - }, - [isOpen] - ); - return ( - <NavCtx.Provider - value={useMemo( - () => ({ - openNotificationModal, - setOpenNotificationModal, - openMessageModal, - setOpenMessageModal, - openContactUsModal, - setOpenContactUsModal, - switchNavModal, - isOpen, - setIsOpen, - renderNavItemText, - }), - [ - openNotificationModal, - setOpenNotificationModal, - openMessageModal, - setOpenMessageModal, - openContactUsModal, - setOpenContactUsModal, - switchNavModal, - isOpen, - setIsOpen, - renderNavItemText, - ] - )} - > - {children} - </NavCtx.Provider> - ); -} - -export const useNav = () => { - const ctx = useContext(NavCtx); - if (!ctx) { - throw new Error('useNav must be used within Nav'); - } - return ctx; -}; - -export const PcNavItemTextBox = styled.div` - overflow: hidden; - transition: all 0.5s ease-out; -`; -export const PcNavItemTextInner = styled.div` - white-space: nowrap; -`; diff --git a/apps/u3/src/contexts/message/XmtpClientCtx.tsx b/apps/u3/src/contexts/message/XmtpClientCtx.tsx index 4bd52cc8..8f5f708d 100644 --- a/apps/u3/src/contexts/message/XmtpClientCtx.tsx +++ b/apps/u3/src/contexts/message/XmtpClientCtx.tsx @@ -13,14 +13,13 @@ import { AttachmentCodec, RemoteAttachmentCodec, } from '@xmtp/content-type-remote-attachment'; -import { isMobile } from 'react-device-detect'; import { useConnectModal } from '@rainbow-me/rainbowkit'; import { loadKeys, storeKeys } from '../../utils/message/xmtp'; import { XMTP_ENV } from '../../constants/xmtp'; export enum MessageRoute { - SEARCH = 'search', - DETAIL = 'detail', + HOME = 'home', + PRIVATE_CHAT = 'private-chat', } type MessageRouteParams = { @@ -50,7 +49,7 @@ const defaultContextValue: XmtpClientCtxValue = { disconnectXmtp: () => {}, canEnableXmtp: false, setCanEnableXmtp: () => {}, - messageRouteParams: { route: MessageRoute.SEARCH }, + messageRouteParams: { route: MessageRoute.HOME }, setMessageRouteParams: () => {}, }; @@ -64,7 +63,7 @@ export function XmtpClientProvider({ children }: PropsWithChildren) { const { isConnected } = useAccount(); const { openConnectModal } = useConnectModal(); const [messageRouteParams, setMessageRouteParams] = - useState<MessageRouteParams>({ route: MessageRoute.SEARCH }); + useState<MessageRouteParams>({ route: MessageRoute.HOME }); /** * // TODO wagmi 的 wallet对象中getAddress, signMessage方法不符合xmtp-js的Signer定义要求,这里是临时方案 @@ -133,7 +132,6 @@ export function XmtpClientProvider({ children }: PropsWithChildren) { }, []); useEffect(() => { - if (isMobile) return; if (canEnableXmtp) { if (signer) { enableXmtpWithSigner(signer); @@ -141,7 +139,7 @@ export function XmtpClientProvider({ children }: PropsWithChildren) { } } setXmtpClient(null); - }, [signer, enableXmtpWithSigner, isMobile, canEnableXmtp]); + }, [signer, enableXmtpWithSigner, canEnableXmtp]); return ( <XmtpClientCtx.Provider diff --git a/apps/u3/src/contexts/notification/NotificationStoreCtx.tsx b/apps/u3/src/contexts/notification/NotificationStoreCtx.tsx index ab451d4b..f7bcbb95 100644 --- a/apps/u3/src/contexts/notification/NotificationStoreCtx.tsx +++ b/apps/u3/src/contexts/notification/NotificationStoreCtx.tsx @@ -18,6 +18,7 @@ import { import useFarcasterNotifications from '../../hooks/social/farcaster/useFarcasterNotifications'; import useUnreadFarcasterNotificationsCount from '../../hooks/social/farcaster/useUnreadFarcasterNotificationsCount'; import { FarcasterNotification } from '../../services/social/api/farcaster'; +import { NotificationType } from '@/services/notification/types/notifications'; interface NotificationStoreCtxValue { notifications: (LensNotification | FarcasterNotification)[] | undefined; @@ -39,7 +40,7 @@ const defaultContextValue: NotificationStoreCtxValue = { loadMore: () => {}, }; -const DEFAULT_PAGE_SIZE = 10; +const DEFAULT_PAGE_SIZE = 20; export const NotificationStoreCtx = createContext(defaultContextValue); // const mergeNotifications = ( @@ -138,6 +139,7 @@ const mergeNotifications = ( export type NotificationConfig = { fid: number; + type?: NotificationType[]; pageSize?: number; }; @@ -181,6 +183,7 @@ export function NotificationStoreProvider({ farcasterUserData, } = useFarcasterNotifications( config.fid, + config.type || [], config.pageSize || DEFAULT_PAGE_SIZE ); diff --git a/apps/u3/src/contexts/profile/ProfileInfoCtx.tsx b/apps/u3/src/contexts/profile/ProfileInfoCtx.tsx new file mode 100644 index 00000000..fac69276 --- /dev/null +++ b/apps/u3/src/contexts/profile/ProfileInfoCtx.tsx @@ -0,0 +1,104 @@ +import { + PropsWithChildren, + createContext, + useCallback, + useContext, + useState, +} from 'react'; + +interface ProfileInfoContextValue { + loadingIdentities: Set<string>; + cachedProfileInfo: Map<string, any>; + isLoadingIdentity: (identity: string) => boolean; + addLoadingIdentity: (identity: string) => void; + removeLoadingIdentity: (identity: string) => void; + isCachedProfileInfo: (identity: string) => boolean; + getCachedProfileInfoWithIdentity: (identity: string) => any; + upsertCachedProfileInfoWithIdentity: (identity: string, info: any) => void; +} +const ctxDefaultValue: ProfileInfoContextValue = { + loadingIdentities: new Set(), + cachedProfileInfo: new Map(), + isLoadingIdentity: () => false, + addLoadingIdentity: () => {}, + removeLoadingIdentity: () => {}, + isCachedProfileInfo: () => false, + getCachedProfileInfoWithIdentity: () => null, + upsertCachedProfileInfoWithIdentity: () => {}, +}; +export const ProfileInfoContext = createContext(ctxDefaultValue); + +export function ProfileInfoProvider({ children }: PropsWithChildren) { + const [loadingIdentities, setLoadingIdentities] = useState( + ctxDefaultValue.loadingIdentities + ); + const [cachedProfileInfo, setCachedProfileInfo] = useState( + ctxDefaultValue.cachedProfileInfo + ); + const isLoadingIdentity = useCallback( + (identity: string) => loadingIdentities.has(identity), + [loadingIdentities] + ); + const addLoadingIdentity = useCallback((identity: string) => { + setLoadingIdentities((prev) => { + const newSet = new Set(prev); + newSet.add(identity); + return newSet; + }); + }, []); + const removeLoadingIdentity = useCallback((identity: string) => { + setLoadingIdentities((prev) => { + const newSet = new Set(prev); + newSet.delete(identity); + return newSet; + }); + }, []); + + const isCachedProfileInfo = useCallback( + (identity: string) => + cachedProfileInfo.has(identity) && !!cachedProfileInfo.get(identity), + [cachedProfileInfo] + ); + const upsertCachedProfileInfoWithIdentity = useCallback( + (identity: string, data: any) => { + setCachedProfileInfo((prev) => { + const newMap = new Map(prev); + newMap.set(identity, data); + return newMap; + }); + }, + [] + ); + const getCachedProfileInfoWithIdentity = useCallback( + (identity: string) => cachedProfileInfo.get(identity), + [cachedProfileInfo] + ); + + return ( + <ProfileInfoContext.Provider + // eslint-disable-next-line react/jsx-no-constructed-context-values + value={{ + loadingIdentities, + cachedProfileInfo, + isLoadingIdentity, + addLoadingIdentity, + removeLoadingIdentity, + isCachedProfileInfo, + upsertCachedProfileInfoWithIdentity, + getCachedProfileInfoWithIdentity, + }} + > + {children} + </ProfileInfoContext.Provider> + ); +} + +export function useProfileInfoCtx() { + const context = useContext(ProfileInfoContext); + if (!context) { + throw Error( + 'useProfileInfoCtx can only be used within the ProfileInfoProvider component' + ); + } + return context; +} diff --git a/apps/u3/src/contexts/social/FarcasterCtx.tsx b/apps/u3/src/contexts/social/FarcasterCtx.tsx index 13c435a4..de7bcac4 100644 --- a/apps/u3/src/contexts/social/FarcasterCtx.tsx +++ b/apps/u3/src/contexts/social/FarcasterCtx.tsx @@ -1,6 +1,8 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable @typescript-eslint/no-shadow */ import { NobleEd25519Signer, UserDataType } from '@farcaster/hub-web'; +import { useSession } from '@us3r-network/auth-with-rainbowkit'; +import { useProfileState } from '@us3r-network/profile'; import { ReactNode, createContext, @@ -10,45 +12,41 @@ import { useMemo, useState, } from 'react'; -import { useNavigate } from 'react-router-dom'; import { useHotkeys } from 'react-hotkeys-hook'; -import { useSession } from '@us3r-network/auth-with-rainbowkit'; -import { useProfileState } from '@us3r-network/profile'; - -import useLogin from 'src/hooks/shared/useLogin'; -import FarcasterSignerSelectModal from 'src/components/social/farcaster/FarcasterSignerSelectModal'; -import useFarcasterWallet from 'src/hooks/social/farcaster/useFarcasterWallet'; -import useFarcasterQR from 'src/hooks/social/farcaster/useFarcasterQR'; -import useFarcasterTrendChannel from 'src/hooks/social/farcaster/useFarcasterTrendChannel'; -import useFarcasterFollowData from 'src/hooks/social/farcaster/useFarcasterFollowData'; -import useFarcasterChannel, { - FarcasterChannel, -} from 'src/hooks/social/farcaster/useFarcasterChannel'; - -import { - getDefaultFarcaster, - setDefaultFarcaster, -} from 'src/utils/social/farcaster/farcaster-default'; - -import { getPrivateKey } from '../../utils/social/farcaster/farsign-utils'; -import FarcasterQRModal from '../../components/social/farcaster/FarcasterQRModal'; -import useBioLinkActions from '../../hooks/profile/useBioLinkActions'; +import { useNavigate } from 'react-router-dom'; +import { getPrivateKey } from '@/utils/social/farcaster/farsign-utils'; import { BIOLINK_FARCASTER_NETWORK, BIOLINK_PLATFORMS, farcasterHandleToBioLinkHandle, -} from '../../utils/profile/biolink'; -import { - UserData, - userDataObjFromArr, -} from '@/utils/social/farcaster/user-data'; -import usePinupHashes from '@/hooks/social/farcaster/usePinupHashes'; +} from '@/utils/profile/biolink'; +import useBioLinkActions from '@/hooks/profile/useBioLinkActions'; +import FarcasterQRModal from '@/components/social/farcaster/FarcasterQRModal'; import AddPostModal from '@/components/social/AddPostModal'; import QuickSearchModal, { QuickSearchModalName, } from '@/components/social/QuickSearchModal'; import ClaimNotice from '@/components/social/farcaster/ClaimNotice'; +import FarcasterSignerSelectModal from '@/components/social/farcaster/FarcasterSignerSelectModal'; +import { FollowType } from '@/container/profile/Contacts'; +import useLogin from '@/hooks/shared/useLogin'; +import useFarcasterChannel, { + FarcasterChannel, +} from '@/hooks/social/farcaster/useFarcasterChannel'; import useFarcasterClaim from '@/hooks/social/farcaster/useFarcasterClaim'; +import useFarcasterLinks from '@/hooks/social/farcaster/useFarcasterLinks'; +import useFarcasterQR from '@/hooks/social/farcaster/useFarcasterQR'; +import useFarcasterTrendChannel from '@/hooks/social/farcaster/useFarcasterTrendChannel'; +import useFarcasterWallet from '@/hooks/social/farcaster/useFarcasterWallet'; +import usePinupHashes from '@/hooks/social/farcaster/usePinupHashes'; +import { + getDefaultFarcaster, + setDefaultFarcaster, +} from '@/utils/social/farcaster/farcaster-default'; +import { + UserData, + userDataObjFromArr, +} from '@/utils/social/farcaster/user-data'; export type Token = { token: string; @@ -159,13 +157,18 @@ export default function FarcasterProvider({ const [currFid, setCurrFid] = useState<number>(); const [signerSelectModalOpen, setSignerSelectModalOpen] = useState(false); const [following, setFollowing] = useState<string[]>([]); - const { farcasterFollowData } = useFarcasterFollowData({ + const { links: farcasterFollowingLinks } = useFarcasterLinks({ fid: currFid, + pageSize: 999, + type: FollowType.FOLLOWING, }); const { claimStatus, setClaimStatus } = useFarcasterClaim({ currFid }); useEffect(() => { - setFollowing(farcasterFollowData?.followingData || []); - }, [farcasterFollowData]); + const following = farcasterFollowingLinks?.map((item) => + String(item.targetFid) + ); + setFollowing(following || []); + }, [farcasterFollowingLinks]); const { userChannels, getUserChannels, diff --git a/apps/u3/src/features/community/communitySlice.ts b/apps/u3/src/features/community/communitySlice.ts new file mode 100644 index 00000000..f289db9b --- /dev/null +++ b/apps/u3/src/features/community/communitySlice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from '../../store/store'; +import { CommunityInfo } from '@/services/community/types/community'; + +type CommunityState = { + browsingCommunity: CommunityInfo | null; +}; + +const communityState: CommunityState = { + browsingCommunity: null, +}; + +export const communitySlice = createSlice({ + name: 'community', + initialState: communityState, + reducers: { + setBrowsingCommunity: ( + state: CommunityState, + action: PayloadAction<CommunityInfo> + ) => { + state.browsingCommunity = action.payload; + }, + clearBrowsingCommunity: (state: CommunityState) => { + state.browsingCommunity = null; + }, + }, +}); + +const { actions, reducer } = communitySlice; +export const { setBrowsingCommunity, clearBrowsingCommunity } = actions; +export const selectCommunity = (state: RootState) => state.community; +export default reducer; diff --git a/apps/u3/src/features/community/joinCommunitySlice.ts b/apps/u3/src/features/community/joinCommunitySlice.ts new file mode 100644 index 00000000..ed6538d0 --- /dev/null +++ b/apps/u3/src/features/community/joinCommunitySlice.ts @@ -0,0 +1,74 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from '../../store/store'; +import { JoinedCommunitiesData } from '@/services/community/api/community'; + +type JoinCommunityState = { + joinedCommunities: JoinedCommunitiesData; + joinedCommunitiesPending: boolean; + joinActionPendingIds: Array<string | number>; +}; + +const joinCommunityState: JoinCommunityState = { + joinedCommunities: [], + joinedCommunitiesPending: false, + joinActionPendingIds: [], +}; + +export const joinCommunitySlice = createSlice({ + name: 'joinCommunity', + initialState: joinCommunityState, + reducers: { + setJoinedCommunitiesPending: ( + state: JoinCommunityState, + action: PayloadAction<boolean> + ) => { + state.joinedCommunitiesPending = action.payload; + }, + setJoinedCommunities: ( + state: JoinCommunityState, + action: PayloadAction<JoinedCommunitiesData> + ) => { + state.joinedCommunities = action.payload; + }, + addOneToJoinedCommunities: ( + state: JoinCommunityState, + action: PayloadAction<JoinedCommunitiesData[0]> + ) => { + state.joinedCommunities.unshift(action.payload); + }, + removeOneFromJoinedCommunities: ( + state: JoinCommunityState, + action: PayloadAction<string | number> + ) => { + state.joinedCommunities = state.joinedCommunities.filter( + (item) => item.id !== action.payload + ); + }, + addOneToJoinActionPendingIds: ( + state: JoinCommunityState, + action: PayloadAction<string | number> + ) => { + state.joinActionPendingIds.unshift(action.payload); + }, + removeOneFromJoinActionPendingIds: ( + state: JoinCommunityState, + action: PayloadAction<string | number> + ) => { + state.joinActionPendingIds = state.joinActionPendingIds.filter( + (id) => id !== action.payload + ); + }, + }, +}); + +const { actions, reducer } = joinCommunitySlice; +export const { + setJoinedCommunitiesPending, + setJoinedCommunities, + addOneToJoinedCommunities, + removeOneFromJoinedCommunities, + addOneToJoinActionPendingIds, + removeOneFromJoinActionPendingIds, +} = actions; +export const selectJoinCommunity = (state: RootState) => state.joinCommunity; +export default reducer; diff --git a/apps/u3/src/features/shared/websiteSlice.ts b/apps/u3/src/features/shared/websiteSlice.ts index 59ff38e2..0566e476 100644 --- a/apps/u3/src/features/shared/websiteSlice.ts +++ b/apps/u3/src/features/shared/websiteSlice.ts @@ -15,7 +15,13 @@ import { setHomeBannerHiddenToStore, verifyHomeBannerHiddenByStore, } from '../../utils/shared/homeStore'; -import { FeedsType } from '../../components/profile/ProfilePageNav'; + +enum FeedsType { + POSTS = 'posts', + REPOSTS = 'reposts', + REPLIES = 'replies', + LIKES = 'likes', +} type WebsiteState = { mobileNavDisplay: boolean; diff --git a/apps/u3/src/hooks/community/useAllJoinedCommunities.ts b/apps/u3/src/hooks/community/useAllJoinedCommunities.ts new file mode 100644 index 00000000..050e252b --- /dev/null +++ b/apps/u3/src/hooks/community/useAllJoinedCommunities.ts @@ -0,0 +1,47 @@ +import { useCallback } from 'react'; +import { toast } from 'react-toastify'; +import { fetchJoinedCommunities } from '@/services/community/api/community'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { + selectJoinCommunity, + setJoinedCommunities, + setJoinedCommunitiesPending, +} from '@/features/community/joinCommunitySlice'; + +export default function useAllJoinedCommunities() { + const dispatch = useAppDispatch(); + const { joinedCommunities, joinedCommunitiesPending } = + useAppSelector(selectJoinCommunity); + + const loadAllJoinedCommunities = useCallback(async () => { + try { + dispatch(setJoinedCommunitiesPending(true)); + const res = await fetchJoinedCommunities({ + pageSize: 1000, // TODO 用的分页列表接口,默认取1000个作为所有列表 + pageNumber: 1, + }); + const { code, msg, data } = res.data; + if (code === 0) { + dispatch(setJoinedCommunities(data)); + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Load joined communities failed: ${error.message}`); + } finally { + dispatch(setJoinedCommunitiesPending(false)); + } + }, [dispatch]); + + const clearJoinedCommunities = useCallback(() => { + dispatch(setJoinedCommunities([])); + }, [dispatch]); + + return { + joinedCommunities, + joinedCommunitiesPending, + loadAllJoinedCommunities, + clearJoinedCommunities, + }; +} diff --git a/apps/u3/src/hooks/community/useBrowsingCommunity.ts b/apps/u3/src/hooks/community/useBrowsingCommunity.ts new file mode 100644 index 00000000..6b9ba00e --- /dev/null +++ b/apps/u3/src/hooks/community/useBrowsingCommunity.ts @@ -0,0 +1,30 @@ +import { useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import { + setBrowsingCommunity, + clearBrowsingCommunity, + selectCommunity, +} from '@/features/community/communitySlice'; +import { CommunityInfo } from '@/services/community/types/community'; + +export default function useBrowsingCommunity() { + const dispatch = useAppDispatch(); + const { browsingCommunity } = useAppSelector(selectCommunity); + + const setBrowsingCommunityAction = useCallback( + (communityInfo: CommunityInfo) => { + dispatch(setBrowsingCommunity(communityInfo)); + }, + [dispatch] + ); + + const clearBrowsingCommunityAction = useCallback(() => { + dispatch(clearBrowsingCommunity()); + }, [dispatch]); + + return { + browsingCommunity, + setBrowsingCommunity: setBrowsingCommunityAction, + clearBrowsingCommunity: clearBrowsingCommunityAction, + }; +} diff --git a/apps/u3/src/hooks/community/useJoinCommunityAction.ts b/apps/u3/src/hooks/community/useJoinCommunityAction.ts new file mode 100644 index 00000000..3b3f116b --- /dev/null +++ b/apps/u3/src/hooks/community/useJoinCommunityAction.ts @@ -0,0 +1,103 @@ +import { useCallback, useMemo } from 'react'; +import { toast } from 'react-toastify'; +import { + fetchJoiningCommunity, + fetchUnjoiningCommunity, +} from '@/services/community/api/community'; +import { useAppDispatch, useAppSelector } from '@/store/hooks'; +import useLogin from '../shared/useLogin'; +import { + addOneToJoinActionPendingIds, + addOneToJoinedCommunities, + removeOneFromJoinActionPendingIds, + removeOneFromJoinedCommunities, + selectJoinCommunity, +} from '@/features/community/joinCommunitySlice'; +import { ApiRespCode } from '@/services/shared/types'; +import { CommunityInfo } from '@/services/community/types/community'; + +export default function useJoinCommunityAction(communityInfo: CommunityInfo) { + const communityId = communityInfo?.id; + const dispatch = useAppDispatch(); + const { isLogin, login } = useLogin(); + const { joinActionPendingIds, joinedCommunities, joinedCommunitiesPending } = + useAppSelector(selectJoinCommunity); + + const joined = useMemo( + () => joinedCommunities.some((item) => item.id === communityId), + [joinedCommunities, communityId] + ); + const isPending = useMemo( + () => joinActionPendingIds.includes(communityId), + [joinActionPendingIds, communityId] + ); + const isDisabled = useMemo( + () => !communityId || isPending || joinedCommunitiesPending, + [communityId, isPending, joinedCommunitiesPending] + ); + + const joiningAction = useCallback(async () => { + if (!isLogin) { + login(); + return; + } + if (isPending) return; + try { + dispatch(addOneToJoinActionPendingIds(communityId)); + const response = await fetchJoiningCommunity(communityId); + const { code, msg } = response.data; + if (code === ApiRespCode.SUCCESS) { + dispatch(addOneToJoinedCommunities(communityInfo)); + toast.success('Join success'); + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Join failed: ${error.message}`); + } finally { + dispatch(removeOneFromJoinActionPendingIds(communityId)); + } + }, [dispatch, communityId, communityInfo, isPending, isLogin]); + + const unjoiningAction = useCallback(async () => { + if (!isLogin) { + login(); + return; + } + if (isPending) return; + try { + dispatch(addOneToJoinActionPendingIds(communityId)); + const response = await fetchUnjoiningCommunity(communityId); + const { code, msg } = response.data; + if (code === ApiRespCode.SUCCESS) { + dispatch(removeOneFromJoinedCommunities(communityId)); + toast.success('Unjoin success'); + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Unjoin failed: ${error.message}`); + } finally { + dispatch(removeOneFromJoinActionPendingIds(communityId)); + } + }, [dispatch, isPending, communityId, isLogin]); + + const joinChangeAction = () => { + if (joined) { + unjoiningAction(); + } else { + joiningAction(); + } + }; + + return { + joined, + isPending, + isDisabled, + joiningAction, + unjoiningAction, + joinChangeAction, + }; +} diff --git a/apps/u3/src/hooks/community/useLoadCommunityTypes.ts b/apps/u3/src/hooks/community/useLoadCommunityTypes.ts new file mode 100644 index 00000000..0ad823b9 --- /dev/null +++ b/apps/u3/src/hooks/community/useLoadCommunityTypes.ts @@ -0,0 +1,34 @@ +import { useCallback, useState } from 'react'; +import { toast } from 'react-toastify'; +import { + CommunityTypesData, + fetchCommunityTypes, +} from '@/services/community/api/community'; + +export default function useLoadCommunityTypes() { + const [communityTypes, setCommunityTypes] = useState<CommunityTypesData>([]); + const [communityTypesPending, setCommunityTypesPending] = useState(false); + const loadCommunityTypes = useCallback(async () => { + try { + setCommunityTypesPending(true); + const res = await fetchCommunityTypes(); + const { code, msg, data } = res.data; + if (code === 0) { + setCommunityTypes(data); + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Load community types failed: ${error.message}`); + } finally { + setCommunityTypesPending(false); + } + }, []); + + return { + communityTypes, + communityTypesPending, + loadCommunityTypes, + }; +} diff --git a/apps/u3/src/hooks/community/useLoadGrowingCommunities.ts b/apps/u3/src/hooks/community/useLoadGrowingCommunities.ts new file mode 100644 index 00000000..8c71027a --- /dev/null +++ b/apps/u3/src/hooks/community/useLoadGrowingCommunities.ts @@ -0,0 +1,91 @@ +import { useCallback, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; +import { + GrowingCommunitiesData, + fetchGrowingCommunities, +} from '@/services/community/api/community'; + +const PAGE_SIZE = 30; +export const getDefaultGrowingCommunitiesCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + nextPageNumber: 1, + type: '', + }; +}; + +type GrowingCommunitiesCachedData = ReturnType< + typeof getDefaultGrowingCommunitiesCachedData +>; + +type GrowingCommunitiesOpts = { + cachedDataRefValue?: GrowingCommunitiesCachedData; +}; + +export default function useLoadGrowingCommunities( + opts?: GrowingCommunitiesOpts +) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultGrowingCommunitiesCachedData(), + }); + const cachedData = cachedDataRefValue || defaultCachedDataRef.current; + + const [growingCommunities, setGrowingCommunities] = + useState<GrowingCommunitiesData>(cachedData.data); + const [loading, setLoading] = useState(false); + const [pageInfo, setPageInfo] = useState(cachedData.pageInfo); + + const loadGrowingCommunities = useCallback( + async (params?: { type?: string }) => { + const { type } = params || {}; + if (type !== cachedData?.type) { + setGrowingCommunities([]); + setPageInfo({ hasNextPage: true }); + Object.assign(cachedData, { + ...getDefaultGrowingCommunitiesCachedData(), + type, + }); + } + if (cachedData.pageInfo.hasNextPage === false) { + return; + } + setLoading(true); + try { + const res = await fetchGrowingCommunities({ + pageSize: PAGE_SIZE, + pageNumber: cachedData.nextPageNumber, + type: cachedData?.type || undefined, + }); + const { code, msg, data } = res.data; + if (code === 0) { + const newCommunities = data || []; + const hasNextPage = newCommunities.length >= PAGE_SIZE; + setGrowingCommunities((prev) => [...prev, ...newCommunities]); + setPageInfo({ hasNextPage }); + cachedData.data = cachedData.data.concat(newCommunities); + cachedData.nextPageNumber += 1; + cachedData.pageInfo.hasNextPage = hasNextPage; + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Load growing communities failed: ${error.message}`); + } finally { + setLoading(false); + } + }, + [] + ); + + return { + loading, + growingCommunities, + loadGrowingCommunities, + pageInfo, + }; +} diff --git a/apps/u3/src/hooks/community/useLoadJoinedCommunities.ts b/apps/u3/src/hooks/community/useLoadJoinedCommunities.ts new file mode 100644 index 00000000..ad61f1de --- /dev/null +++ b/apps/u3/src/hooks/community/useLoadJoinedCommunities.ts @@ -0,0 +1,89 @@ +import { useCallback, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; +import { + JoinedCommunitiesData, + fetchJoinedCommunities, +} from '@/services/community/api/community'; + +const PAGE_SIZE = 30; +export const getDefaultJoinedCommunitiesCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + nextPageNumber: 1, + type: '', + }; +}; + +type JoinedCommunitiesCachedData = ReturnType< + typeof getDefaultJoinedCommunitiesCachedData +>; + +type JoinedCommunitiesOpts = { + cachedDataRefValue?: JoinedCommunitiesCachedData; +}; + +export default function useLoadJoinedCommunities(opts?: JoinedCommunitiesOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultJoinedCommunitiesCachedData(), + }); + const cachedData = cachedDataRefValue || defaultCachedDataRef.current; + + const [joinedCommunities, setJoinedCommunities] = + useState<JoinedCommunitiesData>(cachedData.data); + const [loading, setLoading] = useState(false); + const [pageInfo, setPageInfo] = useState(cachedData.pageInfo); + + const loadJoinedCommunities = useCallback( + async (params?: { type?: string }) => { + const { type } = params || {}; + if (type !== cachedData?.type) { + setJoinedCommunities([]); + setPageInfo({ hasNextPage: true }); + Object.assign(cachedData, { + ...getDefaultJoinedCommunitiesCachedData(), + type, + }); + } + if (cachedData.pageInfo.hasNextPage === false) { + return; + } + setLoading(true); + try { + const res = await fetchJoinedCommunities({ + pageSize: PAGE_SIZE, + pageNumber: cachedData.nextPageNumber, + type: cachedData?.type || undefined, + }); + const { code, msg, data } = res.data; + if (code === 0) { + const newCommunities = data || []; + const hasNextPage = newCommunities.length >= PAGE_SIZE; + setJoinedCommunities((prev) => [...prev, ...newCommunities]); + setPageInfo({ hasNextPage }); + cachedData.data = cachedData.data.concat(newCommunities); + cachedData.nextPageNumber += 1; + cachedData.pageInfo.hasNextPage = hasNextPage; + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Load joined communities failed: ${error.message}`); + } finally { + setLoading(false); + } + }, + [] + ); + + return { + loading, + joinedCommunities, + loadJoinedCommunities, + pageInfo, + }; +} diff --git a/apps/u3/src/hooks/community/useLoadNewestCommunities.ts b/apps/u3/src/hooks/community/useLoadNewestCommunities.ts new file mode 100644 index 00000000..18ba74c9 --- /dev/null +++ b/apps/u3/src/hooks/community/useLoadNewestCommunities.ts @@ -0,0 +1,89 @@ +import { useCallback, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; +import { + NewestCommunitiesData, + fetchNewestCommunities, +} from '@/services/community/api/community'; + +const PAGE_SIZE = 30; +export const getDefaultNewestCommunitiesCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + nextPageNumber: 1, + type: '', + }; +}; + +type NewestCommunitiesCachedData = ReturnType< + typeof getDefaultNewestCommunitiesCachedData +>; + +type NewestCommunitiesOpts = { + cachedDataRefValue?: NewestCommunitiesCachedData; +}; + +export default function useLoadNewestCommunities(opts?: NewestCommunitiesOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultNewestCommunitiesCachedData(), + }); + const cachedData = cachedDataRefValue || defaultCachedDataRef.current; + + const [newestCommunities, setNewestCommunities] = + useState<NewestCommunitiesData>(cachedData.data); + const [loading, setLoading] = useState(false); + const [pageInfo, setPageInfo] = useState(cachedData.pageInfo); + + const loadNewestCommunities = useCallback( + async (params?: { type?: string }) => { + const { type } = params || {}; + if (type !== cachedData?.type) { + setNewestCommunities([]); + setPageInfo({ hasNextPage: true }); + Object.assign(cachedData, { + ...getDefaultNewestCommunitiesCachedData(), + type, + }); + } + if (cachedData.pageInfo.hasNextPage === false) { + return; + } + setLoading(true); + try { + const res = await fetchNewestCommunities({ + pageSize: PAGE_SIZE, + pageNumber: cachedData.nextPageNumber, + type: cachedData?.type || undefined, + }); + const { code, msg, data } = res.data; + if (code === 0) { + const newCommunities = data || []; + const hasNextPage = newCommunities.length >= PAGE_SIZE; + setNewestCommunities((prev) => [...prev, ...newCommunities]); + setPageInfo({ hasNextPage }); + cachedData.data = cachedData.data.concat(newCommunities); + cachedData.nextPageNumber += 1; + cachedData.pageInfo.hasNextPage = hasNextPage; + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Load newest communities failed: ${error.message}`); + } finally { + setLoading(false); + } + }, + [] + ); + + return { + loading, + newestCommunities, + loadNewestCommunities, + pageInfo, + }; +} diff --git a/apps/u3/src/hooks/community/useLoadTrendingCommunities.ts b/apps/u3/src/hooks/community/useLoadTrendingCommunities.ts new file mode 100644 index 00000000..c4f4ae26 --- /dev/null +++ b/apps/u3/src/hooks/community/useLoadTrendingCommunities.ts @@ -0,0 +1,91 @@ +import { useCallback, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; +import { + TrendingCommunitiesData, + fetchTrendingCommunities, +} from '@/services/community/api/community'; + +const PAGE_SIZE = 30; +export const getDefaultTrendingCommunitiesCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + nextPageNumber: 1, + type: '', + }; +}; + +type TrendingCommunitiesCachedData = ReturnType< + typeof getDefaultTrendingCommunitiesCachedData +>; + +type TrendingCommunitiesOpts = { + cachedDataRefValue?: TrendingCommunitiesCachedData; +}; + +export default function useLoadTrendingCommunities( + opts?: TrendingCommunitiesOpts +) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultTrendingCommunitiesCachedData(), + }); + const cachedData = cachedDataRefValue || defaultCachedDataRef.current; + + const [trendingCommunities, setTrendingCommunities] = + useState<TrendingCommunitiesData>(cachedData.data); + const [loading, setLoading] = useState(false); + const [pageInfo, setPageInfo] = useState(cachedData.pageInfo); + + const loadTrendingCommunities = useCallback( + async (params?: { type?: string }) => { + const { type } = params || {}; + if (type !== cachedData?.type) { + setTrendingCommunities([]); + setPageInfo({ hasNextPage: true }); + Object.assign(cachedData, { + ...getDefaultTrendingCommunitiesCachedData(), + type, + }); + } + if (cachedData.pageInfo.hasNextPage === false) { + return; + } + setLoading(true); + try { + const res = await fetchTrendingCommunities({ + pageSize: PAGE_SIZE, + pageNumber: cachedData.nextPageNumber, + type: cachedData?.type || undefined, + }); + const { code, msg, data } = res.data; + if (code === 0) { + const newCommunities = data || []; + const hasNextPage = newCommunities.length >= PAGE_SIZE; + setTrendingCommunities((prev) => [...prev, ...newCommunities]); + setPageInfo({ hasNextPage }); + cachedData.data = cachedData.data.concat(newCommunities); + cachedData.nextPageNumber += 1; + cachedData.pageInfo.hasNextPage = hasNextPage; + } else { + throw new Error(msg); + } + } catch (error) { + console.error(error); + toast.error(`Load trending communities failed: ${error.message}`); + } finally { + setLoading(false); + } + }, + [] + ); + + return { + loading, + trendingCommunities, + loadTrendingCommunities, + pageInfo, + }; +} diff --git a/apps/u3/src/hooks/profile/useBioLinkListWithWeb3Bio.ts b/apps/u3/src/hooks/profile/useBioLinkListWithWeb3Bio.ts index 02213bf4..27c2227f 100644 --- a/apps/u3/src/hooks/profile/useBioLinkListWithWeb3Bio.ts +++ b/apps/u3/src/hooks/profile/useBioLinkListWithWeb3Bio.ts @@ -74,9 +74,9 @@ export default function useBioLinkListWithWeb3Bio(identity: string) { return lensFirstAddress; } return ( - fcastFirstAddress || - lensFirstAddress || bioLinkList.find((item) => !!item.address)?.address || + lensFirstAddress || + fcastFirstAddress || '' ); }, [identity, fcastBioLinks, lensBioLinks, bioLinkList]); diff --git a/apps/u3/src/hooks/profile/useHasU3ProfileWithDid.ts b/apps/u3/src/hooks/profile/useHasU3ProfileWithDid.ts new file mode 100644 index 00000000..960a6775 --- /dev/null +++ b/apps/u3/src/hooks/profile/useHasU3ProfileWithDid.ts @@ -0,0 +1,27 @@ +import { useProfileState } from '@us3r-network/profile'; +import { useEffect, useState } from 'react'; +import { isDidPkh } from '@/utils/shared/did'; + +export default function useHasU3ProfileWithDid(did: string) { + const { getProfileWithDid } = useProfileState(); + const [u3Profile, setU3Profile] = useState(null); + const [hasU3ProfileLoading, setHasU3ProfileLoading] = useState(false); + useEffect(() => { + (async () => { + if (isDidPkh(did)) { + setHasU3ProfileLoading(true); + const profile = await getProfileWithDid(did); + if (profile) { + setU3Profile(profile); + } + setHasU3ProfileLoading(false); + } else { + setU3Profile(null); + } + })(); + }, [did]); + return { + u3Profile, + hasU3ProfileLoading, + }; +} diff --git a/apps/u3/src/hooks/profile/usePlatformProfileInfoData.ts b/apps/u3/src/hooks/profile/usePlatformProfileInfoData.ts index 6cac40a6..8eaeaec8 100644 --- a/apps/u3/src/hooks/profile/usePlatformProfileInfoData.ts +++ b/apps/u3/src/hooks/profile/usePlatformProfileInfoData.ts @@ -1,18 +1,17 @@ import { useEffect, useMemo } from 'react'; import { useProfiles } from '@lens-protocol/react-web'; import useBioLinkListWithWeb3Bio from './useBioLinkListWithWeb3Bio'; -import { useFarcasterCtx } from '../../contexts/social/FarcasterCtx'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; import useUpsertFarcasterUserData from '../social/farcaster/useUpsertFarcasterUserData'; -import useFarcasterFollowNum from '../social/farcaster/useFarcasterFollowNum'; -import { PlatformAccountsData } from '../../components/profile/profile-info/PlatformAccounts'; -import { SocialPlatform } from '../../services/social/types'; -import getAvatar from '../../utils/social/lens/getAvatar'; +import useFarcasterUserStats from '../social/farcaster/useFarcasterUserStats'; +import { PlatformAccountData, SocialPlatform } from '@/services/social/types'; +import getAvatar from '@/utils/social/lens/getAvatar'; import { getBio, getHandle, getName, getOwnedByAddress, -} from '../../utils/social/lens/profile'; +} from '@/utils/social/lens/profile'; import useFetchFidWithFname from '../social/farcaster/useFetchFidWithFname'; export default function usePlatformProfileInfoData({ @@ -51,9 +50,9 @@ export default function usePlatformProfileInfoData({ } }, [fid, farcasterUserData]); - const { farcasterFollowData } = useFarcasterFollowNum(fid); + const { farcasterUserStats } = useFarcasterUserStats(fid); - const platformAccounts: PlatformAccountsData = useMemo(() => { + const platformAccounts: PlatformAccountData[] = useMemo(() => { const accounts = []; for (const fcastProfile of fcastBioLinks) { accounts.push({ @@ -82,17 +81,20 @@ export default function usePlatformProfileInfoData({ return accounts; }, [lensProfiles, fcastBioLinks, fid]); - const followersCount = useMemo(() => { - const lensFollowersCount = lensProfileFirst?.stats.followers || 0; + const postCount = useMemo(() => { + const lensCount = lensProfileFirst?.stats.posts || 0; + return lensCount + farcasterUserStats.postCount || 0; + }, [lensProfileFirst, farcasterUserStats]); - return lensFollowersCount + farcasterFollowData.followers; - }, [lensProfileFirst, farcasterFollowData]); + const followerCount = useMemo(() => { + const lensCount = lensProfileFirst?.stats.followers || 0; + return lensCount + farcasterUserStats.followerCount; + }, [lensProfileFirst, farcasterUserStats]); const followingCount = useMemo(() => { - const lensFollowersCount = lensProfileFirst?.stats.following || 0; - - return lensFollowersCount + farcasterFollowData.following; - }, [lensProfileFirst, farcasterFollowData]); + const lensCount = lensProfileFirst?.stats.following || 0; + return lensCount + farcasterUserStats.followingCount; + }, [lensProfileFirst, farcasterUserStats]); return { fid, @@ -100,7 +102,8 @@ export default function usePlatformProfileInfoData({ lensProfiles, recommendAddress, platformAccounts, - followersCount, + postCount, + followerCount, followingCount, bioLinkLoading, lensProfilesLoading, diff --git a/apps/u3/src/hooks/profile/useU3ProfileInfoData.ts b/apps/u3/src/hooks/profile/useU3ProfileInfoData.ts index 61601428..a49179e4 100644 --- a/apps/u3/src/hooks/profile/useU3ProfileInfoData.ts +++ b/apps/u3/src/hooks/profile/useU3ProfileInfoData.ts @@ -4,22 +4,21 @@ import useBioLinkListWithDid from './useBioLinkListWithDid'; import { farcasterHandleToBioLinkHandle, lensHandleToBioLinkHandle, -} from '../../utils/profile/biolink'; +} from '@/utils/profile/biolink'; import useBioLinkListWithWeb3Bio from './useBioLinkListWithWeb3Bio'; -import { useFarcasterCtx } from '../../contexts/social/FarcasterCtx'; -import { getAddressWithDidPkh } from '../../utils/shared/did'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { getAddressWithDidPkh } from '@/utils/shared/did'; import useUpsertFarcasterUserData from '../social/farcaster/useUpsertFarcasterUserData'; -import useFarcasterFollowNum from '../social/farcaster/useFarcasterFollowNum'; +import useFarcasterUserStats from '../social/farcaster/useFarcasterUserStats'; import useFarcasterUserData from '../social/farcaster/useFarcasterUserData'; -import { PlatformAccountsData } from '../../components/profile/profile-info/PlatformAccounts'; -import { SocialPlatform } from '../../services/social/types'; -import getAvatar from '../../utils/social/lens/getAvatar'; +import { PlatformAccountData, SocialPlatform } from '@/services/social/types'; +import getAvatar from '@/utils/social/lens/getAvatar'; import { getBio, getHandle, getName, getOwnedByAddress, -} from '../../utils/social/lens/profile'; +} from '@/utils/social/lens/profile'; import useFetchFidWithFname from '../social/farcaster/useFetchFidWithFname'; export default function useU3ProfileInfoData({ @@ -97,7 +96,7 @@ export default function useU3ProfileInfoData({ } }, [fid, farcasterUserData]); - const { farcasterFollowData } = useFarcasterFollowNum(`${fid}`); + const { farcasterUserStats } = useFarcasterUserStats(`${fid}`); const userData = useFarcasterUserData({ fid: `${fid}`, @@ -105,7 +104,7 @@ export default function useU3ProfileInfoData({ }); const platformAccounts = useMemo(() => { - const accounts: PlatformAccountsData = []; + const accounts: PlatformAccountData[] = []; if (userData?.fid && userData?.display) { accounts.push({ platform: SocialPlatform.Farcaster, @@ -133,17 +132,20 @@ export default function useU3ProfileInfoData({ return accounts; }, [lensProfiles, userData, web3FcastBioLinks]); - const followersCount = useMemo(() => { - const lensFollowersCount = lensProfileFirst?.stats.followers || 0; + const postCount = useMemo(() => { + const lensCount = lensProfileFirst?.stats.posts || 0; + return lensCount + farcasterUserStats.postCount || 0; + }, [lensProfileFirst, farcasterUserStats]); - return lensFollowersCount + farcasterFollowData.followers || 0; - }, [lensProfileFirst, farcasterFollowData]); + const followerCount = useMemo(() => { + const lensCount = lensProfileFirst?.stats.followers || 0; + return lensCount + farcasterUserStats.followerCount || 0; + }, [lensProfileFirst, farcasterUserStats]); const followingCount = useMemo(() => { - const lensFollowersCount = lensProfileFirst?.stats.following || 0; - - return lensFollowersCount + farcasterFollowData.following || 0; - }, [lensProfileFirst, farcasterFollowData]); + const lensCount = lensProfileFirst?.stats.following || 0; + return lensCount + farcasterUserStats.followingCount || 0; + }, [lensProfileFirst, farcasterUserStats]); return { fid, @@ -151,9 +153,10 @@ export default function useU3ProfileInfoData({ address, lensProfiles, platformAccounts, - followersCount, + postCount, + followerCount, followingCount, - farcasterFollowData, + // farcasterFollowData, bioLinkLoading, web3BioLoading, fidLoading, diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterFollowData.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterFollowData.ts deleted file mode 100644 index 58268e2e..00000000 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterFollowData.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { getFarcasterLinks } from 'src/services/social/api/farcaster'; - -export default function useFarcasterFollowData({ - fid, -}: { - fid?: string | number; -}) { - const [loading, setLoading] = useState(false); - const [farcasterFollowData, setFarcasterFollowData] = useState({ - followingCount: 0, - followerCount: 0, - followerData: [], - followingData: [], - farcasterUserData: {}, - }); - - const getFarcasterFollowData = useCallback(async () => { - if (!fid) return; - const resp = await getFarcasterLinks(fid, true); - const followData = resp.data.data; - const temp: { [key: string]: { type: number; value: string }[] } = {}; - followData.farcasterUserData.forEach((item) => { - if (temp[item.fid]) { - temp[item.fid].push(item); - } else { - temp[item.fid] = [item]; - } - }); - - setFarcasterFollowData({ - ...followData, - farcasterUserData: temp, - }); - }, [fid]); - - useEffect(() => { - setLoading(true); - getFarcasterFollowData() - .catch(console.error) - .finally(() => { - setLoading(false); - }); - }, [getFarcasterFollowData]); - - return { - loading, - farcasterFollowData, - }; -} diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterFollowNum.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterFollowNum.ts deleted file mode 100644 index 1b5dddff..00000000 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterFollowNum.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { getFarcasterFollow } from 'src/services/social/api/farcaster'; - -export default function useFarcasterFollowNum(fid: string) { - const [farcasterFollowData, setFarcasterFollowData] = useState({ - following: 0, - followers: 0, - }); - - const getFarcasterFollowData = useCallback(async () => { - if (!fid) return; - const resp = await getFarcasterFollow(fid); - const followData = resp.data.data; - setFarcasterFollowData({ - ...followData, - }); - }, [fid]); - - useEffect(() => { - getFarcasterFollowData().catch(console.error); - }, [getFarcasterFollowData]); - - return { - farcasterFollowData, - }; -} diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterFollowing.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterFollowing.ts index e61f4aba..120407ef 100644 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterFollowing.ts +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterFollowing.ts @@ -1,20 +1,35 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import { getFarcasterFollowing } from 'src/services/social/api/farcaster'; import { userDataObjFromArr } from 'src/utils/social/farcaster/user-data'; -const farcasterFollowingData = { - data: [], - pageInfo: { - hasNextPage: true, - }, - userData: {}, - userDataObj: {}, - endTimestamp: Date.now(), - endCursor: '', +export const getDefaultFarcasterFollowingCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + userData: {}, + userDataObj: {}, + endTimestamp: Date.now(), + endCursor: '', + }; }; +type FarcasterFollowingCachedData = ReturnType< + typeof getDefaultFarcasterFollowingCachedData +>; +type FarcasterFollowingOpts = { + cachedDataRefValue?: FarcasterFollowingCachedData; +}; + +export default function useFarcasterFollowing(opts?: FarcasterFollowingOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultFarcasterFollowingCachedData(), + }); + const farcasterFollowingData = + cachedDataRefValue || defaultCachedDataRef.current; -export default function useFarcasterFollowing() { const { currFid } = useFarcasterCtx(); const [farcasterFollowing, setFarcasterFollowing] = useState<any[]>( farcasterFollowingData.data @@ -74,14 +89,3 @@ export default function useFarcasterFollowing() { farcasterFollowingUserDataObj, }; } - -export function resetFarcasterFollowingData() { - farcasterFollowingData.data = []; - farcasterFollowingData.pageInfo = { - hasNextPage: true, - }; - farcasterFollowingData.userData = {}; - farcasterFollowingData.userDataObj = {}; - farcasterFollowingData.endTimestamp = Date.now(); - farcasterFollowingData.endCursor = ''; -} diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterLinks.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterLinks.ts new file mode 100644 index 00000000..37283eb5 --- /dev/null +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterLinks.ts @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useState } from 'react'; +import { debounce, uniqBy } from 'lodash'; +import { + FarcasterLink, + PAGE_SIZE, + getFarcasterUserLinks, +} from '@/services/social/api/farcaster'; +import { FollowType } from '@/container/profile/Contacts'; + +export default function useFarcasterLinks({ + fid, + pageSize, + type, +}: { + fid: string | number; + pageSize?: number; + type: FollowType; +}) { + const [links, setLinks] = useState<FarcasterLink[]>([]); + const [farcasterUserData, setFarcasterUserData] = useState<{ + [key: string]: { type: number; value: string }[]; + }>({}); + const [hasMore, setHasMore] = useState(false); + const [firstLoading, setFirstLoading] = useState(false); + const [moreLoading, setMoreLoading] = useState(false); + const [endCursor, setEndCursor] = useState(''); + + const loadMore = useCallback(async () => { + if (moreLoading || !hasMore) { + return; + } + if (!fid) return; + if (!pageSize) pageSize = PAGE_SIZE; + setMoreLoading(true); + try { + const resp = await getFarcasterUserLinks({ + fid, + type, + pageSize, + endCursor, + }); + const { + links: newLinks, + pageInfo, + farcasterUserData: userData, + } = resp.data.data; + const temp: { [key: string]: { type: number; value: string }[] } = {}; + userData.forEach((item) => { + if (temp[item.fid]) { + temp[item.fid].push(item); + } else { + temp[item.fid] = [item]; + } + }); + switch (type) { + case FollowType.FOLLOWER: + setLinks(uniqBy([...links, ...newLinks], 'fid')); + break; + case FollowType.FOLLOWING: + setLinks(uniqBy([...links, ...newLinks], 'targetFid')); + break; + default: + break; + } + setFarcasterUserData({ ...farcasterUserData, ...temp }); + if (pageInfo) { + setHasMore(pageInfo.hasNextPage); + setEndCursor(pageInfo.endCursor); + } + } catch (error) { + console.error(error); + } finally { + setMoreLoading(false); + } + }, [moreLoading, hasMore, endCursor, links, farcasterUserData, fid]); + + const load = async () => { + if (firstLoading) return; + if (!fid) return; + setFirstLoading(true); + try { + const resp = await getFarcasterUserLinks({ + fid, + type, + pageSize, + }); + if (!resp?.data?.data) return; + const { + links: newLinks, + pageInfo, + farcasterUserData: userData, + } = resp.data.data; + const temp: { [key: string]: { type: number; value: string }[] } = {}; + userData.forEach((item) => { + if (temp[item.fid]) { + temp[item.fid].push(item); + } else { + temp[item.fid] = [item]; + } + }); + switch (type) { + case FollowType.FOLLOWER: + setLinks(uniqBy(newLinks, 'fid')); + break; + case FollowType.FOLLOWING: + setLinks(uniqBy(newLinks, 'targetFid')); + break; + default: + break; + } + setFarcasterUserData({ ...farcasterUserData, ...temp }); + if (pageInfo) { + setHasMore(pageInfo.hasNextPage); + setEndCursor(pageInfo.endCursor); + } + } catch (error) { + console.error(error); + } finally { + setFirstLoading(false); + } + }; + + useEffect(() => { + debounce(load, 500)(); + }, [fid]); + + return { + firstLoading, + moreLoading, + hasMore, + links, + farcasterUserData, + loadMore, + }; +} diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterNotifications.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterNotifications.ts index 41df3835..d228c678 100644 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterNotifications.ts +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterNotifications.ts @@ -5,9 +5,11 @@ import { FarcasterNotification, getFarcasterNotifications, } from '../../../services/social/api/farcaster'; +import { NotificationType } from '@/services/notification/types/notifications'; export default function useFarcasterNotifications( fid: number, + type: NotificationType[], pageSize: number ) { const [farcasterNotifications, setFarcasterNotifications] = useState< @@ -30,6 +32,7 @@ export default function useFarcasterNotifications( const resp = await getFarcasterNotifications({ fid, pageSize, + type, endFarcasterCursor: cursor, }); const { @@ -72,7 +75,7 @@ export default function useFarcasterNotifications( if (!fid) return; setLoading(true); try { - const resp = await getFarcasterNotifications({ fid, pageSize }); + const resp = await getFarcasterNotifications({ fid, pageSize, type }); if (!resp?.data?.data) return; const { notifications, diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterTrending.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterTrending.ts index a4385508..bd310317 100644 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterTrending.ts +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterTrending.ts @@ -1,25 +1,37 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { getFarcasterTrending } from 'src/services/social/api/farcaster'; import { userDataObjFromArr } from 'src/utils/social/farcaster/user-data'; const PAGE_SIZE = 30; -const farcasterTrendingData = { - data: [], - pageInfo: { - hasNextPage: true, - }, - userData: {}, - userDataObj: {}, - index: 0, +export const getDefaultFarcasterTrendingCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + userData: {}, + userDataObj: {}, + index: 0, + trendingIdSet: new Set(), + }; }; -const trendingIdSet: Set<string> = new Set(); +type FarcasterTrendingCachedData = ReturnType< + typeof getDefaultFarcasterTrendingCachedData +>; -type FarcasterTrendingParams = { +type FarcasterTrendingOpts = { channelId?: string; + cachedDataRefValue?: FarcasterTrendingCachedData; }; -export default function useFarcasterTrending(params?: FarcasterTrendingParams) { - const { channelId } = params || {}; +export default function useFarcasterTrending(opts?: FarcasterTrendingOpts) { + const { channelId, cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultFarcasterTrendingCachedData(), + }); + const farcasterTrendingData = + cachedDataRefValue || defaultCachedDataRef.current; + const [farcasterTrending, setFarcasterTrending] = useState<any[]>( farcasterTrendingData.data ); // TODO any @@ -30,6 +42,8 @@ export default function useFarcasterTrending(params?: FarcasterTrendingParams) { useState(farcasterTrendingData.userDataObj); const loadFarcasterTrending = useCallback(async () => { + console.log('pageInfo', pageInfo); + if (pageInfo.hasNextPage === false) { return; } @@ -51,10 +65,10 @@ export default function useFarcasterTrending(params?: FarcasterTrendingParams) { const newTrending = casts.filter((cast: any) => { const { id } = cast.data; - if (trendingIdSet.has(id)) { + if (farcasterTrendingData.trendingIdSet.has(id)) { return false; } - trendingIdSet.add(id); + farcasterTrendingData.trendingIdSet.add(id); return true; }); diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterUserStats.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterUserStats.ts new file mode 100644 index 00000000..b4fba4e0 --- /dev/null +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterUserStats.ts @@ -0,0 +1,27 @@ +import { useCallback, useEffect, useState } from 'react'; +import { getFarcasterUserStats } from 'src/services/social/api/farcaster'; + +export default function useFarcasterUserStats(fid: string) { + const [farcasterUserStats, setFarcasterUserStats] = useState({ + followerCount: 0, + followingCount: 0, + postCount: 0, + }); + + const getUserStatics = useCallback(async () => { + if (!fid) return; + const resp = await getFarcasterUserStats(fid); + const userStats = resp.data.data; + setFarcasterUserStats({ + ...userStats, + }); + }, [fid]); + + useEffect(() => { + getUserStatics().catch(console.error); + }, [getUserStatics]); + + return { + farcasterUserStats, + }; +} diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterWhatsnew.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterWhatsnew.ts index 1b95dcf2..b4374bac 100644 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterWhatsnew.ts +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterWhatsnew.ts @@ -1,20 +1,35 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { getFarcasterWhatsnew } from 'src/services/social/api/farcaster'; import { userDataObjFromArr } from 'src/utils/social/farcaster/user-data'; -const farcasterWhatsnewData = { - data: [], - pageInfo: { - hasNextPage: true, - }, - userData: {}, - userDataObj: {}, - endTimestamp: Date.now(), - endCursor: '', +export const getDefaultFarcasterWhatsnewCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + userData: {}, + userDataObj: {}, + endTimestamp: Date.now(), + endCursor: '', + }; +}; +type FarcasterWhatsnewCachedData = ReturnType< + typeof getDefaultFarcasterWhatsnewCachedData +>; + +type FarcasterWhatsnewOpts = { + cachedDataRefValue?: FarcasterWhatsnewCachedData; }; +export default function useFarcasterWhatsnew(opts?: FarcasterWhatsnewOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultFarcasterWhatsnewCachedData(), + }); + const farcasterWhatsnewData = + cachedDataRefValue || defaultCachedDataRef.current; -export default function useFarcasterWhatsnew() { // TODO any const [farcasterWhatsnew, setFarcasterWhatsnew] = useState<any[]>( farcasterWhatsnewData.data diff --git a/apps/u3/src/hooks/social/farcaster/useLazyQueryFidWithAddress.ts b/apps/u3/src/hooks/social/farcaster/useLazyQueryFidWithAddress.ts deleted file mode 100644 index 11403af0..00000000 --- a/apps/u3/src/hooks/social/farcaster/useLazyQueryFidWithAddress.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * @Author: bufan bufan@hotmail.com - * @Date: 2023-10-20 19:07:48 - * @LastEditors: bufan bufan@hotmail.com - * @LastEditTime: 2023-10-26 14:17:56 - * @FilePath: /u3/apps/u3/src/hooks/farcaster/useLazyQueryFidWithAddress.ts - * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE - */ -import { useLazyQuery } from '@airstack/airstack-react'; -import { useMemo } from 'react'; - -export default function useLazyQueryFidWithAddress(address) { - const query = ` - query MyQuery { - Socials( - input: { - filter: { - dappName: {_eq: farcaster}, - userAssociatedAddresses: {_eq: "${address}"}}, - blockchain: ethereum - } - ) { - Social { - userId - userAddress - } - } - } - `; - const [fetch, { data, loading, error }] = useLazyQuery(query); - const { Social } = data?.Socials || {}; - - // TODO 先对比userId 取最小的那个,后续返回fids - const fid = useMemo( - () => - Social?.reduce( - (acc, cur) => (Number(acc.userId) < Number(cur.userId) ? acc : cur), - {} - )?.userId as string, - [Social] - ); - - return { fetch, fid, loading, error }; -} diff --git a/apps/u3/src/hooks/social/farcaster/useNotifications.ts b/apps/u3/src/hooks/social/farcaster/useNotifications.ts deleted file mode 100644 index ff0443d4..00000000 --- a/apps/u3/src/hooks/social/farcaster/useNotifications.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { debounce } from 'lodash'; - -import { - FarcasterNotification, - getFarcasterNotifications, - getFarcasterUnreadNotificationCount, - clearFarcasterUnreadNotification, -} from '../../../services/social/api/farcaster'; - -export default function useLoadFarcasterNotifications(fid: number) { - const [farcasterNotifications, setFarcasterNotifications] = useState< - FarcasterNotification[] - >([]); - const [farcasterUserData, setFarcasterUserData] = useState<{ - [key: string]: { type: number; value: string }[]; - }>({}); - const [hasMore, setHasMore] = useState(false); - const [loading, setLoading] = useState(false); - const [cursor, setCursor] = useState(''); - - const loadMoreFarcasterNotifications = useCallback(async () => { - if (loading || !hasMore) { - return; - } - setLoading(true); - try { - const resp = await getFarcasterNotifications({ fid }); - const { - notifications, - pageInfo, - farcasterUserData: userData, - } = resp.data.data; - const temp: { [key: string]: { type: number; value: string }[] } = {}; - userData.forEach((item) => { - if (temp[item.fid]) { - temp[item.fid].push(item); - } else { - temp[item.fid] = [item]; - } - }); - setFarcasterNotifications([...farcasterNotifications, ...notifications]); - setFarcasterUserData({ ...farcasterUserData, ...temp }); - if (pageInfo) { - setHasMore(pageInfo.hasNextPage); - setCursor(pageInfo.endFarcasterCursor); - } - } catch (error) { - console.error(error); - } finally { - setLoading(false); - } - }, [ - loading, - hasMore, - cursor, - farcasterNotifications, - farcasterUserData, - fid, - ]); - - const loadFarcasterNotifications = async () => { - setLoading(true); - try { - const resp = await getFarcasterNotifications({ fid }); - const { - notifications, - pageInfo, - farcasterUserData: userData, - } = resp.data.data; - const temp: { [key: string]: { type: number; value: string }[] } = {}; - userData.forEach((item) => { - if (temp[item.fid]) { - temp[item.fid].push(item); - } else { - temp[item.fid] = [item]; - } - }); - setFarcasterNotifications(notifications); - setFarcasterUserData({ ...farcasterUserData, ...temp }); - if (pageInfo) { - setHasMore(pageInfo.hasNextPage); - setCursor(pageInfo.endFarcasterCursor); - } - } catch (error) { - console.error(error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - debounce(loadFarcasterNotifications, 500)(); - }, [fid]); - - return { - loading, - hasMore, - next: cursor, - farcasterNotifications, - farcasterUserData, - loadMoreFarcasterNotifications, - }; -} diff --git a/apps/u3/src/hooks/social/lens/useLensFollowing.ts b/apps/u3/src/hooks/social/lens/useLensFollowing.ts index 7758a504..6ab9dafe 100644 --- a/apps/u3/src/hooks/social/lens/useLensFollowing.ts +++ b/apps/u3/src/hooks/social/lens/useLensFollowing.ts @@ -1,17 +1,31 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useAccessToken as useLensAccessToken } from '@lens-protocol/react-web'; import { useLensCtx } from 'src/contexts/social/AppLensCtx'; import { getLensFollowing } from 'src/services/social/api/lens'; -const lensFollowingData = { - data: [], - pageInfo: { - hasNextPage: true, - }, - endLensCursor: '', +export const getDefaultLensFollowingCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + endLensCursor: '', + }; }; +type LensFollowingCachedData = ReturnType< + typeof getDefaultLensFollowingCachedData +>; +type LensFollowingOpts = { + cachedDataRefValue?: LensFollowingCachedData; +}; + +export default function useLensFollowing(opts?: LensFollowingOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultLensFollowingCachedData(), + }); + const lensFollowingData = cachedDataRefValue || defaultCachedDataRef.current; -export default function useLensFollowing() { const { sessionProfile: lensSessionProfile } = useLensCtx(); const { id: lensSessionProfileId } = lensSessionProfile || {}; // TODO any @@ -57,11 +71,3 @@ export default function useLensFollowing() { pageInfo, }; } - -export function resetLensFollowingData() { - lensFollowingData.data = []; - lensFollowingData.pageInfo = { - hasNextPage: true, - }; - lensFollowingData.endLensCursor = ''; -} diff --git a/apps/u3/src/hooks/social/lens/useLensTrending.ts b/apps/u3/src/hooks/social/lens/useLensTrending.ts index 179f5950..32fe373a 100644 --- a/apps/u3/src/hooks/social/lens/useLensTrending.ts +++ b/apps/u3/src/hooks/social/lens/useLensTrending.ts @@ -1,16 +1,31 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { useAccessToken as useLensAccessToken } from '@lens-protocol/react-web'; import { getLensTrending } from 'src/services/social/api/lens'; -const lensTrendingData = { - data: [], - pageInfo: { - hasNextPage: true, - }, - endLensCursor: '', +export const getDefaultLensTrendingCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + endLensCursor: '', + }; }; -export default function useLensTrending() { +type LensTrendingCachedData = ReturnType< + typeof getDefaultLensTrendingCachedData +>; + +type LensTrendingOpts = { + cachedDataRefValue?: LensTrendingCachedData; +}; +export default function useLensTrending(opts?: LensTrendingOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultLensTrendingCachedData(), + }); + const lensTrendingData = cachedDataRefValue || defaultCachedDataRef.current; + // TODO any const [lensTrending, setLensTrending] = useState<any[]>( lensTrendingData.data diff --git a/apps/u3/src/hooks/social/useAllFollowing.ts b/apps/u3/src/hooks/social/useAllFollowing.ts index ba66ab5a..b8905090 100644 --- a/apps/u3/src/hooks/social/useAllFollowing.ts +++ b/apps/u3/src/hooks/social/useAllFollowing.ts @@ -1,25 +1,41 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useAccessToken as useLensAccessToken } from '@lens-protocol/react-web'; import { useLensCtx } from 'src/contexts/social/AppLensCtx'; import { useFarcasterCtx } from 'src/contexts/social/FarcasterCtx'; import { userDataObjFromArr } from 'src/utils/social/farcaster/user-data'; import { getAllFollowing } from 'src/services/social/api/all'; -const allFollowingData = { - data: [], - pageInfo: { - hasNextPage: true, - hasFarcasterNextPage: true, - hasLensNextPage: true, - endFarcasterCursor: '', - endLensCursor: '', - }, - userData: {}, - userDataObj: {}, - endTimestamp: Date.now(), +export const getDefaultAllFollowingCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + hasFarcasterNextPage: true, + hasLensNextPage: true, + endFarcasterCursor: '', + endLensCursor: '', + }, + userData: {}, + userDataObj: {}, + endTimestamp: Date.now(), + }; +}; +type AllFollowingCachedData = ReturnType< + typeof getDefaultAllFollowingCachedData +>; + +type AllFollowingOpts = { + channelId?: string; + cachedDataRefValue?: AllFollowingCachedData; }; -export default function useAllFollowing() { +export default function useAllFollowing(opts?: AllFollowingOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultAllFollowingCachedData(), + }); + const allFollowingData = cachedDataRefValue || defaultCachedDataRef.current; + const { currFid } = useFarcasterCtx(); const { sessionProfile: lensSessionProfile } = useLensCtx(); const { id: lensSessionProfileId } = lensSessionProfile || {}; @@ -98,17 +114,3 @@ export default function useAllFollowing() { allUserDataObj, }; } - -export function resetAllFollowingData() { - allFollowingData.data = []; - allFollowingData.pageInfo = { - hasNextPage: true, - hasLensNextPage: true, - hasFarcasterNextPage: true, - endFarcasterCursor: '', - endLensCursor: '', - }; - allFollowingData.userData = {}; - allFollowingData.userDataObj = {}; - allFollowingData.endTimestamp = 0; -} diff --git a/apps/u3/src/hooks/social/useAllWhatsnew.ts b/apps/u3/src/hooks/social/useAllWhatsnew.ts index 2b73b72c..d1aeff7f 100644 --- a/apps/u3/src/hooks/social/useAllWhatsnew.ts +++ b/apps/u3/src/hooks/social/useAllWhatsnew.ts @@ -1,23 +1,36 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { toast } from 'react-toastify'; import { useAccessToken as useLensAccessToken } from '@lens-protocol/react-web'; import { userDataObjFromArr } from 'src/utils/social/farcaster/user-data'; import { getAllWhatsnew } from 'src/services/social/api/all'; -const allWhatsnewData = { - data: [], - pageInfo: { - hasNextPage: true, - }, - userData: {}, - userDataObj: {}, - endFarcasterCursor: '', - endTimestamp: Date.now(), - endLensCursor: '', +export const getDefaultAllWhatsnewCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + userData: {}, + userDataObj: {}, + endFarcasterCursor: '', + endTimestamp: Date.now(), + endLensCursor: '', + }; +}; +type AllWhatsnewCachedData = ReturnType<typeof getDefaultAllWhatsnewCachedData>; + +type AllWhatsnewOpts = { + channelId?: string; + cachedDataRefValue?: AllWhatsnewCachedData; }; -export default function useAllWhatsnew() { +export default function useAllWhatsnew(opts?: AllWhatsnewOpts) { + const { cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultAllWhatsnewCachedData(), + }); + const allWhatsnewData = cachedDataRefValue || defaultCachedDataRef.current; // TODO any const [allWhatsnew, setAllWhatsnew] = useState<any[]>(allWhatsnewData.data); const [loading, setLoading] = useState(false); diff --git a/apps/u3/src/hooks/social/useChannelFeeds.ts b/apps/u3/src/hooks/social/useChannelFeeds.ts index e684e2fe..9c2fe27d 100644 --- a/apps/u3/src/hooks/social/useChannelFeeds.ts +++ b/apps/u3/src/hooks/social/useChannelFeeds.ts @@ -1,117 +1,106 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { - FarcasterPageInfo, - getFarcasterChannelFeeds, -} from 'src/services/social/api/farcaster'; +import { useCallback, useRef, useState } from 'react'; +import { getFarcasterChannelFeeds } from 'src/services/social/api/farcaster'; import { FEEDS_PAGE_SIZE } from 'src/services/social/api/feeds'; +import { toast } from 'react-toastify'; import { userDataObjFromArr } from '@/utils/social/farcaster/user-data'; -import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; -export default function useChannelFeeds() { - const { channelId } = useParams(); - const [mounted, setMounted] = useState(false); - const { farcasterChannels } = useFarcasterCtx(); - const [feeds, setFeeds] = useState<{ [key: string]: any[] }>({}); - const [pageInfo, setPageInfo] = useState<FarcasterPageInfo>(); - const [farcasterUserDataObj, setFarcasterUserDataObj] = useState({}); +export const getDefaultFarcasterWhatsnewCachedData = () => { + return { + data: [], + pageInfo: { + hasNextPage: true, + }, + userData: {}, + userDataObj: {}, + endTimestamp: Date.now(), + endCursor: '', + }; +}; +type FarcasterWhatsnewCachedData = ReturnType< + typeof getDefaultFarcasterWhatsnewCachedData +>; - const [firstLoading, setFirstLoading] = useState(false); - const [moreLoading, setMoreLoading] = useState(false); +type FarcasterWhatsnewOpts = { + channelId?: string; + cachedDataRefValue?: FarcasterWhatsnewCachedData; +}; - const channel = useMemo(() => { - const channelData = farcasterChannels.find( - (c) => c.channel_id === channelId - ); - return channelData; - }, [channelId, farcasterChannels]); +// TODO useChannelFeeds 考虑合到 useFarcasterWhatsnew 逻辑中 +export default function useChannelFeeds(opts?: FarcasterWhatsnewOpts) { + const { channelId, cachedDataRefValue } = opts || {}; + const defaultCachedDataRef = useRef({ + ...getDefaultFarcasterWhatsnewCachedData(), + }); + const farcasterWhatsnewData = + cachedDataRefValue || defaultCachedDataRef.current; - const loadChannelCasts = useCallback(async () => { - if (!channel) return; - if (feeds[channel.channel_id]?.length > 0) return; - try { - setFirstLoading(true); - const resp = await getFarcasterChannelFeeds({ - channelId: channel.channel_id, - pageSize: FEEDS_PAGE_SIZE, - }); - if (resp.data.code !== 0) { - console.error('loadChannelCasts error'); - return; - } + // TODO any + const [farcasterWhatsnew, setFarcasterWhatsnew] = useState<any[]>( + farcasterWhatsnewData.data + ); + const [loading, setLoading] = useState(false); + const [pageInfo, setPageInfo] = useState(farcasterWhatsnewData.pageInfo); - setFeeds((prev) => ({ - ...prev, - [channel.channel_id]: resp.data.data.data, - })); - setPageInfo(resp.data.data.pageInfo); - const userDataObj = userDataObjFromArr(resp.data.data.farcasterUserData); - setFarcasterUserDataObj((pre) => ({ ...pre, ...userDataObj })); - } catch (error) { - console.error(error); - setFeeds((prev) => ({ ...prev, [channel.channel_id]: [] })); - setPageInfo(undefined); - setFarcasterUserDataObj({}); - } finally { - setFirstLoading(false); - } - }, [channel]); + const [farcasterWhatsnewUserDataObj, setFarcasterWhatsnewUserDataObj] = + useState(farcasterWhatsnewData.userDataObj); - const loadMoreFeeds = useCallback(async () => { - console.log('loadMoreFeeds'); - if (!channel) return; + const loadFarcasterWhatsnew = useCallback(async () => { + if (pageInfo.hasNextPage === false) { + return; + } + setLoading(true); try { - setMoreLoading(true); const resp = await getFarcasterChannelFeeds({ - channelId: channel.channel_id, + channelId, pageSize: FEEDS_PAGE_SIZE, - endCursor: pageInfo?.endCursor, - endTimestamp: pageInfo?.endTimestamp, + endCursor: farcasterWhatsnewData.endCursor, + endTimestamp: farcasterWhatsnewData.endTimestamp, }); if (resp.data.code !== 0) { - console.error('loadChannelCasts error'); + toast.error(`fail to get farcaster whatsnew ${resp.data.msg}`); + setLoading(false); return; } - setFeeds((prev) => ({ - ...prev, - [channel.channel_id]: [ - ...prev[channel.channel_id], - ...resp.data.data.data, - ], - })); - setPageInfo(resp.data.data.pageInfo); - const userDataObj = userDataObjFromArr(resp.data.data.farcasterUserData); - setFarcasterUserDataObj((pre) => ({ ...pre, ...userDataObj })); - } catch (error) { - console.error(error); - } finally { - setMoreLoading(false); - } - }, [pageInfo]); + const { data } = resp.data; - useEffect(() => { - if (!mounted) return; - loadChannelCasts(); - }, [mounted, loadChannelCasts]); + const { + data: casts, + farcasterUserData, + pageInfo: whatsnewPageInfo, + } = data; - useEffect(() => { - setMounted(true); - }, []); + if (casts.length > 0) { + setFarcasterWhatsnew((pre) => [...pre, ...casts]); + farcasterWhatsnewData.data = farcasterWhatsnewData.data.concat(casts); + } + if (farcasterUserData.length > 0) { + const userDataObj = userDataObjFromArr(farcasterUserData); - const channelFeeds = useMemo(() => { - if (!channel) return []; - return feeds[channel.channel_id] || []; - }, [feeds, channel]); + setFarcasterWhatsnewUserDataObj((pre) => ({ ...pre, ...userDataObj })); + farcasterWhatsnewData.userDataObj = { + ...farcasterWhatsnewData.userDataObj, + ...userDataObj, + }; + } + + setPageInfo(whatsnewPageInfo); + farcasterWhatsnewData.pageInfo = whatsnewPageInfo; + farcasterWhatsnewData.endCursor = whatsnewPageInfo.endCursor; + farcasterWhatsnewData.endTimestamp = whatsnewPageInfo.endTimestamp; + } catch (err) { + console.error(err); + toast.error('fail to get farcaster whatsnew'); + } finally { + setLoading(false); + } + }, [pageInfo, channelId]); return { - firstLoading, - moreLoading, - loadFirstFeeds: loadChannelCasts, - loadMoreFeeds, - channel, - feeds: channelFeeds, + loading, + loadFarcasterWhatsnew, + farcasterWhatsnew, + farcasterWhatsnewUserDataObj, pageInfo, - farcasterUserDataObj, }; } diff --git a/apps/u3/src/route/nav.tsx b/apps/u3/src/route/nav.tsx deleted file mode 100644 index 88ad0d4f..00000000 --- a/apps/u3/src/route/nav.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * @Author: shixuewen friendlysxw@163.com - * @Date: 2022-12-12 13:59:01 - * @LastEditors: bufan bufan@hotmail.com - * @LastEditTime: 2023-12-15 09:18:03 - * @Description: file description - */ -import React, { ReactNode } from 'react'; -import { isMobile } from 'react-device-detect'; -import { ReactComponent as CompassSvg } from './svgs/compass.svg'; -import { ReactComponent as NewsSvg } from './svgs/news.svg'; -import { ReactComponent as SocialSvg } from './svgs/social.svg'; -import { ReactComponent as DappSvg } from './svgs/dapp.svg'; -import { ReactComponent as BookmarkSvg } from './svgs/bookmark.svg'; -import { ReactComponent as BellSvg } from './svgs/bell.svg'; - -import { CutomRouteObject, getRoute, RouteKey } from './routes'; -import NotificationButton from '@/components/notification/NotificationNavBtn'; -import MobileNotificationNavBtn from '@/components/notification/MobileNotificationNavBtn'; - -export type CustomNavObject = { - name: string; - activeRouteKeys: RouteKey[]; // 指定哪些路由key下,该nav被激活(可用来高亮显示,有子菜单展开子菜单等...) - icon?: ReactNode; - children?: CustomNavObject[]; - key?: string; - route?: CutomRouteObject; - component?: JSX.Element; -}; -const navMap = { - explore: { - name: 'Explore', - activeRouteKeys: [RouteKey.home], - icon: React.createElement(CompassSvg), - route: getRoute(RouteKey.home), - }, - news: { - name: 'Apps', - activeRouteKeys: [ - RouteKey.newsLayout, - RouteKey.links, - RouteKey.link, - RouteKey.contents, - RouteKey.content, - RouteKey.contentCreate, - RouteKey.events, - RouteKey.event, - RouteKey.eventCreate, - RouteKey.eventEdit, - ], - icon: React.createElement(NewsSvg), - route: getRoute(RouteKey.newsLayout), - }, - social: { - name: 'Social', - activeRouteKeys: [ - RouteKey.social, - RouteKey.socialLayout, - RouteKey.socialPostDetailFcast, - RouteKey.socialPostDetailLens, - ], - icon: React.createElement(SocialSvg), - route: getRoute(RouteKey.socialLayout), - }, - notifications: { - name: 'notifications', - activeRouteKeys: [RouteKey.notification], - component: isMobile ? ( - <MobileNotificationNavBtn key={RouteKey.notification} /> - ) : ( - <NotificationButton key={RouteKey.notification} /> - ), - }, - apps: { - name: 'Apps', - activeRouteKeys: [RouteKey.dappStore, RouteKey.dapp], - icon: React.createElement(DappSvg), - route: getRoute(RouteKey.dappStore), - }, - save: { - name: 'Save', - activeRouteKeys: [RouteKey.save], - icon: React.createElement(BookmarkSvg), - route: getRoute(RouteKey.save), - }, -}; -export const navs: CustomNavObject[] = [ - ...(isMobile - ? [ - navMap.explore, - navMap.social, - navMap.news, - navMap.notifications, - navMap.apps, - navMap.save, - ] - : [ - navMap.explore, - navMap.news, - navMap.social, - navMap.notifications, - navMap.save, - ]), - // { - // name: 'profile', - // activeRouteKeys: [RouteKey.profile], - // icon: React.createElement(UserCircleSvg), - // route: getRoute(RouteKey.profile), - // }, - // { - // name: 'activity', - // activeRouteKeys: [RouteKey.activity], - // icon: React.createElement(LineChartUpSvg), - // route: getRoute(RouteKey.activity), - // }, - // { - // name: 'asset', - // activeRouteKeys: [RouteKey.asset], - // icon: React.createElement(WalletSvg), - // route: getRoute(RouteKey.asset), - // }, - // { - // name: 'gallery', - // activeRouteKeys: [RouteKey.gallery], - // icon: React.createElement(ImageSvg), - // route: getRoute(RouteKey.gallery), - // }, -]; diff --git a/apps/u3/src/route/path.ts b/apps/u3/src/route/path.ts index 69323b7a..eb3d70d3 100644 --- a/apps/u3/src/route/path.ts +++ b/apps/u3/src/route/path.ts @@ -1,10 +1,23 @@ -import { isMobile } from 'react-device-detect'; +// explore +export const getExploreFcPostDetailPath = (postId: string | number) => + `/social/post-detail/fcast/${postId}`; +export const getExploreLensPostDetailPath = (postId: string | number) => + `/social/post-detail/lens/${postId}`; + +// community export const getCommunityPath = (communityId: string) => - isMobile ? `/social/channel/${communityId}` : `/community/${communityId}`; + `/community/${communityId}`; +export const isCommunityPath = (path: string) => path.includes('/community/'); export const getCommunityPostsPath = (communityId: string) => `/community/${communityId}/posts`; +export const getCommunityLinksPath = (communityId: string) => + `/community/${communityId}/links`; +export const isCommunityLinksPath = (path: string) => { + const p = path.split('/'); + return p[1] === 'community' && p[3] === 'links'; +}; export const getCommunityNftPath = (communityId: string, contract: string) => `/community/${communityId}/nft/${contract}`; diff --git a/apps/u3/src/route/routes.tsx b/apps/u3/src/route/routes.tsx index ee1a8b4d..25a0f6ee 100644 --- a/apps/u3/src/route/routes.tsx +++ b/apps/u3/src/route/routes.tsx @@ -15,9 +15,11 @@ export enum RouteKey { home = 'home', // poster posterGallery = 'posterGallery', + casterDaily = 'casterDaily', // profile profile = 'profile', profileByUser = 'profileByUser', + contacts = 'contacts', asset = 'asset', gallery = 'gallery', activity = 'activity', @@ -26,12 +28,17 @@ export enum RouteKey { farcasterSignup = 'farcasterSignup', farcasterProfile = 'farcasterProfile', // community + communities = 'communities', + trendingCommunities = 'trendingCommunities', + newestCommunities = 'newestCommunities', + joinedCommunities = 'joinedCommunities', community = 'community', communityPostsLayout = 'communityPostsLayout', communityPostsFcTrending = 'communityPostsFcTrending', communityPostsFcNewest = 'communityPostsFcNewest', communityPostFcDetail = 'communityPostFcDetail', communityMembers = 'communityMembers', + communityLinks = 'communityLinks', iframeLayout = 'iframeLayout', // news newsLayout = 'newsLayout', @@ -69,10 +76,17 @@ export enum RouteKey { dappStore = 'dappStore', dapp = 'dapp', dappCreate = 'dappCreate', - // save - save = 'save', + // fav + fav = 'fav', + favPosts = 'favPosts', + favLinks = 'favLinks', + favApps = 'favApps', // notification notification = 'notification', + notificationActivity = 'notificationActivity', + notificationMention = 'notificationMention', + // message + message = 'message', // others noMatch = 'noMatch', policy = 'policy', @@ -108,24 +122,321 @@ export const NoMatchRoute: CutomRouteObject = { export const routes: CutomRouteObject[] = [ { path: '/', - element: loadContainerElement('Explore'), + element: loadContainerElement('explore/ExploreLayout'), key: RouteKey.home, title: 'Explore', + children: [ + { + path: '', + element: loadContainerElement('explore/Home'), + key: RouteKey.home, + title: 'Explore', + }, + { + path: '/poster-gallery', + element: loadContainerElement('poster/PosterGallery'), + key: RouteKey.posterGallery, + title: 'Poster Gallery', + } as CutomRouteObject, + { + path: '/caster-daily', + element: loadContainerElement('poster/CasterDaily'), + key: RouteKey.casterDaily, + title: 'Caster Daily', + } as CutomRouteObject, + { + path: 'communities', + element: loadContainerElement('community/Communities'), + key: RouteKey.communities, + title: 'Communities', + children: [ + { + path: '', + element: <Navigate to="trending" />, + key: RouteKey.trendingCommunities, + } as CutomRouteObject, + { + path: 'trending', + element: loadContainerElement('community/CommunitiesTrending'), + key: RouteKey.trendingCommunities, + title: 'Trending Communities', + }, + { + path: 'newest', + element: loadContainerElement('community/CommunitiesNewest'), + key: RouteKey.newestCommunities, + title: 'Newest Communities', + }, + { + path: 'joined', + element: loadContainerElement('community/CommunitiesJoined'), + key: RouteKey.joinedCommunities, + title: 'Joined Communities', + permissions: [RoutePermission.login], + }, + ], + }, + // social + { + path: '/social', + element: loadContainerElement('social/SocialLayout'), + key: RouteKey.socialLayout, + title: 'Social', + children: [ + { + path: '', + element: <Navigate to="all" />, + key: RouteKey.home, + } as CutomRouteObject, + { + path: 'all', // social allPlatform + element: loadContainerElement('social/SocialAll'), + key: RouteKey.socialAll, + children: [ + { + path: '', // default trending + element: loadContainerElement('social/SocialAllTrending'), + key: RouteKey.socialAllTrending, + }, + { + path: 'following', + element: loadContainerElement('social/SocialAllFollowing'), + key: RouteKey.socialAllFollowing, + } as CutomRouteObject, + { + path: 'whatsnew', + element: loadContainerElement('social/SocialAllWhatsnew'), + key: RouteKey.socialAllWhatsnew, + } as CutomRouteObject, + ], + }, + { + path: 'farcaster', + element: loadContainerElement('social/SocialFarcaster'), + key: RouteKey.socialFarcaster, + children: [ + { + path: '', // default trending + element: loadContainerElement('social/SocialFarcasterTrending'), + key: RouteKey.socialFarcasterTrending, + }, + { + path: 'following', + element: loadContainerElement( + 'social/SocialFarcasterFollowing' + ), + key: RouteKey.socialFarcasterFollowing, + } as CutomRouteObject, + { + path: 'whatsnew', + element: loadContainerElement('social/SocialFarcasterWhatsnew'), + key: RouteKey.socialFarcasterWhatsnew, + }, + ], + }, + { + path: 'lens', // social Lens platform + element: loadContainerElement('social/SocialLens'), + key: RouteKey.socialLens, + children: [ + { + path: '', // default trending + element: loadContainerElement('social/SocialLensTrending'), + key: RouteKey.socialLensTrending, + }, + { + path: 'following', + element: loadContainerElement('social/SocialLensFollowing'), + key: RouteKey.socialLensFollowing, + } as CutomRouteObject, + { + path: 'whatsnew', + element: loadContainerElement('social/SocialLensWhatsnew'), + key: RouteKey.socialLensWhatsnew, + }, + ], + }, + { + path: 'channel/:channelId', + element: loadContainerElement('social/SocialChannel'), + key: RouteKey.socialChannel, + }, + { + path: 'post-detail/lens/:publicationId', + element: loadContainerElement('social/LensPostDetail'), + key: RouteKey.socialPostDetailLens, + } as CutomRouteObject, + { + path: 'post-detail/fcast/:castId', + element: loadContainerElement('social/FarcasterPostDetail'), + key: RouteKey.socialPostDetailFcast, + }, + { + path: 'suggest-follow', + element: loadContainerElement('social/SocialSuggestFollow'), + key: RouteKey.socialSuggestFollow, + }, + ], + }, + ], }, - // poster - { - path: '/poster-gallery', - element: loadContainerElement('PosterGallery'), - key: RouteKey.posterGallery, - title: 'Poster Gallery', - }, + // profile { path: '/u', - element: loadContainerElement('profile/Profile'), + element: loadContainerElement('profile/ProfileLayout'), key: RouteKey.profile, permissions: [RoutePermission.login], title: 'Profile', + children: [ + { + path: '', + element: loadContainerElement('profile/Posts'), + key: RouteKey.profile, + permissions: [RoutePermission.login], + title: 'Posts', + }, + { + path: 'contacts', + element: loadContainerElement('profile/Contacts'), + key: RouteKey.contacts, + title: 'Contacts', + permissions: [RoutePermission.login], + }, + { + path: 'fav', + element: loadContainerElement('profile/Fav'), + key: RouteKey.fav, + title: 'Favorites', + permissions: [RoutePermission.login], + }, + { + path: 'activity', + element: loadContainerElement('profile/Activity'), + key: RouteKey.activity, + permissions: [RoutePermission.login], + title: 'Activity', + }, + { + path: 'asset', + element: loadContainerElement('profile/Asset'), + key: RouteKey.asset, + permissions: [RoutePermission.login], + title: 'Asset', + }, + { + path: 'gallery', + element: loadContainerElement('profile/Gallery'), + key: RouteKey.gallery, + permissions: [RoutePermission.login], + title: 'Gallery', + }, + { + path: ':user', + element: loadContainerElement('profile/Posts'), + key: RouteKey.profileByUser, + title: 'Posts', + } as CutomRouteObject, + { + path: 'contacts/:user', + element: loadContainerElement('profile/Contacts'), + key: RouteKey.contacts, + title: 'Contacts', + } as CutomRouteObject, + { + path: 'fav/:user', + element: loadContainerElement('profile/Fav'), + key: RouteKey.fav, + title: 'Favorites', + } as CutomRouteObject, + { + path: 'activity/:user', + element: loadContainerElement('profile/Activity'), + key: RouteKey.activity, + title: 'Activity', + } as CutomRouteObject, + { + path: 'asset/:user', + element: loadContainerElement('profile/Asset'), + key: RouteKey.asset, + title: 'Asset', + } as CutomRouteObject, + { + path: 'gallery/:user', + element: loadContainerElement('profile/Gallery'), + key: RouteKey.gallery, + title: 'Gallery', + } as CutomRouteObject, + ], + }, + { + path: '/policy', + element: loadContainerElement('Policy'), + key: RouteKey.policy, + }, + // fav + { + path: '/fav', + element: loadContainerElement('fav/FavLayout'), + key: RouteKey.fav, + permissions: [RoutePermission.login], + title: 'Favorites', + children: [ + { + path: 'posts', + element: loadContainerElement('fav/Fav'), + key: RouteKey.favPosts, + title: 'Favorite Posts', + }, + { + path: 'links', + element: loadContainerElement('fav/Fav'), + key: RouteKey.favLinks, + title: 'Favorite Links', + }, + { + path: 'apps', + element: loadContainerElement('fav/Apps'), + key: RouteKey.favApps, + title: 'Favorite Apps', + }, + ], + }, + // notification + { + path: '/notification', + element: loadContainerElement('notification/NotificationLayout'), + key: RouteKey.notification, + permissions: [RoutePermission.login], + title: 'Notifications', + children: [ + { + path: '', + element: loadContainerElement('notification/Notification'), + key: RouteKey.notification, + title: 'Notifications', + }, + { + path: 'activity', + element: loadContainerElement('notification/Notification'), + key: RouteKey.notificationActivity, + title: 'Notifications', + }, + { + path: 'mention', + element: loadContainerElement('notification/Notification'), + key: RouteKey.notificationMention, + title: 'Notifications', + }, + ], + }, + // message + { + path: '/message', + element: loadContainerElement('message/MessageLayout'), + key: RouteKey.message, + title: 'Message', + permissions: [RoutePermission.login], }, // community { @@ -183,6 +494,12 @@ export const routes: CutomRouteObject[] = [ key: RouteKey.communityMembers, title: 'Members', }, + { + path: 'links', + element: loadContainerElement('community/PostsFcMentionedLinks'), + key: RouteKey.communityLinks, + title: 'Links', + }, { path: 'point', element: loadContainerElement('community/IframeLayout'), @@ -197,33 +514,6 @@ export const routes: CutomRouteObject[] = [ }, ], }, - { - path: '/u/:user', - element: loadContainerElement('profile/Profile'), - key: RouteKey.profileByUser, - title: 'Profile', - }, - { - path: '/activity', - element: loadContainerElement('Activity'), - key: RouteKey.activity, - permissions: [RoutePermission.login], - title: 'Activity', - }, - { - path: '/asset', - element: loadContainerElement('Asset'), - key: RouteKey.asset, - permissions: [RoutePermission.login], - title: 'Asset', - }, - { - path: '/gallery', - element: loadContainerElement('Gallery'), - key: RouteKey.gallery, - permissions: [RoutePermission.login], - title: 'Gallery', - }, // news { path: '/b', @@ -296,111 +586,6 @@ export const routes: CutomRouteObject[] = [ }, ], }, - // social - { - path: '/social', - element: loadContainerElement('social/SocialLayout'), - key: RouteKey.socialLayout, - title: 'Social', - children: [ - { - path: '', - element: <Navigate to="all" />, - key: RouteKey.home, - } as CutomRouteObject, - { - path: 'all', // social allPlatform - element: loadContainerElement('social/SocialAll'), - key: RouteKey.socialAll, - children: [ - { - path: '', // default trending - element: loadContainerElement('social/SocialAllTrending'), - key: RouteKey.socialAllTrending, - }, - { - path: 'following', - element: loadContainerElement('social/SocialAllFollowing'), - key: RouteKey.socialAllFollowing, - } as CutomRouteObject, - { - path: 'whatsnew', - element: loadContainerElement('social/SocialAllWhatsnew'), - key: RouteKey.socialAllWhatsnew, - } as CutomRouteObject, - ], - }, - { - path: 'farcaster', - element: loadContainerElement('social/SocialFarcaster'), - key: RouteKey.socialFarcaster, - children: [ - { - path: '', // default trending - element: loadContainerElement('social/SocialFarcasterTrending'), - key: RouteKey.socialFarcasterTrending, - }, - { - path: 'following', - element: loadContainerElement('social/SocialFarcasterFollowing'), - key: RouteKey.socialFarcasterFollowing, - } as CutomRouteObject, - { - path: 'whatsnew', - element: loadContainerElement('social/SocialFarcasterWhatsnew'), - key: RouteKey.socialFarcasterWhatsnew, - }, - ], - }, - { - path: 'lens', // social Lens platform - element: loadContainerElement('social/SocialLens'), - key: RouteKey.socialLens, - children: [ - { - path: '', // default trending - element: loadContainerElement('social/SocialLensTrending'), - key: RouteKey.socialLensTrending, - }, - { - path: 'following', - element: loadContainerElement('social/SocialLensFollowing'), - key: RouteKey.socialLensFollowing, - } as CutomRouteObject, - { - path: 'whatsnew', - element: loadContainerElement('social/SocialLensWhatsnew'), - key: RouteKey.socialLensWhatsnew, - }, - ], - }, - { - path: 'trends', - element: loadContainerElement('social/SocialTrends'), - key: RouteKey.socialTrendsChannel, - }, - { - path: 'channel/:channelId', - element: loadContainerElement('social/SocialChannel'), - key: RouteKey.socialChannel, - }, - { - path: 'post-detail/lens/:publicationId', - element: loadContainerElement('social/LensPostDetail'), - key: RouteKey.socialPostDetailLens, - } as CutomRouteObject, - { - path: 'post-detail/fcast/:castId', - element: loadContainerElement('social/FarcasterPostDetail'), - key: RouteKey.socialPostDetailFcast, - }, - { - path: 'suggest-follow', - element: loadContainerElement('social/SocialSuggestFollow'), - key: RouteKey.socialSuggestFollow, - }, - ], - }, { path: '/farcaster', element: loadContainerElement('social/FarcasterLayout'), @@ -442,25 +627,6 @@ export const routes: CutomRouteObject[] = [ key: RouteKey.dappCreate, permissions: [RoutePermission.login, RoutePermission.admin], }, - // save - { - path: '/save', - element: loadContainerElement('Save'), - key: RouteKey.save, - permissions: [RoutePermission.login], - }, - { - path: '/policy', - element: loadContainerElement('Policy'), - key: RouteKey.policy, - }, - // notification - { - path: '/notification', - element: loadContainerElement('Notification'), - key: RouteKey.notification, - title: 'Notifications', - }, NoMatchRoute, // deprecated diff --git a/apps/u3/src/route/svgs/bell.svg b/apps/u3/src/route/svgs/bell.svg deleted file mode 100644 index 6cb6d187..00000000 --- a/apps/u3/src/route/svgs/bell.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="15" height="17" viewBox="0 0 15 17" fill="none"> - <path d="M5.84634 14.5C6.28705 14.9149 6.86595 15.1666 7.49998 15.1666C8.13401 15.1666 8.71291 14.9149 9.15362 14.5M11.25 5.83331C11.25 4.77245 10.8549 3.75503 10.1516 3.00489C9.44837 2.25474 8.49454 1.83331 7.49998 1.83331C6.50542 1.83331 5.55159 2.25474 4.84833 3.00489C4.14507 3.75503 3.74998 4.77245 3.74998 5.83331C3.74998 7.89344 3.26277 9.30395 2.71852 10.2369C2.25944 11.0239 2.02989 11.4174 2.03831 11.5272C2.04763 11.6487 2.07177 11.695 2.16359 11.7677C2.24652 11.8333 2.62035 11.8333 3.36802 11.8333H11.6319C12.3796 11.8333 12.7534 11.8333 12.8364 11.7677C12.9282 11.695 12.9523 11.6487 12.9617 11.5272C12.9701 11.4174 12.7405 11.0239 12.2814 10.2369C11.7372 9.30395 11.25 7.89344 11.25 5.83331Z" stroke="#718096" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/bookmark.svg b/apps/u3/src/route/svgs/bookmark.svg deleted file mode 100644 index c13c84fe..00000000 --- a/apps/u3/src/route/svgs/bookmark.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none"> - <g clip-path="url(#clip0_2286_23151)"> - <path d="M12.0853 16.2099C11.8603 16.2099 11.6073 16.1453 11.3729 16.028L8.02454 14.3533L4.6555 16.0253C4.44214 16.1389 4.1979 16.1986 3.94886 16.1986C3.62524 16.1998 3.30986 16.0966 3.04965 15.9042C2.57814 15.5504 2.33653 14.9378 2.44806 14.3798L3.13711 10.8052L0.496058 8.3437C0.295623 8.14062 0.15216 7.88837 0.0800791 7.61229C0.00799854 7.33622 0.00983978 7.04603 0.0854178 6.77089L0.0906991 6.75273C0.282699 6.17729 0.759012 5.77409 1.33654 5.69673L5.0059 5.02954L6.65286 1.68456C6.91742 1.15495 7.45551 0.814636 8.02454 0.814636C8.61814 0.814636 9.17014 1.16776 9.40157 1.69417L11.0433 5.02854L14.7132 5.67017C14.9983 5.71148 15.2654 5.83429 15.4823 6.02382C15.6993 6.21335 15.8568 6.46153 15.9361 6.7385C16.0249 7.01114 16.0362 7.30312 15.9687 7.58181C15.9011 7.8605 15.7574 8.11492 15.5537 8.31665L15.5446 8.3257L12.911 10.8057L13.5766 14.3867C13.6838 14.9593 13.4561 15.5378 12.9819 15.8982C12.7279 16.1024 12.4112 16.2125 12.0853 16.2099ZM8.02581 13.1465L11.5831 14.9255C11.6876 14.9781 11.808 15.0112 11.8904 15.0112C12.0172 15.0112 12.1349 14.9722 12.2214 14.9L12.2375 14.8867C12.4397 14.7351 12.5382 14.494 12.494 14.258L11.7781 10.4017L14.6096 7.73529C14.7834 7.55904 14.8429 7.30665 14.7657 7.07457L14.758 7.05017C14.7275 6.94019 14.6654 6.84159 14.5793 6.76659C14.4933 6.69159 14.3872 6.64348 14.2741 6.62825L14.2579 6.6257L10.3401 5.94079L8.57678 2.35923C8.49118 2.15915 8.25886 2.01395 8.02429 2.01395C7.79382 2.01395 7.57334 2.15826 7.46165 2.38107L5.70965 5.93922L1.77118 6.65465C1.53196 6.68465 1.34614 6.84353 1.26029 7.09129C1.19934 7.3177 1.27157 7.5905 1.43798 7.76057L4.2727 10.4021L3.52879 14.2604C3.48406 14.4835 3.58973 14.7403 3.78518 14.887C3.88318 14.9607 4.01014 15.0013 4.14326 15.0013C4.2459 15.0013 4.34429 14.9774 4.42911 14.9318L4.44134 14.9246L8.02581 13.1465Z" fill="#718096"/> - </g> - <defs> - <clipPath id="clip0_2286_23151"> - <rect width="16" height="16" fill="white" transform="translate(0 0.5)"/> - </clipPath> - </defs> -</svg> diff --git a/apps/u3/src/route/svgs/compass.svg b/apps/u3/src/route/svgs/compass.svg deleted file mode 100644 index 0374b6ba..00000000 --- a/apps/u3/src/route/svgs/compass.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none"> - <path d="M8 15.1666C11.6819 15.1666 14.6667 12.1819 14.6667 8.49998C14.6667 4.81808 11.6819 1.83331 8 1.83331C4.3181 1.83331 1.33333 4.81808 1.33333 8.49998C1.33333 12.1819 4.3181 15.1666 8 15.1666Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> - <path d="M9.81473 6.01062C10.1404 5.90206 10.3033 5.84777 10.4116 5.88639C10.5058 5.92 10.58 5.99416 10.6136 6.0884C10.6522 6.19669 10.5979 6.35954 10.4894 6.68524L9.49766 9.66033C9.46674 9.75309 9.45128 9.79947 9.42493 9.83799C9.40159 9.8721 9.37212 9.90158 9.338 9.92491C9.29949 9.95126 9.25311 9.96672 9.16035 9.99764L6.18526 10.9893C5.85956 11.0979 5.69671 11.1522 5.58841 11.1136C5.49417 11.08 5.42001 11.0058 5.38641 10.9116C5.34779 10.8033 5.40207 10.6404 5.51064 10.3147L6.50233 7.33963C6.53325 7.24687 6.54871 7.20049 6.57506 7.16197C6.5984 7.12786 6.62787 7.09838 6.66199 7.07505C6.7005 7.0487 6.74688 7.03324 6.83964 7.00232L9.81473 6.01062Z" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/dapp.svg b/apps/u3/src/route/svgs/dapp.svg deleted file mode 100644 index 8055aea0..00000000 --- a/apps/u3/src/route/svgs/dapp.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.9172 2.27344H9.80314C9.26251 2.27344 8.82345 2.7125 8.82345 3.25312V7.36719C8.82345 7.90781 9.26251 8.34688 9.80314 8.34688H13.9172C14.4578 8.34688 14.8969 7.92656 14.8969 7.41094V6.66094C14.8969 6.39062 14.6781 6.17031 14.4063 6.17031C14.136 6.17031 13.9156 6.38906 13.9156 6.66094V7.36875H9.80314V3.25469H13.9172V3.69375C13.9172 3.96406 14.1359 4.18438 14.4078 4.18438C14.6781 4.18438 14.8984 3.96562 14.8984 3.69375V3.19844C14.8984 2.68906 14.4578 2.27344 13.9172 2.27344ZM7.29689 2.27344H3.18126C2.64064 2.27344 2.20157 2.7125 2.20157 3.25312V7.36719C2.20157 7.90781 2.64064 8.34688 3.18126 8.34688H7.29689C7.83751 8.34688 8.27658 7.90781 8.27658 7.36719V3.25469C8.27658 2.71406 7.83751 2.27344 7.29689 2.27344ZM7.29689 7.36875H3.18126V3.25469H7.29689V7.36875ZM7.24689 8.84219H3.13282C2.5922 8.84219 2.15314 9.28125 2.15314 9.82187V13.9375C2.15314 14.4781 2.5922 14.9172 3.13282 14.9172H7.24689C7.78751 14.9172 8.22657 14.4781 8.22657 13.9375V9.82344C8.22814 9.28281 7.78751 8.84219 7.24689 8.84219ZM7.24689 13.9375H3.13282V9.82344H7.24689V13.9375ZM13.8688 8.84219H9.75314C9.21251 8.84219 8.77345 9.28125 8.77345 9.82187V13.9375C8.77345 14.4781 9.21251 14.9172 9.75314 14.9172H13.8672C14.4078 14.9172 14.8469 14.4781 14.8469 13.9375V9.82344C14.8484 9.28281 14.4094 8.84219 13.8688 8.84219ZM13.8688 13.9375H9.75314V9.82344H13.8672V13.9375H13.8688Z" fill="#718096"/> -<path d="M13.9172 5.16563C13.9172 5.23006 13.9299 5.29386 13.9545 5.35338C13.9792 5.41291 14.0153 5.46699 14.0609 5.51255C14.1064 5.55811 14.1605 5.59425 14.22 5.61891C14.2796 5.64356 14.3434 5.65625 14.4078 5.65625C14.4722 5.65625 14.536 5.64356 14.5956 5.61891C14.6551 5.59425 14.7092 5.55811 14.7547 5.51255C14.8003 5.46699 14.8364 5.41291 14.8611 5.35338C14.8857 5.29386 14.8984 5.23006 14.8984 5.16563C14.8984 5.1012 14.8857 5.0374 14.8611 4.97787C14.8364 4.91835 14.8003 4.86426 14.7547 4.8187C14.7092 4.77314 14.6551 4.73701 14.5956 4.71235C14.536 4.68769 14.4722 4.675 14.4078 4.675C14.3434 4.675 14.2796 4.68769 14.22 4.71235C14.1605 4.73701 14.1064 4.77314 14.0609 4.8187C14.0153 4.86426 13.9792 4.91835 13.9545 4.97787C13.9299 5.0374 13.9172 5.1012 13.9172 5.16563Z" fill="#718096"/> -</svg> diff --git a/apps/u3/src/route/svgs/heart.svg b/apps/u3/src/route/svgs/heart.svg deleted file mode 100644 index d8f7c25f..00000000 --- a/apps/u3/src/route/svgs/heart.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.49543 3.42388C7.16253 1.8656 4.93983 1.44643 3.26979 2.87334C1.59976 4.30026 1.36464 6.68598 2.67613 8.3736C3.76654 9.77674 7.06651 12.7361 8.14806 13.6939C8.26906 13.801 8.32957 13.8546 8.40014 13.8757C8.46173 13.8941 8.52913 13.8941 8.59072 13.8757C8.66129 13.8546 8.72179 13.801 8.8428 13.6939C9.92435 12.7361 13.2243 9.77674 14.3147 8.3736C15.6262 6.68598 15.4198 4.28525 13.7211 2.87334C12.0223 1.46144 9.82833 1.8656 8.49543 3.42388Z" stroke="#718096" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/home.svg b/apps/u3/src/route/svgs/home.svg deleted file mode 100644 index e91de6d8..00000000 --- a/apps/u3/src/route/svgs/home.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none"> - <path d="M6.5 10.667C7.06692 11.0872 7.7564 11.3337 8.5 11.3337C9.2436 11.3337 9.93307 11.0872 10.5 10.667" - stroke="#718096" stroke-linecap="round"/> - <path d="M14.9238 8.63827L14.738 9.92981C14.413 12.1881 14.2506 13.3173 13.4672 13.9918C12.6838 14.6663 11.535 14.6663 9.23732 14.6663H7.76252C5.46483 14.6663 4.31598 14.6663 3.5326 13.9918C2.74922 13.3173 2.58675 12.1881 2.26181 9.92981L2.07596 8.63827C1.823 6.88021 1.69652 6.0012 2.05683 5.24964C2.41714 4.49807 3.18404 4.04123 4.71786 3.12755L5.64108 2.57759C7.03394 1.74787 7.73038 1.33301 8.49992 1.33301C9.26945 1.33301 9.96592 1.74787 11.3587 2.57759L12.282 3.12755C13.8158 4.04123 14.5827 4.49807 14.943 5.24964" stroke="#718096" stroke-linecap="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/image.svg b/apps/u3/src/route/svgs/image.svg deleted file mode 100644 index 2e4c7052..00000000 --- a/apps/u3/src/route/svgs/image.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.3 14H5.12091C4.71704 14 4.5151 14 4.42159 13.9201C4.34045 13.8508 4.29739 13.7469 4.30577 13.6405C4.31541 13.5179 4.45821 13.3751 4.74379 13.0895L10.4124 7.42091C10.6764 7.15691 10.8084 7.0249 10.9607 6.97544C11.0946 6.93193 11.2388 6.93193 11.3727 6.97544C11.5249 7.0249 11.6569 7.1569 11.9209 7.42091L14.5 10V10.8M11.3 14C12.4201 14 12.9802 14 13.408 13.782C13.7843 13.5903 14.0903 13.2843 14.282 12.908C14.5 12.4802 14.5 11.9201 14.5 10.8M11.3 14H5.7C4.5799 14 4.01984 14 3.59202 13.782C3.21569 13.5903 2.90973 13.2843 2.71799 12.908C2.5 12.4802 2.5 11.9201 2.5 10.8V5.2C2.5 4.0799 2.5 3.51984 2.71799 3.09202C2.90973 2.71569 3.21569 2.40973 3.59202 2.21799C4.01984 2 4.5799 2 5.7 2H11.3C12.4201 2 12.9802 2 13.408 2.21799C13.7843 2.40973 14.0903 2.71569 14.282 3.09202C14.5 3.51984 14.5 4.0799 14.5 5.2V10.8M7.5 5.66667C7.5 6.40305 6.90305 7 6.16667 7C5.43029 7 4.83333 6.40305 4.83333 5.66667C4.83333 4.93029 5.43029 4.33333 6.16667 4.33333C6.90305 4.33333 7.5 4.93029 7.5 5.66667Z" stroke="#718096" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/line-chart-up.svg b/apps/u3/src/route/svgs/line-chart-up.svg deleted file mode 100644 index 73c59cc3..00000000 --- a/apps/u3/src/route/svgs/line-chart-up.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.5 14H3.56667C3.1933 14 3.00661 14 2.86401 13.9273C2.73856 13.8634 2.63658 13.7614 2.57266 13.636C2.5 13.4934 2.5 13.3067 2.5 12.9333V2M13.8333 5.33333L11.2208 8.12177C11.1217 8.22745 11.0722 8.28029 11.0125 8.3076C10.9598 8.3317 10.9017 8.34164 10.844 8.33644C10.7786 8.33055 10.7143 8.29718 10.5858 8.23045L8.41421 7.10288C8.28569 7.03615 8.22143 7.00278 8.15602 6.99689C8.09829 6.99169 8.04021 7.00163 7.98749 7.02574C7.92777 7.05305 7.87826 7.10589 7.77925 7.21156L5.16667 10" stroke="#718096" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/news.svg b/apps/u3/src/route/svgs/news.svg deleted file mode 100644 index 51e3ba7c..00000000 --- a/apps/u3/src/route/svgs/news.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none"> - <g clip-path="url(#clip0_2285_23108)"> - <path d="M12.9762 0.5H12.0159C11.778 0.5 11.5832 0.69479 11.5832 0.932616C11.5832 1.17044 11.778 1.36523 12.0159 1.36523H12.9762C13.9275 1.36523 14.7022 2.13986 14.7022 3.09343V13.9066C14.7022 14.8601 13.9253 15.6348 12.9717 15.6348H2.59343C1.63986 15.6348 0.865232 14.8601 0.865232 13.9066V3.09343C0.865232 2.13986 1.63986 1.36523 2.59343 1.36523H3.61042C3.84824 1.36523 4.04304 1.17044 4.04304 0.932616C4.04304 0.69479 3.84824 0.5 3.61042 0.5H2.59343C1.16421 0.5 0 1.66421 0 3.09343V13.9066C0 15.3358 1.16421 16.5 2.59343 16.5H12.974C14.4032 16.5 15.5674 15.3358 15.5674 13.9066V3.09343C15.5651 1.66421 14.4032 0.5 12.9762 0.5Z" fill="#718096"/> - <path d="M6.51518 4.5338H9.12859C9.47199 4.5338 9.75032 4.30246 9.75032 4.0169C9.75032 3.73134 9.47199 3.5 9.12859 3.5H6.51518C6.17179 3.5 5.89346 3.73134 5.89346 4.0169C5.89165 4.30065 6.16998 4.5338 6.51518 4.5338ZM11.0155 7.81411H4.60302C4.54699 7.81411 4.5 7.76893 4.5 7.7111V7.15263C4.5 7.0966 4.54518 7.04961 4.60302 7.04961H11.0155C11.0715 7.04961 11.1185 7.09479 11.1185 7.15263V7.7111C11.1185 7.76893 11.0715 7.81411 11.0155 7.81411ZM11.0155 13.5H7.9484C7.89237 13.5 7.84538 13.4548 7.84538 13.397V12.8385C7.84538 12.7825 7.89057 12.7355 7.9484 12.7355H11.0155C11.0715 12.7355 11.1185 12.7807 11.1185 12.8385V13.397C11.1185 13.453 11.0715 13.5 11.0155 13.5ZM6.9634 13.5H4.65362C4.5976 13.5 4.55061 13.4548 4.55061 13.397V9.12443C4.55061 9.06841 4.59579 9.02142 4.65362 9.02142H6.9634C7.01943 9.02142 7.06642 9.0666 7.06642 9.12443V13.397C7.06642 13.453 7.01943 13.5 6.9634 13.5ZM11.0155 9.80038H7.9484C7.89237 9.80038 7.84538 9.7552 7.84538 9.69736V9.13889C7.84538 9.08287 7.89057 9.03587 7.9484 9.03587H11.0155C11.0715 9.03587 11.1185 9.08106 11.1185 9.13889V9.69736C11.1185 9.7552 11.0715 9.80038 11.0155 9.80038ZM11.0155 11.6565H7.9484C7.89237 11.6565 7.84538 11.6113 7.84538 11.5535V10.995C7.84538 10.939 7.89057 10.892 7.9484 10.892H11.0155C11.0715 10.892 11.1185 10.9372 11.1185 10.995V11.5535C11.1185 11.6113 11.0715 11.6565 11.0155 11.6565Z" fill="#718096"/> - </g> - <defs> - <clipPath id="clip0_2285_23108"> - <rect width="16" height="16" fill="white" transform="translate(0 0.5)"/> - </clipPath> - </defs> -</svg> diff --git a/apps/u3/src/route/svgs/social.svg b/apps/u3/src/route/svgs/social.svg deleted file mode 100644 index a59bb800..00000000 --- a/apps/u3/src/route/svgs/social.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none"> - <g clip-path="url(#clip0_2285_23112)"> - <path d="M10.7651 6.85058H9.95144C9.95144 7.38591 9.76582 7.84338 9.39453 8.223C9.02326 8.60261 8.57584 8.79242 8.05227 8.79242C7.88078 8.79242 7.71419 8.76813 7.55245 8.71955C7.39072 8.67097 7.24087 8.60527 7.10291 8.52242C6.96493 8.43958 6.83884 8.33978 6.72462 8.223C6.61039 8.1062 6.51279 7.97728 6.43176 7.83622C6.35076 7.69514 6.28648 7.53944 6.23897 7.36909C6.19147 7.19875 6.16771 7.02606 6.16771 6.85105H5.35403C5.35403 7.61994 5.6181 8.27439 6.14624 8.81437C6.67438 9.35439 7.31216 9.62438 8.05959 9.62438C8.80701 9.62438 9.44481 9.35439 9.97293 8.81437C10.5011 8.27437 10.7651 7.61994 10.7651 6.85105V6.85058H10.7651ZM15.8902 9.7262C15.8902 9.38552 15.845 9.05214 15.7545 8.72608C15.6641 8.40003 15.5356 8.09827 15.3689 7.8208C15.2023 7.54333 15.0024 7.29031 14.7691 7.06172C14.5369 6.83394 14.2751 6.63997 13.991 6.48528C13.9719 5.39531 13.6958 4.39288 13.1628 3.47792C12.6297 2.56297 11.9111 1.83802 11.0068 1.30298C10.1025 0.767969 9.11735 0.500312 8.05134 0.5C7.65143 0.5 7.25884 0.541422 6.87356 0.62425C6.48826 0.707094 6.11942 0.823875 5.76704 0.974609C5.41463 1.12533 5.07914 1.31031 4.76057 1.52956C4.44199 1.74881 4.15171 1.99216 3.8898 2.25969C3.62785 2.52719 3.38755 2.82164 3.16885 3.14302C2.95017 3.46439 2.76452 3.80259 2.61195 4.15762C2.45936 4.51264 2.34041 4.88727 2.25512 5.28153C2.16985 5.67578 2.12705 6.0772 2.12676 6.48578C1.54623 6.80684 1.08465 7.257 0.741999 7.83625C0.399351 8.41547 0.228027 9.04562 0.228027 9.7267C0.228027 10.5734 0.480222 11.3277 0.984597 11.9895C1.48897 12.6512 2.1362 13.0892 2.92628 13.3035L2.11257 15.975H2.92625L3.19762 15.143L3.92586 12.6467H3.9117C3.8925 12.6563 3.8782 12.6612 3.86874 12.6612C3.08812 12.6612 2.42185 12.3765 1.86996 11.8073C1.31808 11.238 1.04198 10.5543 1.04167 9.75609C1.04167 9.28897 1.144 8.85345 1.34868 8.44955C1.55337 8.04564 1.83174 7.70744 2.18383 7.43495C2.33612 8.53456 2.75019 9.51506 3.42604 10.3764C4.10191 11.2378 4.93947 11.8631 5.93879 12.2524L5.06802 15.143L4.8108 15.9605H5.62447L5.89586 15.143L7.05219 11.7269C5.87196 11.4837 4.88924 10.8803 4.10403 9.91678C3.31884 8.95327 2.92625 7.84384 2.92625 6.58852C2.92625 5.63464 3.15467 4.75395 3.61155 3.94644C4.06841 3.13894 4.69187 2.50145 5.48196 2.03402C6.27204 1.56658 7.12864 1.33303 8.05181 1.33333C8.61345 1.33333 9.15606 1.42098 9.67963 1.59631C10.2032 1.77166 10.6744 2.02219 11.0932 2.34794C11.512 2.67367 11.8784 3.0508 12.1924 3.47931C12.5064 3.90781 12.7514 4.38958 12.9275 4.92459C13.1035 5.45961 13.1916 6.01439 13.1916 6.58897C13.1916 7.8443 12.799 8.95372 12.0138 9.91725C11.2286 10.8808 10.2411 11.4841 9.05145 11.7274L10.2224 15.1435L10.4938 15.9609H11.2932L11.0506 15.1435L10.1799 12.2529C11.1697 11.8636 12.0049 11.2383 12.6853 10.3769C13.3657 9.51552 13.7821 8.53503 13.9344 7.43541C14.2865 7.70789 14.5649 8.04609 14.7695 8.45002C14.9742 8.85392 15.0765 9.28944 15.0765 9.75656C15.0765 10.5547 14.8004 11.2384 14.2482 11.8077C13.696 12.377 13.0298 12.6616 12.2495 12.6616C12.24 12.6616 12.2304 12.6568 12.2207 12.6471H12.1919L12.9201 15.1435L13.1915 15.9754H14.0052L13.1915 13.3039C13.9816 13.0896 14.6288 12.6516 15.1332 11.9899C15.6376 11.3281 15.8898 10.5739 15.8898 9.72711L15.8902 9.7262Z" fill="#718096"/> - </g> - <defs> - <clipPath id="clip0_2285_23112"> - <rect width="16" height="16" fill="white" transform="translate(0 0.5)"/> - </clipPath> - </defs> -</svg> diff --git a/apps/u3/src/route/svgs/user-circle.svg b/apps/u3/src/route/svgs/user-circle.svg deleted file mode 100644 index 2d9354ff..00000000 --- a/apps/u3/src/route/svgs/user-circle.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.04424 13.4596C4.44979 12.5041 5.39666 11.834 6.50004 11.834H10.5C11.6034 11.834 12.5503 12.5041 12.9558 13.4596M11.1667 6.83398C11.1667 8.30674 9.9728 9.50065 8.50004 9.50065C7.02728 9.50065 5.83337 8.30674 5.83337 6.83398C5.83337 5.36123 7.02728 4.16732 8.50004 4.16732C9.9728 4.16732 11.1667 5.36123 11.1667 6.83398ZM15.1667 8.50065C15.1667 12.1826 12.1819 15.1673 8.50004 15.1673C4.81814 15.1673 1.83337 12.1826 1.83337 8.50065C1.83337 4.81875 4.81814 1.83398 8.50004 1.83398C12.1819 1.83398 15.1667 4.81875 15.1667 8.50065Z" stroke="#718096" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/route/svgs/wallet.svg b/apps/u3/src/route/svgs/wallet.svg deleted file mode 100644 index 8f98fece..00000000 --- a/apps/u3/src/route/svgs/wallet.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.1667 5.3342V3.0013C11.1667 2.44681 11.1667 2.16956 11.0499 1.99918C10.9478 1.85032 10.7898 1.74919 10.6118 1.71892C10.4082 1.68426 10.1565 1.80044 9.653 2.03281L3.73934 4.76219C3.29034 4.96942 3.06583 5.07304 2.9014 5.23374C2.75604 5.37581 2.64508 5.54923 2.577 5.74075C2.5 5.95739 2.5 6.20465 2.5 6.69917V10.0009M11.5 9.66753H11.5067M2.5 7.46753L2.5 11.8675C2.5 12.6143 2.5 12.9876 2.64532 13.2728C2.77316 13.5237 2.97713 13.7277 3.22801 13.8555C3.51323 14.0009 3.8866 14.0009 4.63333 14.0009H12.3667C13.1134 14.0009 13.4868 14.0009 13.772 13.8555C14.0229 13.7277 14.2268 13.5237 14.3547 13.2728C14.5 12.9876 14.5 12.6143 14.5 11.8675V7.46753C14.5 6.72079 14.5 6.34742 14.3547 6.06221C14.2268 5.81133 14.0229 5.60735 13.772 5.47952C13.4868 5.3342 13.1134 5.3342 12.3667 5.3342L4.63333 5.33419C3.8866 5.33419 3.51323 5.3342 3.22801 5.47952C2.97713 5.60735 2.77316 5.81132 2.64532 6.06221C2.5 6.34742 2.5 6.72079 2.5 7.46753ZM11.8333 9.66753C11.8333 9.85162 11.6841 10.0009 11.5 10.0009C11.3159 10.0009 11.1667 9.85162 11.1667 9.66753C11.1667 9.48343 11.3159 9.33419 11.5 9.33419C11.6841 9.33419 11.8333 9.48343 11.8333 9.66753Z" stroke="#718096" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/apps/u3/src/services/community/api/community.ts b/apps/u3/src/services/community/api/community.ts index 41a35249..5e2dc0d4 100644 --- a/apps/u3/src/services/community/api/community.ts +++ b/apps/u3/src/services/community/api/community.ts @@ -1,15 +1,127 @@ import { CommunityEntity, CommunityStatistics, + CommunityTypeEntity, MemberEntity, } from '../types/community'; import request, { RequestPromise } from '../../shared/api/request'; import { ApiResp } from '@/services/shared/types'; -export type FetchCommunityResponse = CommunityEntity & CommunityStatistics; +export type CommunityTypesData = Array<CommunityTypeEntity>; +export function fetchCommunityTypes(): RequestPromise< + ApiResp<CommunityTypesData> +> { + return request({ + url: `/topics/community-types`, + method: 'get', + }); +} + +export type TrendingCommunitiesParams = { + pageSize?: number; + pageNumber?: number; + type?: string; +}; +export type TrendingCommunitiesData = Array< + CommunityEntity & CommunityStatistics +>; +export function fetchTrendingCommunities( + params: TrendingCommunitiesParams +): RequestPromise<ApiResp<TrendingCommunitiesData>> { + return request({ + url: `/topics/trending`, + method: 'get', + params, + }); +} + +export type NewestCommunitiesParams = { + pageSize?: number; + pageNumber?: number; + type?: string; +}; +export type NewestCommunitiesData = Array< + CommunityEntity & CommunityStatistics +>; +export function fetchNewestCommunities( + params: NewestCommunitiesParams +): RequestPromise<ApiResp<NewestCommunitiesData>> { + return request({ + url: `/topics/newest`, + method: 'get', + params, + }); +} + +export type JoinedCommunitiesParams = { + pageSize?: number; + pageNumber?: number; + type?: string; +}; +export type JoinedCommunitiesData = Array< + CommunityEntity & CommunityStatistics +>; +export function fetchJoinedCommunities( + params: JoinedCommunitiesParams +): RequestPromise<ApiResp<JoinedCommunitiesData>> { + return request({ + url: `/topics/joined`, + method: 'get', + params, + headers: { + needToken: true, + }, + }); +} + +export type GrowingCommunitiesParams = { + pageSize?: number; + pageNumber?: number; + type?: string; +}; +export type GrowingCommunitiesData = Array< + CommunityEntity & CommunityStatistics +>; +export function fetchGrowingCommunities( + params: GrowingCommunitiesParams +): RequestPromise<ApiResp<GrowingCommunitiesData>> { + return request({ + url: `/topics/trending`, + method: 'get', + params, + }); +} + +export type JoiningCommunityData = null; +export function fetchJoiningCommunity( + topicId: string | number +): RequestPromise<ApiResp<JoiningCommunityData>> { + return request({ + url: `/topics/${topicId}/joining`, + method: 'post', + headers: { + needToken: true, + }, + }); +} + +export type UnjoiningCommunityData = null; +export function fetchUnjoiningCommunity( + topicId: string | number +): RequestPromise<ApiResp<UnjoiningCommunityData>> { + return request({ + url: `/topics/${topicId}/unjoining`, + method: 'post', + headers: { + needToken: true, + }, + }); +} + +export type CommunityData = CommunityEntity & CommunityStatistics; export function fetchCommunity( id: string | number -): RequestPromise<ApiResp<FetchCommunityResponse>> { +): RequestPromise<ApiResp<CommunityData>> { return request({ url: `/topics/channel?id=${id}`, method: 'get', diff --git a/apps/u3/src/services/community/types/community.ts b/apps/u3/src/services/community/types/community.ts index 558de26c..64fc94ad 100644 --- a/apps/u3/src/services/community/types/community.ts +++ b/apps/u3/src/services/community/types/community.ts @@ -28,6 +28,7 @@ export type CommunityEntity = { channel_id: string; parent_url: string; }>; + channelId?: string; }; export type CommunityStatistics = { @@ -47,3 +48,10 @@ export type MemberEntity = { }; export type CommunityInfo = CommunityEntity & CommunityStatistics; + +export type CommunityTypeEntity = { + id: number; + type: string; + created_at: string; + last_modified_at: string; +}; diff --git a/apps/u3/src/services/notification/types/notifications.ts b/apps/u3/src/services/notification/types/notifications.ts new file mode 100644 index 00000000..36e39e5e --- /dev/null +++ b/apps/u3/src/services/notification/types/notifications.ts @@ -0,0 +1,6 @@ +export enum NotificationType { + FOLLOW = 'follow', + REACTION = 'reaction', + REPLY = 'reply', + MENTION = 'mention', +} diff --git a/apps/u3/src/services/social/api/farcaster.ts b/apps/u3/src/services/social/api/farcaster.ts index 415f0bf2..e7f5ca14 100644 --- a/apps/u3/src/services/social/api/farcaster.ts +++ b/apps/u3/src/services/social/api/farcaster.ts @@ -9,16 +9,19 @@ import { } from '../types'; import { REACT_APP_API_SOCIAL_URL } from '../../../constants'; import request from '@/services/shared/api/request'; +import { FollowType } from '@/container/profile/Contacts'; +import { NotificationType } from '@/services/notification/types/notifications'; // console.log({ REACT_APP_API_SOCIAL_URL }); - +export const PAGE_SIZE = 25; export type FarcasterNotification = { + type: NotificationType; message_fid: number; message_type: number; message_timestamp: string; message_hash: Buffer; userData: unknown; - reaction_type?: number; + reactions_type?: number; casts_id?: string; casts_hash?: Buffer; casts_text?: string; @@ -144,10 +147,12 @@ export function getFarcasterEmbedCast({ export function getFarcasterNotifications({ fid, + type, endFarcasterCursor, pageSize, }: { fid: number; + type: NotificationType[]; endFarcasterCursor?: string; pageSize?: number; }): AxiosPromise< @@ -162,6 +167,7 @@ export function getFarcasterNotifications({ method: 'get', params: { fid, + type, pageSize, next: endFarcasterCursor, withInfo: true, @@ -206,14 +212,15 @@ export function clearFarcasterUnreadNotification({ }); } -export function getFarcasterFollow(fid: string | number): AxiosPromise< +export function getFarcasterUserStats(fid: string | number): AxiosPromise< ApiResp<{ - followers: number; - following: number; + followerCount: number; + followingCount: number; + postCount: number; }> > { return axios({ - url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/follow`, + url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/statics`, method: 'get', params: { fid, @@ -221,24 +228,37 @@ export function getFarcasterFollow(fid: string | number): AxiosPromise< }); } -export function getFarcasterLinks( - fid: string | number, - withInfo = false -): AxiosPromise< +export type FarcasterLink = { + fid: number; + targetFid: number; + type: number; +}; + +export function getFarcasterUserLinks({ + fid, + type, + pageSize, + endCursor, +}: { + fid: string | number; + type: FollowType; + endCursor?: string; + pageSize?: number; +}): AxiosPromise< ApiResp<{ - followerCount: number; - followingCount: number; - followerData: string[]; - followingData: string[]; - farcasterUserData: { fid: string; type: number; value: string }[]; + links: FarcasterLink[]; + farcasterUserData: FarcasterUserData[]; + pageInfo: FarcasterPageInfo; }> > { return axios({ - url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/links`, + url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/followLinks`, method: 'get', params: { fid, - withInfo, + type, + pageSize, + endCursor, }, }); } diff --git a/apps/u3/src/services/social/types/index.ts b/apps/u3/src/services/social/types/index.ts index 36691356..82b6e089 100644 --- a/apps/u3/src/services/social/types/index.ts +++ b/apps/u3/src/services/social/types/index.ts @@ -94,6 +94,16 @@ export enum SocialPlatform { Lens = 'lens', } +export type PlatformAccountData = { + platform: SocialPlatform; + avatar: string; + name: string; + handle: string; + id: string | number; + bio: string; + address?: string; +}; + export type GalxeDataListItem = { id: string; name: string; diff --git a/apps/u3/src/store/store.ts b/apps/u3/src/store/store.ts index 159c644b..013c6874 100644 --- a/apps/u3/src/store/store.ts +++ b/apps/u3/src/store/store.ts @@ -19,6 +19,8 @@ import frensHandles from '../features/frens/frensHandles'; import userGroupFavorites from '../features/shared/userGroupFavorites'; import configsTopics from '../features/shared/topics'; import configsPlatforms from '../features/shared/platforms'; +import joinCommunity from '@/features/community/joinCommunitySlice'; +import community from '@/features/community/communitySlice'; export const store = configureStore({ reducer: { @@ -33,6 +35,8 @@ export const store = configureStore({ frensHandles, configsTopics, configsPlatforms, + joinCommunity, + community, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/apps/u3/src/utils/community/getCommunityNavs.ts b/apps/u3/src/utils/community/getCommunityNavs.ts new file mode 100644 index 00000000..4b44235d --- /dev/null +++ b/apps/u3/src/utils/community/getCommunityNavs.ts @@ -0,0 +1,57 @@ +import { + getCommunityAppPath, + getCommunityLinksPath, + getCommunityNftPath, + getCommunityPointPath, + getCommunityPostsPath, + getCommunityTokenPath, +} from '@/route/path'; +import { CommunityInfo } from '@/services/community/types/community'; + +export default function getCommunityNavs( + channelId: string, + communityInfo: CommunityInfo +) { + const { nfts, tokens, points, apps } = communityInfo || {}; + const mainNavs = [ + { title: 'Posts', href: getCommunityPostsPath(channelId) }, + { title: 'Links', href: getCommunityLinksPath(channelId) }, + // { title: 'Members', href: `/community/${channelId}/members` }, + ]; + const nft = nfts?.length > 0 ? nfts[0] : null; + if (nft) { + mainNavs.push({ + title: 'NFT', + href: getCommunityNftPath(channelId, nft?.contract), + }); + } + + const token = tokens?.length > 0 ? tokens[0] : null; + if (token) { + mainNavs.push({ + title: 'Token', + href: getCommunityTokenPath(channelId, token?.contract), + }); + } + + const point = points?.length > 0 ? points[0] : null; + if (point) { + mainNavs.push({ + title: 'Points', + href: getCommunityPointPath(channelId), + }); + } + + const dappNavs = apps?.map((dapp) => { + return { + title: dapp.name, + href: getCommunityAppPath(channelId, dapp.name), + icon: dapp.logo, + }; + }); + + return { + mainNavs, + dappNavs, + }; +} diff --git a/apps/u3/src/utils/shared/props.ts b/apps/u3/src/utils/shared/props.ts new file mode 100644 index 00000000..bfa9c6f9 --- /dev/null +++ b/apps/u3/src/utils/shared/props.ts @@ -0,0 +1,31 @@ +import { CSSProperties, ReactNode } from 'react'; + +export interface StyleProps { + /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. */ + className?: string; + /** The inline [style](https://developer.mozilla.org/en-US/docs/Web/API/Element/style) for the element. */ + style?: CSSProperties; +} + +export type RenderChildren<V> = + | ReactNode + | ((values: V) => ReactNode) + | undefined; + +export type ChildrenRenderProps<T, V> = Omit<T, 'children'> & { + children?: RenderChildren<V>; +}; + +export function childrenRender<T, V>( + children: T, + values: V, + defaultChildren?: ReactNode +) { + if (typeof children === 'function') { + return children(values); + } + if (children === undefined) { + return defaultChildren; + } + return children; +} diff --git a/apps/u3/src/utils/shared/share.ts b/apps/u3/src/utils/shared/share.ts index 2f8f7a24..e8742b58 100644 --- a/apps/u3/src/utils/shared/share.ts +++ b/apps/u3/src/utils/shared/share.ts @@ -5,7 +5,11 @@ * @LastEditTime: 2023-12-01 17:28:04 * @Description: file description */ -import { getCommunityFcPostDetailPath } from '@/route/path'; +import { + getCommunityFcPostDetailPath, + getExploreFcPostDetailPath, + getExploreLensPostDetailPath, +} from '@/route/path'; import { SHARE_DOMAIN } from '../../constants'; export const getEventShareUrl = (id: string | number) => { @@ -29,11 +33,11 @@ export const getLinkShareUrl = (url: string) => { }; export const getSocialDetailShareUrlWithLens = (id: string | number) => { - return `${SHARE_DOMAIN}/social/post-detail/lens/${id}`; + return `${SHARE_DOMAIN}${getExploreLensPostDetailPath(id)}`; }; export const getSocialDetailShareUrlWithFarcaster = (id: string | number) => { - return `${SHARE_DOMAIN}/social/post-detail/fcast/${id}`; + return `${SHARE_DOMAIN}${getExploreFcPostDetailPath(id)}`; }; export const getCommunityPostDetailShareUrlWithFarcaster = ( diff --git a/apps/u3/src/utils/shared/shortPubKey.ts b/apps/u3/src/utils/shared/shortPubKey.ts index 50f30b5b..7e9528ca 100644 --- a/apps/u3/src/utils/shared/shortPubKey.ts +++ b/apps/u3/src/utils/shared/shortPubKey.ts @@ -12,12 +12,12 @@ export function shortPubKey( ) { if (!key) return ''; const split = ops?.split; - const len = ops?.len || 6; + const len = ops?.len || 4; if (split) { return key.slice(0, len) + split + key.slice(-len); } - return key.slice(0, len) + '..'.repeat(len / 4) + key.slice(-len); + return key.slice(0, len + 2) + '..'.repeat(len / 4) + key.slice(-len); } export function shortPubKeyHash(hashKey: string) {