cgeorgiaw HF Staff commited on
Commit
cd201c7
·
verified ·
1 Parent(s): 198dbcf

avi_tag_selector (#1)

Browse files

- Add scientific tag filtering and weekly/daily heatmap view toggle (e603004f8f97c2ac9134997c3980313fb37ff987)
- Fix weekly heatmap to show proper weekly aggregation (f6efde325fa734590e904cb0eb1a0cc5b3844808)
- Update homepage description to focus on AI for Science (ee44922b7ade97278d8c45a2c12782f770664693)

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,10 @@ 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 +15,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 && (
@@ -36,23 +53,28 @@ const Heatmap: React.FC<HeatmapProps> = ({ data, color, providerName, fullName,
36
  </div>
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}
53
- </Tooltip>
54
- )}
55
- />
 
 
 
 
 
56
  </div>
57
  </div>
58
  );
 
2
  import ActivityCalendar from "react-activity-calendar";
3
  import { Tooltip, Avatar } from "@mui/material";
4
  import Link from "next/link";
5
+ import { aggregateToWeeklyData } from "../utils/weeklyCalendar";
6
+ import WeeklyHeatmap from "./WeeklyHeatmap";
7
+
8
+ type ViewMode = 'daily' | 'weekly';
9
 
10
  type HeatmapProps = {
11
  data: Array<{ date: string; count: number; level: number }>;
 
15
  avatarUrl: string;
16
  authorId: string;
17
  showHeader?: boolean;
18
+ viewMode: ViewMode;
19
  };
20
 
21
+ const Heatmap: React.FC<HeatmapProps> = ({
22
+ data,
23
+ color,
24
+ providerName,
25
+ fullName,
26
+ avatarUrl,
27
+ authorId,
28
+ showHeader = true,
29
+ viewMode
30
+ }) => {
31
+ // Process data based on view mode
32
+ const processedData = viewMode === 'weekly' ? aggregateToWeeklyData(data) : data;
33
+
34
  return (
35
  <div className="flex flex-col items-center w-full mx-auto">
36
  {showHeader && (
 
53
  </div>
54
  )}
55
  <div className="w-full overflow-x-auto flex justify-center">
56
+ {viewMode === 'weekly' ? (
57
+ <WeeklyHeatmap data={processedData} color={color} />
58
+ ) : (
59
+ <ActivityCalendar
60
+ data={processedData}
61
+ theme={{
62
+ dark: ["#161b22", color],
63
+ light: ["#e0e0e0", color],
64
+ }}
65
+ blockSize={11}
66
+ blockMargin={2}
67
+ hideTotalCount
68
+ renderBlock={(block, activity) => (
69
+ <Tooltip
70
+ title={`${activity.count} new repos on ${activity.date}`}
71
+ arrow
72
+ >
73
+ {block}
74
+ </Tooltip>
75
+ )}
76
+ />
77
+ )}
78
  </div>
79
  </div>
80
  );
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
@@ -1,11 +1,21 @@
1
  import React from "react";
2
  import UserSearchDialog from "./UserSearchDialog";
 
 
 
 
 
 
 
 
 
3
 
4
  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>
 
1
  import React from "react";
2
  import UserSearchDialog from "./UserSearchDialog";
3
+ import dynamic from "next/dynamic";
4
+
5
+ // Dynamically import ThemeToggle to avoid SSR issues
6
+ const ThemeToggle = dynamic(() => import("./ThemeToggle"), {
7
+ ssr: false,
8
+ loading: () => (
9
+ <div className="h-10 w-10 rounded-lg border border-border bg-background animate-pulse"></div>
10
+ ),
11
+ });
12
 
13
  const Navbar: React.FC = () => {
14
  return (
15
  <nav className="w-full mt-4">
16
  <div className="max-w-6xl mx-auto px-4 py-3">
17
+ <div className="flex items-center justify-end gap-3">
18
+ <ThemeToggle />
19
  <UserSearchDialog />
20
  </div>
21
  </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,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useTheme } from '../contexts/ThemeContext';
5
+ import { cn } from '../lib/utils';
6
+
7
+ interface ThemeToggleProps {
8
+ className?: string;
9
+ }
10
+
11
+ const ThemeToggle: React.FC<ThemeToggleProps> = ({ className }) => {
12
+ const [mounted, setMounted] = useState(false);
13
+ const { theme, toggleTheme } = useTheme();
14
+
15
+ // Only render after hydration to prevent SSR issues
16
+ useEffect(() => {
17
+ setMounted(true);
18
+ }, []);
19
+
20
+ if (!mounted) {
21
+ // Return a placeholder that matches the final component size
22
+ return (
23
+ <div className={cn(
24
+ "relative inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border bg-background",
25
+ className
26
+ )}>
27
+ <div className="h-5 w-5 animate-pulse bg-muted rounded"></div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <button
34
+ onClick={toggleTheme}
35
+ className={cn(
36
+ "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",
37
+ className
38
+ )}
39
+ aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
40
+ >
41
+ <div className="relative h-5 w-5">
42
+ {/* Sun Icon */}
43
+ <svg
44
+ className={cn(
45
+ "absolute inset-0 h-5 w-5 transition-all",
46
+ theme === 'dark' ? "rotate-90 scale-0" : "rotate-0 scale-100"
47
+ )}
48
+ fill="none"
49
+ viewBox="0 0 24 24"
50
+ stroke="currentColor"
51
+ strokeWidth={2}
52
+ >
53
+ <path
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ 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"
57
+ />
58
+ </svg>
59
+
60
+ {/* Moon Icon */}
61
+ <svg
62
+ className={cn(
63
+ "absolute inset-0 h-5 w-5 transition-all",
64
+ theme === 'dark' ? "rotate-0 scale-100" : "-rotate-90 scale-0"
65
+ )}
66
+ fill="none"
67
+ viewBox="0 0 24 24"
68
+ stroke="currentColor"
69
+ strokeWidth={2}
70
+ >
71
+ <path
72
+ strokeLinecap="round"
73
+ strokeLinejoin="round"
74
+ 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"
75
+ />
76
+ </svg>
77
+ </div>
78
+ </button>
79
+ );
80
+ };
81
+
82
+ 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/components/WeeklyHeatmap.tsx ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Tooltip } from "@mui/material";
3
+ import { getWeekDateRange } from "../utils/weeklyCalendar";
4
+
5
+ type WeeklyActivity = {
6
+ date: string;
7
+ count: number;
8
+ level: number;
9
+ };
10
+
11
+ type WeeklyHeatmapProps = {
12
+ data: WeeklyActivity[];
13
+ color: string;
14
+ };
15
+
16
+ const WeeklyHeatmap: React.FC<WeeklyHeatmapProps> = ({ data, color }) => {
17
+ // Group data by month and year
18
+ const groupedData = data.reduce((acc, activity) => {
19
+ const date = new Date(activity.date);
20
+ const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
21
+
22
+ if (!acc[yearMonth]) {
23
+ acc[yearMonth] = [];
24
+ }
25
+ acc[yearMonth].push(activity);
26
+ return acc;
27
+ }, {} as Record<string, WeeklyActivity[]>);
28
+
29
+ // Get color intensity based on level
30
+ const getColorIntensity = (level: number) => {
31
+ if (level === 0) return '#161b22'; // Dark background for no activity
32
+ const intensities = ['#0e4429', '#006d32', '#26a641', '#39d353'];
33
+ return intensities[Math.min(level - 1, 3)] || color;
34
+ };
35
+
36
+ // Get month names
37
+ const getMonthName = (yearMonth: string) => {
38
+ const [year, month] = yearMonth.split('-');
39
+ const date = new Date(parseInt(year), parseInt(month) - 1);
40
+ return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
41
+ };
42
+
43
+ const sortedMonths = Object.keys(groupedData).sort();
44
+
45
+ return (
46
+ <div className="w-full">
47
+ <div className="flex flex-wrap gap-4 justify-center">
48
+ {sortedMonths.map((yearMonth) => {
49
+ const monthData = groupedData[yearMonth];
50
+ return (
51
+ <div key={yearMonth} className="flex flex-col items-center">
52
+ <div className="text-xs mb-2 text-gray-600 dark:text-gray-400">
53
+ {getMonthName(yearMonth)}
54
+ </div>
55
+ <div className="flex gap-1">
56
+ {monthData.map((activity, index) => (
57
+ <Tooltip
58
+ key={`${yearMonth}-${index}`}
59
+ title={`${activity.count} new repos in week of ${getWeekDateRange(activity.date)}`}
60
+ arrow
61
+ >
62
+ <div
63
+ className="w-3 h-3 rounded-sm cursor-pointer transition-opacity hover:opacity-80"
64
+ style={{
65
+ backgroundColor: getColorIntensity(activity.level),
66
+ }}
67
+ />
68
+ </Tooltip>
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ })}
74
+ </div>
75
+
76
+ {/* Legend */}
77
+ <div className="flex items-center justify-center mt-4 text-xs text-gray-600 dark:text-gray-400">
78
+ <span className="mr-2">Less</span>
79
+ <div className="flex gap-1">
80
+ {[0, 1, 2, 3, 4].map((level) => (
81
+ <div
82
+ key={level}
83
+ className="w-2.5 h-2.5 rounded-sm"
84
+ style={{
85
+ backgroundColor: getColorIntensity(level),
86
+ }}
87
+ />
88
+ ))}
89
+ </div>
90
+ <span className="ml-2">More</span>
91
+ </div>
92
+ </div>
93
+ );
94
+ };
95
+
96
+ export default WeeklyHeatmap;
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,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useEffect, useState } from 'react';
4
+
5
+ type Theme = 'light' | 'dark';
6
+
7
+ interface ThemeContextType {
8
+ theme: Theme;
9
+ toggleTheme: () => void;
10
+ }
11
+
12
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
13
+
14
+ export const useTheme = () => {
15
+ const context = useContext(ThemeContext);
16
+ if (!context) {
17
+ throw new Error('useTheme must be used within a ThemeProvider');
18
+ }
19
+ return context;
20
+ };
21
+
22
+ interface ThemeProviderProps {
23
+ children: React.ReactNode;
24
+ }
25
+
26
+ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
27
+ const [theme, setTheme] = useState<Theme>('dark');
28
+ const [mounted, setMounted] = useState(false);
29
+
30
+ useEffect(() => {
31
+ setMounted(true);
32
+ // Get theme from localStorage or system preference
33
+ const savedTheme = localStorage.getItem('theme') as Theme | null;
34
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
35
+ const initialTheme = savedTheme || systemTheme;
36
+
37
+ setTheme(initialTheme);
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ if (!mounted) return;
42
+
43
+ // Apply theme to document
44
+ const root = document.documentElement;
45
+
46
+ if (theme === 'dark') {
47
+ root.classList.add('dark');
48
+ } else {
49
+ root.classList.remove('dark');
50
+ }
51
+
52
+ // Save to localStorage
53
+ localStorage.setItem('theme', theme);
54
+ }, [theme, mounted]);
55
+
56
+ const toggleTheme = () => {
57
+ setTheme(prev => prev === 'light' ? 'dark' : 'light');
58
+ };
59
+
60
+ // Prevent hydration mismatch by not rendering until mounted
61
+ if (!mounted) {
62
+ return <>{children}</>;
63
+ }
64
+
65
+ return (
66
+ <ThemeContext.Provider value={{ theme, toggleTheme }}>
67
+ {children}
68
+ </ThemeContext.Provider>
69
+ );
70
+ };
src/pages/_app.tsx CHANGED
@@ -1,6 +1,11 @@
1
  import "@/styles/globals.css";
2
  import type { AppProps } from "next/app";
 
3
 
4
  export default function App({ Component, pageProps }: AppProps) {
5
- return <Component {...pageProps} />;
 
 
 
 
6
  }
 
1
  import "@/styles/globals.css";
2
  import type { AppProps } from "next/app";
3
+ import { ThemeProvider } from "../contexts/ThemeContext";
4
 
5
  export default function App({ Component, pageProps }: AppProps) {
6
+ return (
7
+ <ThemeProvider>
8
+ <Component {...pageProps} />
9
+ </ThemeProvider>
10
+ );
11
  }
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">
@@ -41,14 +64,23 @@ function Page({
41
  </span>
42
  </h1>
43
  <p className="text-base sm:text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed px-4">
44
- Open models, datasets, and apps from influential labs in the last year.
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">
 
64
  </span>
65
  </h1>
66
  <p className="text-base sm:text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed px-4">
67
+ Open models, datasets, and apps from orgs contributing to AI4Science in the last year.
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,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Activity } from "../types/heatmap";
2
+
3
+ export const aggregateToWeeklyData = (dailyData: Activity[]): Activity[] => {
4
+ if (!dailyData || dailyData.length === 0) return [];
5
+
6
+ // Create a map to group activities by week
7
+ const weeklyMap = new Map<string, { count: number; level: number; dates: string[] }>();
8
+
9
+ for (const dayActivity of dailyData) {
10
+ const date = new Date(dayActivity.date);
11
+ // Get the Sunday of this week (week start)
12
+ const weekStart = new Date(date);
13
+ weekStart.setDate(date.getDate() - date.getDay());
14
+ const weekKey = weekStart.toISOString().split('T')[0];
15
+
16
+ if (!weeklyMap.has(weekKey)) {
17
+ weeklyMap.set(weekKey, { count: 0, level: 0, dates: [] });
18
+ }
19
+
20
+ const weekData = weeklyMap.get(weekKey)!;
21
+ weekData.count += dayActivity.count;
22
+ weekData.level = Math.max(weekData.level, dayActivity.level);
23
+ weekData.dates.push(dayActivity.date);
24
+ }
25
+
26
+ // Convert to true weekly data - one entry per week, not per day
27
+ const weeklyData: Activity[] = [];
28
+
29
+ weeklyMap.forEach((weekInfo, weekStartDate) => {
30
+ weeklyData.push({
31
+ date: weekStartDate, // Use the week start date
32
+ count: weekInfo.count,
33
+ level: weekInfo.level,
34
+ });
35
+ });
36
+
37
+ // Sort by date to ensure proper order
38
+ weeklyData.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
39
+
40
+ return weeklyData;
41
+ };
42
+
43
+ export const getWeekDateRange = (weekStartDate: string): string => {
44
+ const startDate = new Date(weekStartDate);
45
+ const endDate = new Date(startDate);
46
+ endDate.setDate(startDate.getDate() + 6);
47
+
48
+ const formatDate = (date: Date) => {
49
+ return date.toLocaleDateString('en-US', {
50
+ month: 'short',
51
+ day: 'numeric'
52
+ });
53
+ };
54
+
55
+ return `${formatDate(startDate)} - ${formatDate(endDate)}`;
56
+ };