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 +0 -0
- package.json +3 -3
- src/components/ClientNavbar.tsx +20 -0
- src/components/Heatmap.tsx +40 -18
- src/components/HeatmapGrid.tsx +16 -1
- src/components/Navbar.tsx +11 -1
- src/components/OrganizationCard.tsx +5 -1
- src/components/TagSelector.tsx +61 -0
- src/components/ThemeToggle.tsx +82 -0
- src/components/ViewToggle.tsx +41 -0
- src/components/WeeklyHeatmap.tsx +96 -0
- src/constants/organizations.ts +83 -14
- src/contexts/ThemeContext.tsx +70 -0
- src/pages/_app.tsx +6 -1
- src/pages/index.tsx +37 -5
- src/styles/globals.css +27 -29
- src/types/heatmap.ts +1 -0
- src/utils/weeklyCalendar.ts +56 -0
    	
        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. | 
| 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> = ({  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 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 | 
            -
                     | 
| 40 | 
            -
                      data={ | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
                         | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
                         | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
                         | 
| 52 | 
            -
                           | 
| 53 | 
            -
             | 
| 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 | 
            -
              {  | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
               | 
| 9 | 
            -
              {  | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
               | 
| 14 | 
            -
              {  | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 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  | 
|  | |
|  | |
|  | |
|  | |
| 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  | 
| 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 | 
            -
                          { | 
| 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={ | 
| 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 | 
            -
               | 
| 37 | 
            -
                : | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 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 | 
            +
            };
         | 

