feat(club-002): club PWA: join flow, digital card and installable shell

This commit is contained in:
Deploy
2026-08-26 19:21:49 +02:00
parent b2ccc70dc6
commit c263b44355
20 changed files with 1059 additions and 100 deletions

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
export async function GET(request: NextRequest) {
return handle(request);
}
export async function POST(request: NextRequest) {
return handle(request);
}
export async function PATCH(request: NextRequest) {
return handle(request);
}
async function handle(request: NextRequest): Promise<NextResponse> {
const path = request.nextUrl.pathname.replace('/api/', '');
const url = `${API}/${path}${request.nextUrl.search}`;
const headers: Record<string, string> = { accept: 'application/json' };
const cookie = request.headers.get('cookie');
if (cookie) headers.cookie = cookie;
const contentType = request.headers.get('content-type');
if (contentType) headers['content-type'] = contentType;
const init: RequestInit = { method: request.method, headers };
if (request.method !== 'GET' && request.method !== 'HEAD') {
init.body = await request.text();
}
try {
const response = await fetch(url, init);
const body = await response.json().catch(() => ({}));
const next = NextResponse.json(body, { status: response.status });
const setCookie = response.headers.get('set-cookie');
if (setCookie) next.headers.set('set-cookie', setCookie);
return next;
} catch (error) {
return NextResponse.json(
{ error: { code: 'PROXY_ERROR', message: error instanceof Error ? error.message : 'Club proxy failed' } },
{ status: 502 },
);
}
}

View File

@@ -0,0 +1,5 @@
import { ClubExperience } from '@/components/club/ClubExperience';
export default function ClubCardPage() {
return <ClubExperience mode="card" />;
}

View File

@@ -0,0 +1,5 @@
import { ClubExperience } from '@/components/club/ClubExperience';
export default function ClubJoinPage() {
return <ClubExperience mode="join" />;
}

View File

@@ -0,0 +1,5 @@
import { ClubExperience } from '@/components/club/ClubExperience';
export default function ClubPage() {
return <ClubExperience mode="landing" />;
}

View File

@@ -1,4 +1,4 @@
import type { Metadata } from 'next';
import type { Metadata, Viewport } from 'next';
import { Open_Sans } from 'next/font/google';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
@@ -16,8 +16,16 @@ export const metadata: Metadata = {
title: 'mercadodevida — Productos naturales y orgánicos',
description:
'Tienda online de productos naturales, orgánicos y saludables. Envío a toda España. Calidad certificada.',
applicationName: 'mercadodevida',
manifest: '/manifest.webmanifest',
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'Club MdV',
},
icons: {
icon: '/images/favicon.png',
apple: '/images/club-icon-192.png',
},
openGraph: {
title: 'mercadodevida — Productos naturales y orgánicos',
@@ -26,6 +34,10 @@ export const metadata: Metadata = {
},
};
export const viewport: Viewport = {
themeColor: '#70AD47',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" suppressHydrationWarning className={opensans.variable}>

View File

@@ -0,0 +1,30 @@
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Club Mercado de Vida',
short_name: 'Club MdV',
description: 'Tarjeta digital y acceso rápido al Club de Clientes de Mercado de Vida.',
start_url: '/club',
scope: '/',
display: 'standalone',
orientation: 'portrait',
background_color: '#F5F9EF',
theme_color: '#70AD47',
categories: ['shopping', 'lifestyle'],
icons: [
{
src: '/images/club-icon-192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable',
},
{
src: '/images/club-icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
],
};
}