Skip to main content
The Statistics Explorer allows users to browse Tamborrada Infantil data by year (2018-present) or view global statistics across all years. It features dynamic year selection, category-based statistics, and seamless navigation.

Overview

Key features of the statistics explorer:

Year Selection

Browse statistics for any year from 2018 to present

Global View

View aggregated statistics across all years

Category Tabs

Multiple statistical categories per year

Dynamic Loading

Server-side rendering with React Query hydration

Header Navigation

The header includes a dynamic year selector dropdown:
app/(frontend)/components/Header/components/Desktop.tsx
import Link from 'next/link';
import { useStatisticsY } from '../../../hooks/useStatisticsY';
import { useDesktopMenu } from '../hooks/useDesktopMenu';
import { motion } from 'framer-motion';

export function Desktop() {
  const { pathname } = useHeader();
  const { years } = useStatisticsY();
  const { yearsShow, toggleYearsShow } = useDesktopMenu();

  return (
    <nav aria-label="Navegación principal" className="flex items-center justify-center gap-7">
      {/* Title */}
      <Link href="/" className="text-2xl font-semibold">
        Tamborradata
      </Link>

      {/* Statistics dropdown */}
      <div className="relative group">
        <button
          onClick={() => toggleYearsShow()}
          className="text-lg font-medium cursor-pointer hover:text-(--eye-catching-text)"
        >
          Estadísticas
        </button>

        {/* Year selector */}
        {yearsShow && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="absolute top-full mt-2"
          >
            <ul className="grid grid-cols-[repeat(auto-fit,minmax(70px,1fr))] gap-2 bg-(--color-header) border rounded-lg py-4 px-6">
              <li>
                <Link
                  href="/statistics/global"
                  className="text-lg block hover:text-(--eye-catching-text)"
                >
                  Global
                </Link>
              </li>
              {years?.map((year: number) => (
                <li key={year}>
                  <Link
                    href={`/statistics/${year}`}
                    className="text-lg block hover:text-(--eye-catching-text)"
                  >
                    {year}
                  </Link>
                </li>
              ))}
            </ul>
          </motion.div>
        )}
      </div>

      {/* Search link */}
      <Link href="/search" className="text-lg font-medium">
        Buscar participante
      </Link>
    </nav>
  );
}
File location: app/(frontend)/components/Header/components/Desktop.tsx:7
The year selector is populated dynamically from the /api/years endpoint, ensuring only available years are shown.

Year Dropdown Hook

hooks/useDesktopMenu.ts
import { useState } from 'react';

export function useDesktopMenu() {
  const [yearsShow, setYearsShow] = useState(false);

  function toggleYearsShow() {
    setYearsShow((prev) => !prev);
  }

  return {
    yearsShow,
    toggleYearsShow
  };
}
File location: app/(frontend)/components/Header/hooks/useDesktopMenu.ts:3

Year Statistics Page

Page Structure

Each year has a dedicated dynamic route:
/statistics/2024  → Statistics for 2024
/statistics/2023  → Statistics for 2023
/statistics/2022  → Statistics for 2022
...

Page Component

app/(frontend)/statistics/[year]/page.tsx
import type { Metadata } from 'next';
import { YearPageContent } from './YearPageContent';
import { YearStructuredData } from './YearStructuredData';

export async function generateMetadata({
  params
}: {
  params: Promise<{ year: string }>;
}): Promise<Metadata> {
  const { year } = await params;
  const pageTitle = `Estadísticas de la Tamborrada Infantil ${year}`;
  const pageDescription = `Análisis de la Tamborrada Infantil ${year}: participantes, nombres más comunes, colegios destacados y tendencias anuales.`;
  const canonicalUrl = `https://tamborradata.com/statistics/${year}`;

  return {
    title: pageTitle,
    description: pageDescription,
    alternates: { canonical: canonicalUrl },
    openGraph: {
      title: pageTitle,
      description: pageDescription,
      url: canonicalUrl,
      type: 'article'
    }
  };
}

export default async function YearPage({ params }: { params: Promise<{ year: string }> }) {
  const { year } = await params;

  return (
    <>
      <YearStructuredData year={year} />
      <YearPageContent />
    </>
  );
}
File location: app/(frontend)/statistics/[year]/page.tsx:6

Content Component

app/(frontend)/statistics/[year]/YearPageContent.tsx
'use client';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';
import { useYears } from './hooks/useYears';
import {
  NamesSurnamesDiversity,
  CommonNamesBySchool,
  TotalParticipants,
  TopSurnames,
  NewSchools,
  TopSchools,
  TopNames,
  NewNames
} from './components';

export function YearPageContent() {
  const { stats, year } = useYears();

  return (
    <article className="w-full flex flex-col gap-6" aria-labelledby="year-page-title">
      <h1 id="year-page-title" className="text-2xl md:text-3xl font-bold">
        <span className="hidden md:block">Estadísticas de la Tamborrada Infantil {year}</span>
        <span className="block md:hidden">Tamborrada Infantil {year}</span>
      </h1>

      <div className="w-full text-sm sm:text-md md:text-base flex flex-col gap-3">
        <ReactMarkdown>{stats.intro[0]?.summary}</ReactMarkdown>
      </div>

      <hr className="w-full border border-(--color-border)" />

      {/* Statistics categories */}
      <TopNames />
      <TopSurnames />
      <NewNames />
      <NamesSurnamesDiversity />
      <TopSchools />
      <NewSchools />
      <CommonNamesBySchool />
      <TotalParticipants />

      <hr className="w-full border border-(--color-border)" />

      <div className="w-full text-sm sm:text-md md:text-base flex flex-col gap-3">
        <ReactMarkdown>{stats.outro[0]?.summary}</ReactMarkdown>
      </div>
    </article>
  );
}
File location: app/(frontend)/statistics/[year]/YearPageContent.tsx:18

Statistical Categories

Each year page displays multiple statistical categories:

Available Categories

Top Names (TopNames)
  • Most common first names
  • Top 15 with chart/table toggle
  • Shows count per name
New Names (NewNames)
  • Names that appeared for the first time this year
  • Highlights cultural trends

Category Component Structure

All categories follow the same pattern:
Example: TopNames component
import ReactMarkdown from 'react-markdown';
import { useTopNames } from './hooks/useTopNames';
import { TopNamesTable } from './components/TopNamesTable';
import { hasData } from '@/app/(frontend)/helpers/hasData';
import dynamic from 'next/dynamic';

const TopNamesChart = dynamic(
  () => import('./components/TopNamesChart').then((mod) => mod.TopNamesChart),
  { ssr: false, loading: () => <LoadingChart /> }
);

export function TopNames() {
  const { topNamesStats, chart, showChart } = useTopNames();

  if (!hasData(topNamesStats)) return null;

  return (
    <section className="w-full">
      <h2 className="text-lg md:text-2xl font-bold">
        Nombres mas repetidos —{' '}
        <span className="text-sm rounded p-1 bg-(--color-primary)">
          {topNamesStats[0].category}
        </span>
      </h2>
      
      <article className="flex flex-col items-start justify-center py-5">
        <button
          onClick={showChart}
          className="hidden md:block py-1 px-3 mb-3 rounded cursor-pointer"
        >
          {chart ? 'Ver tabla' : 'Ver gráfico'}
        </button>
        {chart ? <TopNamesChart {...useTopNames()} /> : <TopNamesTable {...useTopNames()} />}
      </article>
      
      <div className="w-full text-sm flex flex-col gap-3">
        <ReactMarkdown>{topNamesStats[0].summary}</ReactMarkdown>
      </div>
    </section>
  );
}
File location: app/(frontend)/statistics/[year]/components/TopNames/TopNames.tsx:14

Global Statistics

Global Page Route

The global statistics page aggregates data across all years:
/statistics/global  → All-time statistics

Global Page Component

app/(frontend)/statistics/global/page.tsx
import { Metadata } from 'next';
import { GlobalStructuredData } from './GlobalStructuredData';
import { GlobalPageContent } from './GlobalPageContent';

const pageTitle = 'Estadísticas globales de la Tamborrada Infantil';
const pageDescription = 'Análisis global de la Tamborrada Infantil desde 2018: evolución de nombres, colegios, participación y tendencias culturales año a año.';

export const metadata: Metadata = {
  title: pageTitle,
  description: pageDescription,
  alternates: { canonical: 'https://tamborradata.com/statistics/global' }
};

export default function GlobalPage() {
  return (
    <>
      <GlobalStructuredData />
      <GlobalPageContent />
    </>
  );
}
File location: app/(frontend)/statistics/global/page.tsx:4

Global Categories

Global statistics include:
  • Total Participants Evolution - Participation trends over time (line chart)
  • Top Names (All-Time) - Most popular names across all years
  • Top Surnames (All-Time) - Most common surnames historically
  • Top Schools - Schools with highest participation
  • Schools Evolution - How participation changed over time
  • Most Constant Schools - Schools that participated every year
  • Longest Names - Participants with longest names
  • Common Name by School - Most popular name per school (global)
Global statistics use line charts to show trends over time, while year-specific stats use bar charts for single-year comparisons.

Data Fetching

React Query Hook

app/(frontend)/statistics/[year]/hooks/useYears.ts
import { useStatisticsQuery } from '@/app/(frontend)/hooks/query/useStatisticsQuery';
import { useParams } from 'next/navigation';
import { Statistics } from '../types/types';

export function useYears() {
  const { year }: { year: string } = useParams();
  const { data: statistics, isLoading, isError, error } = useStatisticsQuery<Statistics>(year);

  return {
    year,
    statistics,
    stats: statistics?.statistics,
    isLoading,
    isError,
    error,
    isUpdating: statistics?.isUpdating
  };
}
File location: app/(frontend)/statistics/[year]/hooks/useYears.ts:5

Query Configuration

hooks/query/useStatisticsQuery.ts
export function useStatisticsQuery<T extends StatsResponse>(year: string) {
  return useQuery({
    queryKey: queryKeys.statistics(year),
    queryFn: ({ signal }) => fetchStatistics<T>(year, signal),
    enabled: Boolean(year),
    staleTime: Infinity,  // Historical data never changes
    gcTime: Infinity,  // Keep in cache forever
    retry: 0,
    refetchOnWindowFocus: (query) => query.state.data?.isUpdating === true,
    refetchInterval: (query) => (query.state.data?.isUpdating ? 3000 : false)
  });
}
Infinite cache: Historical statistics are cached forever since they don’t change. See Annual Updates for the isUpdating system.

Loading States

Page Loader

components/loaders/LoadingPage.tsx
export function LoadingPage() {
  return (
    <div className="w-full h-screen flex items-center justify-center">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-(--color-primary)" />
    </div>
  );
}

Category Skeletons

Each category shows a skeleton while loading:
if (isLoading) return <LoadingTable />;
if (!hasData(data)) return null;

Error Handling

Error Page

components/ErrorPage.tsx
export function ErrorPage() {
  return (
    <div className="w-full h-screen flex flex-col items-center justify-center gap-4">
      <h4 className="text-xl font-bold text-center">
        Error al cargar las estadísticas
      </h4>
      <p className="text-sm text-center text-(--color-text-secondary)">
        Inténtalo de nuevo más tarde
      </p>
    </div>
  );
}

Conditional Rendering

if (isError) return <ErrorPage />;
if (isLoading) return <LoadingPage />;
if (isUpdating) return <UpdatingPage />;

Wrapper Component

All statistics pages use a consistent wrapper:
components/StatsWrapper.tsx
export function StatsWrapper({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex flex-col items-center justify-start w-full py-3 pt-15 mt-10">
      <section className="w-full max-w-6xl flex flex-col items-start justify-start gap-6 p-4 rounded-2xl border border-(--color-border)">
        {children}
      </section>
    </div>
  );
}
File location: app/(frontend)/statistics/components/StatsWrapper.tsx:1

SEO Optimization

Dynamic Metadata

Each year page generates unique metadata:
export async function generateMetadata({ params }): Promise<Metadata> {
  const { year } = await params;
  
  return {
    title: `Estadísticas de la Tamborrada Infantil ${year}`,
    description: `Análisis de la Tamborrada Infantil ${year}: participantes, nombres más comunes, colegios destacados y tendencias anuales.`,
    alternates: { canonical: `https://tamborradata.com/statistics/${year}` },
    openGraph: {
      title: `Estadísticas Tamborrada Infantil ${year}`,
      description: `Explorar datos de ${year}`,
      url: `https://tamborradata.com/statistics/${year}`
    }
  };
}

Structured Data

JSON-LD structured data for search engines:
YearStructuredData.tsx
export function YearStructuredData({ year }: { year: string }) {
  const structuredData = {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": `Estadísticas de la Tamborrada Infantil ${year}`,
    "datePublished": `${year}-01-20`,
    "author": {
      "@type": "Organization",
      "name": "Tamborradata"
    }
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
    />
  );
}

Data Visualization

Chart and table components

Annual Updates

The isUpdating system

React Query

Data fetching patterns

SEO

SEO optimization strategies

Build docs developers (and LLMs) love