diff --git a/devcon-api/src/clients/recommendation.ts b/devcon-api/src/clients/recommendation.ts index 70bb1b0a2..4112229c0 100644 --- a/devcon-api/src/clients/recommendation.ts +++ b/devcon-api/src/clients/recommendation.ts @@ -100,6 +100,10 @@ export async function GetRecommendedSessions(id: string, includeFeatured?: boole }, ], }, + include: { + speakers: true, + slot_room: true, + }, orderBy: { slot_start: 'asc', }, diff --git a/devcon-app/src/components/domain/app/dc7/dashboard/index.tsx b/devcon-app/src/components/domain/app/dc7/dashboard/index.tsx index a8c005f8b..fcecf9e4c 100644 --- a/devcon-app/src/components/domain/app/dc7/dashboard/index.tsx +++ b/devcon-app/src/components/domain/app/dc7/dashboard/index.tsx @@ -10,7 +10,7 @@ import PassportLogoBlack from 'assets/images/dc-7/passport-logo-black.png' import { NotificationCard } from 'components/domain/app/dc7/profile/notifications' import { PersonalizedSuggestions } from 'components/domain/app/dc7/sessions/recommendations' import { useRecoilState, useRecoilValue } from 'recoil' -import { devaBotVisibleAtom, notificationsAtom, sessionsAtom, useSeenNotifications } from 'pages/_app' +import { devaBotVisibleAtom, notificationsAtom, sessionsAtom, speakersAtom, useSeenNotifications } from 'pages/_app' import FoodIcon from 'assets/icons/food-beverage.svg' import CityGuideIcon from 'assets/icons/city-guide.svg' import VideoIcon from 'assets/icons/video-play.svg' @@ -25,6 +25,8 @@ import { Link } from 'components/common/link' import { TruncateMiddle } from 'utils/formatting' import ChevronRight from 'assets/icons/chevron_right.svg' import { FancyLoader } from 'lib/components/loader/loader' +import { RecommendedSpeakers } from '../speakers/recommendations' +import { useSpeakerData } from 'services/event-data' export const cardClass = 'flex flex-col lg:border lg:border-solid lg:border-[#E4E6EB] rounded-3xl relative lg:bg-[#fbfbfb]' @@ -157,6 +159,7 @@ const featuredClass = export const Dashboard = () => { const accountContext = useAccountContext() const sessions = useRecoilValue(sessionsAtom) + const speakers = useSpeakerData() const draggableLink = useDraggableLink() const [_, setDevaBotVisible] = useRecoilState(devaBotVisibleAtom) const { account, loading } = accountContext @@ -291,7 +294,13 @@ export const Dashboard = () => {
- +
+ +
+
+
+ +
) } diff --git a/devcon-app/src/components/domain/app/dc7/sessions/recommendations.tsx b/devcon-app/src/components/domain/app/dc7/sessions/recommendations.tsx index ae7bbb216..43bd0cb2e 100644 --- a/devcon-app/src/components/domain/app/dc7/sessions/recommendations.tsx +++ b/devcon-app/src/components/domain/app/dc7/sessions/recommendations.tsx @@ -8,6 +8,7 @@ import { useAccountContext } from 'context/account-context' import { APP_CONFIG } from 'utils/config' import { Separator } from 'lib/components/ui/separator' import cn from 'classnames' +import { FancyLoader } from 'lib/components/loader/loader' interface Props { sessions: SessionType[] @@ -32,7 +33,7 @@ export function PersonalizedSuggestions({ sessions, standalone }: Props) { [sessions, account] ) - const { data: recommended } = useQuery({ + const { data: recommended, isLoading } = useQuery({ queryKey: ['account', 'sessions', 'recommended', account?.id], queryFn: async () => { if (!account?.id) { @@ -100,21 +101,16 @@ export function PersonalizedSuggestions({ sessions, standalone }: Props) { name: 'Recommended', list: recommended, }, - ].map(({ name, list }) => { - const isEmpty = !list?.length + ].map(({ name }) => { return (
{ - if (!isEmpty) { - setFilter(name.toLowerCase() as 'featured' | 'personal' | 'recommended') - } + setFilter(name.toLowerCase() as 'featured' | 'personal' | 'recommended') }} > {name} @@ -126,6 +122,31 @@ export function PersonalizedSuggestions({ sessions, standalone }: Props) { )}
+ {filter === 'personal' && !sessionList?.length && ( +
+

+ + Complete your profile + {' '} + to see personalized recommendations. +

+
+ )} + {filter === 'recommended' && isLoading && ( +
+ +
+ )} + {filter === 'recommended' && !isLoading && !sessionList?.length && ( +
+

+ + Complete your profile + {' '} + to see personalized recommendations. +

+
+ )}
{sessionList?.map((session: SessionType, index: number) => ( diff --git a/devcon-app/src/components/domain/app/dc7/speakers/index.tsx b/devcon-app/src/components/domain/app/dc7/speakers/index.tsx index 2036f2ed1..6e05732b1 100644 --- a/devcon-app/src/components/domain/app/dc7/speakers/index.tsx +++ b/devcon-app/src/components/domain/app/dc7/speakers/index.tsx @@ -27,6 +27,7 @@ import { ScrollUpComponent } from '../sessions' import { Popup } from 'lib/components/pop-up' import { useAccountContext } from 'context/account-context' import moment from 'moment' +import { RecommendedSpeakers } from './recommendations' // import { SessionFilterAdvanced } from '../sessions' export const cardClass = @@ -397,60 +398,33 @@ export const SpeakerList = ({ speakers }: { speakers: SpeakerType[] | null }) => setVisibleSpeakers(filteredSpeakers.slice(0, page * SPEAKERS_PER_PAGE)) }, [page, filteredSpeakers]) - return ( -
- + const onSpeakerSelect = (e: any, speaker: SpeakerType) => { + const result = draggableLink.onClick(e) -
Featured Speakers
+ if (!result) return -
- -
- {featuredSpeakers.map((speaker, index) => ( - { - const result = draggableLink.onClick(e) + if (pathname === '/speakers' && isLargeScreen) e.preventDefault() - if (!result) return + if (isLargeScreen) { + if (selectedSpeaker?.sourceId === speaker.sourceId && pathname === '/speakers') { + setSelectedSpeaker(null) + } else { + setSelectedSpeaker(speaker) + } + } - if (pathname === '/speakers' && isLargeScreen) e.preventDefault() + setDevaBotVisible(false) + } - if (isLargeScreen) { - if (selectedSpeaker?.sourceId === speaker.sourceId && pathname === '/speakers') { - setSelectedSpeaker(null) - } else { - setSelectedSpeaker(speaker) - } - } + return ( +
+ - setDevaBotVisible(false) - }} - > -
- {speaker.name} -
-
-

{speaker.name}

- - ))} -
- -
+
setDevaBotVisible('Recommend speakers who know about ')}> diff --git a/devcon-app/src/components/domain/app/dc7/speakers/recommendations.tsx b/devcon-app/src/components/domain/app/dc7/speakers/recommendations.tsx new file mode 100644 index 000000000..033fab960 --- /dev/null +++ b/devcon-app/src/components/domain/app/dc7/speakers/recommendations.tsx @@ -0,0 +1,183 @@ +import React, { useMemo, useState } from 'react' +import SwipeToScroll from 'lib/components/event-schedule/swipe-to-scroll' +import { Speaker as SpeakerType } from 'types/Speaker' +import { Session as SessionType } from 'types/Session' +import { Link } from 'components/common/link' +import Image from 'next/image' +import { useQuery } from '@tanstack/react-query' +import { useAccountContext } from 'context/account-context' +import { APP_CONFIG } from 'utils/config' +import { Separator } from 'lib/components/ui/separator' +import cn from 'classnames' +import moment from 'moment' +import { useDraggableLink } from 'lib/hooks/useDraggableLink' +import css from './speakers.module.scss' +import { FancyLoader } from 'lib/components/loader/loader' + +interface Props { + speakers: SpeakerType[] + selectedSpeaker?: SpeakerType | null + standalone?: boolean + onSpeakerSelect?: (e: any, speaker: SpeakerType) => void +} + +export function RecommendedSpeakers({ speakers, selectedSpeaker, standalone, onSpeakerSelect }: Props) { + const { account } = useAccountContext() + const [filter, setFilter] = useState<'featured' | 'social'>('featured') + const draggableLink = useDraggableLink() + + const featuredSpeakers = useMemo( + () => + speakers + ?.filter(speaker => + speaker.sessions?.some(session => session.featured && moment(session.slot_start).isAfter(moment())) + ) + .sort(() => Math.random() - 0.5), + [speakers] + ) + + const { data: recommended, isLoading } = useQuery({ + queryKey: ['account', 'speakers', 'recommended', account?.id], + queryFn: async () => { + if (!account?.id) { + console.log('Not logged in... No recommendations') + return [] + } + + try { + const response = await fetch(`${APP_CONFIG.API_BASE_URL}/account/speakers/recommended`, { + method: 'GET', + credentials: 'include', + }) + + const { data } = await response.json() + return data.sort(() => Math.random() - 0.5) + } catch (error) { + console.error('Error fetching recommended speakers', error) + return [] + } + }, + }) + + const speakerList = useMemo(() => { + if (filter === 'featured') return featuredSpeakers + if (filter === 'social') return recommended + return [] + }, [filter, featuredSpeakers, recommended]) + + return ( + <> +
+ Speaker Highlights{' '} + {standalone && ( + +

Go to Speakers

+ + )} +
+ + {standalone && ( + +
+
setFilter('featured')} + > + Featured +
+ + + + {[ + { + id: 'social', + name: 'Onchain Social', + }, + ].map(({ id, name }) => { + return ( +
{ + setFilter(id.toLowerCase() as 'featured' | 'social') + }} + > + {name} +
+ ) + })} +
+
+ )} + +
+ +
+ {filter === 'social' && isLoading && ( +
+ +
+ )} + {filter === 'social' && !isLoading && !speakerList?.length && ( +
+

+ + Connect your wallet + {' '} + to include your onchain social graph. Your social connections are based on{' '} + + Farcaster + + ,{' '} + + Lens + {' '} + and{' '} + + Ethereum Follow Protocol + + . +

+
+ )} + {speakerList?.map((speaker: SpeakerType, index: number) => ( + onSpeakerSelect?.(e, speaker)} + > +
+ {speaker.name} +
+
+

{speaker.name}

+ + ))} +
+ +
+ + ) +}