Ilha Marketing
This commit is contained in:
parent
f606b41ddd
commit
a1cd43641c
@ -1,6 +1,5 @@
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||
import BaseDivider from './BaseDivider'
|
||||
import BaseIcon from './BaseIcon'
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import React, { ReactNode, useEffect, useState } from 'react'
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||
import menuAside from '../menuAside'
|
||||
|
||||
@ -1,166 +1,547 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { FormEvent, ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import SectionFullScreen from '../components/SectionFullScreen';
|
||||
import LayoutGuest from '../layouts/Guest';
|
||||
import BaseDivider from '../components/BaseDivider';
|
||||
import BaseButtons from '../components/BaseButtons';
|
||||
import { getPageTitle } from '../config';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
||||
|
||||
type MarketPlan = {
|
||||
id: string;
|
||||
marketName: string;
|
||||
deliveryFocus: string;
|
||||
expectedCustomers: number;
|
||||
weather: string;
|
||||
tourism: string;
|
||||
createdAt: string;
|
||||
checklist: string[];
|
||||
featuredProducts: string[];
|
||||
revenueEstimate: number;
|
||||
};
|
||||
|
||||
export default function Starter() {
|
||||
const [illustrationImage, setIllustrationImage] = useState({
|
||||
src: undefined,
|
||||
photographer: undefined,
|
||||
photographer_url: undefined,
|
||||
})
|
||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
||||
const [contentType, setContentType] = useState('video');
|
||||
const [contentPosition, setContentPosition] = useState('left');
|
||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||
type PlanForm = {
|
||||
marketName: string;
|
||||
deliveryFocus: string;
|
||||
expectedCustomers: string;
|
||||
weather: string;
|
||||
tourism: string;
|
||||
};
|
||||
|
||||
const title = 'Ilha Market'
|
||||
const storageKey = 'ilha-market-day-plans';
|
||||
|
||||
// Fetch Pexels image/video
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const image = await getPexelsImage();
|
||||
const video = await getPexelsVideo();
|
||||
setIllustrationImage(image);
|
||||
setIllustrationVideo(video);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
const featuredByFocus: Record<string, string[]> = {
|
||||
tropical: ['Coco gelado', 'Açaí da ilha', 'Banana-prata', 'Suco de caju'],
|
||||
bakery: ['Pão francês', 'Bolo de fubá', 'Pão de queijo', 'Café coado'],
|
||||
fishing: ['Tilápia fresca', 'Camarão', 'Isca artesanal', 'Gelo para pesca'],
|
||||
farming: ['Mandioca', 'Couve', 'Tomate cereja', 'Flores tropicais'],
|
||||
};
|
||||
|
||||
const imageBlock = (image) => (
|
||||
<div
|
||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||
style={{
|
||||
backgroundImage: `${
|
||||
image
|
||||
? `url(${image?.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}}
|
||||
>
|
||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={image?.photographer_url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Photo by {image?.photographer} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const focusLabels: Record<string, string> = {
|
||||
tropical: 'Tropical essentials',
|
||||
bakery: 'Bakery morning rush',
|
||||
fishing: 'Fishing dock supply',
|
||||
farming: 'Farm-to-market harvest',
|
||||
};
|
||||
|
||||
const videoBlock = (video) => {
|
||||
if (video?.video_files?.length > 0) {
|
||||
return (
|
||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
||||
<video
|
||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
>
|
||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
||||
<a
|
||||
className='text-[8px]'
|
||||
href={video?.user?.url}
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Video by {video.user.name} on Pexels
|
||||
</a>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
const weatherLabels: Record<string, string> = {
|
||||
sunny: 'Sunny beach day',
|
||||
rainy: 'Warm tropical rain',
|
||||
storm: 'Storm watch',
|
||||
fog: 'Foggy mountain morning',
|
||||
};
|
||||
|
||||
const tourismLabels: Record<string, string> = {
|
||||
low: 'Local residents only',
|
||||
normal: 'Balanced village day',
|
||||
high: 'Cruise/tourist surge',
|
||||
};
|
||||
|
||||
const currencyFormatter = new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL',
|
||||
});
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
function buildChecklist(form: PlanForm, customers: number) {
|
||||
const base = [
|
||||
`Receive ${focusLabels[form.deliveryFocus].toLowerCase()} delivery at Porto da Esperança.`,
|
||||
'Restock hero shelf, cold aisle, and checkout impulse basket before opening.',
|
||||
'Assign one employee to PIX/card checkout and one to shelf recovery.',
|
||||
];
|
||||
|
||||
if (form.weather === 'storm') {
|
||||
base.push('Move umbrellas, candles, batteries, and bottled water to the front display.');
|
||||
}
|
||||
|
||||
if (form.weather === 'sunny') {
|
||||
base.push('Boost cold drinks, beach snacks, sunscreen, and fresh fruit near the entrance.');
|
||||
}
|
||||
|
||||
if (form.tourism === 'high') {
|
||||
base.push('Prepare bilingual welcome signs and souvenir bundles for tourists.');
|
||||
}
|
||||
|
||||
if (customers > 80) {
|
||||
base.push('Schedule an afternoon micro-delivery so shelves do not empty after lunch.');
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
function calculateRevenue(form: PlanForm, customers: number) {
|
||||
const focusMultiplier = form.deliveryFocus === 'bakery' ? 31 : form.deliveryFocus === 'fishing' ? 44 : form.deliveryFocus === 'farming' ? 35 : 38;
|
||||
const tourismMultiplier = form.tourism === 'high' ? 1.34 : form.tourism === 'low' ? 0.82 : 1;
|
||||
const weatherMultiplier = form.weather === 'storm' ? 0.72 : form.weather === 'rainy' ? 0.9 : form.weather === 'sunny' ? 1.18 : 0.96;
|
||||
|
||||
return Math.round(customers * focusMultiplier * tourismMultiplier * weatherMultiplier);
|
||||
}
|
||||
|
||||
function createPlan(form: PlanForm): MarketPlan {
|
||||
const expectedCustomers = Number(form.expectedCustomers);
|
||||
|
||||
return {
|
||||
id: `${Date.now()}`,
|
||||
marketName: form.marketName.trim(),
|
||||
deliveryFocus: form.deliveryFocus,
|
||||
expectedCustomers,
|
||||
weather: form.weather,
|
||||
tourism: form.tourism,
|
||||
createdAt: new Date().toISOString(),
|
||||
checklist: buildChecklist(form, expectedCustomers),
|
||||
featuredProducts: featuredByFocus[form.deliveryFocus],
|
||||
revenueEstimate: calculateRevenue(form, expectedCustomers),
|
||||
};
|
||||
}
|
||||
|
||||
export default function IlhaMarketLanding() {
|
||||
const [form, setForm] = useState<PlanForm>({
|
||||
marketName: 'Ilha Market',
|
||||
deliveryFocus: 'tropical',
|
||||
expectedCustomers: '64',
|
||||
weather: 'sunny',
|
||||
tourism: 'normal',
|
||||
});
|
||||
const [plans, setPlans] = useState<MarketPlan[]>([]);
|
||||
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedPlans = window.localStorage.getItem(storageKey);
|
||||
if (savedPlans) {
|
||||
const parsedPlans = JSON.parse(savedPlans) as MarketPlan[];
|
||||
setPlans(parsedPlans);
|
||||
setSelectedPlanId(parsedPlans[0]?.id ?? null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load Ilha Market plans from localStorage:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(plans));
|
||||
} catch (err) {
|
||||
console.error('Failed to save Ilha Market plans to localStorage:', err);
|
||||
}
|
||||
}, [plans]);
|
||||
|
||||
const draftPlan = useMemo(() => {
|
||||
const customers = Number(form.expectedCustomers) || 0;
|
||||
|
||||
return {
|
||||
checklist: buildChecklist(form, customers),
|
||||
featuredProducts: featuredByFocus[form.deliveryFocus],
|
||||
revenueEstimate: calculateRevenue(form, customers),
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
const selectedPlan = plans.find((plan) => plan.id === selectedPlanId) ?? plans[0] ?? null;
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
const expectedCustomers = Number(form.expectedCustomers);
|
||||
|
||||
if (form.marketName.trim().length < 3) {
|
||||
setError('Name the market with at least 3 characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(expectedCustomers) || expectedCustomers < 10 || expectedCustomers > 250) {
|
||||
setError('Expected customers must be between 10 and 250 for this playable slice.');
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPlan = createPlan(form);
|
||||
setPlans((currentPlans) => [nextPlan, ...currentPlans].slice(0, 6));
|
||||
setSelectedPlanId(nextPlan.id);
|
||||
setSuccess(`${nextPlan.marketName} is ready to open with ${nextPlan.featuredProducts.length} featured product groups.`);
|
||||
};
|
||||
|
||||
const removePlan = (planId: string) => {
|
||||
setPlans((currentPlans) => {
|
||||
const nextPlans = currentPlans.filter((plan) => plan.id !== planId);
|
||||
setSelectedPlanId(nextPlans[0]?.id ?? null);
|
||||
return nextPlans;
|
||||
});
|
||||
setSuccess('Market day removed from the planner.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
contentPosition === 'background'
|
||||
? {
|
||||
backgroundImage: `${
|
||||
illustrationImage
|
||||
? `url(${illustrationImage.src?.original})`
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
||||
}`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'left center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Ilha Market')}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Ilha Market is a cozy Brazilian island supermarket management and life simulation game concept."
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<SectionFullScreen bg='violet'>
|
||||
<div
|
||||
className={`flex ${
|
||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
||||
} min-h-screen w-full`}
|
||||
>
|
||||
{contentType === 'image' && contentPosition !== 'background'
|
||||
? imageBlock(illustrationImage)
|
||||
: null}
|
||||
{contentType === 'video' && contentPosition !== 'background'
|
||||
? videoBlock(illustrationVideo)
|
||||
: null}
|
||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
||||
<CardBoxComponentTitle title="Welcome to your Ilha Market app!"/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
||||
<main className="min-h-screen overflow-hidden bg-[#fff7e8] text-[#183d35]">
|
||||
<section className="relative isolate px-5 py-6 sm:px-8 lg:px-12">
|
||||
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_15%_15%,rgba(255,181,93,0.36),transparent_28%),radial-gradient(circle_at_80%_4%,rgba(42,157,143,0.26),transparent_32%),linear-gradient(135deg,#fff7e8_0%,#dff7ed_48%,#ffe3b3_100%)]" />
|
||||
<div className="absolute -right-20 top-24 -z-10 h-72 w-72 rounded-full bg-[#ff7a59]/20 blur-3xl" />
|
||||
<div className="absolute -bottom-20 left-10 -z-10 h-80 w-80 rounded-full bg-[#2a9d8f]/20 blur-3xl" />
|
||||
|
||||
<nav className="mx-auto flex max-w-7xl items-center justify-between rounded-full border border-white/70 bg-white/55 px-5 py-3 shadow-sm backdrop-blur">
|
||||
<Link href="/" className="flex items-center gap-3 font-semibold tracking-tight text-[#183d35]">
|
||||
<span className="grid h-10 w-10 place-items-center rounded-full bg-[#ff7a59] text-xl shadow-lg shadow-[#ff7a59]/25">🏝️</span>
|
||||
<span className="text-lg">Ilha Market™</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||||
<a href="#planner" className="hidden rounded-full px-4 py-2 text-[#24564b] transition hover:bg-white/70 sm:inline-flex">
|
||||
Market planner
|
||||
</a>
|
||||
<Link href="/login" className="rounded-full bg-[#183d35] px-4 py-2 text-white shadow-lg shadow-[#183d35]/20 transition hover:bg-[#25574d] focus:outline-none focus:ring-2 focus:ring-[#ffb55d]">
|
||||
Admin login
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
href='/login'
|
||||
label='Login'
|
||||
color='info'
|
||||
className='w-full'
|
||||
/>
|
||||
</nav>
|
||||
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</div>
|
||||
</div>
|
||||
</SectionFullScreen>
|
||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mx-auto grid max-w-7xl items-center gap-12 py-16 lg:grid-cols-[1.05fr_0.95fr] lg:py-24">
|
||||
<div>
|
||||
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-white/80 bg-white/60 px-4 py-2 text-sm font-semibold text-[#2a6f62] shadow-sm backdrop-blur">
|
||||
<span className="h-2 w-2 rounded-full bg-[#2a9d8f]" />
|
||||
Cozy Rio-coast life sim + supermarket management
|
||||
</div>
|
||||
<h1 className="max-w-4xl text-5xl font-black leading-[0.95] tracking-[-0.04em] text-[#163830] sm:text-6xl lg:text-7xl">
|
||||
Reopen the island’s only market and bring the community back to life.
|
||||
</h1>
|
||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-[#42665e]">
|
||||
Plan delivery days, stock tropical products, prepare PIX/card checkout, and build the first playable management loop for <strong>Ilha Market™</strong> — a warm, stylized Brazilian island game concept.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<a href="#planner" className="rounded-full bg-[#ff7a59] px-6 py-3 text-center font-bold text-white shadow-xl shadow-[#ff7a59]/25 transition hover:-translate-y-0.5 hover:bg-[#ef6846] focus:outline-none focus:ring-2 focus:ring-[#183d35]">
|
||||
Start a market day
|
||||
</a>
|
||||
<Link href="/login" className="rounded-full border border-[#183d35]/15 bg-white/70 px-6 py-3 text-center font-bold text-[#183d35] shadow-sm transition hover:-translate-y-0.5 hover:bg-white focus:outline-none focus:ring-2 focus:ring-[#ffb55d]">
|
||||
Open admin interface
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<dl className="mt-10 grid max-w-2xl grid-cols-3 gap-3">
|
||||
{[
|
||||
['9+', 'Island regions'],
|
||||
['4', 'Payment modes'],
|
||||
['24h', 'Living routines'],
|
||||
].map(([value, label]) => (
|
||||
<div key={label} className="rounded-3xl border border-white/70 bg-white/55 p-4 text-center shadow-sm backdrop-blur">
|
||||
<dt className="text-2xl font-black text-[#183d35]">{value}</dt>
|
||||
<dd className="text-xs font-semibold uppercase tracking-[0.18em] text-[#5f8278]">{label}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute -left-6 top-12 h-24 w-24 rounded-full bg-[#ffcf6d] blur-2xl" />
|
||||
<div className="relative rounded-[2.5rem] border border-white/80 bg-white/65 p-4 shadow-2xl shadow-[#2a6f62]/15 backdrop-blur">
|
||||
<div className="overflow-hidden rounded-[2rem] bg-[#8fd8c1]">
|
||||
<div className="relative min-h-[460px] bg-[linear-gradient(180deg,#7ed2e6_0%,#dff7ed_38%,#fff0bd_39%,#fff0bd_51%,#2a9d8f_52%,#1f7c70_100%)] p-6">
|
||||
<div className="absolute left-8 top-8 h-20 w-20 rounded-full bg-[#fff2a8] shadow-[0_0_60px_rgba(255,242,168,0.85)]" />
|
||||
<div className="absolute bottom-10 left-6 right-6 h-20 rounded-[100%] bg-[#106a73]/25 blur-sm" />
|
||||
<div className="absolute bottom-16 left-10 right-10 h-24 rounded-[100%] border-t-4 border-white/60" />
|
||||
|
||||
<div className="absolute bottom-20 left-1/2 w-[78%] -translate-x-1/2 rounded-[2rem] bg-[#fff7e8] p-4 shadow-2xl">
|
||||
<div className="rounded-[1.5rem] bg-[#ff7a59] px-5 py-3 text-center text-xl font-black text-white shadow-lg">ILHA MARKET</div>
|
||||
<div className="mt-4 grid grid-cols-3 gap-3">
|
||||
<div className="h-24 rounded-2xl bg-[#2a9d8f] p-2 shadow-inner">
|
||||
<div className="h-full rounded-xl bg-[#b7eadb]" />
|
||||
</div>
|
||||
<div className="h-24 rounded-2xl bg-[#f4a261] p-2 shadow-inner">
|
||||
<div className="h-full rounded-xl bg-[#ffe0a8]" />
|
||||
</div>
|
||||
<div className="h-24 rounded-2xl bg-[#2a9d8f] p-2 shadow-inner">
|
||||
<div className="h-full rounded-xl bg-[#b7eadb]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between rounded-2xl bg-[#183d35] px-4 py-3 text-sm font-bold text-white">
|
||||
<span>PIX • Credit • Cash</span>
|
||||
<span>Open 08:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-24 right-10 grid h-16 w-16 place-items-center rounded-full bg-[#4b2e21] text-3xl shadow-xl">🧺</div>
|
||||
<div className="absolute bottom-24 left-12 grid h-14 w-14 place-items-center rounded-full bg-[#24564b] text-2xl shadow-xl">🐟</div>
|
||||
<div className="absolute left-10 top-36 text-6xl drop-shadow-lg">🌴</div>
|
||||
<div className="absolute right-12 top-28 text-5xl drop-shadow-lg">🌺</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-[#183d35] px-5 py-10 text-white sm:px-8 lg:px-12">
|
||||
<div className="mx-auto grid max-w-7xl gap-4 md:grid-cols-4">
|
||||
{[
|
||||
['Explore', 'Village, beach, forest, mangrove, farm, harbor, lookout, caves.'],
|
||||
['Manage', 'Deliveries, shelves, pricing, staff, reports, PIX and card checkout.'],
|
||||
['Connect', 'NPC routines, families, friendships, quests, festivals and reputation.'],
|
||||
['Grow', 'Farm, fish, craft, restore buildings, upgrade tourism and the harbor.'],
|
||||
].map(([title, text]) => (
|
||||
<article key={title} className="rounded-3xl border border-white/10 bg-white/10 p-5 shadow-xl shadow-black/5">
|
||||
<h2 className="text-lg font-black text-[#ffcf6d]">{title}</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-white/75">{text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="planner" className="px-5 py-16 sm:px-8 lg:px-12">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="mb-8 max-w-3xl">
|
||||
<p className="text-sm font-black uppercase tracking-[0.26em] text-[#e46b4e]">Playable MVP slice</p>
|
||||
<h2 className="mt-3 text-4xl font-black tracking-[-0.03em] text-[#183d35] sm:text-5xl">Market Day Planner</h2>
|
||||
<p className="mt-4 text-[#54756d]">
|
||||
Create a daily operating plan for Dona Percília’s reopened supermarket. The planner validates inputs, generates a stocking checklist, saves recent plans in this browser, and lets you inspect or remove each plan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
|
||||
<form onSubmit={handleSubmit} className="rounded-[2rem] border border-[#e7d9bd] bg-white p-6 shadow-xl shadow-[#dcae72]/10">
|
||||
<div className="mb-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-[#183d35]">Plan opening day</h3>
|
||||
<p className="text-sm text-[#66877f]">Tune demand, weather, and delivery focus.</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#dff7ed] px-3 py-1 text-xs font-black uppercase tracking-[0.2em] text-[#2a6f62]">Live</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="block">
|
||||
<span className="text-sm font-bold text-[#24564b]">Market name</span>
|
||||
<input
|
||||
value={form.marketName}
|
||||
onChange={(event) => setForm({ ...form, marketName: event.target.value })}
|
||||
className="mt-2 w-full rounded-2xl border border-[#d9cdb5] bg-[#fffaf1] px-4 py-3 text-[#183d35] outline-none transition focus:border-[#2a9d8f] focus:ring-4 focus:ring-[#2a9d8f]/15"
|
||||
placeholder="Ilha Market"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="text-sm font-bold text-[#24564b]">Delivery focus</span>
|
||||
<select
|
||||
value={form.deliveryFocus}
|
||||
onChange={(event) => setForm({ ...form, deliveryFocus: event.target.value })}
|
||||
className="mt-2 w-full rounded-2xl border border-[#d9cdb5] bg-[#fffaf1] px-4 py-3 text-[#183d35] outline-none transition focus:border-[#2a9d8f] focus:ring-4 focus:ring-[#2a9d8f]/15"
|
||||
>
|
||||
{Object.entries(focusLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-sm font-bold text-[#24564b]">Expected customers</span>
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
max="250"
|
||||
value={form.expectedCustomers}
|
||||
onChange={(event) => setForm({ ...form, expectedCustomers: event.target.value })}
|
||||
className="mt-2 w-full rounded-2xl border border-[#d9cdb5] bg-[#fffaf1] px-4 py-3 text-[#183d35] outline-none transition focus:border-[#2a9d8f] focus:ring-4 focus:ring-[#2a9d8f]/15"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="text-sm font-bold text-[#24564b]">Weather</span>
|
||||
<select
|
||||
value={form.weather}
|
||||
onChange={(event) => setForm({ ...form, weather: event.target.value })}
|
||||
className="mt-2 w-full rounded-2xl border border-[#d9cdb5] bg-[#fffaf1] px-4 py-3 text-[#183d35] outline-none transition focus:border-[#2a9d8f] focus:ring-4 focus:ring-[#2a9d8f]/15"
|
||||
>
|
||||
{Object.entries(weatherLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-sm font-bold text-[#24564b]">Tourism demand</span>
|
||||
<select
|
||||
value={form.tourism}
|
||||
onChange={(event) => setForm({ ...form, tourism: event.target.value })}
|
||||
className="mt-2 w-full rounded-2xl border border-[#d9cdb5] bg-[#fffaf1] px-4 py-3 text-[#183d35] outline-none transition focus:border-[#2a9d8f] focus:ring-4 focus:ring-[#2a9d8f]/15"
|
||||
>
|
||||
{Object.entries(tourismLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="mt-5 rounded-2xl bg-[#fff0ed] px-4 py-3 text-sm font-semibold text-[#ad3826]">{error}</p>}
|
||||
{success && <p className="mt-5 rounded-2xl bg-[#e8fbf3] px-4 py-3 text-sm font-semibold text-[#1d725d]">{success}</p>}
|
||||
|
||||
<button type="submit" className="mt-6 w-full rounded-2xl bg-[#183d35] px-5 py-4 font-black text-white shadow-xl shadow-[#183d35]/20 transition hover:-translate-y-0.5 hover:bg-[#25574d] focus:outline-none focus:ring-4 focus:ring-[#ffb55d]/50">
|
||||
Generate and save market plan
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[0.92fr_1.08fr]">
|
||||
<article className="rounded-[2rem] border border-[#e7d9bd] bg-[#fffdf8] p-6 shadow-xl shadow-[#dcae72]/10">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-[#183d35]">Live forecast</h3>
|
||||
<p className="text-sm text-[#66877f]">Preview before saving.</p>
|
||||
</div>
|
||||
<span className="rounded-2xl bg-[#ffcf6d] px-3 py-2 text-sm font-black text-[#704313]">
|
||||
{currencyFormatter.format(draftPlan.revenueEstimate)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-2">
|
||||
{draftPlan.featuredProducts.map((product) => (
|
||||
<span key={product} className="rounded-full bg-[#dff7ed] px-3 py-1 text-sm font-bold text-[#24564b]">{product}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ol className="mt-6 space-y-3">
|
||||
{draftPlan.checklist.map((item, index) => (
|
||||
<li key={item} className="flex gap-3 rounded-2xl bg-[#f5ecdc] p-3 text-sm leading-6 text-[#42665e]">
|
||||
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-white font-black text-[#e46b4e]">{index + 1}</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</article>
|
||||
|
||||
<article className="rounded-[2rem] border border-[#e7d9bd] bg-white p-6 shadow-xl shadow-[#dcae72]/10">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-[#183d35]">Saved market days</h3>
|
||||
<p className="text-sm text-[#66877f]">Recent plans are stored locally for this MVP.</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#183d35] px-3 py-1 text-sm font-black text-white">{plans.length}</span>
|
||||
</div>
|
||||
|
||||
{plans.length === 0 ? (
|
||||
<div className="mt-6 rounded-3xl border border-dashed border-[#d9cdb5] bg-[#fffaf1] p-8 text-center">
|
||||
<div className="text-4xl">🛒</div>
|
||||
<p className="mt-3 font-black text-[#183d35]">No saved market days yet.</p>
|
||||
<p className="mt-1 text-sm text-[#66877f]">Generate a plan to create the first playable management checkpoint.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 space-y-3">
|
||||
{plans.map((plan) => (
|
||||
<button
|
||||
key={plan.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedPlanId(plan.id)}
|
||||
className={`w-full rounded-3xl border p-4 text-left transition focus:outline-none focus:ring-4 focus:ring-[#2a9d8f]/15 ${selectedPlan?.id === plan.id ? 'border-[#2a9d8f] bg-[#e8fbf3]' : 'border-[#efe2ca] bg-[#fffaf1] hover:border-[#ffb55d]'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="font-black text-[#183d35]">{plan.marketName}</span>
|
||||
<span className="text-sm font-black text-[#e46b4e]">{currencyFormatter.format(plan.revenueEstimate)}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-[#66877f]">
|
||||
{focusLabels[plan.deliveryFocus]} • {weatherLabels[plan.weather]} • {plan.expectedCustomers} customers
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article className="mt-6 rounded-[2rem] border border-[#e7d9bd] bg-[#183d35] p-6 text-white shadow-2xl shadow-[#183d35]/15">
|
||||
{selectedPlan ? (
|
||||
<div className="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
|
||||
<div>
|
||||
<p className="text-sm font-black uppercase tracking-[0.24em] text-[#ffcf6d]">Selected plan detail</p>
|
||||
<h3 className="mt-3 text-3xl font-black">{selectedPlan.marketName}</h3>
|
||||
<p className="mt-3 text-white/70">
|
||||
Created {dateFormatter.format(new Date(selectedPlan.createdAt))}. Prepare for {tourismLabels[selectedPlan.tourism].toLowerCase()} with {weatherLabels[selectedPlan.weather].toLowerCase()}.
|
||||
</p>
|
||||
<div className="mt-6 grid grid-cols-2 gap-3">
|
||||
<div className="rounded-3xl bg-white/10 p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.18em] text-white/50">Customers</p>
|
||||
<p className="mt-1 text-2xl font-black">{selectedPlan.expectedCustomers}</p>
|
||||
</div>
|
||||
<div className="rounded-3xl bg-white/10 p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-[0.18em] text-white/50">Forecast</p>
|
||||
<p className="mt-1 text-2xl font-black">{currencyFormatter.format(selectedPlan.revenueEstimate)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removePlan(selectedPlan.id)}
|
||||
className="mt-5 rounded-full border border-white/20 px-5 py-3 text-sm font-bold text-white/80 transition hover:border-[#ffcf6d] hover:text-[#ffcf6d] focus:outline-none focus:ring-4 focus:ring-[#ffcf6d]/30"
|
||||
>
|
||||
Remove selected plan
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-3xl bg-white p-5 text-[#183d35]">
|
||||
<h4 className="font-black">Featured shelf set</h4>
|
||||
<div className="mt-4 grid gap-2">
|
||||
{selectedPlan.featuredProducts.map((product) => (
|
||||
<div key={product} className="rounded-2xl bg-[#fff0d0] px-4 py-3 text-sm font-bold">{product}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-3xl bg-white p-5 text-[#183d35]">
|
||||
<h4 className="font-black">Opening checklist</h4>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-6 text-[#54756d]">
|
||||
{selectedPlan.checklist.map((item) => (
|
||||
<li key={item} className="flex gap-2">
|
||||
<span className="text-[#2a9d8f]">✓</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center">
|
||||
<div className="text-5xl">🌅</div>
|
||||
<h3 className="mt-4 text-2xl font-black">Select or create a plan to inspect the playable detail view.</h3>
|
||||
<p className="mt-2 text-white/70">This is the first working loop for the Ilha Market management game.</p>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
IlhaMarketLanding.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user