Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Jesus-Puertos/h-ayuntamiento/llms.txt

Use this file to discover all available pages before exploring further.

Component Types

The platform uses two types of components:

Astro Components (.astro)

Server-rendered HTML components with optional JavaScript.
  • Rendered once at build/request time
  • No JavaScript shipped to client by default
  • Perfect for static content
  • Can import and embed React components
Example:
---
// components/Hero.astro
const { title, subtitle } = Astro.props;
---

<section class="hero">
  <h1>{title}</h1>
  <p>{subtitle}</p>
</section>

<style>
  .hero {
    padding: 4rem 2rem;
    text-align: center;
  }
</style>

React Components (.tsx)

Client-side interactive components using React 19.
  • Full React features (hooks, state, effects)
  • Hydrated on client based on directive
  • Used for interactive UI (forms, modals, real-time data)
  • TypeScript for type safety
Example:
// components/AuthButtons.tsx
import { useState } from 'react';

export default function AuthButtons() {
  const [loading, setLoading] = useState(false);

  return (
    <button
      onClick={() => setLoading(true)}
      className="px-4 py-2 bg-orange-500 text-white rounded"
    >
      {loading ? 'Loading...' : 'Sign In'}
    </button>
  );
}

Client Directives

Control when React components hydrate on the client.

client:load

Hydrate immediately on page load.
<AuthButtons client:load />
Use for:
  • Authentication UI (needs to check session immediately)
  • Critical interactive elements above the fold

client:idle

Hydrate when browser is idle (after initial page load).
<ChatNachito client:idle />
Use for:
  • Non-critical interactive elements
  • Chatbots, help widgets
  • Analytics components

client:visible

Hydrate when component enters viewport.
<TurismoOnboarding client:visible />
Use for:
  • Below-the-fold interactive content
  • Image galleries, carousels
  • Lazy-loaded features

client:media

Hydrate based on media query.
<MobileNav client:media="(max-width: 768px)" />
Use for:
  • Mobile-only components
  • Responsive interactive features

client:only

Skip server rendering, only render on client.
<ThreeJsScene client:only="react" />
Use for:
  • Components that depend on browser APIs
  • Libraries that don’t support SSR

Component Organization

components/
├── Header.tsx              # Site-wide components
├── Footer.astro
├── AuthButtons.tsx
├── ShareButtons.tsx

├── turismo/                # Feature-specific
│   ├── TurismoOnboarding.tsx
│   ├── TurismoHeader.astro
│   ├── FavoriteButton.tsx
│   └── RouteCard.astro

├── ui/                     # Reusable UI primitives
│   ├── resizable-navbar.tsx
│   ├── Gallery.astro
│   └── SileoToaster.tsx

└── xochitlanis/            # Event-specific
    ├── Hero.astro
    ├── CountDown.astro
    └── FAQ.astro

Naming Conventions

  • PascalCase for all components (e.g., TurismoOnboarding.tsx)
  • Descriptive names (e.g., AuthButtons not Buttons)
  • Feature prefixes for clarity (e.g., TurismoHeader not Header2)

Styling Patterns

Tailwind CSS Classes

Utility-first approach with Tailwind CSS 4.x.
<button class="px-6 py-3 bg-gradient-to-r from-orange-600 to-orange-500 text-white rounded-xl font-bold hover:shadow-lg transition transform hover:-translate-y-0.5">
  Click me
</button>

Scoped Styles (Astro)

Component-scoped CSS in Astro components:
<div class="card">
  <h2>Title</h2>
</div>

<style>
  .card {
    padding: 2rem;
    border-radius: 1rem;
    background: white;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  }

  h2 {
    color: #ff8200;  /* Scoped to this component only */
  }
</style>

Dynamic Classes

Conditional classes with class:list:
<div class:list={[
  'base-class',
  { 'active': isActive },
  { 'disabled': isDisabled },
  customClass
]}>
  Content
</div>
In React with clsx or className:
import clsx from 'clsx';

<button
  className={clsx(
    'px-4 py-2 rounded',
    loading && 'opacity-50 cursor-not-allowed',
    variant === 'primary' ? 'bg-orange-500 text-white' : 'bg-gray-200'
  )}
>
  {children}
</button>

Props & Types

Astro Component Props

---
// components/AtractivoCard.astro
import type { Atractivo } from '@/data/turismo/atractivos';

interface Props {
  item: Atractivo;
  size?: 'sm' | 'md' | 'lg';
  class?: string;
}

const { item, size = 'md', class: className } = Astro.props;
---

<a href={`/turismo/atractivos/${item.slug}`} class={className}>
  <img src={item.imagen} alt={item.titulo} />
  <h3>{item.titulo}</h3>
</a>

React Component Props

// components/AuthButtons.tsx
interface AuthButtonsProps {
  onAuthSuccess?: () => void;
}

export default function AuthButtons({ onAuthSuccess }: AuthButtonsProps) {
  // ...
}

TypeScript Interfaces

// types/turismo.ts
export interface Atractivo {
  slug: string;
  titulo: string;
  categoria: 'Naturaleza' | 'Cultura' | 'Aventura' | 'Gastronomía';
  imagen: string;
  ubicacion: {
    lat: number;
    lng: number;
  };
}

Passing Data Between Components

Parent to Child (Props)

---
import AtractivoCard from '@/components/AtractivoCard.astro';
import { atractivos } from '@/data/turismo/atractivos';
---

<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
  {atractivos.map(item => (
    <AtractivoCard item={item} size="md" />
  ))}
</div>

React to React (State Lifting)

// Parent component
function TurismoFlow() {
  const [step, setStep] = useState(1);
  const [preferences, setPreferences] = useState({});

  return (
    <>
      <StepIndicator currentStep={step} />
      <PreferenceForm
        preferences={preferences}
        onUpdate={setPreferences}
        onNext={() => setStep(step + 1)}
      />
    </>
  );
}

Astro to React (Props)

---
import TurismoOnboarding from '@/components/turismo/TurismoOnboarding.tsx';
import { getCurrentUser } from '@/lib/supabase';

const user = await getCurrentUser();
---

<TurismoOnboarding
  client:load
  userId={user?.id}
  userName={user?.user_metadata?.name}
/>

Slots

Astro Slots

Pass content into components:
---
// components/Card.astro
const { title } = Astro.props;
---

<div class="card">
  <h2>{title}</h2>
  <slot />  <!-- Default slot -->
  <footer>
    <slot name="footer" />  <!-- Named slot -->
  </footer>
</div>
Usage:
<Card title="Welcome">
  <p>This goes in the default slot</p>
  <div slot="footer">
    <button>Learn More</button>
  </div>
</Card>

React Children

interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Event Handling

React Events

export default function SearchBar() {
  const [query, setQuery] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('Search:', query);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Buscar..."
      />
      <button type="submit">Buscar</button>
    </form>
  );
}

Astro Script Tags

Client-side JavaScript in Astro components:
<button id="toggle">Toggle</button>
<div id="content" style="display: none;">Hidden content</div>

<script>
  const toggle = document.getElementById('toggle');
  const content = document.getElementById('content');

  toggle?.addEventListener('click', () => {
    if (content) {
      content.style.display = content.style.display === 'none' ? 'block' : 'none';
    }
  });
</script>
Astro <script> tags run once per page. For component-specific logic, use React components with client:* directives.

Fetching Data

Server-Side (Astro)

---
import { supabase } from '@/lib/supabase';

const { data: routes } = await supabase
  .from('user_routes')
  .select('*')
  .order('created_at', { ascending: false })
  .limit(5);
---

<ul>
  {routes?.map(route => (
    <li>{route.route_name}</li>
  ))}
</ul>

Client-Side (React)

import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';

export default function RecentRoutes() {
  const [routes, setRoutes] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchRoutes() {
      const { data } = await supabase
        .from('user_routes')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(5);

      setRoutes(data || []);
      setLoading(false);
    }

    fetchRoutes();
  }, []);

  if (loading) return <div>Cargando...</div>;

  return (
    <ul>
      {routes.map(route => (
        <li key={route.id}>{route.route_name}</li>
      ))}
    </ul>
  );
}

Performance Best Practices

1. Use Astro Components by Default

Only use React when you need interactivity:
<!-- ✅ Good: Static card, use Astro -->
<AtractivoCard item={item} />

<!-- ❌ Avoid: Unnecessary React for static content -->
<AtractivoCardReact client:load item={item} />

2. Choose the Right Directive

<!-- ✅ Good: Lazy load below-fold content -->
<TurismoOnboarding client:visible />

<!-- ❌ Avoid: Eager load everything -->
<TurismoOnboarding client:load />

3. Minimize Props to React Components

<!-- ✅ Good: Pass only what's needed -->
<FavoriteButton client:load atractivoSlug={item.slug} />

<!-- ❌ Avoid: Passing entire objects (serialization overhead) -->
<FavoriteButton client:load atractivo={item} allAtractivos={atractivos} />

4. Use CSS for Animations

/* ✅ Good: CSS transitions */
.button {
  transition: transform 0.2s;
}
.button:hover {
  transform: translateY(-2px);
}
// ❌ Avoid: JavaScript animations for simple effects
function Button() {
  const [y, setY] = useState(0);
  return <button style={{ transform: `translateY(${y}px)` }}>...</button>
}

Component Composition

Layout Components

---
// layouts/BaseLayout.astro
import Header from '@/components/Header.tsx';
import Footer from '@/components/Footer.astro';

const { title, description } = Astro.props;
---

<!DOCTYPE html>
<html lang="es">
<head>
  <title>{title}</title>
  <meta name="description" content={description} />
</head>
<body>
  <Header client:load />
  <main>
    <slot />  <!-- Page content -->
  </main>
  <Footer />
</body>
</html>
Usage:
---
import BaseLayout from '@/layouts/BaseLayout.astro';
---

<BaseLayout title="Turismo" description="Descubre Zongolica">
  <h1>Bienvenido</h1>
  <p>Contenido de la página</p>
</BaseLayout>

Compound Components

---
// components/ui/Card.astro
---
<div class="card">
  <slot />
</div>

---
// components/ui/CardHeader.astro
---
<header class="card-header">
  <slot />
</header>

---
// components/ui/CardBody.astro
---
<div class="card-body">
  <slot />
</div>
Usage:
<Card>
  <CardHeader>
    <h2>Cascada de Texpico</h2>
  </CardHeader>
  <CardBody>
    <p>Una hermosa cascada...</p>
  </CardBody>
</Card>

Testing Components

Visual Testing

npm run dev
# Navigate to http://localhost:4321

E2E Testing with Playwright

// tests/components.spec.ts
import { test, expect } from '@playwright/test';

test('AuthButtons shows login form', async ({ page }) => {
  await page.goto('/turismo');
  await expect(page.getByRole('button', { name: 'Iniciar sesión' })).toBeVisible();
});

Next Steps

Tourism Components

TurismoOnboarding, FavoriteButton, and more

UI Components

Header, Footer, and reusable UI

Tech Stack

React, Astro, and libraries

Folder Structure

Where components live

Build docs developers (and LLMs) love