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.

Ticket Generation System

The ticket generation system creates shareable digital tickets for user-created tourism routes. Tickets include route details, QR codes, and social sharing capabilities.
Tickets are generated via /api/turismo/ticket.ts and stored in WordPress as custom post types.

Ticket Architecture

Share Code Generation

Unique 10-character codes are generated using nanoid:
src/lib/generateTicket.ts
import { nanoid } from 'nanoid';

// Generate unique 10-character code for sharing routes
export function generateShareCode(): string {
  return nanoid(10);
}

Example Share Codes

  • V1StGXR8_Z
  • 3z4xS9yK1n
  • mT8qW2rL5p
Share codes are case-sensitive and must be exactly 10 characters.

API Endpoint: Create Ticket

POST /api/turismo/ticket

Creates a new ticket and stores it in WordPress.
src/pages/api/turismo/ticket.ts
export const POST: APIRoute = async ({ request }) => {
  const { name, email, phone, shareCode } = await request.json();

  if (!name || !email || !shareCode) {
    return jsonResponse({ error: 'Missing required fields' }, 400);
  }

  const ticketData = {
    shareCode,
    name,
    email,
    phone: phone ?? '',
    routeName: 'Ruta Zongolica',
    atractivos: [],
    badges: [],
    createdAt: new Date().toISOString(),
  };

  // Save to WordPress custom post type
  const wpRes = await fetch(WP_TICKETS_ENDPOINT, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: getAuthHeader(),
    },
    body: JSON.stringify({
      title: `Ticket ${name}`,
      status: 'publish',
      slug: shareCode,
      content: JSON.stringify(ticketData),
    }),
  });

  const ticketUrl = `/ruta/${shareCode}`;
  return jsonResponse({ ticketUrl, shareCode });
};

Request Body

{
  "name": "Juan Pérez",
  "email": "juan@example.com",
  "phone": "2721234567",
  "shareCode": "V1StGXR8_Z"
}

Response

{
  "ticketUrl": "/ruta/V1StGXR8_Z",
  "shareCode": "V1StGXR8_Z",
  "wpId": 12345
}

WordPress Integration

Tickets are stored as WordPress custom post types using Application Passwords for authentication:

Environment Variables

.env
WP_BASE_URL=https://zongolica.mx
WP_TICKETS_ENDPOINT=https://zongolica.mx/wp-json/wp/v2/turismo_ticket
WP_APP_USER=your-wp-username
WP_APP_PASSWORD=your-app-password

Authentication

src/pages/api/turismo/ticket.ts
function getAuthHeader() {
  if (!WP_APP_USER || !WP_APP_PASSWORD) return null;
  const token = Buffer.from(`${WP_APP_USER}:${WP_APP_PASSWORD}`).toString('base64');
  return `Basic ${token}`;
}
WordPress Application Passwords provide secure API access without exposing user credentials.

Ticket Data Structure

interface TicketData {
  shareCode: string;        // Unique 10-char code
  name: string;             // User name
  email: string;            // User email
  phone: string;            // Optional phone
  routeName: string;        // Route title
  atractivos: string[];     // Attraction slugs
  badges: string[];         // Badge IDs
  createdAt: string;        // ISO timestamp
}

Saving Routes to Database

Routes are saved to Supabase before generating tickets:
src/lib/supabase.ts
export interface UserRoute {
  id: string;
  user_id: string;
  user_name?: string;
  route_name: string;
  atractivos: string[];     // Array of attraction slugs
  ticket_url: string;       // Generated ticket URL
  share_code: string;       // Unique share code
  badges: string[];         // Badge IDs
  created_at: string;
}

export async function saveUserRoute(route: Omit<UserRoute, 'id' | 'created_at'>) {
  const { data, error } = await supabase
    .from('user_routes')
    .insert([route])
    .select()
    .single();
  return { data, error };
}

Retrieving Routes

Get Route by Share Code

src/lib/supabase.ts
export async function getUserRoute(shareCode: string) {
  const { data, error } = await supabase
    .from('user_routes')
    .select('*, user:user_id(*)')
    .eq('share_code', shareCode)
    .single();
  return { data, error };
}

Get All User Routes

src/lib/supabase.ts
export async function getUserRoutes(userId: string) {
  const { data, error } = await supabase
    .from('user_routes')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false });
  return { data, error };
}

Get Latest Route

src/lib/supabase.ts
export async function getLatestUserRoute(userId: string) {
  const { data, error } = await supabase
    .from('user_routes')
    .select('*')
    .eq('user_id', userId)
    .order('created_at', { ascending: false })
    .limit(1)
    .single();
  return data;
}

Ticket Page: /turismo/ticket/[code].astro

The ticket page displays the route details with QR code:
src/pages/turismo/ticket/[code].astro
---
import { getUserRoute } from '@/lib/supabase';

const { code } = Astro.params;
const { data: route } = await getUserRoute(code);

if (!route) {
  return Astro.redirect('/404');
}

const ticketUrl = `${Astro.url.origin}/turismo/ticket/${code}`;
---

<div class="ticket-container">
  <h1>{route.route_name}</h1>
  <p>Creado por: {route.user_name}</p>
  
  <!-- QR Code -->
  <div class="qr-code">
    <img src={`https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${ticketUrl}`} />
  </div>
  
  <!-- Attractions -->
  <div class="attractions">
    {route.atractivos.map(slug => (
      <AttractionCard slug={slug} />
    ))}
  </div>
  
  <!-- Share Buttons -->
  <ShareButtons url={ticketUrl} />
</div>

QR Code Generation

QR codes are generated using the QR Server API:
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${ticketUrl}`;

QR Code Options

  • size: 300x300 (adjustable)
  • data: Full ticket URL
  • format: PNG (default)
  • ecc: Error correction level (L, M, Q, H)
QR codes link directly to the ticket page, allowing easy sharing and scanning at attractions.

Social Sharing

Tickets include social sharing buttons:
const shareData = {
  title: route.route_name,
  text: `Mira mi ruta turística en Zongolica`,
  url: ticketUrl,
};

// Web Share API
if (navigator.share) {
  navigator.share(shareData);
} else {
  // Fallback to copy link
  navigator.clipboard.writeText(ticketUrl);
}

Share Platforms

WhatsApp

Share via WhatsApp with pre-filled message

Facebook

Post ticket to Facebook timeline

Twitter

Tweet with route details and link

Copy Link

Copy ticket URL to clipboard

Complete Flow Example

// 1. User creates route with 3 attractions
const attractions = [
  'cascada-atlahuitzia',
  'parroquia-san-francisco-de-asis',
  'cristo-rey'
];

// 2. Generate share code
const shareCode = generateShareCode(); // "V1StGXR8_Z"

// 3. Save route to Supabase
const route = {
  user_id: currentUser.id,
  user_name: currentUser.name,
  route_name: 'Mi Ruta Perfecta',
  atractivos: attractions,
  ticket_url: `/ruta/${shareCode}`,
  share_code: shareCode,
  badges: ['primera_ruta'],
};
await saveUserRoute(route);

// 4. Create ticket in WordPress
const response = await fetch('/api/turismo/ticket', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: currentUser.name,
    email: currentUser.email,
    shareCode,
  }),
});

const { ticketUrl } = await response.json();
// ticketUrl: "/ruta/V1StGXR8_Z"

// 5. Redirect to ticket page
window.location.href = ticketUrl;

Error Handling

src/pages/api/turismo/ticket.ts
if (!WP_BASE_URL || !WP_TICKETS_ENDPOINT) {
  return jsonResponse({ error: 'WP_BASE_URL not configured' }, 500);
}

if (!authHeader) {
  return jsonResponse({ error: 'WP credentials not configured' }, 500);
}

if (!name || !email || !shareCode) {
  return jsonResponse({ error: 'Missing required fields' }, 400);
}

if (!wpRes.ok) {
  const text = await wpRes.text();
  return jsonResponse({ error: 'WP error', details: text }, 500);
}

Security Considerations

  • Share codes are public but unguessable (10-char random)
  • No sensitive data stored in tickets
  • WordPress credentials via environment variables only
  • HTTPS required for production

Routes

User-created routes and sharing

Badges

Badge system and unlocking

Attractions

All 17 tourist attractions

Recommendations

AI-powered recommendations

Build docs developers (and LLMs) love