Compare commits

..

1 Commits

Author SHA1 Message Date
Flatlogic Bot
f74d84a773 [14:54] [prep] Generating schema
[14:54] [prep] Detecting template and stack
[14:54] [prep] Generating configuration
[14:54] [prep] Packaging project files ... done
[14:54] [net] Creating secure endpoint
[14:54] [net] Provisioning TLS certificate
[14:54] [net] Assigning subdomain
[14:54] [net] Secure endpoint configured
[14:54] [db] Initializing workspace database
[14:54] [db] Applying credentials
[14:54] [db] Running initial migrations
[14:54] [db] Workspace initialized
[14:54] [build] Preparing runtime
[14:54] [build] Installing dependencies
[14:54] [build] Services configured
[14:54] [health] Starting services
[14:54] [health] Application is up
2026-05-29 17:00:57 +00:00
3 changed files with 654 additions and 143 deletions

View File

@ -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'

View File

@ -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'

View File

@ -1,166 +1,679 @@
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 Itinerary = {
id: string;
title: string;
duration: string;
price: string;
tag: string;
summary: string;
highlights: string[];
};
type Inquiry = {
id: string;
name: string;
email: string;
phone: string;
itinerary: string;
travelers: string;
date: string;
message: string;
createdAt: string;
};
type SiteDraft = {
headline: string;
subheadline: string;
announcement: string;
featuredItinerary: string;
};
const CONTACT_PHONE_PRIMARY = '+251908850477';
const CONTACT_PHONE_SECONDARY = '+251913110497';
const CONTACT_EMAIL = 'ethopictourandtravel@gmail.com';
const siteDefaults: SiteDraft = {
headline: 'Ethopic Ethiopia Tour & Travel',
subheadline:
'Authentic, safe and sustainable journeys through Ethiopia with Seyoum — a certified guide shaped by Awash wildlife, conservation and local culture.',
announcement: 'Now planning 2026 private city tours, wildlife safaris and cultural expeditions.',
featuredItinerary: 'addis-3-day',
};
const itineraries: Itinerary[] = [
{
id: 'addis-3-day',
title: 'Addis Ababa City & Nearby Tour',
duration: '3 days',
price: 'From $60/day per person',
tag: 'City culture',
summary:
'Unity Park, National Museum, Meskel Square, Merkato, coffee ceremony and traditional Ethiopian lunch.',
highlights: ['Lucy fossil history', 'Imperial palace gardens', 'Merkato market walk'],
},
{
id: 'awash-2-day',
title: 'Awash National Park & Aledeghi',
duration: '1 night / 2 days',
price: 'Custom private quote',
tag: 'Wildlife',
summary:
'Early drive from Addis Ababa to Aledeghi Wildlife Reserve, Awash Falls, river gorge, hot springs and sunrise safari.',
highlights: ['Grevys zebra & oryx', 'Awash Falls Lodge options', '450+ bird species'],
},
{
id: 'south-omo-bale-8-day',
title: 'South Omo Valley & Bale Mountains',
duration: '8 days / 7 nights',
price: 'Budget from $1,150 sharing',
tag: 'Culture + nature',
summary:
'A photography-friendly route combining South Omo cultural encounters with Bale Mountains scenery and wildlife.',
highlights: ['4WD with driver', 'Local tribe visits', 'Park entrance fees'],
},
{
id: 'coffee-wildlife-7-day',
title: 'Coffee, Wildlife & Nature Tour',
duration: '7 days / 6 nights',
price: 'Custom private quote',
tag: 'Coffee origin',
summary:
'Addis Ababa, Jimma, Chebera Churchura, Maze, Yirgalem and Hawassa for coffee farms, forests and safaris.',
highlights: ['Coffee cooperatives', 'Elephants & buffalo', 'Yirgalem nature stays'],
},
{
id: 'north-danakil-9-day',
title: 'Northern Ethiopia & Danakil Adventure',
duration: '9 days / 8 nights',
price: 'Budget from $2,100 sharing',
tag: 'Adventure',
summary:
'Danakil Depression, Erta Ale, Axum, Gheralta, Lalibela, Simien Mountains and Bahir Dar with domestic flights recommended.',
highlights: ['Dallol colors', 'Rock-hewn churches', 'Simien gelada viewpoints'],
},
];
const inquiryDefaults = {
name: '',
email: '',
phone: '',
itinerary: itineraries[0].id,
travelers: '2',
date: '',
message: '',
};
const inputClass =
'w-full rounded-2xl border border-amber-200/80 bg-white/90 px-4 py-3 text-sm text-stone-900 shadow-sm outline-none transition focus:border-emerald-500 focus:ring-4 focus:ring-emerald-500/15';
const storageKeys = {
siteDraft: 'ethopic-site-draft',
inquiries: 'ethopic-inquiries',
};
function formatDate(value: string) {
if (!value) return 'Flexible dates';
return new Intl.DateTimeFormat('en', {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(`${value}T00:00:00`));
}
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);
const [siteDraft, setSiteDraft] = useState<SiteDraft>(siteDefaults);
const [editorDraft, setEditorDraft] = useState<SiteDraft>(siteDefaults);
const [inquiryDraft, setInquiryDraft] = useState(inquiryDefaults);
const [inquiries, setInquiries] = useState<Inquiry[]>([]);
const [selectedInquiryId, setSelectedInquiryId] = useState<string>('');
const [formError, setFormError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [publishMessage, setPublishMessage] = useState('');
const title = 'App Preview'
useEffect(() => {
try {
const storedSiteDraft = window.localStorage.getItem(storageKeys.siteDraft);
const storedInquiries = window.localStorage.getItem(storageKeys.inquiries);
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
if (storedSiteDraft) {
const parsedSiteDraft = JSON.parse(storedSiteDraft) as SiteDraft;
setSiteDraft({ ...siteDefaults, ...parsedSiteDraft });
setEditorDraft({ ...siteDefaults, ...parsedSiteDraft });
}
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>
);
if (storedInquiries) {
const parsedInquiries = JSON.parse(storedInquiries) as Inquiry[];
setInquiries(parsedInquiries);
setSelectedInquiryId(parsedInquiries[0]?.id || '');
}
} catch (error) {
console.error('Failed to load Ethopic landing state', error);
}
}, []);
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 selectedItinerary = useMemo(
() => itineraries.find((item) => item.id === inquiryDraft.itinerary) || itineraries[0],
[inquiryDraft.itinerary],
);
const featuredItinerary = useMemo(
() => itineraries.find((item) => item.id === siteDraft.featuredItinerary) || itineraries[0],
[siteDraft.featuredItinerary],
);
const selectedInquiry = useMemo(
() => inquiries.find((item) => item.id === selectedInquiryId) || inquiries[0],
[inquiries, selectedInquiryId],
);
const handlePublish = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const nextDraft = {
headline: editorDraft.headline.trim() || siteDefaults.headline,
subheadline: editorDraft.subheadline.trim() || siteDefaults.subheadline,
announcement: editorDraft.announcement.trim() || siteDefaults.announcement,
featuredItinerary: editorDraft.featuredItinerary,
};
setSiteDraft(nextDraft);
window.localStorage.setItem(storageKeys.siteDraft, JSON.stringify(nextDraft));
setPublishMessage('Published locally. The hero and featured tour updated instantly.');
};
const handleInquirySubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setFormError('');
setSuccessMessage('');
if (!inquiryDraft.name.trim() || !inquiryDraft.email.trim() || !inquiryDraft.phone.trim()) {
setFormError('Please add your name, email and phone number so Seyoum can reply.');
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inquiryDraft.email.trim())) {
setFormError('Please enter a valid email address.');
return;
}
const newInquiry: Inquiry = {
...inquiryDraft,
id: `ETH-${Date.now()}`,
name: inquiryDraft.name.trim(),
email: inquiryDraft.email.trim(),
phone: inquiryDraft.phone.trim(),
message: inquiryDraft.message.trim(),
createdAt: new Date().toISOString(),
};
const nextInquiries = [newInquiry, ...inquiries].slice(0, 6);
setInquiries(nextInquiries);
setSelectedInquiryId(newInquiry.id);
window.localStorage.setItem(storageKeys.inquiries, JSON.stringify(nextInquiries));
setSuccessMessage(`Request ${newInquiry.id} created. Use the contact buttons to send it to Ethopic.`);
setInquiryDraft(inquiryDefaults);
};
const contactBody = selectedInquiry
? `Hello Ethopic Tour and Travel,%0D%0A%0D%0AMy name is ${encodeURIComponent(
selectedInquiry.name,
)}. I am interested in ${encodeURIComponent(
itineraries.find((item) => item.id === selectedInquiry.itinerary)?.title || selectedInquiry.itinerary,
)} for ${encodeURIComponent(selectedInquiry.travelers)} traveler(s) around ${encodeURIComponent(
formatDate(selectedInquiry.date),
)}.%0D%0A%0D%0APhone: ${encodeURIComponent(selectedInquiry.phone)}%0D%0AEmail: ${encodeURIComponent(
selectedInquiry.email,
)}%0D%0AMessage: ${encodeURIComponent(selectedInquiry.message || 'Please share availability and pricing.')}`
: '';
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('Ethopic Ethiopia Tour & Travel')}</title>
<meta
name="description"
content="Ethopic Ethiopia Tour & Travel by Seyoum — Ethiopian city tours, wildlife safaris, coffee routes, Danakil adventures and sustainable cultural experiences."
/>
</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 App Preview app!"/>
<main className="min-h-screen bg-[#fff8ed] text-stone-950">
<section className="relative isolate overflow-hidden bg-[#12372a] text-white">
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,_rgba(245,158,11,0.35),_transparent_35%),radial-gradient(circle_at_bottom_right,_rgba(16,185,129,0.22),_transparent_35%)]" />
<div className="mx-auto flex max-w-7xl flex-col gap-10 px-6 py-6 lg:px-8 lg:py-8">
<nav className="flex flex-wrap items-center justify-between gap-4 rounded-full border border-white/15 bg-white/10 px-5 py-3 backdrop-blur">
<Link href="/" className="flex items-center gap-3 font-black tracking-tight">
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-[#f59e0b] text-lg text-[#12372a]">
</span>
<span>Ethopic Tour & Travel</span>
</Link>
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold">
<a href="#itineraries" className="rounded-full px-4 py-2 text-white/85 hover:bg-white/10">
Itineraries
</a>
<a href="#biography" className="rounded-full px-4 py-2 text-white/85 hover:bg-white/10">
Seyoum
</a>
<a href="#planner" className="rounded-full px-4 py-2 text-white/85 hover:bg-white/10">
Plan a trip
</a>
<Link href="/login" className="rounded-full bg-white px-5 py-2 text-[#12372a] shadow-lg shadow-black/10">
Login / Admin
</Link>
</div>
</nav>
<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>
<div className="grid items-center gap-10 py-10 lg:grid-cols-[1.08fr_0.92fr] lg:py-16">
<div>
<div className="mb-5 inline-flex rounded-full border border-amber-300/30 bg-amber-300/15 px-4 py-2 text-sm font-bold text-amber-100">
{siteDraft.announcement}
</div>
<h1 className="max-w-4xl text-5xl font-black leading-[0.95] tracking-tight md:text-7xl">
{siteDraft.headline}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-emerald-50/90 md:text-xl">{siteDraft.subheadline}</p>
<div className="mt-8 flex flex-wrap gap-3">
<a
href="#planner"
className="rounded-full bg-[#f59e0b] px-6 py-3 text-sm font-black text-[#12372a] shadow-xl shadow-amber-950/30 transition hover:-translate-y-0.5 hover:bg-[#fbbf24]"
>
Build my Ethiopia trip
</a>
<a
href={`mailto:${CONTACT_EMAIL}`}
className="rounded-full border border-white/25 px-6 py-3 text-sm font-black text-white transition hover:bg-white/10"
>
Email Ethopic
</a>
</div>
<dl className="mt-10 grid max-w-2xl grid-cols-3 gap-3 text-center">
{[
['48', 'years local expertise'],
['5', 'signature routes'],
['24h', 'reply target'],
].map(([value, label]) => (
<div key={label} className="rounded-3xl border border-white/15 bg-white/10 p-4 backdrop-blur">
<dt className="text-3xl font-black text-amber-200">{value}</dt>
<dd className="mt-1 text-xs font-semibold uppercase tracking-wide text-emerald-50/70">{label}</dd>
</div>
))}
</dl>
</div>
<div className="relative">
<div className="absolute -right-8 -top-8 h-40 w-40 rounded-full bg-amber-400/30 blur-3xl" />
<div className="relative overflow-hidden rounded-[2.5rem] border border-white/15 bg-white/12 p-4 shadow-2xl shadow-black/30 backdrop-blur">
<div className="min-h-[520px] rounded-[2rem] bg-[linear-gradient(145deg,_rgba(6,78,59,0.22),_rgba(245,158,11,0.20)),url('https://images.unsplash.com/photo-1516026672322-bc52d61a55d5?auto=format&fit=crop&w=1200&q=80')] bg-cover bg-center p-5">
<div className="flex h-full flex-col justify-between gap-64">
<div className="w-fit rounded-full bg-white/90 px-4 py-2 text-sm font-black text-[#12372a] shadow-lg">
Certified Ethiopian guide-led experiences
</div>
<div className="rounded-[1.75rem] bg-[#12372a]/90 p-5 shadow-2xl backdrop-blur">
<p className="text-xs font-black uppercase tracking-[0.3em] text-amber-200">Featured tour</p>
<h2 className="mt-3 text-3xl font-black">{featuredItinerary.title}</h2>
<p className="mt-3 text-sm leading-6 text-emerald-50/85">{featuredItinerary.summary}</p>
<div className="mt-5 flex flex-wrap gap-2">
<span className="rounded-full bg-white/10 px-3 py-1 text-xs font-bold">{featuredItinerary.duration}</span>
<span className="rounded-full bg-amber-300 px-3 py-1 text-xs font-black text-[#12372a]">
{featuredItinerary.price}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="mx-auto grid max-w-7xl gap-6 px-6 py-10 lg:grid-cols-3 lg:px-8">
{[
['Call / WhatsApp', `${CONTACT_PHONE_PRIMARY} · ${CONTACT_PHONE_SECONDARY}`],
['Email', CONTACT_EMAIL],
['Base', 'Born near Awash National Park · Addis Ababa departures'],
].map(([label, value]) => (
<div key={label} className="rounded-[2rem] border border-amber-200 bg-white p-6 shadow-sm">
<p className="text-sm font-black uppercase tracking-[0.2em] text-emerald-700">{label}</p>
<p className="mt-3 text-lg font-black text-stone-900">{value}</p>
</div>
))}
</section>
<section id="biography" className="mx-auto grid max-w-7xl gap-8 px-6 py-10 lg:grid-cols-[0.9fr_1.1fr] lg:px-8">
<div className="rounded-[2.5rem] bg-[#12372a] p-8 text-white shadow-xl">
<div className="flex h-56 items-center justify-center rounded-[2rem] border border-dashed border-amber-200/50 bg-white/10 text-center">
<div>
<p className="text-5xl">📸</p>
<p className="mt-3 text-sm font-bold text-emerald-50/80">Seyoum profile photo placeholder</p>
<p className="mt-1 text-xs text-emerald-50/60">Upload the ZIP photo to replace this in the next iteration.</p>
</div>
</div>
<h2 className="mt-8 text-4xl font-black">Meet Seyoum</h2>
<p className="mt-4 text-emerald-50/85">
Professional guide, conservation advocate and lifelong host for travelers seeking Ethiopias true spirit.
</p>
</div>
<div className="rounded-[2.5rem] bg-white p-8 shadow-sm ring-1 ring-amber-200/80">
<p className="text-sm font-black uppercase tracking-[0.25em] text-amber-600">Personal biography</p>
<div className="mt-5 space-y-4 text-lg leading-8 text-stone-700">
<p>
Seyoum was born and raised near Awash National Park, where early exposure to wildlife and conservation
shaped his passion for tourism. His fathers work at the park introduced him to nature protection and
local culture.
</p>
<p>
At 48 years old, Seyoum is professionally certified and began working as a local guide at age 12. Tourism
has been both his career and way of life from national park scout to driver-guide for tour companies and
freelance guide.
</p>
<p>
His lifelong dedication to tourism and conservation drives Ethopic to share Ethiopia through authentic,
safe and sustainable travel experiences that connect visitors with the countrys true spirit.
</p>
</div>
</div>
</section>
<section id="itineraries" className="mx-auto max-w-7xl px-6 py-12 lg:px-8">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div>
<p className="text-sm font-black uppercase tracking-[0.25em] text-emerald-700">Signature itineraries</p>
<h2 className="mt-3 text-4xl font-black tracking-tight md:text-5xl">Ready-to-customize Ethiopia routes</h2>
</div>
<a href="#planner" className="w-fit rounded-full bg-[#12372a] px-6 py-3 text-sm font-black text-white shadow-lg">
Request a tailored quote
</a>
</div>
<div className="mt-8 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{itineraries.map((item) => (
<article
key={item.id}
className="group rounded-[2rem] border border-amber-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-xl"
>
<div className="flex items-center justify-between gap-3">
<span className="rounded-full bg-emerald-100 px-3 py-1 text-xs font-black text-emerald-800">{item.tag}</span>
<span className="text-sm font-bold text-stone-500">{item.duration}</span>
</div>
<h3 className="mt-5 text-2xl font-black text-stone-950">{item.title}</h3>
<p className="mt-3 text-sm leading-6 text-stone-600">{item.summary}</p>
<ul className="mt-5 space-y-2 text-sm font-semibold text-stone-700">
{item.highlights.map((highlight) => (
<li key={highlight} className="flex gap-2">
<span className="text-emerald-600"></span>
{highlight}
</li>
))}
</ul>
<p className="mt-5 rounded-2xl bg-amber-50 px-4 py-3 text-sm font-black text-amber-800">{item.price}</p>
</article>
))}
</div>
</section>
<section id="planner" className="mx-auto grid max-w-7xl gap-8 px-6 py-12 lg:grid-cols-[1.05fr_0.95fr] lg:px-8">
<div className="rounded-[2.5rem] bg-white p-6 shadow-xl shadow-amber-950/5 ring-1 ring-amber-200/80 md:p-8">
<p className="text-sm font-black uppercase tracking-[0.25em] text-emerald-700">Trip request workflow</p>
<h2 className="mt-3 text-4xl font-black tracking-tight">Create a draft inquiry</h2>
<p className="mt-3 text-stone-600">
Choose a route, add traveler details, then send the generated request by email or phone.
</p>
<form className="mt-8 grid gap-4 md:grid-cols-2" onSubmit={handleInquirySubmit}>
<label className="text-sm font-bold text-stone-700">
Full name
<input
className={`${inputClass} mt-2`}
value={inquiryDraft.name}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, name: event.target.value })}
placeholder="Your name"
/>
</label>
<label className="text-sm font-bold text-stone-700">
Email
<input
className={`${inputClass} mt-2`}
value={inquiryDraft.email}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, email: event.target.value })}
placeholder="you@example.com"
type="email"
/>
</label>
<label className="text-sm font-bold text-stone-700">
Phone / WhatsApp
<input
className={`${inputClass} mt-2`}
value={inquiryDraft.phone}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, phone: event.target.value })}
placeholder="+251..."
/>
</label>
<label className="text-sm font-bold text-stone-700">
Preferred start date
<input
className={`${inputClass} mt-2`}
value={inquiryDraft.date}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, date: event.target.value })}
type="date"
/>
</label>
<label className="text-sm font-bold text-stone-700">
Itinerary
<select
className={`${inputClass} mt-2`}
value={inquiryDraft.itinerary}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, itinerary: event.target.value })}
>
{itineraries.map((item) => (
<option key={item.id} value={item.id}>
{item.title}
</option>
))}
</select>
</label>
<label className="text-sm font-bold text-stone-700">
Travelers
<input
className={`${inputClass} mt-2`}
min="1"
value={inquiryDraft.travelers}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, travelers: event.target.value })}
type="number"
/>
</label>
<label className="text-sm font-bold text-stone-700 md:col-span-2">
Notes
<textarea
className={`${inputClass} mt-2 min-h-28`}
value={inquiryDraft.message}
onChange={(event) => setInquiryDraft({ ...inquiryDraft, message: event.target.value })}
placeholder="Tell Seyoum your interests, hotel level, pace, dietary needs or special requests."
/>
</label>
<div className="md:col-span-2">
{formError && <p className="mb-3 rounded-2xl bg-red-50 px-4 py-3 text-sm font-bold text-red-700">{formError}</p>}
{successMessage && (
<p className="mb-3 rounded-2xl bg-emerald-50 px-4 py-3 text-sm font-bold text-emerald-700">
{successMessage}
</p>
)}
<button
type="submit"
className="w-full rounded-2xl bg-[#12372a] px-6 py-4 text-sm font-black text-white shadow-lg transition hover:-translate-y-0.5 hover:bg-[#0f2f24]"
>
Create inquiry draft
</button>
</div>
</form>
</div>
<aside className="space-y-5">
<div className="rounded-[2.5rem] bg-[#12372a] p-7 text-white shadow-xl">
<p className="text-sm font-black uppercase tracking-[0.25em] text-amber-200">Selected route</p>
<h3 className="mt-4 text-3xl font-black">{selectedItinerary.title}</h3>
<p className="mt-3 text-sm leading-6 text-emerald-50/85">{selectedItinerary.summary}</p>
<div className="mt-5 flex flex-wrap gap-2">
<span className="rounded-full bg-white/10 px-3 py-1 text-xs font-bold">{selectedItinerary.duration}</span>
<span className="rounded-full bg-amber-300 px-3 py-1 text-xs font-black text-[#12372a]">
{selectedItinerary.price}
</span>
</div>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
<div className="rounded-[2.5rem] bg-white p-7 shadow-sm ring-1 ring-amber-200/80">
<div className="flex items-center justify-between gap-4">
<h3 className="text-2xl font-black">Recent inquiry drafts</h3>
<span className="rounded-full bg-amber-100 px-3 py-1 text-xs font-black text-amber-800">{inquiries.length}</span>
</div>
{inquiries.length === 0 ? (
<div className="mt-5 rounded-3xl border border-dashed border-amber-300 bg-amber-50/70 p-6 text-sm text-stone-600">
No draft inquiries yet. Submit the planner form to create the first one.
</div>
) : (
<div className="mt-5 grid gap-3">
{inquiries.map((item) => (
<button
key={item.id}
type="button"
onClick={() => setSelectedInquiryId(item.id)}
className={`rounded-2xl border p-4 text-left transition ${
selectedInquiry?.id === item.id
? 'border-emerald-600 bg-emerald-50'
: 'border-amber-200 bg-white hover:bg-amber-50'
}`}
>
<p className="text-sm font-black text-stone-950">{item.name}</p>
<p className="mt-1 text-xs text-stone-500">
{itineraries.find((tour) => tour.id === item.itinerary)?.title} · {formatDate(item.date)}
</p>
</button>
))}
</div>
)}
{selectedInquiry && (
<div className="mt-5 rounded-3xl bg-stone-950 p-5 text-white">
<p className="text-xs font-black uppercase tracking-[0.25em] text-amber-200">{selectedInquiry.id}</p>
<h4 className="mt-2 text-xl font-black">{selectedInquiry.name}</h4>
<p className="mt-2 text-sm text-stone-300">
{selectedInquiry.travelers} traveler(s) · {formatDate(selectedInquiry.date)}
</p>
<p className="mt-4 text-sm leading-6 text-stone-200">
{selectedInquiry.message || 'No notes added yet.'}
</p>
<div className="mt-5 grid gap-2 sm:grid-cols-2">
<a
href={`mailto:${CONTACT_EMAIL}?subject=Ethopic inquiry ${selectedInquiry.id}&body=${contactBody}`}
className="rounded-2xl bg-amber-300 px-4 py-3 text-center text-sm font-black text-stone-950"
>
Send email
</a>
<a
href={`tel:${CONTACT_PHONE_PRIMARY}`}
className="rounded-2xl border border-white/20 px-4 py-3 text-center text-sm font-black text-white"
>
Call now
</a>
</div>
</div>
)}
</div>
</aside>
</section>
<section id="publish" className="mx-auto max-w-7xl px-6 py-12 lg:px-8">
<div className="grid gap-8 rounded-[2.5rem] bg-stone-950 p-6 text-white shadow-2xl md:p-8 lg:grid-cols-[0.9fr_1.1fr]">
<div>
<p className="text-sm font-black uppercase tracking-[0.25em] text-amber-200">Editable website MVP</p>
<h2 className="mt-3 text-4xl font-black tracking-tight">Quick publish editor</h2>
<p className="mt-4 leading-7 text-stone-300">
This first iteration lets the site owner adjust the hero message and featured itinerary, then publish the
update instantly in this browser. The admin login remains available for the full back office.
</p>
<Link
href="/login"
className="mt-6 inline-flex rounded-full bg-white px-6 py-3 text-sm font-black text-stone-950"
>
Open login system
</Link>
</div>
<form className="grid gap-4 rounded-[2rem] bg-white p-5 text-stone-950" onSubmit={handlePublish}>
<label className="text-sm font-bold text-stone-700">
Hero headline
<input
className={`${inputClass} mt-2`}
value={editorDraft.headline}
onChange={(event) => setEditorDraft({ ...editorDraft, headline: event.target.value })}
/>
</label>
<label className="text-sm font-bold text-stone-700">
Hero subheadline
<textarea
className={`${inputClass} mt-2 min-h-24`}
value={editorDraft.subheadline}
onChange={(event) => setEditorDraft({ ...editorDraft, subheadline: event.target.value })}
/>
</label>
<label className="text-sm font-bold text-stone-700">
Announcement pill
<input
className={`${inputClass} mt-2`}
value={editorDraft.announcement}
onChange={(event) => setEditorDraft({ ...editorDraft, announcement: event.target.value })}
/>
</label>
<label className="text-sm font-bold text-stone-700">
Featured itinerary
<select
className={`${inputClass} mt-2`}
value={editorDraft.featuredItinerary}
onChange={(event) => setEditorDraft({ ...editorDraft, featuredItinerary: event.target.value })}
>
{itineraries.map((item) => (
<option key={item.id} value={item.id}>
{item.title}
</option>
))}
</select>
</label>
{publishMessage && (
<p className="rounded-2xl bg-emerald-50 px-4 py-3 text-sm font-bold text-emerald-700">{publishMessage}</p>
)}
<button
type="submit"
className="rounded-2xl bg-[#f59e0b] px-6 py-4 text-sm font-black text-stone-950 shadow-lg transition hover:-translate-y-0.5"
>
Publish landing update
</button>
</form>
</div>
</section>
</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>
<footer className="border-t border-amber-200 bg-white px-6 py-8">
<div className="mx-auto flex max-w-7xl flex-col gap-4 text-sm text-stone-600 md:flex-row md:items-center md:justify-between">
<p>© 2026 Ethopic Ethiopia Tour & Travel. Built for authentic Ethiopian journeys.</p>
<div className="flex flex-wrap gap-4 font-bold text-stone-900">
<a href={`mailto:${CONTACT_EMAIL}`}>{CONTACT_EMAIL}</a>
<a href={`tel:${CONTACT_PHONE_PRIMARY}`}>{CONTACT_PHONE_PRIMARY}</a>
<Link href="/login">Admin login</Link>
</div>
</div>
</footer>
</main>
</>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};