diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..fcbd9b9 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -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' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -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' diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 254a0a9..c91d1a2 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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: ['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 [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(siteDefaults); + const [editorDraft, setEditorDraft] = useState(siteDefaults); + const [inquiryDraft, setInquiryDraft] = useState(inquiryDefaults); + const [inquiries, setInquiries] = useState([]); + const [selectedInquiryId, setSelectedInquiryId] = useState(''); + 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) => ( -
- -
- ); + 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 ( -
- - -
) - } + 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) => { + 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) => { + 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 ( -
+ <> - {getPageTitle('Starter Page')} + {getPageTitle('Ethopic Ethiopia Tour & Travel')} + - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

+
+
+
+
+ + +
+
+
+ {siteDraft.announcement} +
+

+ {siteDraft.headline} +

+

{siteDraft.subheadline}

+ +
+ {[ + ['48', 'years local expertise'], + ['5', 'signature routes'], + ['24h', 'reply target'], + ].map(([value, label]) => ( +
+
{value}
+
{label}
+
+ ))} +
+
+ +
+
+
+
+
+
+ Certified Ethiopian guide-led experiences +
+
+

Featured tour

+

{featuredItinerary.title}

+

{featuredItinerary.summary}

+
+ {featuredItinerary.duration} + + {featuredItinerary.price} + +
+
+
+
+
+
- - - +
+ +
+ {[ + ['Call / WhatsApp', `${CONTACT_PHONE_PRIMARY} · ${CONTACT_PHONE_SECONDARY}`], + ['Email', CONTACT_EMAIL], + ['Base', 'Born near Awash National Park · Addis Ababa departures'], + ].map(([label, value]) => ( +
+

{label}

+

{value}

+
+ ))} +
+ +
+
+
+
+

📸

+

Seyoum profile photo placeholder

+

Upload the ZIP photo to replace this in the next iteration.

+
+
+

Meet Seyoum

+

+ Professional guide, conservation advocate and lifelong host for travelers seeking Ethiopia’s true spirit. +

+
+
+

Personal biography

+
+

+ 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. +

+

+ 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. +

+

+ 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. +

+
+
+
+ +
+
+
+

Signature itineraries

+

Ready-to-customize Ethiopia routes

+
+ + Request a tailored quote + +
+ +
+ {itineraries.map((item) => ( +
+
+ {item.tag} + {item.duration} +
+

{item.title}

+

{item.summary}

+
    + {item.highlights.map((highlight) => ( +
  • + + {highlight} +
  • + ))} +
+

{item.price}

+
+ ))} +
+
+ +
+
+

Trip request workflow

+

Create a draft inquiry

+

+ Choose a route, add traveler details, then send the generated request by email or phone. +

+ +
+ + + + + + +