diff --git a/apps/u3/public/social/imgs/channel-home.png b/apps/u3/public/social/imgs/channel-home.png new file mode 100644 index 00000000..0ceb2bbf Binary files /dev/null and b/apps/u3/public/social/imgs/channel-home.png differ diff --git a/apps/u3/src/api/farcaster.ts b/apps/u3/src/api/farcaster.ts index 4442c857..5f6992e9 100644 --- a/apps/u3/src/api/farcaster.ts +++ b/apps/u3/src/api/farcaster.ts @@ -264,7 +264,7 @@ export function getFarcasterChannelFeeds({ } export function getFarcasterChannelTrends( - limit = 5 + limit = 500 ): AxiosPromise> { return axios({ url: `${REACT_APP_API_SOCIAL_URL}/3r/farcaster/channel/trends`, diff --git a/apps/u3/src/components/social/PostCard.tsx b/apps/u3/src/components/social/PostCard.tsx index 1fab8219..ceaf70ce 100644 --- a/apps/u3/src/components/social/PostCard.tsx +++ b/apps/u3/src/components/social/PostCard.tsx @@ -133,7 +133,7 @@ export const PostCardWrapper = styled.div<{ isDetail?: boolean }>` box-sizing: border-box; display: flex; flex-direction: column; - gap: 20px; + gap: 15px; cursor: ${(props) => (props.isDetail ? 'initial' : 'pointer')}; &:hover { background: ${(props) => (props.isDetail ? '#212228' : '#39424c')}; diff --git a/apps/u3/src/components/social/SocialPageNav.tsx b/apps/u3/src/components/social/SocialPageNav.tsx index 5e32ef32..75851ecc 100644 --- a/apps/u3/src/components/social/SocialPageNav.tsx +++ b/apps/u3/src/components/social/SocialPageNav.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { useNavigate } from 'react-router-dom'; import MobilePageHeader from '../common/mobile/MobilePageHeader'; import { ArrowLeft } from '../icons/ArrowLeft'; +import { getChannelFormName } from '../../utils/social/getChannel'; export enum FeedsType { FOLLOWING = 'following', @@ -45,8 +46,15 @@ export default function SocialPageNav({ ); } -export function SocialBackNav({ title = 'Post' }: { title?: string }) { +export function SocialBackNav({ + title = 'Post', + isChannel, +}: { + title?: string; + isChannel?: boolean; +}) { const navigate = useNavigate(); + const channel = isChannel ? getChannelFormName(title) : null; return ( {!isMobile && ( @@ -64,7 +72,15 @@ export function SocialBackNav({ title = 'Post' }: { title?: string }) { > - {title} +
+ {channel && ( + <> + # + + + )} + {title} +
{!isMobile && }
@@ -159,15 +175,26 @@ const SocialNavCenter = styled.div` background: var(--neutral-100, #1a1e23); cursor: pointer; } - > span { - overflow: hidden; - color: #fff; - text-overflow: ellipsis; - font-family: Rubik; - font-size: 18px; - font-style: normal; - font-weight: 700; - line-height: normal; + > div.channels { + display: flex; + align-items: center; + gap: 5px; + > span { + overflow: hidden; + color: #fff; + text-overflow: ellipsis; + font-family: Rubik; + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: normal; + } + } + img { + width: 18px; + height: 18px; + object-fit: cover; + border-radius: 2px; } `; const SocialNavRight = styled.div` diff --git a/apps/u3/src/components/social/farcaster/ChannelItem.tsx b/apps/u3/src/components/social/farcaster/ChannelItem.tsx new file mode 100644 index 00000000..ca3a1df4 --- /dev/null +++ b/apps/u3/src/components/social/farcaster/ChannelItem.tsx @@ -0,0 +1,71 @@ +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; + +export default function ChannelItem({ + data, +}: { + data: { + name?: string; + channel_description?: string; + parent_url: string; + image: string; + channel_id: string; + count: string; + }; +}) { + return ( + + + # + + {data.name || data.channel_description} + + {`${data.count} posts today`} + + ); +} + +const ItemWrapper = styled(Link)` + /* width: 100%; */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: start; + gap: 5px; + padding: 20px; + text-decoration: none; +`; + +const NameWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + font-weight: 700; + > span { + color: #fff; + } + img { + width: 16px; + height: 16px; + border-radius: 2px; + object-fit: cover; + } +`; +const NameText = styled.div` + font-size: 16px; + color: white; + font-style: normal; + font-weight: 700; +`; +const HandleText = styled.div` + font-size: 12px; + color: grey; + font-style: normal; + font-weight: 500; +`; diff --git a/apps/u3/src/components/social/farcaster/FCast.tsx b/apps/u3/src/components/social/farcaster/FCast.tsx index d957f03b..e2383a4f 100644 --- a/apps/u3/src/components/social/farcaster/FCast.tsx +++ b/apps/u3/src/components/social/farcaster/FCast.tsx @@ -121,6 +121,7 @@ export default function FCast({ )} + {cast.parent_url && } { e.stopPropagation(); @@ -142,7 +143,6 @@ export default function FCast({ farcasterUserData={farcasterUserData} /> - {cast.parent_url && } ); } diff --git a/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx b/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx index 52f23f9a..06d0bbd0 100644 --- a/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx +++ b/apps/u3/src/components/social/farcaster/FarcasterChannel.tsx @@ -37,10 +37,11 @@ const ChannelBox = styled.div` align-items: center; text-decoration: none; gap: 5px; + font-weight: 700; .channel-img { - width: 20px; - height: 20px; - border-radius: 50%; + width: 16px; + height: 16px; + border-radius: 2px; } &:hover { text-decoration: underline; diff --git a/apps/u3/src/components/social/farcaster/TrendChannel.tsx b/apps/u3/src/components/social/farcaster/TrendChannel.tsx index b3a0956f..3d6d94f8 100644 --- a/apps/u3/src/components/social/farcaster/TrendChannel.tsx +++ b/apps/u3/src/components/social/farcaster/TrendChannel.tsx @@ -1,53 +1,29 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { getFarcasterChannelTrends } from 'src/api/farcaster'; +import { useNavigate } from 'react-router-dom'; +import { useFarcasterCtx } from 'src/contexts/FarcasterCtx'; import styled from 'styled-components'; - -import FarcasterChannelData from '../../../constants/warpcast.json'; +import ChannelItem from './ChannelItem'; export default function TrendChannel() { - const [trendChannel, setTrendChannel] = useState< - { - parent_url: string; - count: string; - }[] - >([]); - const loadTrendChannel = async () => { - const resp = await getFarcasterChannelTrends(); - if (resp.data.code !== 0) { - console.error(resp.data.msg); - return; - } - setTrendChannel(resp.data.data); - }; - useEffect(() => { - loadTrendChannel(); - }, []); - - const channels = useMemo(() => { - return FarcasterChannelData.map((c) => { - const trend = trendChannel.find((t) => t.parent_url === c.parent_url); - if (!trend) return null; - return { - ...trend, - ...c, - }; - }) - .filter((c) => c !== null) - .sort((a, b) => { - return Number(b.count) - Number(a.count); - }); - }, [trendChannel]); - - if (trendChannel.length === 0) { + const navigate = useNavigate(); + const { channels } = useFarcasterCtx(); + if (channels.length === 0) { return null; } return ( - Trending Channels +
+ Trends + { + navigate(`/social/trends`); + }} + > + View All + +
- {channels.map((item) => { + {channels.slice(0, 5).map((item) => { return ; })} @@ -55,69 +31,21 @@ export default function TrendChannel() { ); } -function ChannelItem({ - data, -}: { - data: { - name?: string; - channel_description?: string; - parent_url: string; - image: string; - channel_id: string; - count: string; - }; -}) { - return ( - - - - {data.name || data.channel_description} - {`${data.count} posts today`} - - - ); -} - -const ItemWrapper = styled(Link)` - /* width: 100%; */ +const Header = styled.div` + width: 100%; display: flex; flex-direction: row; - justify-content: center; - align-items: center; - gap: 20px; - padding: 20px; - text-decoration: none; - > img { - width: 36px; - height: 36px; - border-radius: 50%; - object-fit: cover; - } + justify-content: space-between; `; -const NameWrapper = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 20px; -`; -const NameText = styled.div` +const MoreButton = styled.button` + cursor: pointer; + color: #718096; font-size: 16px; - color: white; - font-style: normal; - font-weight: 700; - line-height: 0; -`; -const HandleText = styled.div` - font-size: 12px; - color: grey; - font-style: normal; font-weight: 500; - line-height: 0; + border: none; + outline: none; + background: inherit; `; const Wrapper = styled.div` @@ -146,7 +74,4 @@ const ChannelListWrapper = styled.div` border: 1px solid #718096; border-radius: 20px; background-color: #212228; - > :not(:first-child) { - border-top: 1px solid #718096; - } `; diff --git a/apps/u3/src/container/SocialLayout.tsx b/apps/u3/src/container/SocialLayout.tsx index 253d4817..5676cfb0 100644 --- a/apps/u3/src/container/SocialLayout.tsx +++ b/apps/u3/src/container/SocialLayout.tsx @@ -68,8 +68,11 @@ export default function Home() { ]); const titleElem = useMemo(() => { + if (location.pathname.includes('social/trends')) { + return ; + } if (location.pathname.includes('social/channel') && channelName) { - return ; + return ; } if (location.pathname.includes('post-detail')) { return ; diff --git a/apps/u3/src/container/SocialTrends.tsx b/apps/u3/src/container/SocialTrends.tsx new file mode 100644 index 00000000..1c24ae49 --- /dev/null +++ b/apps/u3/src/container/SocialTrends.tsx @@ -0,0 +1,42 @@ +import ChannelItem from 'src/components/social/farcaster/ChannelItem'; +import { useFarcasterCtx } from 'src/contexts/FarcasterCtx'; +import styled from 'styled-components'; + +export default function SocialTrends() { + const { channels } = useFarcasterCtx(); + if (channels.length === 0) { + return null; + } + + return ( + + + {channels.map((item) => { + return ; + })} + + + ); +} + +const Wrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 20px; + box-sizing: border-box; +`; + +const ChannelListWrapper = styled.div` + width: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; + /* gap: 20px; */ + border: 1px solid #718096; + border-radius: 20px; + background-color: #212228; + > :not(:first-child) { + border-top: 1px solid #718096; + } +`; diff --git a/apps/u3/src/contexts/FarcasterCtx.tsx b/apps/u3/src/contexts/FarcasterCtx.tsx index cd194333..2dfe96be 100644 --- a/apps/u3/src/contexts/FarcasterCtx.tsx +++ b/apps/u3/src/contexts/FarcasterCtx.tsx @@ -25,9 +25,14 @@ import { setPrivateKey, setSignedKeyRequest, } from '../utils/farsign-utils'; -import { getFarcasterSignature, getFarcasterUserInfo } from '../api/farcaster'; +import { + getFarcasterChannelTrends, + getFarcasterSignature, + getFarcasterUserInfo, +} from '../api/farcaster'; import FarcasterQRModal from '../components/social/farcaster/FarcasterQRModal'; import FarcasterIframeModal from '../components/social/farcaster/FarcasterIframeModal'; +import FarcasterChannelData from '../constants/warpcast.json'; export const publicClient = createPublicClient({ chain: goerli, @@ -61,6 +66,15 @@ export type Signer = { export type FarcasterUserData = { [key: string]: { type: number; value: string }[]; }; + +export type FarcasterChannel = { + name?: string; + channel_description?: string; + parent_url: string; + image: string; + channel_id: string; + count: string; +}; export interface FarcasterContextData { currFid: number | undefined; currUserInfo: @@ -75,6 +89,7 @@ export interface FarcasterContextData { farcasterUserData: FarcasterUserData; setFarcasterUserData: React.Dispatch>; setIframeUrl: React.Dispatch>; + channels: FarcasterChannel[]; } const FarcasterContext = createContext(null); @@ -117,6 +132,39 @@ export default function FarcasterProvider({ const [currFid, setCurrFid] = useState(); const [openQR, setOpenQR] = useState(false); + const [trendChannel, setTrendChannel] = useState< + { + parent_url: string; + count: string; + }[] + >([]); + const loadTrendChannel = async () => { + const resp = await getFarcasterChannelTrends(); + if (resp.data.code !== 0) { + console.error(resp.data.msg); + return; + } + setTrendChannel(resp.data.data); + }; + useEffect(() => { + loadTrendChannel(); + }, []); + + const channels = useMemo(() => { + return FarcasterChannelData.map((c) => { + const trend = trendChannel.find((t) => t.parent_url === c.parent_url); + if (!trend) return null; + return { + ...trend, + ...c, + }; + }) + .filter((c) => c !== null) + .sort((a, b) => { + return Number(b.count) - Number(a.count); + }); + }, [trendChannel]); + const openQRModal = useMemo(() => { if (signer.isConnected) { return false; @@ -261,6 +309,7 @@ export default function FarcasterProvider({ farcasterUserData, setFarcasterUserData, setIframeUrl, + channels, }} > {children} diff --git a/apps/u3/src/route/routes.tsx b/apps/u3/src/route/routes.tsx index 99cae16d..59939043 100644 --- a/apps/u3/src/route/routes.tsx +++ b/apps/u3/src/route/routes.tsx @@ -42,6 +42,7 @@ export enum RouteKey { socialLayout = 'socialLayout', social = 'social', socialChannel = 'socialChannel', + socialTrendsChannel = 'socialTrendsChannel', socialPostDetailLens = 'socialPostDetailLens', socialPostDetailFcast = 'socialPostDetailFcast', socialSuggestFollow = 'socialSuggestFollow', @@ -233,6 +234,11 @@ export const routes: CutomRouteObject[] = [ element: loadContainerElement('Social'), key: RouteKey.social, }, + { + path: 'trends', + element: loadContainerElement('SocialTrends'), + key: RouteKey.socialTrendsChannel, + }, { path: 'channel/:channelName', element: loadContainerElement('SocialChannel'), diff --git a/apps/u3/src/utils/social/getChannel.ts b/apps/u3/src/utils/social/getChannel.ts new file mode 100644 index 00000000..529b3092 --- /dev/null +++ b/apps/u3/src/utils/social/getChannel.ts @@ -0,0 +1,15 @@ +import FarcasterChannelData from '../../constants/warpcast.json'; + +export function getChannelFormName(name: string) { + const channel = FarcasterChannelData.find( + (c) => c.name === name || c.channel_description === name + ); + if (channel) { + return channel; + } + return null; +} + +export function getChannel() { + return FarcasterChannelData; +}