Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/icons/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 34 additions & 34 deletions src/app/(map)/map/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
"use client";

import { useState } from "react";
import { MapView } from "@/components/map/MapView";
import { useTranslations } from "next-intl";
import { MapView, type MapDestinationClick } from "@/components/map/MapView";
import { MapHeader } from "@/components/map/MapHeader";
import { MapDisclaimer } from "@/components/map/MapDisclaimer";
import { DestinationSheet } from "@/components/map/DestinationSheet";
import { TimeSlider } from "@/components/controls/TimeSlider";
import { YearSelector } from "@/components/controls/YearSelector";
import { HolidayPanel } from "@/components/controls/HolidayPanel";
import { Legend } from "@/components/controls/Legend";
import { Sheet } from "@/components/ui/Sheet";
import { useUrlSync } from "@/hooks/useUrlSync";

export default function MapPage() {
useUrlSync();
const t = useTranslations("map");
const [isPanelOpen, setIsPanelOpen] = useState(false);
const [mobileSheetOpen, setMobileSheetOpen] = useState(false);
const [selectedDestination, setSelectedDestination] = useState<MapDestinationClick | null>(null);

function handleTogglePanel() {
setIsPanelOpen((v) => !v);
Expand All @@ -31,13 +36,13 @@ export default function MapPage() {
<div className="flex min-h-0 flex-1">
<div className="relative min-h-0 flex-1">
<div className="absolute inset-0">
<MapView />
<MapView onDestinationClick={setSelectedDestination} />
</div>
</div>

{/* Desktop sidebar — always mounted, animated width */}
<aside
className={`hidden shrink-0 overflow-hidden border-l border-gray-200 bg-white transition-[width] duration-300 ease-in-out md:block ${
className={`hidden shrink-0 overflow-hidden border-l border-line bg-surface-raised transition-[width] duration-300 ease-in-out md:block ${
isPanelOpen ? "w-80" : "w-0 border-l-0"
}`}
>
Expand All @@ -47,49 +52,44 @@ export default function MapPage() {
</aside>
</div>

{/* Footer */}
<footer className="z-10 border-t border-gray-200 bg-white px-4 py-3">
{/* Bottom control dock */}
<footer className="z-10 border-t border-line bg-surface px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2.5 sm:px-4 sm:py-3">
{/* Mobile: year selector + legend side by side */}
<div className="mb-2 flex items-center justify-between gap-3 md:hidden">
<YearSelector className="flex shrink-0 gap-1 rounded-lg bg-gray-100 p-1" />
<Legend className="min-w-0 flex-1 rounded-lg bg-gray-50 px-3 py-2" />
<YearSelector className="flex shrink-0 gap-1 rounded-[3px] border border-line bg-surface-sunken p-1" />
<Legend className="min-w-0 flex-1 rounded-[3px] border border-line bg-surface-sunken px-3 py-1.5" />
</div>

<div className="flex items-end gap-3">
<div className="flex-1">
<div className="min-w-0 flex-1">
<TimeSlider className="px-1" />
</div>
<div className="hidden w-44 shrink-0 md:block">
<Legend className="rounded-lg bg-gray-50 px-3 py-2" />
<div className="hidden w-44 shrink-0 pb-1 md:block">
<Legend className="rounded-[3px] border border-line bg-surface-sunken px-3 py-2" />
</div>
</div>
</footer>

{/* Destination details sheet (opened by tapping a map dot) */}
<DestinationSheet
selected={selectedDestination}
onClose={() => setSelectedDestination(null)}
/>

{/* Mobile bottom sheet for HolidayPanel */}
{mobileSheetOpen && (
<div className="fixed inset-0 z-50 md:hidden">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40"
onClick={() => setMobileSheetOpen(false)}
/>
{/* Sheet */}
<div className="absolute bottom-0 left-0 right-0 max-h-[70vh] overflow-y-auto rounded-t-2xl bg-white p-4 shadow-2xl">
<div className="mb-3 flex items-center justify-between">
<div className="mx-auto h-1 w-10 rounded-full bg-gray-300" />
</div>
<button
onClick={() => setMobileSheetOpen(false)}
className="absolute right-3 top-3 rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<HolidayPanel className="p-0" />
</div>
</div>
)}
<div className="md:hidden">
<Sheet
open={mobileSheetOpen}
onClose={() => {
setMobileSheetOpen(false);
setIsPanelOpen(false);
}}
title={t("holidays")}
closeLabel={t("close")}
>
<HolidayPanel className="max-h-none p-0 pb-4" />
</Sheet>
</div>
</>
);
}
42 changes: 42 additions & 0 deletions src/app/(marketing)/best-time/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { getLocale, getTranslations } from "next-intl/server";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { BestTimeFinder } from "@/components/best-time/BestTimeFinder";

export async function generateMetadata(): Promise<Metadata> {
const locale = await getLocale();
const t = await getTranslations({ locale, namespace: "bestTime" });
return {
title: t("metaTitle"),
description: t("metaDescription"),
};
}

export default async function BestTimePage() {
const t = await getTranslations("bestTime");

return (
<>
<SiteHeader />
<main className="min-h-screen bg-atlas-field">
<div className="mx-auto max-w-4xl px-4 py-12 sm:px-6 sm:py-16">
<SectionHeading
eyebrow={<span className="inline-flex items-center gap-2">✦ {t("eyebrow")}</span>}
title={t("title")}
lede={t("lede")}
className="animate-on-scroll"
/>
<div className="animate-on-scroll-delay-1 mt-10">
<Suspense>
<BestTimeFinder />
</Suspense>
</div>
</div>
</main>
<SiteFooter />
</>
);
}
43 changes: 43 additions & 0 deletions src/app/(marketing)/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { getLocale, getTranslations } from "next-intl/server";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { CompareView } from "@/components/compare/CompareView";

export async function generateMetadata(): Promise<Metadata> {
const locale = await getLocale();
const t = await getTranslations({ locale, namespace: "compare" });
return {
title: t("metaTitle"),
// Content is driven by the visitor's saved places — not indexable.
robots: { index: false },
};
}

export default async function ComparePage() {
const t = await getTranslations("compare");

return (
<>
<SiteHeader />
<main className="min-h-screen bg-atlas-field">
<div className="mx-auto max-w-4xl px-4 py-12 sm:px-6 sm:py-16">
<SectionHeading
eyebrow={<span>⇄ {t("eyebrow")}</span>}
title={t("title")}
lede={t("lede")}
className="animate-on-scroll"
/>
<div className="animate-on-scroll-delay-1 mt-10">
<Suspense>
<CompareView />
</Suspense>
</div>
</div>
</main>
<SiteFooter />
</>
);
}
39 changes: 27 additions & 12 deletions src/app/(marketing)/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { getTranslations } from "next-intl/server";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
import { ContactForm } from "@/components/contact/ContactForm";
import { Card } from "@/components/ui/Card";
import { StampFrame } from "@/components/ui/Ornaments";

export const metadata: Metadata = {
title: "Contact — PackedPlaces.com",
description:
"Get in touch with the PackedPlaces.com team — questions, feedback, and suggestions welcome.",
robots: { index: false, follow: false },
};
export async function generateMetadata(): Promise<Metadata> {
const c = await getTranslations("contact");
return {
title: c("metaTitle"),
description: c("metaDescription"),
robots: { index: false, follow: false },
};
}

export default async function ContactPage() {
const c = await getTranslations("contact");
Expand All @@ -18,15 +22,26 @@ export default async function ContactPage() {
<>
<SiteHeader />

<section className="bg-sand-50 py-16">
<div className="mx-auto max-w-2xl px-6">
<h1 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
<section className="bg-atlas-field py-16 sm:py-20">
<div className="mx-auto max-w-5xl px-6">
<h1 className="font-display text-4xl leading-[1.08] text-ink sm:text-5xl">
{c("title")}
</h1>
<p className="mt-4 text-gray-600">{c("subtitle")}</p>
<div aria-hidden className="mt-5 h-px w-16 bg-accent/70" />

<div className="mt-10 grid gap-10 md:grid-cols-[1fr_20rem] md:gap-14">
<div>
<ContactForm />
</div>

<div className="mt-10">
<ContactForm />
<aside>
<Card className="p-6">
<StampFrame className="h-14 w-14 text-accent" />
<p className="mt-5 text-sm leading-relaxed text-ink-muted">
{c("subtitle")}
</p>
</Card>
</aside>
</div>
</div>
</section>
Expand Down
Loading
Loading