feat(club-002): club PWA: join flow, digital card and installable shell
This commit is contained in:
44
project/frontend/src/app/api/club/[...path]/route.ts
Normal file
44
project/frontend/src/app/api/club/[...path]/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
5
project/frontend/src/app/club/card/page.tsx
Normal file
5
project/frontend/src/app/club/card/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ClubExperience } from '@/components/club/ClubExperience';
|
||||
|
||||
export default function ClubCardPage() {
|
||||
return <ClubExperience mode="card" />;
|
||||
}
|
||||
5
project/frontend/src/app/club/join/page.tsx
Normal file
5
project/frontend/src/app/club/join/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ClubExperience } from '@/components/club/ClubExperience';
|
||||
|
||||
export default function ClubJoinPage() {
|
||||
return <ClubExperience mode="join" />;
|
||||
}
|
||||
5
project/frontend/src/app/club/page.tsx
Normal file
5
project/frontend/src/app/club/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ClubExperience } from '@/components/club/ClubExperience';
|
||||
|
||||
export default function ClubPage() {
|
||||
return <ClubExperience mode="landing" />;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
30
project/frontend/src/app/manifest.ts
Normal file
30
project/frontend/src/app/manifest.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user