adding filtering by recency
Browse files- src/components/SortToggle.tsx +38 -0
- src/pages/index.tsx +27 -9
- src/types/heatmap.ts +1 -0
- src/utils/ranking.ts +51 -7
src/components/SortToggle.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
|
| 3 |
+
export type SortMethod = "activity" | "recent";
|
| 4 |
+
|
| 5 |
+
interface SortToggleProps {
|
| 6 |
+
sortMethod: SortMethod;
|
| 7 |
+
onToggle: (method: SortMethod) => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export default function SortToggle({ sortMethod, onToggle }: SortToggleProps) {
|
| 11 |
+
return (
|
| 12 |
+
<div className="flex items-center justify-center gap-2 mb-8">
|
| 13 |
+
<span className="text-sm text-muted-foreground">Sort by:</span>
|
| 14 |
+
<div className="inline-flex rounded-lg border border-border bg-muted p-1">
|
| 15 |
+
<button
|
| 16 |
+
onClick={() => onToggle("activity")}
|
| 17 |
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
| 18 |
+
sortMethod === "activity"
|
| 19 |
+
? "bg-background text-foreground shadow-sm"
|
| 20 |
+
: "text-muted-foreground hover:text-foreground"
|
| 21 |
+
}`}
|
| 22 |
+
>
|
| 23 |
+
Most Active
|
| 24 |
+
</button>
|
| 25 |
+
<button
|
| 26 |
+
onClick={() => onToggle("recent")}
|
| 27 |
+
className={`px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
| 28 |
+
sortMethod === "recent"
|
| 29 |
+
? "bg-background text-foreground shadow-sm"
|
| 30 |
+
: "text-muted-foreground hover:text-foreground"
|
| 31 |
+
}`}
|
| 32 |
+
>
|
| 33 |
+
Most Recent
|
| 34 |
+
</button>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
);
|
| 38 |
+
}
|
src/pages/index.tsx
CHANGED
|
@@ -4,7 +4,8 @@ import OrganizationButton from "../components/OrganizationButton";
|
|
| 4 |
import HeatmapGrid from "../components/HeatmapGrid";
|
| 5 |
import Navbar from "../components/Navbar";
|
| 6 |
import TagSelector from "../components/TagSelector";
|
| 7 |
-
import {
|
|
|
|
| 8 |
import { ORGANIZATIONS, SCIENTIFIC_TAGS } from "../constants/organizations";
|
| 9 |
|
| 10 |
interface PageProps {
|
|
@@ -19,6 +20,7 @@ function Page({
|
|
| 19 |
const [isLoading, setIsLoading] = useState(true);
|
| 20 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 21 |
const [showAllOrgs, setShowAllOrgs] = useState(false);
|
|
|
|
| 22 |
|
| 23 |
useEffect(() => {
|
| 24 |
if (calendarData && Object.keys(calendarData).length > 0) {
|
|
@@ -31,13 +33,23 @@ function Page({
|
|
| 31 |
if (selectedTags.length === 0) {
|
| 32 |
return providers;
|
| 33 |
}
|
| 34 |
-
|
| 35 |
return providers.filter(provider => {
|
| 36 |
if (!provider.tags) return false;
|
| 37 |
return selectedTags.some(tag => provider.tags!.includes(tag));
|
| 38 |
});
|
| 39 |
}, [providers, selectedTags]);
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
const handleTagToggle = (tagId: string) => {
|
| 42 |
setSelectedTags(prev => {
|
| 43 |
if (prev.includes(tagId)) {
|
|
@@ -78,6 +90,12 @@ function Page({
|
|
| 78 |
/>
|
| 79 |
</div>
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
<div className="mb-16 mx-auto max-w-full">
|
| 82 |
{/* Organization Buttons with Horizontal Scroll */}
|
| 83 |
<div className="relative px-4">
|
|
@@ -90,7 +108,7 @@ function Page({
|
|
| 90 |
}}
|
| 91 |
>
|
| 92 |
<div className="flex gap-6 py-2 w-max mx-auto">
|
| 93 |
-
{(showAllOrgs ?
|
| 94 |
<OrganizationButton
|
| 95 |
key={provider.fullName || provider.authors[0]}
|
| 96 |
provider={provider}
|
|
@@ -105,22 +123,22 @@ function Page({
|
|
| 105 |
<div className="absolute left-0 top-0 bottom-4 w-8 bg-gradient-to-r from-background via-background/80 to-transparent pointer-events-none" />
|
| 106 |
<div className="absolute right-0 top-0 bottom-4 w-8 bg-gradient-to-l from-background via-background/80 to-transparent pointer-events-none" />
|
| 107 |
</div>
|
| 108 |
-
|
| 109 |
{/* Show More/Less Button */}
|
| 110 |
-
{
|
| 111 |
<div className="flex justify-center mt-4">
|
| 112 |
<button
|
| 113 |
onClick={() => setShowAllOrgs(!showAllOrgs)}
|
| 114 |
className="px-6 py-2 text-sm font-medium text-foreground bg-muted hover:bg-muted/80 rounded-full transition-colors duration-200 border border-border"
|
| 115 |
>
|
| 116 |
-
{showAllOrgs ? `Show Less (Top 10)` : `Show All ${
|
| 117 |
</button>
|
| 118 |
</div>
|
| 119 |
)}
|
| 120 |
</div>
|
| 121 |
-
|
| 122 |
-
<HeatmapGrid
|
| 123 |
-
sortedProviders={showAllOrgs ?
|
| 124 |
calendarData={calendarData}
|
| 125 |
isLoading={isLoading}
|
| 126 |
/>
|
|
|
|
| 4 |
import HeatmapGrid from "../components/HeatmapGrid";
|
| 5 |
import Navbar from "../components/Navbar";
|
| 6 |
import TagSelector from "../components/TagSelector";
|
| 7 |
+
import SortToggle, { SortMethod } from "../components/SortToggle";
|
| 8 |
+
import { getProviders, sortProvidersByActivity, sortProvidersByRecentRelease } from "../utils/ranking";
|
| 9 |
import { ORGANIZATIONS, SCIENTIFIC_TAGS } from "../constants/organizations";
|
| 10 |
|
| 11 |
interface PageProps {
|
|
|
|
| 20 |
const [isLoading, setIsLoading] = useState(true);
|
| 21 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 22 |
const [showAllOrgs, setShowAllOrgs] = useState(false);
|
| 23 |
+
const [sortMethod, setSortMethod] = useState<SortMethod>("activity");
|
| 24 |
|
| 25 |
useEffect(() => {
|
| 26 |
if (calendarData && Object.keys(calendarData).length > 0) {
|
|
|
|
| 33 |
if (selectedTags.length === 0) {
|
| 34 |
return providers;
|
| 35 |
}
|
| 36 |
+
|
| 37 |
return providers.filter(provider => {
|
| 38 |
if (!provider.tags) return false;
|
| 39 |
return selectedTags.some(tag => provider.tags!.includes(tag));
|
| 40 |
});
|
| 41 |
}, [providers, selectedTags]);
|
| 42 |
|
| 43 |
+
// Sort providers based on selected sort method
|
| 44 |
+
const sortedProviders = useMemo(() => {
|
| 45 |
+
const providersCopy = [...filteredProviders];
|
| 46 |
+
if (sortMethod === "activity") {
|
| 47 |
+
return sortProvidersByActivity(providersCopy, calendarData);
|
| 48 |
+
} else {
|
| 49 |
+
return sortProvidersByRecentRelease(providersCopy, calendarData);
|
| 50 |
+
}
|
| 51 |
+
}, [filteredProviders, sortMethod, calendarData]);
|
| 52 |
+
|
| 53 |
const handleTagToggle = (tagId: string) => {
|
| 54 |
setSelectedTags(prev => {
|
| 55 |
if (prev.includes(tagId)) {
|
|
|
|
| 90 |
/>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
+
{/* Sort Toggle */}
|
| 94 |
+
<SortToggle
|
| 95 |
+
sortMethod={sortMethod}
|
| 96 |
+
onToggle={setSortMethod}
|
| 97 |
+
/>
|
| 98 |
+
|
| 99 |
<div className="mb-16 mx-auto max-w-full">
|
| 100 |
{/* Organization Buttons with Horizontal Scroll */}
|
| 101 |
<div className="relative px-4">
|
|
|
|
| 108 |
}}
|
| 109 |
>
|
| 110 |
<div className="flex gap-6 py-2 w-max mx-auto">
|
| 111 |
+
{(showAllOrgs ? sortedProviders : sortedProviders.slice(0, 10)).map((provider, index) => (
|
| 112 |
<OrganizationButton
|
| 113 |
key={provider.fullName || provider.authors[0]}
|
| 114 |
provider={provider}
|
|
|
|
| 123 |
<div className="absolute left-0 top-0 bottom-4 w-8 bg-gradient-to-r from-background via-background/80 to-transparent pointer-events-none" />
|
| 124 |
<div className="absolute right-0 top-0 bottom-4 w-8 bg-gradient-to-l from-background via-background/80 to-transparent pointer-events-none" />
|
| 125 |
</div>
|
| 126 |
+
|
| 127 |
{/* Show More/Less Button */}
|
| 128 |
+
{sortedProviders.length > 10 && (
|
| 129 |
<div className="flex justify-center mt-4">
|
| 130 |
<button
|
| 131 |
onClick={() => setShowAllOrgs(!showAllOrgs)}
|
| 132 |
className="px-6 py-2 text-sm font-medium text-foreground bg-muted hover:bg-muted/80 rounded-full transition-colors duration-200 border border-border"
|
| 133 |
>
|
| 134 |
+
{showAllOrgs ? `Show Less (Top 10)` : `Show All ${sortedProviders.length} Organizations`}
|
| 135 |
</button>
|
| 136 |
</div>
|
| 137 |
)}
|
| 138 |
</div>
|
| 139 |
+
|
| 140 |
+
<HeatmapGrid
|
| 141 |
+
sortedProviders={showAllOrgs ? sortedProviders : sortedProviders.slice(0, 10)}
|
| 142 |
calendarData={calendarData}
|
| 143 |
isLoading={isLoading}
|
| 144 |
/>
|
src/types/heatmap.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ProviderInfo {
|
|
| 11 |
numDatasets?: number;
|
| 12 |
numFollowers?: number;
|
| 13 |
numUsers?: number;
|
|
|
|
| 14 |
authorsData?: {
|
| 15 |
author: string;
|
| 16 |
fullName: string;
|
|
|
|
| 11 |
numDatasets?: number;
|
| 12 |
numFollowers?: number;
|
| 13 |
numUsers?: number;
|
| 14 |
+
mostRecentRelease?: string | null;
|
| 15 |
authorsData?: {
|
| 16 |
author: string;
|
| 17 |
fullName: string;
|
src/utils/ranking.ts
CHANGED
|
@@ -37,34 +37,78 @@ export const extractUniqueAuthors = (providers: ProviderInfo[]): string[] => {
|
|
| 37 |
};
|
| 38 |
|
| 39 |
export const sortProvidersByActivity = (
|
| 40 |
-
providers: ProviderInfo[],
|
| 41 |
calendarData: CalendarData
|
| 42 |
): ProviderInfo[] => {
|
| 43 |
return providers.sort((providerA, providerB) => {
|
| 44 |
const providerAData = calendarData[providerA.authors[0]] || [];
|
| 45 |
const providerBData = calendarData[providerB.authors[0]] || [];
|
| 46 |
-
|
| 47 |
const providerAActivityCount = providerAData.reduce(
|
| 48 |
-
(totalCount, day) => totalCount + day.count,
|
| 49 |
0
|
| 50 |
);
|
| 51 |
const providerBActivityCount = providerBData.reduce(
|
| 52 |
-
(totalCount, day) => totalCount + day.count,
|
| 53 |
0
|
| 54 |
);
|
| 55 |
-
|
| 56 |
return providerBActivityCount - providerAActivityCount;
|
| 57 |
});
|
| 58 |
};
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
export const getProviders = async (providers: ProviderInfo[]) => {
|
| 61 |
const uniqueAuthors = extractUniqueAuthors(providers);
|
| 62 |
-
|
| 63 |
const authorModelsData: ModelData[] = await fetchAllAuthorsData(uniqueAuthors);
|
| 64 |
const providersWithMetadata = await fetchAllProvidersData(providers);
|
| 65 |
|
| 66 |
const calendarData = generateCalendarData(authorModelsData, providersWithMetadata);
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
return {
|
| 70 |
calendarData,
|
|
|
|
| 37 |
};
|
| 38 |
|
| 39 |
export const sortProvidersByActivity = (
|
| 40 |
+
providers: ProviderInfo[],
|
| 41 |
calendarData: CalendarData
|
| 42 |
): ProviderInfo[] => {
|
| 43 |
return providers.sort((providerA, providerB) => {
|
| 44 |
const providerAData = calendarData[providerA.authors[0]] || [];
|
| 45 |
const providerBData = calendarData[providerB.authors[0]] || [];
|
| 46 |
+
|
| 47 |
const providerAActivityCount = providerAData.reduce(
|
| 48 |
+
(totalCount, day) => totalCount + day.count,
|
| 49 |
0
|
| 50 |
);
|
| 51 |
const providerBActivityCount = providerBData.reduce(
|
| 52 |
+
(totalCount, day) => totalCount + day.count,
|
| 53 |
0
|
| 54 |
);
|
| 55 |
+
|
| 56 |
return providerBActivityCount - providerAActivityCount;
|
| 57 |
});
|
| 58 |
};
|
| 59 |
|
| 60 |
+
export const sortProvidersByRecentRelease = (
|
| 61 |
+
providers: ProviderInfo[],
|
| 62 |
+
calendarData: CalendarData
|
| 63 |
+
): ProviderInfo[] => {
|
| 64 |
+
return providers.sort((providerA, providerB) => {
|
| 65 |
+
const providerAData = calendarData[providerA.authors[0]] || [];
|
| 66 |
+
const providerBData = calendarData[providerB.authors[0]] || [];
|
| 67 |
+
|
| 68 |
+
const getLatestDate = (data: typeof providerAData) => {
|
| 69 |
+
if (data.length === 0) return null;
|
| 70 |
+
const datesWithActivity = data.filter(day => day.count > 0);
|
| 71 |
+
if (datesWithActivity.length === 0) return null;
|
| 72 |
+
return datesWithActivity.reduce((latest, day) => {
|
| 73 |
+
return new Date(day.date) > new Date(latest.date) ? day : latest;
|
| 74 |
+
}).date;
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
const latestA = getLatestDate(providerAData);
|
| 78 |
+
const latestB = getLatestDate(providerBData);
|
| 79 |
+
|
| 80 |
+
if (!latestA && !latestB) return 0;
|
| 81 |
+
if (!latestA) return 1;
|
| 82 |
+
if (!latestB) return -1;
|
| 83 |
+
|
| 84 |
+
return new Date(latestB).getTime() - new Date(latestA).getTime();
|
| 85 |
+
});
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
export const getProviders = async (providers: ProviderInfo[]) => {
|
| 89 |
const uniqueAuthors = extractUniqueAuthors(providers);
|
| 90 |
+
|
| 91 |
const authorModelsData: ModelData[] = await fetchAllAuthorsData(uniqueAuthors);
|
| 92 |
const providersWithMetadata = await fetchAllProvidersData(providers);
|
| 93 |
|
| 94 |
const calendarData = generateCalendarData(authorModelsData, providersWithMetadata);
|
| 95 |
+
|
| 96 |
+
// Calculate and store most recent release date for each provider
|
| 97 |
+
const providersWithReleaseDate = providersWithMetadata.map(provider => {
|
| 98 |
+
const providerData = calendarData[provider.authors[0]] || [];
|
| 99 |
+
const datesWithActivity = providerData.filter(day => day.count > 0);
|
| 100 |
+
|
| 101 |
+
if (datesWithActivity.length > 0) {
|
| 102 |
+
const latestActivity = datesWithActivity.reduce((latest, day) => {
|
| 103 |
+
return new Date(day.date) > new Date(latest.date) ? day : latest;
|
| 104 |
+
});
|
| 105 |
+
return { ...provider, mostRecentRelease: latestActivity.date };
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return { ...provider, mostRecentRelease: null };
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
const sortedProvidersByActivity = sortProvidersByActivity(providersWithReleaseDate, calendarData);
|
| 112 |
|
| 113 |
return {
|
| 114 |
calendarData,
|