Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
import React, { useMemo, useState, useEffect } from "react";
// --------- Utilities
const classNames = (...c) => c.filter(Boolean).join(" ");
// Central place to manage affiliate/UGC link attributes
function LinkWrapper({ href, children, className, title }) {
return (
<a
href={href}
title={title || "Open link"}
target="_blank"
rel="nofollow sponsored noopener"
data-tracking="affiliate"
className={className}
>
{children}
</a>
);
}
// Sample data — replace with CMS/API later
const SAMPLE_DESTINATIONS = [
{
id: "lisbon",
name: "Lisbon, Portugal",
blurb: "Azulejos, pasteis de nata, and coastal sunsets.",
tag: "City Break",
rating: 4.8,
image:
"https://images.unsplash.com/photo-1505764706515-aa95265c5abc?q=80&w=1600&auto=format&fit=crop",
},
{
id: "kyoto",
name: "Kyoto, Japan",
blurb: "Temples, tea houses, and tranquil lanes.",
tag: "Culture",
rating: 4.9,
image:
"https://images.unsplash.com/photo-1545569341-9eb8b30979d0?q=80&w=1600&auto=format&fit=crop",
},
{
id: "banff",
name: "Banff, Canada",
blurb: "Turquoise lakes and mountain trails.",
tag: "Outdoors",
rating: 4.7,
image:
"https://images.unsplash.com/photo-1508261303786-0eaf2bd8f887?q=80&w=1600&auto=format&fit=crop",
},
{
id: "bali",
name: "Bali, Indonesia",
blurb: "Rice terraces, surf, and spa days.",
tag: "Relax",
rating: 4.6,
image:
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop",
},
];
const SAMPLE_DEALS = [
{
id: "dl-1",
title: "NYC → Lisbon (Roundtrip)",
provider: "Skyscanner",
price: 379,
expires: "2025-12-01",
href: "https://www.skyscanner.net/",
badge: "Flight",
},
{
id: "dl-2",
title: "Kyoto Ryokan 30% Off",
provider: "Booking.com",
price: 112,
expires: "2025-11-30",
href: "https://www.booking.com/",
badge: "Stay",
},
{
id: "dl-3",
title: "Banff Adventure Pass",
provider: "GetYourGuide",
price: 69,
expires: "2026-01-10",
href: "https://www.getyourguide.com/",
badge: "Activity",
},
{
id: "dl-4",
title: "Bali Spa Day + Transfer",
provider: "Klook",
price: 42,
expires: "2025-12-31",
href: "https://www.klook.com/",
badge: "Experience",
},
];
const SAMPLE_UGC = [
{
id: "ugc-1",
creator: "@roamswithrae",
title: "48 hours in Lisbon",
platform: "YouTube",
href: "https://www.youtube.com/",
thumb:
"https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?q=80&w=1600&auto=format&fit=crop",
},
{
id: "ugc-2",
creator: "@packslight",
title: "Solo in Kyoto on a budget",
platform: "TikTok",
href: "https://www.tiktok.com/",
thumb:
"https://images.unsplash.com/photo-1526483360412-f4dbaf036963?q=80&w=1600&auto=format&fit=crop",
},
{
id: "ugc-3",
creator: "@tripswithty",
title: "Banff best sunrise spots",
platform: "Instagram",
href: "https://www.instagram.com/",
thumb:
"https://images.unsplash.com/photo-1482192505345-5655af888cc4?q=80&w=1600&auto=format&fit=crop",
},
];
// --- Instagram integration (profile + post embeds)
const INSTAGRAM_PROFILE = "https://www.instagram.com/kkozic/";
const INSTAGRAM_SEED_POSTS = [
// e.g., "https://www.instagram.com/p/POST_ID/",
];
// --------- Main App
export default function TravelHub() {
const [query, setQuery] = useState("");
const [priceSort, setPriceSort] = useState("asc");
const [newsletterEmail, setNewsletterEmail] = useState("");
// Instagram state (persisted for preview)
const [igPosts, setIgPosts] = useState(() => {
try {
const saved = localStorage.getItem("travelhub_ig_posts");
return saved ? JSON.parse(saved) : INSTAGRAM_SEED_POSTS;
} catch {
return INSTAGRAM_SEED_POSTS;
}
});
const [igInput, setIgInput] = useState("");
// Load Instagram embed script once
useEffect(() => {
const id = "instagram-embed-script";
if (!document.getElementById(id)) {
const s = document.createElement("script");
s.id = id;
s.async = true;
s.src = "https://www.instagram.com/embed.js";
document.body.appendChild(s);
}
}, []);
// Reparse embeds when igPosts changes
useEffect(() => {
try { localStorage.setItem("travelhub_ig_posts", JSON.stringify(igPosts)); } catch {}
const t = setTimeout(() => {
if (window.instgrm && window.instgrm.Embeds) { window.instgrm.Embeds.process(); }
}, 100);
return () => clearTimeout(t);
}, [igPosts]);
const filteredDestinations = useMemo(() => {
const q = query.toLowerCase();
return SAMPLE_DESTINATIONS.filter(
(d) => d.name.toLowerCase().includes(q) || d.tag.toLowerCase().includes(q)
);
}, [query]);
const sortedDeals = useMemo(() => {
const deals = [...SAMPLE_DEALS];
deals.sort((a, b) => (priceSort === "asc" ? a.price - b.price : b.price - a.price));
return deals;
}, [priceSort]);
// Basic JSON-LD for SEO
useEffect(() => {
const script = document.createElement("script");
script.type = "application/ld+json";
script.text = JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
name: "TravelHub",
url: "https://travelhub.example",
potentialAction: {
"@type": "SearchAction",
target: "https://travelhub.example/?q={search_term_string}",
"query-input": "required name=search_term_string",
},
});
document.head.appendChild(script);
return () => document.head.removeChild(script);
}, []);
function onSubscribe(e) {
e.preventDefault();
alert(`Thanks! We'll send deals to: ${newsletterEmail}`);
setNewsletterEmail("");
}
return (
<div className="min-h-screen bg-gradient-to-b from-sky-50 to-white text-slate-800">
{/* Top disclosure */}
<div className="w-full bg-amber-50 text-amber-900 text-sm px-4 py-2 text-center">
We may earn a commission when you buy through links on our site. <span className="font-medium">As an Amazon Associate, we earn from qualifying purchases.</span>
</div>
{/* Nav */}
<header className="backdrop-blur sticky top-0 z-40 bg-white/70 border-b border-slate-200">
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
<a href="#home" className="flex items-center gap-2 font-bold text-xl">
<span className="inline-block h-8 w-8 rounded-2xl bg-sky-500" /> TravelHub
</a>
<nav className="hidden md:flex gap-6 text-sm">
<a href="#experiences" className="hover:text-sky-700">Experiences</a>
<a href="#deals" className="hover:text-sky-700">Deals</a>
<a href="#ugc" className="hover:text-sky-700">Community</a>
<a href="#newsletter" className="hover:text-sky-700">Newsletter</a>
<a href="#insta" className="hover:text-sky-700">My Instagram</a>
</nav>
<a href="#submit" className="rounded-xl bg-slate-900 text-white px-3 py-2 text-sm hover:bg-slate-700">Submit UGC</a>
</div>
</header>
{/* Hero */}
<section id="home" className="relative overflow-hidden">
<div className="absolute inset-0 -z-10">
<img
src="https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?q=80&w=2400&auto=format&fit=crop"
alt="Ocean cliffs"
className="w-full h-full object-cover opacity-80"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/40 to-white/0" />
</div>
<div className="max-w-7xl mx-auto px-4 py-24">
<div className="max-w-2xl">
<h1 className="text-4xl md:text-6xl font-extrabold text-white drop-shadow">
Discover, Deal, Depart ✈️
</h1>
<p className="mt-4 text-white/90 text-lg">
Real traveler stories, hand-picked deals, and a friendly community that helps you go farther for less.
</p>
<div className="mt-6 flex items-center gap-3">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search destinations, e.g. Kyoto"
className="w-full max-w-md rounded-2xl border border-white/60 bg-white/90 px-4 py-3 shadow placeholder-slate-500 focus:outline-none"
/>
<a href="#deals" className="rounded-2xl bg-sky-500 text-white px-5 py-3 font-medium hover:bg-sky-600">
Find Deals
</a>
</div>
</div>
</div>
</section>
{/* Experiences */}
<section id="experiences" className="max-w-7xl mx-auto px-4 py-14">
<div className="flex items-end justify-between gap-4 mb-6">
<div>
<h2 className="text-2xl md:text-3xl font-bold">Fresh Travel Experiences</h2>
<p className="text-slate-600">Mini-guides from our community and team.</p>
</div>
<a href="#submit" className="text-sky-700 hover:underline">Share yours →</a>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{filteredDestinations.map((d) => (
<article key={d.id} className="group rounded-2xl overflow-hidden bg-white shadow-sm hover:shadow-md transition">
<div className="relative h-44">
<img src={d.image} alt={d.name} className="w-full h-full object-cover" />
<span className="absolute top-3 left-3 text-xs bg-white/90 rounded-full px-2 py-1 border">
{d.tag}
</span>
<span className="absolute top-3 right-3 text-xs bg-black/80 text-white rounded-full px-2 py-1">
★ {d.rating}
</span>
</div>
<div className="p-4">
<h3 className="font-semibold">{d.name}</h3>
<p className="text-sm text-slate-600 mt-1">{d.blurb}</p>
<div className="mt-3 flex items-center gap-2">
<a href={`/#guides/${d.id}`} className="text-sm text-sky-700 hover:underline">Read guide</a>
<span className="text-slate-300">•</span>
<LinkWrapper href="https://www.amazon.com/" className="text-sm text-sky-700 hover:underline" title="Amazon gear list">
Packing list
</LinkWrapper>
</div>
</div>
</article>
))}
</div>
</section>
{/* Deals */}
<section id="deals" className="max-w-7xl mx-auto px-4 py-14">
<div className="flex items-end justify-between gap-4 mb-6">
<div>
<h2 className="text-2xl md:text-3xl font-bold">Latest Travel Deals</h2>
<p className="text-slate-600">Flights, stays & activities with affiliate-safe links.</p>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-slate-600">Sort by price</label>
<select
className="rounded-xl border px-3 py-2 bg-white"
value={priceSort}
onChange={(e) => setPriceSort(e.target.value)}
>
<option value="asc">Low → High</option>
<option value="desc">High → Low</option>
</select>
</div>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{sortedDeals.map((d) => (
<article key={d.id} className="rounded-2xl overflow-hidden bg-white shadow-sm border hover:shadow-md transition flex flex-col">
<div className="p-4">
<div className="flex items-center justify-between">
<span className="text-xs bg-sky-50 text-sky-800 border border-sky-200 rounded-full px-2 py-1">{d.badge}</span>
<span className="text-xs text-slate-500">Expires {new Date(d.expires).toLocaleDateString()}</span>
</div>
<h3 className="mt-3 font-semibold leading-tight">{d.title}</h3>
<p className="text-sm text-slate-600">via {d.provider}</p>
<p className="mt-2 text-2xl font-extrabold">${d.price}<span className="text-sm font-medium text-slate-500">/person</span></p>
</div>
<div className="mt-auto p-4 pt-0">
<LinkWrapper href={d.href} className="w-full inline-flex items-center justify-center rounded-xl bg-slate-900 text-white px-4 py-2 font-medium hover:bg-slate-700">
Book this deal
</LinkWrapper>
<p className="mt-2 text-[11px] text-slate-500">
Affiliate disclosure: purchases may earn us a commission at no cost to you.
</p>
</div>
</article>
))}
</div>
</section>
{/* Community UGC */}
<section id="ugc" className="max-w-7xl mx-auto px-4 py-14">
<div className="flex items-end justify-between gap-4 mb-6">
<div>
<h2 className="text-2xl md:text-3xl font-bold">Community Picks (UGC)</h2>
<p className="text-slate-600">Curated videos and posts from real travelers.</p>
</div>
<a href="#submit" className="text-sky-700 hover:underline">Submit your link →</a>
</div>
<div className="grid md:grid-cols-3 gap-6">
{SAMPLE_UGC.map((u) => (
<article key={u.id} className="rounded-2xl overflow-hidden bg-white shadow-sm hover:shadow-md transition">
<div className="relative h-44">
<img src={u.thumb} alt={u.title} className="w-full h-full object-cover" />
<span className="absolute top-3 left-3 text-xs bg-black/80 text-white rounded-full px-2 py-1">
{u.platform}
</span>
</div>
<div className="p-4">
<h3 className="font-semibold leading-tight">{u.title}</h3>
<p className="text-sm text-slate-600">by {u.creator}</p>
<LinkWrapper href={u.href} className="inline-flex items-center gap-2 text-sm text-sky-700 hover:underline mt-2">
Watch post ↗
</LinkWrapper>
</div>
</article>
))}
</div>
<p className="mt-4 text-xs text-slate-500">All UGC is credited to creators and used under platform ToS. We do not host third‑party content.</p>
</section>
{/* Instagram personal section */}
<section id="insta" className="max-w-7xl mx-auto px-4 py-14">
<div className="flex items-end justify-between gap-4 mb-6">
<div>
<h2 className="text-2xl md:text-3xl font-bold">From My Instagram</h2>
<p className="text-slate-600">Hand-picked moments from <a className="text-sky-700 hover:underline" href={INSTAGRAM_PROFILE} target="_blank" rel="noopener">@kkozic</a>. Paste post links to feature them.</p>
</div>
<a href={INSTAGRAM_PROFILE} target="_blank" rel="noopener" className="text-sky-700 hover:underline">Open profile ↗</a>
</div>
{/* Add post UI */}
<form
className="mb-6 grid md:grid-cols-[1fr_auto] gap-3"
onSubmit={(e) => {
e.preventDefault();
if (!igInput) return;
setIgPosts((prev) => Array.from(new Set([igInput.trim(), ...prev])));
setIgInput("");
}}
>
<input
value={igInput}
onChange={(e) => setIgInput(e.target.value)}
placeholder="Paste an Instagram post URL (e.g., https://www.instagram.com/p/POST_ID/)"
className="rounded-xl border px-4 py-3"
/>
<button className="rounded-xl bg-slate-900 text-white px-5 py-3 font-medium hover:bg-slate-700">Add Post</button>
</form>
{/* Embeds */}
{igPosts.length === 0 ? (
<div className="text-slate-500 text-sm">No posts added yet. Paste a post URL above to feature it here.</div>
) : (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{igPosts.map((url) => (
<article key={url} className="bg-white rounded-2xl shadow-sm border overflow-hidden p-2">
<blockquote className="instagram-media" data-instgrm-permalink={url} data-instgrm-version="14" style={{ background: "#FFF", border: 0, margin: 0, padding: 0 }} />
<div className="px-2 pb-3 pt-2 flex items-center justify-between text-sm">
<a href={url} target="_blank" rel="noopener" className="text-sky-700 hover:underline">Open on Instagram</a>
<button
className="text-xs text-slate-500 hover:text-red-600"
onClick={() => setIgPosts((prev) => prev.filter((p) => p !== url))}
>
Remove
</button>
</div>
</article>
))}
</div>
)}
<p className="mt-4 text-xs text-slate-500">Embeds are loaded via Instagram’s official script. For automation later, we can use the Instagram Basic Display API.</p>
</section>
{/* Submit UGC */}
<section id="submit" className="max-w-3xl mx-auto px-4 py-14">
<div className="rounded-2xl border bg-white p-6 shadow-sm">
<h2 className="text-2xl font-bold">Submit your travel link</h2>
<p className="text-slate-600">Share a YouTube, TikTok, Instagram, or blog link. We’ll review for quality and relevance.</p>
<form
className="mt-6 grid gap-4"
onSubmit={(e) => {
e.preventDefault();
alert("Thanks! We'll review your link and be in touch.");
}}
>
<div className="grid md:grid-cols-2 gap-4">
<input required placeholder="Your name" className="rounded-xl border px-4 py-3" />
<input required type="email" placeholder="Email" className="rounded-xl border px-4 py-3" />
</div>
<input required placeholder="UGC URL (YouTube/TikTok/Blog)" className="rounded-xl border px-4 py-3" />
<textarea placeholder="Optional notes (what's inside)" className="rounded-xl border px-4 py-3 min-h-28" />
<button className="rounded-xl bg-sky-600 text-white px-5 py-3 font-medium hover:bg-sky-700 w-full md:w-auto">
Send for review
</button>
<p className="text-xs text-slate-500">By submitting, you confirm you have rights to share this link and agree to our community guidelines.</p>
</form>
</div>
</section>
{/* Newsletter */}
<section id="newsletter" className="max-w-3xl mx-auto px-4 pb-20">
<div className="rounded-2xl border bg-white p-6 shadow-sm">
<h2 className="text-2xl font-bold">Get 1–2 weekly deals</h2>
<p className="text-slate-600">We send only great fares and finds. No spam.</p>
<form onSubmit={onSubscribe} className="mt-4 flex gap-3">
<input
required
type="email"
value={newsletterEmail}
onChange={(e) => setNewsletterEmail(e.target.value)}
placeholder="you@traveler.com"
className="flex-1 rounded-xl border px-4 py-3"
/>
<button className="rounded-xl bg-slate-900 text-white px-5 py-3 font-medium hover:bg-slate-700">
Subscribe
</button>
</form>
</div>
</section>
{/* Footer */}
<footer className="border-t bg-white">
<div className="max-w-7xl mx-auto px-4 py-10 grid md:grid-cols-4 gap-6 text-sm">
<div>
<div className="flex items-center gap-2 font-bold text-lg"><span className="inline-block h-6 w-6 rounded-xl bg-sky-500" /> TravelHub</div>
<p className="mt-2 text-slate-600">Stories, deals, and community for people who love smart travel.</p>
</div>
<div>
<div className="font-semibold mb-2">Explore</div>
<ul className="space-y-1">
<li><a href="#experiences" className="hover:underline">Experiences</a></li>
<li><a href="#deals" className="hover:underline">Deals</a></li>
<li><a href="#ugc" className="hover:underline">Community</a></li>
</ul>
</div>
<div>
<div className="font-semibold mb-2">Monetization</div>
<ul className="space-y-1">
<li>Amazon Associates ready</li>
<li>Affiliate-safe link wrapper</li>
<li>Sponsored posts supported</li>
</ul>
</div>
<div>
<div className="font-semibold mb-2">Legal</div>
<ul className="space-y-1">
<li><a href="#" className="hover:underline">Disclosure</a></li>
<li><a href="#" className="hover:underline">Community Guidelines</a></li>
<li><a href="#" className="hover:underline">Privacy</a></li>
</ul>
</div>
</div>
<div className="text-center text-xs text-slate-500 pb-8">© {new Date().getFullYear()} TravelHub. All rights reserved.</div>
</footer>
{/* Floating submit CTA */}
<a
href="#submit"
className="fixed bottom-6 right-6 rounded-full bg-sky-600 text-white px-5 py-3 shadow-xl hover:bg-sky-700"
>
Submit your UGC
</a>
</div>
);
}