Add full SEO optimization for Spanglish social and language events

- Add comprehensive metadata to root layout with Open Graph, Twitter cards
- Create dynamic sitemap.ts for all pages and events
- Create robots.ts with proper allow/disallow rules
- Add JSON-LD Event structured data to event detail pages
- Add page-specific metadata to events, community, contact, FAQ pages
- Add FAQ structured data schema
- Update footer with local SEO text for Asunción, Paraguay
- Add web manifest for mobile SEO
- Create 404 page with proper noindex
- Optimize image alt text and add lazy loading
- Add NEXT_PUBLIC_SITE_URL env variable
- Add about/ folder to gitignore
This commit is contained in:
root
2026-01-30 21:05:25 +00:00
parent d0ea55dc5b
commit 47ba754f05
40 changed files with 2659 additions and 420 deletions

View File

@@ -1,13 +1,14 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { eventsApi, mediaApi, Event } from '@/lib/api';
import { eventsApi, Event } from '@/lib/api';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input';
import { PlusIcon, PencilIcon, TrashIcon, EyeIcon, PhotoIcon, ArrowUpTrayIcon, DocumentDuplicateIcon, ArchiveBoxIcon } from '@heroicons/react/24/outline';
import MediaPicker from '@/components/MediaPicker';
import { PlusIcon, PencilIcon, TrashIcon, EyeIcon, PhotoIcon, DocumentDuplicateIcon, ArchiveBoxIcon } from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import clsx from 'clsx';
@@ -18,8 +19,6 @@ export default function AdminEventsPage() {
const [showForm, setShowForm] = useState(false);
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [formData, setFormData] = useState<{
title: string;
@@ -166,25 +165,6 @@ export default function AdminEventsPage() {
}
};
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const result = await mediaApi.upload(file, editingEvent?.id, 'event');
// Use proxied path so it works through Next.js rewrites
setFormData({ ...formData, bannerUrl: result.url });
toast.success('Image uploaded successfully');
} catch (error: any) {
toast.error(error.message || 'Failed to upload image');
} finally {
setUploading(false);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
@@ -360,53 +340,13 @@ export default function AdminEventsPage() {
</select>
</div>
{/* Image Upload */}
<div>
<label className="block text-sm font-medium mb-1">Event Banner Image</label>
<div className="mt-2">
{formData.bannerUrl ? (
<div className="relative">
<img
src={formData.bannerUrl}
alt="Event banner"
className="w-full h-40 object-cover rounded-btn"
/>
<button
type="button"
onClick={() => setFormData({ ...formData, bannerUrl: '' })}
className="absolute top-2 right-2 bg-red-500 text-white p-1 rounded-full hover:bg-red-600"
>
<TrashIcon className="w-4 h-4" />
</button>
</div>
) : (
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-secondary-light-gray rounded-btn p-8 text-center cursor-pointer hover:border-primary-yellow transition-colors"
>
{uploading ? (
<div className="flex flex-col items-center">
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full" />
<p className="mt-2 text-sm text-gray-500">Uploading...</p>
</div>
) : (
<div className="flex flex-col items-center">
<PhotoIcon className="w-12 h-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-600">Click to upload event image</p>
<p className="text-xs text-gray-400">JPEG, PNG, GIF, WebP (max 5MB)</p>
</div>
)}
</div>
)}
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp,image/avif"
onChange={handleImageUpload}
className="hidden"
/>
</div>
</div>
{/* Image Upload / Media Picker */}
<MediaPicker
value={formData.bannerUrl}
onChange={(url) => setFormData({ ...formData, bannerUrl: url })}
relatedId={editingEvent?.id}
relatedType="event"
/>
<div className="flex gap-3 pt-4">
<Button type="submit" isLoading={saving}>

View File

@@ -14,6 +14,8 @@ import {
CheckCircleIcon,
XCircleIcon,
ArrowPathIcon,
TicketIcon,
Cog6ToothIcon,
} from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
@@ -38,6 +40,7 @@ export default function PaymentOptionsPage() {
cashEnabled: true,
cashInstructions: null,
cashInstructionsEs: null,
allowDuplicateBookings: false,
});
useEffect(() => {
@@ -399,6 +402,66 @@ export default function PaymentOptionsPage() {
</div>
</Card>
{/* Booking Settings */}
<Card className="mb-6">
<div className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<Cog6ToothIcon className="w-5 h-5 text-purple-600" />
</div>
<div>
<h3 className="font-semibold text-lg">
{locale === 'es' ? 'Configuración de Reservas' : 'Booking Settings'}
</h3>
<p className="text-sm text-gray-500">
{locale === 'es' ? 'Opciones adicionales para el proceso de reserva' : 'Additional options for the booking process'}
</p>
</div>
</div>
<div className="space-y-4 pt-4 border-t">
{/* Allow Duplicate Bookings */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<TicketIcon className="w-5 h-5 text-gray-400" />
<div>
<p className="font-medium text-gray-900">
{locale === 'es' ? 'Permitir Reservas Múltiples' : 'Allow Multiple Bookings'}
</p>
<p className="text-sm text-gray-500">
{locale === 'es'
? 'Permitir que un usuario reserve varios tickets para el mismo evento con el mismo email'
: 'Allow a user to book multiple tickets for the same event with the same email'}
</p>
</div>
</div>
<button
onClick={() => updateOption('allowDuplicateBookings', !options.allowDuplicateBookings)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
options.allowDuplicateBookings ? 'bg-primary-yellow' : 'bg-gray-300'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
options.allowDuplicateBookings ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{options.allowDuplicateBookings && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 ml-8">
<p className="text-sm text-yellow-800">
{locale === 'es'
? '⚠️ Cuando está habilitado, los usuarios pueden crear múltiples reservas para el mismo evento. Esto es útil para reservar en nombre de amigos o familiares.'
: '⚠️ When enabled, users can create multiple bookings for the same event. This is useful for booking on behalf of friends or family.'}
</p>
</div>
)}
</div>
</div>
</Card>
{/* Summary */}
<Card>
<div className="p-6">