This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: url types on explore

+181 -52
+1
src/webapp/app/api/feature-flags/route.ts
··· 11 11 return NextResponse.json({ 12 12 similarCards: false, 13 13 cardSearch: false, 14 + urlTypeFilter: false, 14 15 }); 15 16 } 16 17 }
+50 -44
src/webapp/features/feeds/components/feedControls/FeedControls.tsx
··· 10 10 import Link from 'next/link'; 11 11 import { usePathname, useRouter } from 'next/navigation'; 12 12 import { BiCollection } from 'react-icons/bi'; 13 + import FeedFilters from '../feedFilters/FeedFilters'; 14 + import { useFeatureFlags } from '@/lib/clientFeatureFlags'; 13 15 14 16 const options = [ 15 17 { value: 'explore', label: 'Latest', href: '/explore' }, ··· 23 25 export default function FeedControls() { 24 26 const pathname = usePathname(); 25 27 const router = useRouter(); 28 + const { data: featureFlags } = useFeatureFlags(); 26 29 27 30 const segment = pathname.split('/')[2]; 28 31 const currentValue = segment || 'explore'; ··· 36 39 37 40 return ( 38 41 <ScrollAreaAutosize type="scroll"> 39 - <Group gap={'xs'} wrap="nowrap"> 40 - <Combobox 41 - store={combobox} 42 - onOptionSubmit={(value) => { 43 - const option = options.find((o) => o.value === value); 44 - if (option) { 45 - router.push(option.href); 46 - } 47 - combobox.closeDropdown(); 48 - }} 49 - width={200} 50 - > 51 - <Combobox.Target> 42 + <Group gap={'xs'} justify="space-between" wrap="nowrap"> 43 + <Group gap={'xs'}> 44 + <Combobox 45 + store={combobox} 46 + onOptionSubmit={(value) => { 47 + const option = options.find((o) => o.value === value); 48 + if (option) { 49 + router.push(option.href); 50 + } 51 + combobox.closeDropdown(); 52 + }} 53 + width={200} 54 + > 55 + <Combobox.Target> 56 + <Button 57 + variant="light" 58 + color="gray" 59 + leftSection={<Combobox.Chevron />} 60 + onClick={() => combobox.toggleDropdown()} 61 + > 62 + {selected?.label || 'Select feed'} 63 + </Button> 64 + </Combobox.Target> 65 + 66 + <Combobox.Dropdown> 67 + <Combobox.Options> 68 + {options.map((option) => ( 69 + <Combobox.Option 70 + key={option.value} 71 + value={option.value} 72 + active={option.value === currentValue} 73 + > 74 + {option.label} 75 + </Combobox.Option> 76 + ))} 77 + </Combobox.Options> 78 + </Combobox.Dropdown> 79 + </Combobox> 80 + {isGemsFeed && ( 52 81 <Button 53 82 variant="light" 54 - color="gray" 55 - leftSection={<Combobox.Chevron />} 56 - onClick={() => combobox.toggleDropdown()} 83 + color="grape" 84 + component={Link} 85 + href={'/explore/gems-of-2025/collections'} 86 + leftSection={<BiCollection size={18} />} 57 87 > 58 - {selected?.label || 'Select feed'} 88 + Gem Collections 59 89 </Button> 60 - </Combobox.Target> 61 - 62 - <Combobox.Dropdown> 63 - <Combobox.Options> 64 - {options.map((option) => ( 65 - <Combobox.Option 66 - key={option.value} 67 - value={option.value} 68 - active={option.value === currentValue} 69 - > 70 - {option.label} 71 - </Combobox.Option> 72 - ))} 73 - </Combobox.Options> 74 - </Combobox.Dropdown> 75 - </Combobox> 76 - {isGemsFeed && ( 77 - <Button 78 - variant="light" 79 - color="grape" 80 - component={Link} 81 - href={'/explore/gems-of-2025/collections'} 82 - leftSection={<BiCollection size={18} />} 83 - > 84 - Gem Collections 85 - </Button> 86 - )} 90 + )} 91 + </Group> 92 + {featureFlags?.urlTypeFilter && <FeedFilters />} 87 93 </Group> 88 94 </ScrollAreaAutosize> 89 95 );
+90
src/webapp/features/feeds/components/feedFilters/FeedFilters.tsx
··· 1 + 'use client'; 2 + 3 + import { getUrlTypeIcon } from '@/lib/utils/icon'; 4 + import { Button, Group, Popover } from '@mantine/core'; 5 + import { upperFirst } from '@mantine/hooks'; 6 + import { UrlType } from '@semble/types'; 7 + import { useRouter, useSearchParams } from 'next/navigation'; 8 + import { useOptimistic, useState, useTransition } from 'react'; 9 + import { MdFilterList } from 'react-icons/md'; 10 + 11 + export default function FeedFilters() { 12 + const [opened, setOpened] = useState(false); 13 + const router = useRouter(); 14 + const searchParams = useSearchParams(); 15 + 16 + const typeFromUrl = searchParams.get('type') as UrlType | null; 17 + 18 + const [optimisticType, setOptimisticType] = useOptimistic<UrlType | null>( 19 + typeFromUrl, 20 + ); 21 + 22 + const [, startTransition] = useTransition(); 23 + 24 + const SelectedIcon = 25 + optimisticType === null ? MdFilterList : getUrlTypeIcon(optimisticType); 26 + 27 + const handleFilterClick = (type?: UrlType) => { 28 + const nextType = type ?? null; 29 + 30 + startTransition(() => { 31 + setOptimisticType(nextType); 32 + 33 + const params = new URLSearchParams(searchParams.toString()); 34 + if (nextType) { 35 + params.set('type', nextType); 36 + } else { 37 + params.delete('type'); 38 + } 39 + 40 + router.push(`?${params.toString()}`, { scroll: false }); 41 + }); 42 + 43 + setOpened(false); 44 + }; 45 + 46 + return ( 47 + <Popover opened={opened} onChange={setOpened} shadow="sm"> 48 + <Popover.Target> 49 + <Button 50 + variant="light" 51 + color="lime" 52 + leftSection={<SelectedIcon />} 53 + onClick={() => setOpened((o) => !o)} 54 + > 55 + {optimisticType ? upperFirst(optimisticType) : 'All Cards'} 56 + </Button> 57 + </Popover.Target> 58 + 59 + <Popover.Dropdown maw={300}> 60 + <Group gap={6}> 61 + <Button 62 + size="xs" 63 + color="lime" 64 + variant={optimisticType === null ? 'filled' : 'light'} 65 + onClick={() => handleFilterClick()} 66 + > 67 + All Cards 68 + </Button> 69 + 70 + {Object.values(UrlType).map((type) => { 71 + const Icon = getUrlTypeIcon(type); 72 + 73 + return ( 74 + <Button 75 + key={type} 76 + size="xs" 77 + color="lime" 78 + variant={optimisticType === type ? 'filled' : 'light'} 79 + leftSection={<Icon />} 80 + onClick={() => handleFilterClick(type)} 81 + > 82 + {upperFirst(type)} 83 + </Button> 84 + ); 85 + })} 86 + </Group> 87 + </Popover.Dropdown> 88 + </Popover> 89 + ); 90 + }
+6 -1
src/webapp/features/feeds/containers/gemsFeedContainer/GemsFeedContainer.tsx
··· 7 7 import GemsFeedContainerError from './Error.GemsFeedContainer'; 8 8 import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; 9 9 import RefetchButton from '@/components/navigation/refetchButton/RefetchButton'; 10 + import { useSearchParams } from 'next/navigation'; 11 + import { UrlType } from '@semble/types'; 10 12 11 13 export default function GemsFeedContainer() { 14 + const searchParams = useSearchParams(); 15 + const selectedUrlType = searchParams.get('type') as UrlType; 16 + 12 17 const { 13 18 data, 14 19 error, ··· 18 23 isFetchingNextPage, 19 24 isRefetching, 20 25 refetch, 21 - } = useGemsFeed(); 26 + } = useGemsFeed({ urlType: selectedUrlType }); 22 27 23 28 const allActivities = 24 29 data?.pages.flatMap((page) => page.activities ?? []) ?? [];
+6 -1
src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
··· 7 7 import MyFeedContainerError from './Error.MyFeedContainer'; 8 8 import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll'; 9 9 import RefetchButton from '@/components/navigation/refetchButton/RefetchButton'; 10 + import { UrlType } from '@semble/types'; 11 + import { useSearchParams } from 'next/navigation'; 10 12 11 13 export default function MyFeedContainer() { 14 + const searchParams = useSearchParams(); 15 + const selectedUrlType = searchParams.get('type') as UrlType; 16 + 12 17 const { 13 18 data, 14 19 error, ··· 18 23 isFetchingNextPage, 19 24 isRefetching, 20 25 refetch, 21 - } = useGlobalFeed(); 26 + } = useGlobalFeed({ urlType: selectedUrlType }); 22 27 23 28 const allActivities = 24 29 data?.pages.flatMap((page) => page.activities ?? []) ?? [];
+14 -2
src/webapp/features/feeds/lib/feedKeys.ts
··· 1 + import { UrlType } from '@semble/types'; 2 + 1 3 export const feedKeys = { 2 4 all: () => ['feeds'] as const, 3 - infinite: (limit?: number) => [...feedKeys.all(), 'infinite', limit], 5 + infinite: (limit?: number, urlType?: UrlType) => [ 6 + ...feedKeys.all(), 7 + 'infinite', 8 + limit, 9 + urlType, 10 + ], 4 11 gems: () => [...feedKeys.all(), 'gems'] as const, 5 - gemsInfinite: (limit?: number) => [...feedKeys.gems(), 'infinite', limit], 12 + gemsInfinite: (limit?: number, urlType?: UrlType) => [ 13 + ...feedKeys.gems(), 14 + [...feedKeys.infinite()], 15 + urlType, 16 + limit, 17 + ], 6 18 };
+8 -2
src/webapp/features/feeds/lib/queries/useGemsFeed.tsx
··· 1 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 2 import { getGemsActivityFeed } from '../dal'; 3 3 import { feedKeys } from '../feedKeys'; 4 + import { UrlType } from '@semble/types'; 4 5 5 6 interface Props { 6 7 limit?: number; 8 + urlType?: UrlType; 7 9 } 8 10 9 11 export default function useGemsFeed(props?: Props) { 10 12 const limit = props?.limit ?? 15; 11 13 12 14 const query = useSuspenseInfiniteQuery({ 13 - queryKey: feedKeys.gemsInfinite(limit), 15 + queryKey: feedKeys.gemsInfinite(limit, props?.urlType), 14 16 staleTime: 10000, 15 17 initialPageParam: 1, 16 18 refetchOnWindowFocus: false, 17 19 queryFn: ({ pageParam = 1 }) => { 18 - return getGemsActivityFeed({ limit, page: pageParam }); 20 + return getGemsActivityFeed({ 21 + limit, 22 + page: pageParam, 23 + urlType: props?.urlType, 24 + }); 19 25 }, 20 26 getNextPageParam: (lastPage) => { 21 27 if (lastPage.pagination.hasMore) {
+4 -2
src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx
··· 1 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 2 import { getGlobalFeed } from '../dal'; 3 3 import { feedKeys } from '../feedKeys'; 4 + import { UrlType } from '@semble/types'; 4 5 5 6 interface Props { 6 7 limit?: number; 8 + urlType?: UrlType; 7 9 } 8 10 9 11 export default function useGlobalFeed(props?: Props) { 10 12 const limit = props?.limit ?? 15; 11 13 12 14 const query = useSuspenseInfiniteQuery({ 13 - queryKey: feedKeys.infinite(limit), 15 + queryKey: feedKeys.infinite(limit, props?.urlType), 14 16 staleTime: 10000, 15 17 initialPageParam: 1, 16 18 refetchOnWindowFocus: false, 17 19 queryFn: ({ pageParam = 1 }) => { 18 - return getGlobalFeed({ limit, page: pageParam }); 20 + return getGlobalFeed({ limit, page: pageParam, urlType: props?.urlType }); 19 21 }, 20 22 getNextPageParam: (lastPage) => { 21 23 if (lastPage.pagination.hasMore) {
+1
src/webapp/lib/clientFeatureFlags.ts
··· 4 4 5 5 interface FeatureFlags { 6 6 cardSearch: boolean; 7 + urlTypeFilter: boolean; 7 8 } 8 9 9 10 async function fetchFeatureFlags(): Promise<FeatureFlags> {
+1
src/webapp/lib/serverFeatureFlags.ts
··· 9 9 10 10 return { 11 11 cardSearch: isApprovedUser || process.env.VERCEL_ENV !== 'production', 12 + urlTypeFilter: isApprovedUser || process.env.VERCEL_ENV !== 'production', 12 13 }; 13 14 }