Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f74d84a773 |
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
|
|||||||
@ -1,166 +1,679 @@
|
|||||||
|
import React, { FormEvent, ReactElement, useEffect, useMemo, useState } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
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 LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
type Itinerary = {
|
||||||
export default function Starter() {
|
id: string;
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
title: string;
|
||||||
src: undefined,
|
duration: string;
|
||||||
photographer: undefined,
|
price: string;
|
||||||
photographer_url: undefined,
|
tag: string;
|
||||||
})
|
summary: string;
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
highlights: string[];
|
||||||
const [contentType, setContentType] = useState('video');
|
|
||||||
const [contentPosition, setContentPosition] = useState('left');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
const title = 'App Preview'
|
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
const image = await getPexelsImage();
|
|
||||||
const video = await getPexelsVideo();
|
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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 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>)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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: ['Grevy’s 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 [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('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const storedSiteDraft = window.localStorage.getItem(storageKeys.siteDraft);
|
||||||
|
const storedInquiries = window.localStorage.getItem(storageKeys.inquiries);
|
||||||
|
|
||||||
|
if (storedSiteDraft) {
|
||||||
|
const parsedSiteDraft = JSON.parse(storedSiteDraft) as SiteDraft;
|
||||||
|
setSiteDraft({ ...siteDefaults, ...parsedSiteDraft });
|
||||||
|
setEditorDraft({ ...siteDefaults, ...parsedSiteDraft });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 (
|
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>
|
<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>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<main className="min-h-screen bg-[#fff8ed] text-stone-950">
|
||||||
<div
|
<section className="relative isolate overflow-hidden bg-[#12372a] text-white">
|
||||||
className={`flex ${
|
<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%)]" />
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<div className="mx-auto flex max-w-7xl flex-col gap-10 px-6 py-6 lg:px-8 lg:py-8">
|
||||||
} min-h-screen w-full`}
|
<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="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]"
|
||||||
>
|
>
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
Build my Ethiopia trip
|
||||||
? imageBlock(illustrationImage)
|
</a>
|
||||||
: null}
|
<a
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
href={`mailto:${CONTACT_EMAIL}`}
|
||||||
? videoBlock(illustrationVideo)
|
className="rounded-full border border-white/25 px-6 py-3 text-sm font-black text-white transition hover:bg-white/10"
|
||||||
: null}
|
>
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
Email Ethopic
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
</a>
|
||||||
<CardBoxComponentTitle title="Welcome to your App Preview app!"/>
|
</div>
|
||||||
|
<dl className="mt-10 grid max-w-2xl grid-cols-3 gap-3 text-center">
|
||||||
<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>
|
['48', 'years local expertise'],
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
['5', 'signature routes'],
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
['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>
|
||||||
|
|
||||||
<BaseButtons>
|
<div className="relative">
|
||||||
<BaseButton
|
<div className="absolute -right-8 -top-8 h-40 w-40 rounded-full bg-amber-400/30 blur-3xl" />
|
||||||
href='/login'
|
<div className="relative overflow-hidden rounded-[2.5rem] border border-white/15 bg-white/12 p-4 shadow-2xl shadow-black/30 backdrop-blur">
|
||||||
label='Login'
|
<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">
|
||||||
color='info'
|
<div className="flex h-full flex-col justify-between gap-64">
|
||||||
className='w-full'
|
<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 Ethiopia’s 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 father’s 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 country’s 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>
|
||||||
|
|
||||||
</BaseButtons>
|
<div className="md:col-span-2">
|
||||||
</CardBox>
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</SectionFullScreen>
|
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
<div className="rounded-[2.5rem] bg-white p-7 shadow-sm ring-1 ring-amber-200/80">
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<div className="flex items-center justify-between gap-4">
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
<h3 className="text-2xl font-black">Recent inquiry drafts</h3>
|
||||||
Privacy Policy
|
<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>
|
</Link>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<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) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user