+
+
- {selectedHubs.length > 0 && !hideSelectedItems && }
+
+ );
+}
+
+export function HubsSelected({
+ selectedHubs,
+ onChange,
+}: Readonly<{
+ selectedHubs: Hub[];
+ onChange: (hubs: Hub[]) => void;
+}>) {
+ return (
+
+ {selectedHubs.map((hub) => (
+
+ Topic: {hub.name}
+
+
+ ))}
);
}
diff --git a/components/ui/SortDropdown.tsx b/components/ui/SortDropdown.tsx
index 63887d94b..7bac78bef 100644
--- a/components/ui/SortDropdown.tsx
+++ b/components/ui/SortDropdown.tsx
@@ -1,5 +1,5 @@
import { FC, useState } from 'react';
-import { ChevronDown } from 'lucide-react';
+import { ChevronDown, ArrowDownUp } from 'lucide-react';
import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu';
export interface SortOption {
@@ -34,8 +34,9 @@ export const SortDropdown: FC
= ({
type="button"
className={`flex w-full items-center gap-2 border border-gray-200 bg-gray-50 hover:bg-gray-100 rounded-lg px-3 py-1.5 text-sm min-w-[120px] justify-between ${className}`}
>
- {activeOption.label}
-
+
+ {activeOption.label}
+
);
diff --git a/hooks/useFeed.ts b/hooks/useFeed.ts
index 19c3fc8b5..2d8c054c6 100644
--- a/hooks/useFeed.ts
+++ b/hooks/useFeed.ts
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { FeedEntry } from '@/types/feed';
import { FeedService } from '@/services/feed.service';
import { useSession } from 'next-auth/react';
+import { isEqual, omit } from 'lodash';
export type FeedTab = 'following' | 'latest' | 'popular';
export type FundingTab = 'all' | 'open' | 'closed';
@@ -19,6 +20,7 @@ interface UseFeedOptions {
entries: FeedEntry[];
hasMore: boolean;
};
+ hubIds?: (string | number)[]; // Hub id's to filter by
}
export const useFeed = (activeTab: FeedTab | FundingTab, options: UseFeedOptions = {}) => {
@@ -30,6 +32,18 @@ export const useFeed = (activeTab: FeedTab | FundingTab, options: UseFeedOptions
const [currentTab, setCurrentTab] = useState(activeTab);
const [currentOptions, setCurrentOptions] = useState(options);
+ // Re-load the feed if any of the relevant options change
+ const omitCheckKeys = ['initialData']; // Keys to ignore when comparing options
+ useEffect(() => {
+ const filteredOptions = omit(options, omitCheckKeys);
+ const filteredCurrentOptions = omit(currentOptions, omitCheckKeys);
+
+ if (!isEqual(filteredOptions, filteredCurrentOptions)) {
+ setCurrentOptions(options);
+ loadFeed();
+ }
+ }, [options]);
+
// Only load the feed when the component mounts or when the session status changes
// We no longer reload when activeTab changes, as that will be handled by page navigation
useEffect(() => {
@@ -56,57 +70,16 @@ export const useFeed = (activeTab: FeedTab | FundingTab, options: UseFeedOptions
}
}, [status, activeTab]);
- // Check if options have changed
- useEffect(() => {
- // Compare relevant options (excluding initialData which shouldn't trigger a reload)
- const relevantOptionsChanged =
- options.hubSlug !== currentOptions.hubSlug ||
- options.contentType !== currentOptions.contentType ||
- options.source !== currentOptions.source ||
- options.endpoint !== currentOptions.endpoint ||
- options.fundraiseStatus !== currentOptions.fundraiseStatus ||
- options.createdBy !== currentOptions.createdBy ||
- options.ordering !== currentOptions.ordering;
-
- if (relevantOptionsChanged) {
- setCurrentOptions(options);
- loadFeed();
- }
- }, [options]);
-
- const loadFeed = async () => {
- setIsLoading(true);
- try {
- const result = await FeedService.getFeed({
- page: 1,
- pageSize: 20,
- feedView: activeTab as FeedTab, // Only pass feedView if it's a FeedTab
- hubSlug: options.hubSlug,
- contentType: options.contentType,
- source: options.source,
- endpoint: options.endpoint,
- fundraiseStatus: options.fundraiseStatus,
- createdBy: options.createdBy,
- ordering: options.ordering,
- });
- setEntries(result.entries);
- setHasMore(result.hasMore);
- setPage(1);
- } catch (error) {
- console.error('Error loading feed:', error);
- } finally {
- setIsLoading(false);
+ // Load feed items for first or subsequent pages.
+ const loadFeed = async (pageNumber: number = 1) => {
+ if (pageNumber > 1 && (!hasMore || isLoading)) {
+ return;
}
- };
-
- const loadMore = async () => {
- if (!hasMore || isLoading) return;
setIsLoading(true);
try {
- const nextPage = page + 1;
const result = await FeedService.getFeed({
- page: nextPage,
+ page: pageNumber,
pageSize: 20,
feedView: activeTab as FeedTab, // Only pass feedView if it's a FeedTab
hubSlug: options.hubSlug,
@@ -116,12 +89,17 @@ export const useFeed = (activeTab: FeedTab | FundingTab, options: UseFeedOptions
fundraiseStatus: options.fundraiseStatus,
createdBy: options.createdBy,
ordering: options.ordering,
+ hubIds: options.hubIds,
});
- setEntries((prev) => [...prev, ...result.entries]);
+ if (pageNumber === 1) {
+ setEntries(result.entries);
+ } else {
+ setEntries((prev) => [...prev, ...result.entries]);
+ }
setHasMore(result.hasMore);
- setPage(nextPage);
+ setPage(pageNumber);
} catch (error) {
- console.error('Error loading more feed items:', error);
+ console.error('Error loading feed for page:', pageNumber, error);
} finally {
setIsLoading(false);
}
@@ -131,7 +109,7 @@ export const useFeed = (activeTab: FeedTab | FundingTab, options: UseFeedOptions
entries,
isLoading,
hasMore,
- loadMore,
+ loadMore: () => loadFeed(page + 1),
refresh: loadFeed,
};
};
diff --git a/services/feed.service.ts b/services/feed.service.ts
index f58822b26..092d2e778 100644
--- a/services/feed.service.ts
+++ b/services/feed.service.ts
@@ -4,12 +4,27 @@ import { Bounty, BountyType, transformBounty } from '@/types/bounty';
import { transformUser, User } from '@/types/user';
import { transformAuthorProfile } from '@/types/authorProfile';
import { Fundraise, transformFundraise } from '@/types/funding';
+import { Topic, transformTopic } from '@/types/topic';
+
+type Endpoints = 'feed' | 'funding_feed' | 'grant_feed' | undefined;
export class FeedService {
private static readonly BASE_PATH = '/api/feed';
private static readonly FUNDING_PATH = '/api/funding_feed';
private static readonly GRANT_PATH = '/api/grant_feed';
+ // Determine which endpoint to use
+ private static getEndpointPath(endpoint: Endpoints) {
+ switch (endpoint) {
+ case 'funding_feed':
+ return this.FUNDING_PATH;
+ case 'grant_feed':
+ return this.GRANT_PATH;
+ default:
+ return this.BASE_PATH;
+ }
+ }
+
static async getFeed(params?: {
page?: number;
pageSize?: number;
@@ -17,11 +32,12 @@ export class FeedService {
hubSlug?: string;
contentType?: string;
source?: 'all' | 'researchhub';
- endpoint?: 'feed' | 'funding_feed' | 'grant_feed';
+ endpoint?: Endpoints;
fundraiseStatus?: 'OPEN' | 'CLOSED';
grantId?: number;
createdBy?: number;
ordering?: string;
+ hubIds?: (string | number)[];
}): Promise<{ entries: FeedEntry[]; hasMore: boolean }> {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.append('page', params.page.toString());
@@ -34,14 +50,11 @@ export class FeedService {
if (params?.grantId) queryParams.append('grant_id', params.grantId.toString());
if (params?.createdBy) queryParams.append('created_by', params.createdBy.toString());
if (params?.ordering) queryParams.append('ordering', params.ordering);
+ if (params?.hubIds && params.hubIds.length > 0) {
+ queryParams.append('hub_ids', JSON.stringify(params.hubIds));
+ }
- // Determine which endpoint to use
- const basePath =
- params?.endpoint === 'funding_feed'
- ? this.FUNDING_PATH
- : params?.endpoint === 'grant_feed'
- ? this.GRANT_PATH
- : this.BASE_PATH;
+ const basePath = this.getEndpointPath(params?.endpoint);
const url = `${basePath}/${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
try {
@@ -243,4 +256,22 @@ export class FeedService {
return transformFundraise(formattedRawFundraise);
}
+
+ static async getFeedHubs(endpoint: Endpoints): Promise {
+ // Hub search not implemented for feed
+ if (endpoint === 'feed') {
+ return [];
+ }
+
+ let basePath = this.getEndpointPath(endpoint);
+ const path = `${basePath}/hubs/`;
+
+ try {
+ const response = await ApiClient.get(path);
+ return response.map((raw) => transformTopic(raw));
+ } catch (error) {
+ console.error(`Error fetching ${endpoint} hubs`, error);
+ return [];
+ }
+ }
}