Avijit Ghosh commited on
Commit
e603004
·
1 Parent(s): 198dbcf

Add scientific tag filtering and weekly/daily heatmap view toggle

Browse files

Features added:
- Scientific area tag selector with 9 categories (antibody, hormones, materials-science, drug-discovery, biology, medicine, physics, data, education)
- Tagged all 14 organizations with relevant scientific areas
- Weekly activity view as default (aggregated from daily data)
- Toggle between weekly and daily heatmap views
- Enhanced tooltips for both view modes
- Improved visual design with proper theme support
- Responsive layout for all screen sizes

Technical changes:
- Added TagSelector component for filtering organizations
- Added ViewToggle component for switching heatmap granularity
- Updated ProviderInfo type to include tags property
- Created weeklyCalendar utility for data aggregation
- Enhanced Heatmap component to support both view modes
- Updated all related components to pass viewMode prop
- Added theme context and toggle (prepared for future use)

package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -19,7 +19,7 @@
19
  "class-variance-authority": "^0.7.0",
20
  "clsx": "^2.1.1",
21
  "lucide-react": "^0.427.0",
22
- "next": "14.2.5",
23
  "react": "^18",
24
  "react-activity-calendar": "^2.2.11",
25
  "react-dom": "^18",
@@ -27,11 +27,11 @@
27
  "tailwindcss-animate": "^1.0.7"
28
  },
29
  "devDependencies": {
30
- "typescript": "^5",
31
  "@types/node": "^20",
32
  "@types/react": "^18",
33
  "@types/react-dom": "^18",
34
  "postcss": "^8",
35
- "tailwindcss": "^3.4.1"
 
36
  }
37
  }
 
19
  "class-variance-authority": "^0.7.0",
20
  "clsx": "^2.1.1",
21
  "lucide-react": "^0.427.0",
22
+ "next": "^14.2.33",
23
  "react": "^18",
24
  "react-activity-calendar": "^2.2.11",
25
  "react-dom": "^18",
 
27
  "tailwindcss-animate": "^1.0.7"
28
  },
29
  "devDependencies": {
 
30
  "@types/node": "^20",
31
  "@types/react": "^18",
32
  "@types/react-dom": "^18",
33
  "postcss": "^8",
34
+ "tailwindcss": "^3.4.1",
35
+ "typescript": "^5"
36
  }
37
  }
src/components/ClientNavbar.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React from "react";
4
+ import UserSearchDialog from "./UserSearchDialog";
5
+ import ThemeToggle from "./ThemeToggle";
6
+
7
+ const Navbar: React.FC = () => {
8
+ return (
9
+ <nav className="w-full mt-4">
10
+ <div className="max-w-6xl mx-auto px-4 py-3">
11
+ <div className="flex items-center justify-end gap-3">
12
+ <ThemeToggle />
13
+ <UserSearchDialog />
14
+ </div>
15
+ </div>
16
+ </nav>
17
+ );
18
+ };
19
+
20
+ export default Navbar;
src/components/Heatmap.tsx CHANGED
@@ -2,6 +2,9 @@ import React from "react";
2
  import ActivityCalendar from "react-activity-calendar";
3
  import { Tooltip, Avatar } from "@mui/material";
4
  import Link from "next/link";
 
 
 
5
 
6
  type HeatmapProps = {
7
  data: Array<{ date: string; count: number; level: number }>;
@@ -11,9 +14,22 @@ type HeatmapProps = {
11
  avatarUrl: string;
12
  authorId: string;
13
  showHeader?: boolean;
 
14
  };
15
 
16
- const Heatmap: React.FC<HeatmapProps> = ({ data, color, providerName, fullName, avatarUrl, authorId, showHeader = true }) => {
 
 
 
 
 
 
 
 
 
 
 
 
17
  return (
18
  <div className="flex flex-col items-center w-full mx-auto">
19
  {showHeader && (
@@ -37,16 +53,20 @@ const Heatmap: React.FC<HeatmapProps> = ({ data, color, providerName, fullName,
37
  )}
38
  <div className="w-full overflow-x-auto flex justify-center">
39
  <ActivityCalendar
40
- data={data}
41
  theme={{
42
  dark: ["#161b22", color],
43
  light: ["#e0e0e0", color],
44
  }}
45
- blockSize={11}
46
  hideTotalCount
47
  renderBlock={(block, activity) => (
48
  <Tooltip
49
- title={`${activity.count} new repos on ${activity.date}`}
 
 
 
 
50
  arrow
51
  >
52
  {block}
 
2
  import ActivityCalendar from "react-activity-calendar";
3
  import { Tooltip, Avatar } from "@mui/material";
4
  import Link from "next/link";
5
+ import { aggregateToWeeklyData, getWeekDateRange } from "../utils/weeklyCalendar";
6
+
7
+ type ViewMode = 'daily' | 'weekly';
8
 
9
  type HeatmapProps = {
10
  data: Array<{ date: string; count: number; level: number }>;
 
14
  avatarUrl: string;
15
  authorId: string;
16
  showHeader?: boolean;
17
+ viewMode: ViewMode;
18
  };
19
 
20
+ const Heatmap: React.FC<HeatmapProps> = ({
21
+ data,
22
+ color,
23
+ providerName,
24
+ fullName,
25
+ avatarUrl,
26
+ authorId,
27
+ showHeader = true,
28
+ viewMode
29
+ }) => {
30
+ // Process data based on view mode
31
+ const processedData = viewMode === 'weekly' ? aggregateToWeeklyData(data) : data;
32
+
33
  return (
34
  <div className="flex flex-col items-center w-full mx-auto">
35
  {showHeader && (
 
53
  )}
54
  <div className="w-full overflow-x-auto flex justify-center">
55
  <ActivityCalendar
56
+ data={processedData}
57
  theme={{
58
  dark: ["#161b22", color],
59
  light: ["#e0e0e0", color],
60
  }}
61
+ blockSize={viewMode === 'weekly' ? 15 : 11}
62
  hideTotalCount
63
  renderBlock={(block, activity) => (
64
  <Tooltip
65
+ title={
66
+ viewMode === 'weekly'
67
+ ? `${activity.count} new repos in week of ${getWeekDateRange(activity.date)}`
68
+ : `${activity.count} new repos on ${activity.date}`
69
+ }
70
  arrow
71
  >
72
  {block}
src/components/HeatmapGrid.tsx CHANGED
@@ -1,7 +1,10 @@
1
- import React from "react";
2
  import { ProviderInfo, CalendarData } from "../types/heatmap";
3
  import OrganizationCard from "./OrganizationCard";
4
  import ProviderHeatmapSkeleton from "./ProviderHeatmapSkeleton";
 
 
 
5
 
6
  interface HeatmapGridProps {
7
  sortedProviders: ProviderInfo[];
@@ -10,6 +13,8 @@ interface HeatmapGridProps {
10
  }
11
 
12
  const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData, isLoading }) => {
 
 
13
  if (isLoading) {
14
  return (
15
  <div className="flex flex-col gap-8 max-w-4xl mx-auto mb-16">
@@ -22,12 +27,22 @@ const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData
22
 
23
  return (
24
  <div className="flex flex-col gap-8 max-w-4xl mx-auto mb-16">
 
 
 
 
 
 
 
 
 
25
  {sortedProviders.map((provider, index) => (
26
  <OrganizationCard
27
  key={provider.fullName || provider.authors[0]}
28
  provider={provider}
29
  calendarData={calendarData}
30
  rank={index + 1}
 
31
  />
32
  ))}
33
  </div>
 
1
+ import React, { useState } from "react";
2
  import { ProviderInfo, CalendarData } from "../types/heatmap";
3
  import OrganizationCard from "./OrganizationCard";
4
  import ProviderHeatmapSkeleton from "./ProviderHeatmapSkeleton";
5
+ import ViewToggle from "./ViewToggle";
6
+
7
+ type ViewMode = 'daily' | 'weekly';
8
 
9
  interface HeatmapGridProps {
10
  sortedProviders: ProviderInfo[];
 
13
  }
14
 
15
  const HeatmapGrid: React.FC<HeatmapGridProps> = ({ sortedProviders, calendarData, isLoading }) => {
16
+ const [viewMode, setViewMode] = useState<ViewMode>('weekly');
17
+
18
  if (isLoading) {
19
  return (
20
  <div className="flex flex-col gap-8 max-w-4xl mx-auto mb-16">
 
27
 
28
  return (
29
  <div className="flex flex-col gap-8 max-w-4xl mx-auto mb-16">
30
+ {/* View Toggle */}
31
+ <div className="flex justify-center">
32
+ <ViewToggle
33
+ viewMode={viewMode}
34
+ onToggle={setViewMode}
35
+ />
36
+ </div>
37
+
38
+ {/* Heatmap Cards */}
39
  {sortedProviders.map((provider, index) => (
40
  <OrganizationCard
41
  key={provider.fullName || provider.authors[0]}
42
  provider={provider}
43
  calendarData={calendarData}
44
  rank={index + 1}
45
+ viewMode={viewMode}
46
  />
47
  ))}
48
  </div>
src/components/Navbar.tsx CHANGED
@@ -5,7 +5,7 @@ const Navbar: React.FC = () => {
5
  return (
6
  <nav className="w-full mt-4">
7
  <div className="max-w-6xl mx-auto px-4 py-3">
8
- <div className="flex items-center justify-end">
9
  <UserSearchDialog />
10
  </div>
11
  </div>
 
5
  return (
6
  <nav className="w-full mt-4">
7
  <div className="max-w-6xl mx-auto px-4 py-3">
8
+ <div className="flex items-center justify-end gap-3">
9
  <UserSearchDialog />
10
  </div>
11
  </div>
src/components/OrganizationCard.tsx CHANGED
@@ -3,13 +3,16 @@ import { ProviderInfo, CalendarData } from "../types/heatmap";
3
  import Heatmap from "./Heatmap";
4
  import OrganizationHeader from "./OrganizationHeader";
5
 
 
 
6
  interface OrganizationCardProps {
7
  provider: ProviderInfo;
8
  calendarData: CalendarData;
9
  rank: number;
 
10
  }
11
 
12
- const OrganizationCard = React.memo(({ provider, calendarData, rank }: OrganizationCardProps) => {
13
  const providerName = provider.fullName || provider.authors[0];
14
  const calendarKey = provider.authors[0];
15
  const totalCount = calendarData[calendarKey]?.reduce((sum, day) => sum + day.count, 0) || 0;
@@ -32,6 +35,7 @@ const OrganizationCard = React.memo(({ provider, calendarData, rank }: Organizat
32
  avatarUrl={provider.avatarUrl ?? ''}
33
  authorId={calendarKey}
34
  showHeader={false}
 
35
  />
36
  </div>
37
 
 
3
  import Heatmap from "./Heatmap";
4
  import OrganizationHeader from "./OrganizationHeader";
5
 
6
+ type ViewMode = 'daily' | 'weekly';
7
+
8
  interface OrganizationCardProps {
9
  provider: ProviderInfo;
10
  calendarData: CalendarData;
11
  rank: number;
12
+ viewMode: ViewMode;
13
  }
14
 
15
+ const OrganizationCard = React.memo(({ provider, calendarData, rank, viewMode }: OrganizationCardProps) => {
16
  const providerName = provider.fullName || provider.authors[0];
17
  const calendarKey = provider.authors[0];
18
  const totalCount = calendarData[calendarKey]?.reduce((sum, day) => sum + day.count, 0) || 0;
 
35
  avatarUrl={provider.avatarUrl ?? ''}
36
  authorId={calendarKey}
37
  showHeader={false}
38
+ viewMode={viewMode}
39
  />
40
  </div>
41
 
src/components/TagSelector.tsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ export interface Tag {
5
+ id: string;
6
+ label: string;
7
+ color?: string;
8
+ }
9
+
10
+ interface TagSelectorProps {
11
+ tags: Tag[];
12
+ selectedTags: string[];
13
+ onTagToggle: (tagId: string) => void;
14
+ className?: string;
15
+ }
16
+
17
+ const TagSelector: React.FC<TagSelectorProps> = ({
18
+ tags,
19
+ selectedTags,
20
+ onTagToggle,
21
+ className,
22
+ }) => {
23
+ return (
24
+ <div className={cn("flex flex-col items-center gap-4", className)}>
25
+ <div className="flex items-center gap-3">
26
+ <h3 className="text-lg font-medium text-foreground">Select Tags</h3>
27
+ <span className="bg-blue-500 text-white text-sm px-2 py-1 rounded-full min-w-[24px] h-6 flex items-center justify-center">
28
+ {selectedTags.length}
29
+ </span>
30
+ </div>
31
+
32
+ <div className="flex flex-wrap gap-3 justify-center max-w-4xl">
33
+ {tags.map((tag) => {
34
+ const isSelected = selectedTags.includes(tag.id);
35
+ return (
36
+ <button
37
+ key={tag.id}
38
+ onClick={() => onTagToggle(tag.id)}
39
+ className={cn(
40
+ "px-4 py-2 rounded-full border-2 transition-all duration-200 font-medium",
41
+ "hover:scale-105 active:scale-95",
42
+ isSelected
43
+ ? "bg-blue-500 text-white border-blue-500 shadow-lg"
44
+ : cn(
45
+ "border-border hover:border-accent-foreground/20",
46
+ "bg-card text-card-foreground hover:bg-accent hover:text-accent-foreground",
47
+ "dark:bg-card dark:text-card-foreground dark:border-border",
48
+ "light:bg-white light:text-gray-700 light:border-gray-300 light:hover:border-gray-400"
49
+ )
50
+ )}
51
+ >
52
+ {tag.label}
53
+ </button>
54
+ );
55
+ })}
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ export default TagSelector;
src/components/ThemeToggle.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { useTheme } from '../contexts/ThemeContext';
3
+ import { cn } from '../lib/utils';
4
+
5
+ interface ThemeToggleProps {
6
+ className?: string;
7
+ }
8
+
9
+ const ThemeToggle: React.FC<ThemeToggleProps> = ({ className }) => {
10
+ const [mounted, setMounted] = useState(false);
11
+ const { theme, toggleTheme } = useTheme();
12
+
13
+ // Only render after hydration to prevent SSR issues
14
+ useEffect(() => {
15
+ setMounted(true);
16
+ }, []);
17
+
18
+ if (!mounted) {
19
+ // Return a placeholder that matches the final component size
20
+ return (
21
+ <div className={cn(
22
+ "relative inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border bg-background",
23
+ className
24
+ )}>
25
+ <div className="h-5 w-5 animate-pulse bg-muted rounded"></div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <button
32
+ onClick={toggleTheme}
33
+ className={cn(
34
+ "relative inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors",
35
+ className
36
+ )}
37
+ aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
38
+ >
39
+ <div className="relative h-5 w-5">
40
+ {/* Sun Icon */}
41
+ <svg
42
+ className={cn(
43
+ "absolute inset-0 h-5 w-5 transition-all",
44
+ theme === 'dark' ? "rotate-90 scale-0" : "rotate-0 scale-100"
45
+ )}
46
+ fill="none"
47
+ viewBox="0 0 24 24"
48
+ stroke="currentColor"
49
+ strokeWidth={2}
50
+ >
51
+ <path
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
55
+ />
56
+ </svg>
57
+
58
+ {/* Moon Icon */}
59
+ <svg
60
+ className={cn(
61
+ "absolute inset-0 h-5 w-5 transition-all",
62
+ theme === 'dark' ? "rotate-0 scale-100" : "-rotate-90 scale-0"
63
+ )}
64
+ fill="none"
65
+ viewBox="0 0 24 24"
66
+ stroke="currentColor"
67
+ strokeWidth={2}
68
+ >
69
+ <path
70
+ strokeLinecap="round"
71
+ strokeLinejoin="round"
72
+ d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
73
+ />
74
+ </svg>
75
+ </div>
76
+ </button>
77
+ );
78
+ };
79
+
80
+ export default ThemeToggle;
src/components/ViewToggle.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ type ViewMode = 'daily' | 'weekly';
5
+
6
+ interface ViewToggleProps {
7
+ viewMode: ViewMode;
8
+ onToggle: (mode: ViewMode) => void;
9
+ className?: string;
10
+ }
11
+
12
+ const ViewToggle: React.FC<ViewToggleProps> = ({ viewMode, onToggle, className }) => {
13
+ return (
14
+ <div className={cn("flex items-center gap-1 bg-muted rounded-lg p-1", className)}>
15
+ <button
16
+ onClick={() => onToggle('weekly')}
17
+ className={cn(
18
+ "px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200",
19
+ viewMode === 'weekly'
20
+ ? "bg-background text-foreground shadow-sm"
21
+ : "text-muted-foreground hover:text-foreground"
22
+ )}
23
+ >
24
+ Weekly
25
+ </button>
26
+ <button
27
+ onClick={() => onToggle('daily')}
28
+ className={cn(
29
+ "px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200",
30
+ viewMode === 'daily'
31
+ ? "bg-background text-foreground shadow-sm"
32
+ : "text-muted-foreground hover:text-foreground"
33
+ )}
34
+ >
35
+ Daily
36
+ </button>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default ViewToggle;
src/constants/organizations.ts CHANGED
@@ -1,18 +1,87 @@
1
  import { ProviderInfo } from "../types/heatmap";
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  export const ORGANIZATIONS: ProviderInfo[] = [
4
- { color: "#ff7000", authors: ["LeMaterial", "Entalpic"] },
5
- { color: "#1877F2", authors: ["arcinstitute"]},
6
- { color: "#10A37F", authors: ["SandboxAQ"] },
7
- { color: "#cc785c", authors: ["Anthropic"] },
8
- { color: "#DB4437", authors: ["polymathic-ai"] },
9
- { color: "#F45098", authors: ["NASA-AIML", "nasa-ibm-ai4science", "nasa-impact"] },
10
- { color: "#FEB800", authors: ["facebook"] },
11
- { color: "#76B900", authors: ["nvidia"] },
12
- { color: "#0088cc", authors: ["Merck"] },
13
- { color: "#0088cc", authors: ["wanglab"] },
14
- { color: "#0088cc", authors: ["jablonkagroup"] },
15
- { color: "#4C6EE6", authors: ["Orbital-Materials"] },
16
- { color: "#4C6EE6", authors: ["Xaira-Therapeutics"] },
17
- { color: "#FEC912", authors: ["hugging-science"] },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  ];
 
1
  import { ProviderInfo } from "../types/heatmap";
2
 
3
+ // Scientific area tags
4
+ export const SCIENTIFIC_TAGS = [
5
+ { id: "antibody", label: "antibody" },
6
+ { id: "hormones", label: "hormones" },
7
+ { id: "materials-science", label: "materials-science" },
8
+ { id: "drug-discovery", label: "drug-discovery" },
9
+ { id: "biology", label: "biology" },
10
+ { id: "medicine", label: "medicine" },
11
+ { id: "physics", label: "physics" },
12
+ { id: "data", label: "data" },
13
+ { id: "education", label: "education" },
14
+ ];
15
+
16
  export const ORGANIZATIONS: ProviderInfo[] = [
17
+ {
18
+ color: "#ff7000",
19
+ authors: ["LeMaterial", "Entalpic"],
20
+ tags: ["materials-science", "drug-discovery"]
21
+ },
22
+ {
23
+ color: "#1877F2",
24
+ authors: ["arcinstitute"],
25
+ tags: ["biology", "medicine", "drug-discovery"]
26
+ },
27
+ {
28
+ color: "#10A37F",
29
+ authors: ["SandboxAQ"],
30
+ tags: ["physics", "materials-science", "drug-discovery"]
31
+ },
32
+ {
33
+ color: "#cc785c",
34
+ authors: ["Anthropic"],
35
+ tags: ["data", "education"]
36
+ },
37
+ {
38
+ color: "#DB4437",
39
+ authors: ["polymathic-ai"],
40
+ tags: ["physics", "data"]
41
+ },
42
+ {
43
+ color: "#F45098",
44
+ authors: ["NASA-AIML", "nasa-ibm-ai4science", "nasa-impact"],
45
+ tags: ["physics", "data", "education"]
46
+ },
47
+ {
48
+ color: "#FEB800",
49
+ authors: ["facebook"],
50
+ tags: ["data", "education"]
51
+ },
52
+ {
53
+ color: "#76B900",
54
+ authors: ["nvidia"],
55
+ tags: ["data", "physics", "materials-science"]
56
+ },
57
+ {
58
+ color: "#0088cc",
59
+ authors: ["Merck"],
60
+ tags: ["drug-discovery", "medicine", "biology"]
61
+ },
62
+ {
63
+ color: "#0088cc",
64
+ authors: ["wanglab"],
65
+ tags: ["biology", "medicine"]
66
+ },
67
+ {
68
+ color: "#0088cc",
69
+ authors: ["jablonkagroup"],
70
+ tags: ["materials-science", "drug-discovery"]
71
+ },
72
+ {
73
+ color: "#4C6EE6",
74
+ authors: ["Orbital-Materials"],
75
+ tags: ["materials-science", "physics"]
76
+ },
77
+ {
78
+ color: "#4C6EE6",
79
+ authors: ["Xaira-Therapeutics"],
80
+ tags: ["drug-discovery", "medicine", "biology", "antibody"]
81
+ },
82
+ {
83
+ color: "#FEC912",
84
+ authors: ["hugging-science"],
85
+ tags: ["data", "education", "biology", "physics"]
86
+ },
87
  ];
src/contexts/ThemeContext.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useContext, useEffect, useState } from 'react';
2
+
3
+ type Theme = 'light' | 'dark';
4
+
5
+ interface ThemeContextType {
6
+ theme: Theme;
7
+ toggleTheme: () => void;
8
+ }
9
+
10
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
11
+
12
+ export const useTheme = () => {
13
+ const context = useContext(ThemeContext);
14
+ if (!context) {
15
+ throw new Error('useTheme must be used within a ThemeProvider');
16
+ }
17
+ return context;
18
+ };
19
+
20
+ interface ThemeProviderProps {
21
+ children: React.ReactNode;
22
+ }
23
+
24
+ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
25
+ const [theme, setTheme] = useState<Theme>('dark');
26
+ const [mounted, setMounted] = useState(false);
27
+
28
+ useEffect(() => {
29
+ setMounted(true);
30
+ // Get theme from localStorage or system preference
31
+ const savedTheme = localStorage.getItem('theme') as Theme | null;
32
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
33
+ const initialTheme = savedTheme || systemTheme;
34
+
35
+ setTheme(initialTheme);
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ if (!mounted) return;
40
+
41
+ // Apply theme to document
42
+ const root = document.documentElement;
43
+
44
+ if (theme === 'dark') {
45
+ root.classList.add('dark');
46
+ } else {
47
+ root.classList.remove('dark');
48
+ }
49
+
50
+ // Save to localStorage
51
+ localStorage.setItem('theme', theme);
52
+ }, [theme, mounted]);
53
+
54
+ const toggleTheme = () => {
55
+ setTheme(prev => prev === 'light' ? 'dark' : 'light');
56
+ };
57
+
58
+ // Prevent hydration mismatch by not rendering until mounted
59
+ if (!mounted) {
60
+ return <>{children}</>;
61
+ }
62
+
63
+ return (
64
+ <ThemeContext.Provider value={{ theme, toggleTheme }}>
65
+ {children}
66
+ </ThemeContext.Provider>
67
+ );
68
+ };
src/pages/index.tsx CHANGED
@@ -1,10 +1,11 @@
1
- import React, { useState, useEffect } from "react";
2
  import { ProviderInfo, ModelData, CalendarData } from "../types/heatmap";
3
  import OrganizationButton from "../components/OrganizationButton";
4
  import HeatmapGrid from "../components/HeatmapGrid";
5
  import Navbar from "../components/Navbar";
 
6
  import { getProviders } from "../utils/ranking";
7
- import { ORGANIZATIONS } from "../constants/organizations";
8
 
9
  interface PageProps {
10
  calendarData: CalendarData;
@@ -16,6 +17,7 @@ function Page({
16
  providers,
17
  }: PageProps) {
18
  const [isLoading, setIsLoading] = useState(true);
 
19
 
20
  useEffect(() => {
21
  if (calendarData && Object.keys(calendarData).length > 0) {
@@ -23,6 +25,27 @@ function Page({
23
  }
24
  }, [calendarData]);
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  return (
28
  <div className="w-full">
@@ -45,10 +68,19 @@ function Page({
45
  </p>
46
  </div>
47
 
 
 
 
 
 
 
 
 
 
48
  <div className="mb-16 mx-auto">
49
  <div className="overflow-x-auto scrollbar-hide">
50
  <div className="flex gap-6 px-4 py-2 min-w-max justify-center">
51
- {providers.map((provider, index) => (
52
  <OrganizationButton
53
  key={provider.fullName || provider.authors[0]}
54
  provider={provider}
@@ -61,7 +93,7 @@ function Page({
61
  </div>
62
 
63
  <HeatmapGrid
64
- sortedProviders={providers}
65
  calendarData={calendarData}
66
  isLoading={isLoading}
67
  />
 
1
+ import React, { useState, useEffect, useMemo } from "react";
2
  import { ProviderInfo, ModelData, CalendarData } from "../types/heatmap";
3
  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 { getProviders } from "../utils/ranking";
8
+ import { ORGANIZATIONS, SCIENTIFIC_TAGS } from "../constants/organizations";
9
 
10
  interface PageProps {
11
  calendarData: CalendarData;
 
17
  providers,
18
  }: PageProps) {
19
  const [isLoading, setIsLoading] = useState(true);
20
+ const [selectedTags, setSelectedTags] = useState<string[]>([]);
21
 
22
  useEffect(() => {
23
  if (calendarData && Object.keys(calendarData).length > 0) {
 
25
  }
26
  }, [calendarData]);
27
 
28
+ // Filter providers based on selected tags
29
+ const filteredProviders = useMemo(() => {
30
+ if (selectedTags.length === 0) {
31
+ return providers;
32
+ }
33
+
34
+ return providers.filter(provider => {
35
+ if (!provider.tags) return false;
36
+ return selectedTags.some(tag => provider.tags!.includes(tag));
37
+ });
38
+ }, [providers, selectedTags]);
39
+
40
+ const handleTagToggle = (tagId: string) => {
41
+ setSelectedTags(prev => {
42
+ if (prev.includes(tagId)) {
43
+ return prev.filter(t => t !== tagId);
44
+ } else {
45
+ return [...prev, tagId];
46
+ }
47
+ });
48
+ };
49
 
50
  return (
51
  <div className="w-full">
 
68
  </p>
69
  </div>
70
 
71
+ {/* Tag Selector */}
72
+ <div className="mb-16">
73
+ <TagSelector
74
+ tags={SCIENTIFIC_TAGS}
75
+ selectedTags={selectedTags}
76
+ onTagToggle={handleTagToggle}
77
+ />
78
+ </div>
79
+
80
  <div className="mb-16 mx-auto">
81
  <div className="overflow-x-auto scrollbar-hide">
82
  <div className="flex gap-6 px-4 py-2 min-w-max justify-center">
83
+ {filteredProviders.map((provider, index) => (
84
  <OrganizationButton
85
  key={provider.fullName || provider.authors[0]}
86
  provider={provider}
 
93
  </div>
94
 
95
  <HeatmapGrid
96
+ sortedProviders={filteredProviders}
97
  calendarData={calendarData}
98
  isLoading={isLoading}
99
  />
src/styles/globals.css CHANGED
@@ -33,35 +33,33 @@
33
  --background-rgb: 255, 255, 255;
34
  }
35
 
36
- @media (prefers-color-scheme: dark) {
37
- :root {
38
- --foreground-rgb: 255, 255, 255;
39
- --background-rgb: 0, 0, 0;
40
- --background: 222.2 84% 4.9%;
41
- --foreground: 210 40% 98%;
42
- --card: 222.2 84% 4.9%;
43
- --card-foreground: 210 40% 98%;
44
- --popover: 222.2 84% 4.9%;
45
- --popover-foreground: 210 40% 98%;
46
- --primary: 210 40% 98%;
47
- --primary-foreground: 222.2 47.4% 11.2%;
48
- --secondary: 217.2 32.6% 17.5%;
49
- --secondary-foreground: 210 40% 98%;
50
- --muted: 217.2 32.6% 17.5%;
51
- --muted-foreground: 215 20.2% 65.1%;
52
- --accent: 217.2 32.6% 17.5%;
53
- --accent-foreground: 210 40% 98%;
54
- --destructive: 0 62.8% 30.6%;
55
- --destructive-foreground: 210 40% 98%;
56
- --border: 217.2 32.6% 17.5%;
57
- --input: 217.2 32.6% 17.5%;
58
- --ring: 212.7 26.8% 83.9%;
59
- --chart-1: 220 70% 50%;
60
- --chart-2: 160 60% 45%;
61
- --chart-3: 30 80% 55%;
62
- --chart-4: 280 65% 60%;
63
- --chart-5: 340 75% 55%;
64
- }
65
  }
66
  }
67
 
 
33
  --background-rgb: 255, 255, 255;
34
  }
35
 
36
+ .dark {
37
+ --foreground-rgb: 255, 255, 255;
38
+ --background-rgb: 0, 0, 0;
39
+ --background: 222.2 84% 4.9%;
40
+ --foreground: 210 40% 98%;
41
+ --card: 222.2 84% 4.9%;
42
+ --card-foreground: 210 40% 98%;
43
+ --popover: 222.2 84% 4.9%;
44
+ --popover-foreground: 210 40% 98%;
45
+ --primary: 210 40% 98%;
46
+ --primary-foreground: 222.2 47.4% 11.2%;
47
+ --secondary: 217.2 32.6% 17.5%;
48
+ --secondary-foreground: 210 40% 98%;
49
+ --muted: 217.2 32.6% 17.5%;
50
+ --muted-foreground: 215 20.2% 65.1%;
51
+ --accent: 217.2 32.6% 17.5%;
52
+ --accent-foreground: 210 40% 98%;
53
+ --destructive: 0 62.8% 30.6%;
54
+ --destructive-foreground: 210 40% 98%;
55
+ --border: 217.2 32.6% 17.5%;
56
+ --input: 217.2 32.6% 17.5%;
57
+ --ring: 212.7 26.8% 83.9%;
58
+ --chart-1: 220 70% 50%;
59
+ --chart-2: 160 60% 45%;
60
+ --chart-3: 30 80% 55%;
61
+ --chart-4: 280 65% 60%;
62
+ --chart-5: 340 75% 55%;
 
 
63
  }
64
  }
65
 
src/types/heatmap.ts CHANGED
@@ -1,6 +1,7 @@
1
  export interface ProviderInfo {
2
  color: string;
3
  authors: string[];
 
4
  fullName?: string;
5
  avatarUrl?: string | null;
6
  isVerified?: boolean;
 
1
  export interface ProviderInfo {
2
  color: string;
3
  authors: string[];
4
+ tags?: string[];
5
  fullName?: string;
6
  avatarUrl?: string | null;
7
  isVerified?: boolean;
src/utils/weeklyCalendar.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Activity } from "../types/heatmap";
2
+
3
+ export const aggregateToWeeklyData = (dailyData: Activity[]): Activity[] => {
4
+ if (!dailyData || dailyData.length === 0) return [];
5
+
6
+ const weeklyData: Activity[] = [];
7
+ let currentWeekStart: Date | null = null;
8
+ let currentWeekCount = 0;
9
+ let currentWeekLevel = 0;
10
+
11
+ for (const dayActivity of dailyData) {
12
+ const date = new Date(dayActivity.date);
13
+ const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
14
+
15
+ // If it's Sunday or we don't have a current week, start a new week
16
+ if (dayOfWeek === 0 || currentWeekStart === null) {
17
+ // Save the previous week if it exists
18
+ if (currentWeekStart !== null) {
19
+ weeklyData.push({
20
+ date: currentWeekStart.toISOString().split('T')[0],
21
+ count: currentWeekCount,
22
+ level: currentWeekLevel,
23
+ });
24
+ }
25
+
26
+ // Start new week
27
+ currentWeekStart = new Date(date);
28
+ currentWeekCount = dayActivity.count;
29
+ currentWeekLevel = dayActivity.level;
30
+ } else {
31
+ // Add to current week
32
+ currentWeekCount += dayActivity.count;
33
+ // Use the maximum level for the week
34
+ currentWeekLevel = Math.max(currentWeekLevel, dayActivity.level);
35
+ }
36
+ }
37
+
38
+ // Don't forget the last week
39
+ if (currentWeekStart !== null) {
40
+ weeklyData.push({
41
+ date: currentWeekStart.toISOString().split('T')[0],
42
+ count: currentWeekCount,
43
+ level: currentWeekLevel,
44
+ });
45
+ }
46
+
47
+ return weeklyData;
48
+ };
49
+
50
+ export const getWeekDateRange = (weekStartDate: string): string => {
51
+ const startDate = new Date(weekStartDate);
52
+ const endDate = new Date(startDate);
53
+ endDate.setDate(startDate.getDate() + 6);
54
+
55
+ const formatDate = (date: Date) => {
56
+ return date.toLocaleDateString('en-US', {
57
+ month: 'short',
58
+ day: 'numeric'
59
+ });
60
+ };
61
+
62
+ return `${formatDate(startDate)} - ${formatDate(endDate)}`;
63
+ };