> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/martin-ratti/PCFIX-Baru/llms.txt
> Use this file to discover all available pages before exploring further.

# Frontend Architecture

> Astro 5 with React islands, Zustand state management, and Tailwind CSS

## Overview

The PC Fix frontend is built with **Astro 5**, leveraging its islands architecture for optimal performance. Interactive components are built with **React 18**, while static content is rendered server-side for instant page loads.

<Info>
  The frontend achieves a **95+ Lighthouse score** through Astro's partial hydration and View Transitions.
</Info>

## Core Technologies

<CardGroup cols={2}>
  <Card title="Astro 5.16.3" icon="rocket">
    Meta-framework for SSR, static generation, and partial hydration
  </Card>

  <Card title="React 18.3" icon="react">
    UI library for interactive islands (cart, forms, admin dashboard)
  </Card>

  <Card title="Tailwind CSS 3.4" icon="palette">
    Utility-first CSS framework for responsive design
  </Card>

  <Card title="TypeScript 5.9" icon="code">
    Type-safe development across the frontend
  </Card>
</CardGroup>

## Astro Configuration

### astro.config.mjs

```javascript astro.config.mjs theme={null}
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import vercel from '@astrojs/vercel';
import node from '@astrojs/node';
import sitemap from '@astrojs/sitemap';
import sentry from '@sentry/astro';

const isVercel = !!process.env.VERCEL;

export default defineConfig({
  site: 'https://pcfixbaru.com.ar',
  output: 'server', // SSR mode

  // Dynamic adapter selection based on environment
  adapter: isVercel
    ? vercel({
        webAnalytics: { enabled: true },
        imageService: true,
      })
    : node({
        mode: 'standalone',
      }),

  integrations: [
    react(),
    tailwind(),
    sitemap({
      filter: (page) => {
        const excludePatterns = [
          '/admin', '/auth', '/checkout', '/perfil',
          '/mis-consultas', '/404', '/success',
          '/acceso-denegado', '/error', '/mantenimiento',
          '/login', '/reset-password', '/cuenta',
          '/tienda/carrito'
        ];
        return !excludePatterns.some(pattern => page.includes(pattern));
      }
    }),
    ...(process.env.PUBLIC_SENTRY_DSN ? [sentry({
      dsn: process.env.PUBLIC_SENTRY_DSN,
      sourceMapsUploadOptions: { enabled: false },
    })] : [])
  ],

  image: {
    domains: [
      'placehold.co',
      'images.unsplash.com',
      'res.cloudinary.com' // Cloudinary CDN
    ],
    remotePatterns: [{ protocol: "https" }],
  },
});
```

<Note>
  The configuration dynamically selects the adapter based on the deployment environment (Vercel vs. Node.js standalone).
</Note>

## Project Structure

```bash theme={null}
packages/web/src/
├── assets/              # Static assets
├── components/          # React components
│   ├── admin/          # Admin dashboard components
│   │   ├── core/       # Dashboard, stats, charts
│   │   ├── dashboard/  # Intelligence, analytics
│   │   ├── products/   # Product management
│   │   └── sales/      # Sales management
│   ├── cart/           # Shopping cart UI
│   ├── checkout/       # Checkout flow
│   ├── layout/         # Header, footer, navigation
│   ├── product/        # Product cards, filters
│   └── ui/             # Reusable UI components
├── data/               # Static data files
├── layouts/            # Astro layouts
│   └── Layout.astro   # Main layout wrapper
├── pages/              # File-based routing
│   ├── admin/         # Admin panel pages
│   ├── api/           # API endpoints (Astro)
│   ├── auth/          # Login, register
│   ├── checkout/      # Checkout pages
│   ├── perfil/        # User profile
│   ├── tienda/        # Store pages
│   └── index.astro    # Homepage
├── stores/             # Zustand stores
│   ├── authStore.ts   # Authentication state
│   ├── cartStore.ts   # Shopping cart state
│   ├── favoritesStore.ts
│   ├── serviceStore.ts
│   └── toastStore.ts
├── styles/             # Global CSS
├── types/              # TypeScript types
└── utils/              # Utility functions
```

## Astro Islands Architecture

### What are Islands?

Astro Islands allow you to mix static HTML with interactive JavaScript components. Only the interactive parts are hydrated on the client.

<Tabs>
  <Tab title="Static Content">
    ```astro pages/tienda/index.astro theme={null}
    ---
    import Layout from '~/layouts/Layout.astro';
    import ProductList from '~/components/product/ProductList';

    // This runs on the server
    const products = await fetch(`${API_URL}/api/products`).then(r => r.json());
    ---

    <Layout title="Tienda">
      <h1>Nuestros Productos</h1>
      <!-- This component is hydrated on the client -->
      <ProductList client:load products={products} />
    </Layout>
    ```
  </Tab>

  <Tab title="Client Directives">
    Astro provides several client directives for controlling hydration:

    | Directive        | Behavior                               |
    | ---------------- | -------------------------------------- |
    | `client:load`    | Hydrate immediately on page load       |
    | `client:idle`    | Hydrate when browser is idle           |
    | `client:visible` | Hydrate when component enters viewport |
    | `client:media`   | Hydrate based on media query           |
    | `client:only`    | Skip SSR, render only on client        |
  </Tab>
</Tabs>

### Example: Shopping Cart Island

```tsx components/cart/CartButton.tsx theme={null}
import { useCartStore } from '~/stores/cartStore';
import { ShoppingCart } from 'lucide-react';

export default function CartButton() {
  const { items, totalItems } = useCartStore();
  
  return (
    <button className="relative p-2 hover:bg-gray-100 rounded-full">
      <ShoppingCart size={24} />
      {totalItems > 0 && (
        <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
          {totalItems}
        </span>
      )}
    </button>
  );
}
```

```astro components/layout/Header.astro theme={null}
---
import CartButton from '~/components/cart/CartButton';
---

<header>
  <nav>
    <!-- Static navigation -->
    <a href="/">Inicio</a>
    <a href="/tienda">Tienda</a>
    
    <!-- Interactive cart button -->
    <CartButton client:load />
  </nav>
</header>
```

## State Management with Zustand

### Cart Store Example

```typescript stores/cartStore.ts theme={null}
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartItem {
  id: number;
  nombre: string;
  precio: number;
  quantity: number;
  foto: string | null;
}

interface CartStore {
  items: CartItem[];
  totalItems: number;
  totalPrice: number;
  addItem: (item: CartItem) => void;
  removeItem: (id: number) => void;
  updateQuantity: (id: number, quantity: number) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartStore>()(persist(
  (set, get) => ({
    items: [],
    totalItems: 0,
    totalPrice: 0,
    
    addItem: (item) => set((state) => {
      const existingItem = state.items.find(i => i.id === item.id);
      
      if (existingItem) {
        return {
          items: state.items.map(i => 
            i.id === item.id 
              ? { ...i, quantity: i.quantity + item.quantity }
              : i
          )
        };
      }
      
      return { items: [...state.items, item] };
    }),
    
    removeItem: (id) => set((state) => ({
      items: state.items.filter(i => i.id !== id)
    })),
    
    updateQuantity: (id, quantity) => set((state) => ({
      items: state.items.map(i => 
        i.id === id ? { ...i, quantity } : i
      )
    })),
    
    clearCart: () => set({ items: [] }),
  }),
  {
    name: 'cart-storage', // localStorage key
  }
));
```

### Auth Store Example

```typescript stores/authStore.ts theme={null}
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: number;
  email: string;
  nombre: string;
  apellido: string;
  role: 'USER' | 'ADMIN';
}

interface AuthStore {
  user: User | null;
  accessToken: string | null;
  isAuthenticated: boolean;
  login: (user: User, accessToken: string) => void;
  logout: () => void;
  updateUser: (user: Partial<User>) => void;
}

export const useAuthStore = create<AuthStore>()(persist(
  (set) => ({
    user: null,
    accessToken: null,
    isAuthenticated: false,
    
    login: (user, accessToken) => set({
      user,
      accessToken,
      isAuthenticated: true,
    }),
    
    logout: () => set({
      user: null,
      accessToken: null,
      isAuthenticated: false,
    }),
    
    updateUser: (userData) => set((state) => ({
      user: state.user ? { ...state.user, ...userData } : null,
    })),
  }),
  {
    name: 'auth-storage',
  }
));
```

## Form Handling

### React Hook Form with Zod Validation

```tsx components/auth/LoginForm.tsx theme={null}
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useAuthStore } from '~/stores/authStore';
import { toast } from 'sonner';

const loginSchema = z.object({
  email: z.string().email('Email inválido'),
  password: z.string().min(6, 'Mínimo 6 caracteres'),
});

type LoginForm = z.infer<typeof loginSchema>;

export default function LoginForm() {
  const { login } = useAuthStore();
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginForm>({
    resolver: zodResolver(loginSchema),
  });

  const onSubmit = async (data: LoginForm) => {
    try {
      const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/api/auth/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      
      if (!response.ok) throw new Error('Login failed');
      
      const { user, accessToken } = await response.json();
      login(user, accessToken);
      toast.success('¡Bienvenido!');
      window.location.href = '/perfil';
    } catch (error) {
      toast.error('Error al iniciar sesión');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email
        </label>
        <input
          {...register('email')}
          type="email"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.email && (
          <p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
        )}
      </div>
      
      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          Contraseña
        </label>
        <input
          {...register('password')}
          type="password"
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
        {errors.password && (
          <p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
        )}
      </div>
      
      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {isSubmitting ? 'Iniciando...' : 'Iniciar Sesión'}
      </button>
    </form>
  );
}
```

## Tailwind CSS Configuration

```javascript tailwind.config.mjs theme={null}
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
    },
  },
  plugins: [],
};
```

## UI Components

### Toast Notifications with Sonner

```tsx components/ui/Toaster.tsx theme={null}
import { Toaster as SonnerToaster } from 'sonner';

export default function Toaster() {
  return (
    <SonnerToaster
      position="top-right"
      richColors
      closeButton
      duration={4000}
    />
  );
}
```

Usage:

```typescript theme={null}
import { toast } from 'sonner';

// Success
toast.success('Producto agregado al carrito');

// Error
toast.error('Error al procesar la solicitud');

// Loading
toast.loading('Procesando...');

// Custom
toast('Evento personalizado', {
  description: 'Descripción adicional',
  action: {
    label: 'Deshacer',
    onClick: () => console.log('Undo'),
  },
});
```

### Swiper Carousels

```tsx components/product/ProductCarousel.tsx theme={null}
import { Swiper, SwiperSlide } from 'swiper/react';
import { Navigation, Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';

interface Product {
  id: number;
  nombre: string;
  foto: string | null;
  precio: number;
}

interface Props {
  products: Product[];
}

export default function ProductCarousel({ products }: Props) {
  return (
    <Swiper
      modules={[Navigation, Pagination]}
      spaceBetween={20}
      slidesPerView={1}
      navigation
      pagination={{ clickable: true }}
      breakpoints={{
        640: { slidesPerView: 2 },
        768: { slidesPerView: 3 },
        1024: { slidesPerView: 4 },
      }}
    >
      {products.map((product) => (
        <SwiperSlide key={product.id}>
          <div className="border rounded-lg p-4">
            <img
              src={product.foto || '/placeholder.jpg'}
              alt={product.nombre}
              className="w-full h-48 object-cover rounded"
            />
            <h3 className="mt-2 font-semibold">{product.nombre}</h3>
            <p className="text-lg font-bold">${product.precio}</p>
          </div>
        </SwiperSlide>
      ))}
    </Swiper>
  );
}
```

## Performance Optimizations

<CardGroup cols={2}>
  <Card title="View Transitions" icon="arrow-right-arrow-left">
    Astro's built-in View Transitions provide smooth page navigation without full page reloads
  </Card>

  <Card title="Image Optimization" icon="image">
    Automatic image optimization with Astro's `<Image />` component and Cloudinary CDN
  </Card>

  <Card title="Partial Hydration" icon="droplet">
    Only interactive components are hydrated, reducing JavaScript payload
  </Card>

  <Card title="Code Splitting" icon="scissors">
    Automatic code splitting per route and component
  </Card>
</CardGroup>

## Testing

### Vitest Configuration

```typescript vitest.config.ts theme={null}
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts',
  },
});
```

### Component Test Example

```typescript components/cart/CartButton.test.tsx theme={null}
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import CartButton from './CartButton';

describe('CartButton', () => {
  it('displays cart item count', () => {
    render(<CartButton />);
    const badge = screen.getByText('3');
    expect(badge).toBeInTheDocument();
  });
});
```

## Next Steps

<CardGroup cols={3}>
  <Card title="Backend API" icon="server" href="/architecture/backend">
    Learn about the Express API
  </Card>

  <Card title="Database Schema" icon="database" href="/architecture/database">
    Explore the data model
  </Card>

  <Card title="Deployment" icon="rocket" href="/deployment/overview">
    Deploy to production
  </Card>
</CardGroup>
