diff --git a/src/webapp/app/api/feature-flags/route.ts b/src/webapp/app/api/feature-flags/route.ts
index 62f566fd..c3188adf 100644
--- a/src/webapp/app/api/feature-flags/route.ts
+++ b/src/webapp/app/api/feature-flags/route.ts
@@ -11,6 +11,7 @@ export async function GET() {
return NextResponse.json({
similarCards: false,
cardSearch: false,
+ urlTypeFilter: false,
});
}
}
diff --git a/src/webapp/features/feeds/components/feedControls/FeedControls.tsx b/src/webapp/features/feeds/components/feedControls/FeedControls.tsx
index 92f3a540..0dcc91dd 100644
--- a/src/webapp/features/feeds/components/feedControls/FeedControls.tsx
+++ b/src/webapp/features/feeds/components/feedControls/FeedControls.tsx
@@ -10,6 +10,8 @@ import {
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { BiCollection } from 'react-icons/bi';
+import FeedFilters from '../feedFilters/FeedFilters';
+import { useFeatureFlags } from '@/lib/clientFeatureFlags';
const options = [
{ value: 'explore', label: 'Latest', href: '/explore' },
@@ -23,6 +25,7 @@ const options = [
export default function FeedControls() {
const pathname = usePathname();
const router = useRouter();
+ const { data: featureFlags } = useFeatureFlags();
const segment = pathname.split('/')[2];
const currentValue = segment || 'explore';
@@ -36,54 +39,57 @@ export default function FeedControls() {
return (
-
- {
- const option = options.find((o) => o.value === value);
- if (option) {
- router.push(option.href);
- }
- combobox.closeDropdown();
- }}
- width={200}
- >
-
+
+
+ {
+ const option = options.find((o) => o.value === value);
+ if (option) {
+ router.push(option.href);
+ }
+ combobox.closeDropdown();
+ }}
+ width={200}
+ >
+
+ }
+ onClick={() => combobox.toggleDropdown()}
+ >
+ {selected?.label || 'Select feed'}
+
+
+
+
+
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+ {isGemsFeed && (
}
- onClick={() => combobox.toggleDropdown()}
+ color="grape"
+ component={Link}
+ href={'/explore/gems-of-2025/collections'}
+ leftSection={}
>
- {selected?.label || 'Select feed'}
+ Gem Collections
-
-
-
-
- {options.map((option) => (
-
- {option.label}
-
- ))}
-
-
-
- {isGemsFeed && (
- }
- >
- Gem Collections
-
- )}
+ )}
+
+ {featureFlags?.urlTypeFilter && }
);
diff --git a/src/webapp/features/feeds/components/feedFilters/FeedFilters.tsx b/src/webapp/features/feeds/components/feedFilters/FeedFilters.tsx
new file mode 100644
index 00000000..3de754fb
--- /dev/null
+++ b/src/webapp/features/feeds/components/feedFilters/FeedFilters.tsx
@@ -0,0 +1,90 @@
+'use client';
+
+import { getUrlTypeIcon } from '@/lib/utils/icon';
+import { Button, Group, Popover } from '@mantine/core';
+import { upperFirst } from '@mantine/hooks';
+import { UrlType } from '@semble/types';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { useOptimistic, useState, useTransition } from 'react';
+import { MdFilterList } from 'react-icons/md';
+
+export default function FeedFilters() {
+ const [opened, setOpened] = useState(false);
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const typeFromUrl = searchParams.get('type') as UrlType | null;
+
+ const [optimisticType, setOptimisticType] = useOptimistic(
+ typeFromUrl,
+ );
+
+ const [, startTransition] = useTransition();
+
+ const SelectedIcon =
+ optimisticType === null ? MdFilterList : getUrlTypeIcon(optimisticType);
+
+ const handleFilterClick = (type?: UrlType) => {
+ const nextType = type ?? null;
+
+ startTransition(() => {
+ setOptimisticType(nextType);
+
+ const params = new URLSearchParams(searchParams.toString());
+ if (nextType) {
+ params.set('type', nextType);
+ } else {
+ params.delete('type');
+ }
+
+ router.push(`?${params.toString()}`, { scroll: false });
+ });
+
+ setOpened(false);
+ };
+
+ return (
+
+
+ }
+ onClick={() => setOpened((o) => !o)}
+ >
+ {optimisticType ? upperFirst(optimisticType) : 'All Cards'}
+
+
+
+
+
+
+
+ {Object.values(UrlType).map((type) => {
+ const Icon = getUrlTypeIcon(type);
+
+ return (
+ }
+ onClick={() => handleFilterClick(type)}
+ >
+ {upperFirst(type)}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/webapp/features/feeds/containers/gemsFeedContainer/GemsFeedContainer.tsx b/src/webapp/features/feeds/containers/gemsFeedContainer/GemsFeedContainer.tsx
index 71fa9cf7..1996403b 100644
--- a/src/webapp/features/feeds/containers/gemsFeedContainer/GemsFeedContainer.tsx
+++ b/src/webapp/features/feeds/containers/gemsFeedContainer/GemsFeedContainer.tsx
@@ -7,8 +7,13 @@ import GemsFeedContainerSkeleton from './Skeleton.GemsFeedContainer';
import GemsFeedContainerError from './Error.GemsFeedContainer';
import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll';
import RefetchButton from '@/components/navigation/refetchButton/RefetchButton';
+import { useSearchParams } from 'next/navigation';
+import { UrlType } from '@semble/types';
export default function GemsFeedContainer() {
+ const searchParams = useSearchParams();
+ const selectedUrlType = searchParams.get('type') as UrlType;
+
const {
data,
error,
@@ -18,7 +23,7 @@ export default function GemsFeedContainer() {
isFetchingNextPage,
isRefetching,
refetch,
- } = useGemsFeed();
+ } = useGemsFeed({ urlType: selectedUrlType });
const allActivities =
data?.pages.flatMap((page) => page.activities ?? []) ?? [];
diff --git a/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx b/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
index 95fa9e91..6e5bbf95 100644
--- a/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
+++ b/src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
@@ -7,8 +7,13 @@ import MyFeedContainerSkeleton from './Skeleton.MyFeedContainer';
import MyFeedContainerError from './Error.MyFeedContainer';
import InfiniteScroll from '@/components/contentDisplay/infiniteScroll/InfiniteScroll';
import RefetchButton from '@/components/navigation/refetchButton/RefetchButton';
+import { UrlType } from '@semble/types';
+import { useSearchParams } from 'next/navigation';
export default function MyFeedContainer() {
+ const searchParams = useSearchParams();
+ const selectedUrlType = searchParams.get('type') as UrlType;
+
const {
data,
error,
@@ -18,7 +23,7 @@ export default function MyFeedContainer() {
isFetchingNextPage,
isRefetching,
refetch,
- } = useGlobalFeed();
+ } = useGlobalFeed({ urlType: selectedUrlType });
const allActivities =
data?.pages.flatMap((page) => page.activities ?? []) ?? [];
diff --git a/src/webapp/features/feeds/lib/feedKeys.ts b/src/webapp/features/feeds/lib/feedKeys.ts
index 7f84f901..ca8cfbf8 100644
--- a/src/webapp/features/feeds/lib/feedKeys.ts
+++ b/src/webapp/features/feeds/lib/feedKeys.ts
@@ -1,6 +1,18 @@
+import { UrlType } from '@semble/types';
+
export const feedKeys = {
all: () => ['feeds'] as const,
- infinite: (limit?: number) => [...feedKeys.all(), 'infinite', limit],
+ infinite: (limit?: number, urlType?: UrlType) => [
+ ...feedKeys.all(),
+ 'infinite',
+ limit,
+ urlType,
+ ],
gems: () => [...feedKeys.all(), 'gems'] as const,
- gemsInfinite: (limit?: number) => [...feedKeys.gems(), 'infinite', limit],
+ gemsInfinite: (limit?: number, urlType?: UrlType) => [
+ ...feedKeys.gems(),
+ [...feedKeys.infinite()],
+ urlType,
+ limit,
+ ],
};
diff --git a/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx b/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx
index 37aa8489..4de7bffc 100644
--- a/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx
+++ b/src/webapp/features/feeds/lib/queries/useGemsFeed.tsx
@@ -1,21 +1,27 @@
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
import { getGemsActivityFeed } from '../dal';
import { feedKeys } from '../feedKeys';
+import { UrlType } from '@semble/types';
interface Props {
limit?: number;
+ urlType?: UrlType;
}
export default function useGemsFeed(props?: Props) {
const limit = props?.limit ?? 15;
const query = useSuspenseInfiniteQuery({
- queryKey: feedKeys.gemsInfinite(limit),
+ queryKey: feedKeys.gemsInfinite(limit, props?.urlType),
staleTime: 10000,
initialPageParam: 1,
refetchOnWindowFocus: false,
queryFn: ({ pageParam = 1 }) => {
- return getGemsActivityFeed({ limit, page: pageParam });
+ return getGemsActivityFeed({
+ limit,
+ page: pageParam,
+ urlType: props?.urlType,
+ });
},
getNextPageParam: (lastPage) => {
if (lastPage.pagination.hasMore) {
diff --git a/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx b/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx
index ec2aedc9..0cae2cec 100644
--- a/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx
+++ b/src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx
@@ -1,21 +1,23 @@
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
import { getGlobalFeed } from '../dal';
import { feedKeys } from '../feedKeys';
+import { UrlType } from '@semble/types';
interface Props {
limit?: number;
+ urlType?: UrlType;
}
export default function useGlobalFeed(props?: Props) {
const limit = props?.limit ?? 15;
const query = useSuspenseInfiniteQuery({
- queryKey: feedKeys.infinite(limit),
+ queryKey: feedKeys.infinite(limit, props?.urlType),
staleTime: 10000,
initialPageParam: 1,
refetchOnWindowFocus: false,
queryFn: ({ pageParam = 1 }) => {
- return getGlobalFeed({ limit, page: pageParam });
+ return getGlobalFeed({ limit, page: pageParam, urlType: props?.urlType });
},
getNextPageParam: (lastPage) => {
if (lastPage.pagination.hasMore) {
diff --git a/src/webapp/lib/clientFeatureFlags.ts b/src/webapp/lib/clientFeatureFlags.ts
index 8777c6f6..cdb678a6 100644
--- a/src/webapp/lib/clientFeatureFlags.ts
+++ b/src/webapp/lib/clientFeatureFlags.ts
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
interface FeatureFlags {
cardSearch: boolean;
+ urlTypeFilter: boolean;
}
async function fetchFeatureFlags(): Promise {
diff --git a/src/webapp/lib/serverFeatureFlags.ts b/src/webapp/lib/serverFeatureFlags.ts
index 1c5e0de0..1c1cca85 100644
--- a/src/webapp/lib/serverFeatureFlags.ts
+++ b/src/webapp/lib/serverFeatureFlags.ts
@@ -9,5 +9,6 @@ export async function getServerFeatureFlags() {
return {
cardSearch: isApprovedUser || process.env.VERCEL_ENV !== 'production',
+ urlTypeFilter: isApprovedUser || process.env.VERCEL_ENV !== 'production',
};
}