Add About and Pricing pages with footer links
This commit is contained in:
parent
4d5c7eedf8
commit
2a835d9875
8 changed files with 183 additions and 57 deletions
6
AGENTS.md
Normal file
6
AGENTS.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Agent kickoff notes
|
||||||
|
|
||||||
|
- Assume full access to network, shell commands, and all files under the current working directory.
|
||||||
|
- On session start, open `PROGRESS.md` to refresh project status.
|
||||||
|
- After that, scan the repo structure (e.g., list key dirs/files) to regain context before continuing work.
|
||||||
|
- After finishing each new feature, create a git commit with a sensible message and update documentation to reflect the changes.
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
- Listing creation amenities UI improved with toggle cards and EV button group.
|
- Listing creation amenities UI improved with toggle cards and EV button group.
|
||||||
- Mermaid docs fixed: all sequence diagrams declare their participants and avoid “->” inside message text; the listing creation diagram message was rewritten to prevent parse errors. Use mermaid.live or browser console to debug future syntax issues (errors flag the offending line/column).
|
- Mermaid docs fixed: all sequence diagrams declare their participants and avoid “->” inside message text; the listing creation diagram message was rewritten to prevent parse errors. Use mermaid.live or browser console to debug future syntax issues (errors flag the offending line/column).
|
||||||
- New amenities added: kitchen, dishwasher, washing machine, barbecue; API/UI/i18n updated and seeds randomized to populate missing prices/amenities. Prisma migration `20250210_more_amenities` applied to shared DB; registry pull secret added to k8s Deployment to avoid image pull errors in prod.
|
- New amenities added: kitchen, dishwasher, washing machine, barbecue; API/UI/i18n updated and seeds randomized to populate missing prices/amenities. Prisma migration `20250210_more_amenities` applied to shared DB; registry pull secret added to k8s Deployment to avoid image pull errors in prod.
|
||||||
|
- Added About and Pricing pages (FI/EN), moved highlights/runtime config to About, and linked footer navigation.
|
||||||
|
|
||||||
To resume:
|
To resume:
|
||||||
1) If desired, render diagrams locally: PlantUML in `docs/plantuml`, draw.io in `docs/drawio`.
|
1) If desired, render diagrams locally: PlantUML in `docs/plantuml`, draw.io in `docs/drawio`.
|
||||||
|
|
|
||||||
58
app/about/page.tsx
Normal file
58
app/about/page.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useI18n } from '../components/I18nProvider';
|
||||||
|
|
||||||
|
const highlights = [
|
||||||
|
{ keyTitle: 'highlightQualityTitle', keyBody: 'highlightQualityBody' },
|
||||||
|
{ keyTitle: 'highlightLocalTitle', keyBody: 'highlightLocalBody' },
|
||||||
|
{ keyTitle: 'highlightApiTitle', keyBody: 'highlightApiBody' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||||
|
const apiBase = process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:3000/api';
|
||||||
|
const appEnv = process.env.APP_ENV || 'local';
|
||||||
|
const appVersion = process.env.NEXT_PUBLIC_VERSION || 'dev';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<section className="panel">
|
||||||
|
<div className="breadcrumb">
|
||||||
|
<Link href="/">{t('homeCrumb')}</Link> / <span>{t('aboutTitle')}</span>
|
||||||
|
</div>
|
||||||
|
<h1>{t('aboutTitle')}</h1>
|
||||||
|
<p style={{ marginTop: 8 }}>{t('aboutLead')}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="cards" style={{ marginTop: 18 }}>
|
||||||
|
{highlights.map((item) => (
|
||||||
|
<div key={item.keyTitle} className="panel">
|
||||||
|
<h3 className="card-title">{t(item.keyTitle as any)}</h3>
|
||||||
|
<p>{t(item.keyBody as any)}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="panel env-card" style={{ marginTop: 18 }}>
|
||||||
|
<h2 className="card-title">{t('runtimeConfigTitle')}</h2>
|
||||||
|
<p style={{ marginTop: 4 }}>{t('runtimeConfigLead')}</p>
|
||||||
|
<div className="meta-grid">
|
||||||
|
<span>
|
||||||
|
<strong>{t('runtimeAppEnv')}</strong> <code>{appEnv}</code>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong>{t('runtimeSiteUrl')}</strong> <code>{siteUrl}</code>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong>{t('runtimeApiBase')}</strong> <code>{apiBase}</code>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong>Version</strong> <code>{appVersion}</code>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
app/components/SiteFooter.tsx
Normal file
24
app/components/SiteFooter.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useI18n } from './I18nProvider';
|
||||||
|
|
||||||
|
export default function SiteFooter() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const version = process.env.NEXT_PUBLIC_VERSION || 'dev';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="site-footer">
|
||||||
|
<div className="footer-row">
|
||||||
|
<span className="footer-text" style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<Link href="/about">{t('footerAbout')}</Link>
|
||||||
|
<Link href="/pricing">{t('footerPricing')}</Link>
|
||||||
|
<Link href="/privacy">{t('footerPrivacy')}</Link>
|
||||||
|
</span>
|
||||||
|
<span className="footer-text">
|
||||||
|
Version <code>{version}</code>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import type { Metadata } from 'next';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
import NavBar from './components/NavBar';
|
import NavBar from './components/NavBar';
|
||||||
import { I18nProvider } from './components/I18nProvider';
|
import { I18nProvider } from './components/I18nProvider';
|
||||||
|
import SiteFooter from './components/SiteFooter';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Lomavuokraus.fi',
|
title: 'Lomavuokraus.fi',
|
||||||
|
|
@ -13,24 +14,13 @@ export default function RootLayout({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const version = process.env.NEXT_PUBLIC_VERSION || 'dev';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<NavBar />
|
<NavBar />
|
||||||
{children}
|
{children}
|
||||||
<footer className="site-footer">
|
<SiteFooter />
|
||||||
<div className="footer-row">
|
|
||||||
<span className="footer-text">
|
|
||||||
<a href="/privacy">Privacy & cookies</a>
|
|
||||||
</span>
|
|
||||||
<span className="footer-text">
|
|
||||||
Version <code>{version}</code>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
45
app/page.tsx
45
app/page.tsx
|
|
@ -18,27 +18,8 @@ type LatestListing = {
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const highlights = [
|
|
||||||
{
|
|
||||||
keyTitle: 'highlightQualityTitle',
|
|
||||||
keyBody: 'highlightQualityBody',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyTitle: 'highlightLocalTitle',
|
|
||||||
keyBody: 'highlightLocalBody',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyTitle: 'highlightApiTitle',
|
|
||||||
keyBody: 'highlightApiBody',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
|
||||||
const apiBase = process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:3000/api';
|
|
||||||
const appEnv = process.env.APP_ENV || 'local';
|
|
||||||
const appVersion = process.env.NEXT_PUBLIC_VERSION || 'dev';
|
|
||||||
const [latest, setLatest] = useState<LatestListing[]>([]);
|
const [latest, setLatest] = useState<LatestListing[]>([]);
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
const [loadingLatest, setLoadingLatest] = useState(false);
|
const [loadingLatest, setLoadingLatest] = useState(false);
|
||||||
|
|
@ -91,32 +72,6 @@ export default function HomePage() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="cards">
|
|
||||||
{highlights.map((item) => (
|
|
||||||
<div key={item.keyTitle} className="panel">
|
|
||||||
<h3 className="card-title">{t(item.keyTitle as any)}</h3>
|
|
||||||
<p>{t(item.keyBody as any)}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="panel env-card">
|
|
||||||
<h3 className="card-title">{t('runtimeConfigTitle')}</h3>
|
|
||||||
<div className="meta-grid">
|
|
||||||
<span>
|
|
||||||
<strong>{t('runtimeAppEnv')}</strong> <code>{appEnv}</code>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong>{t('runtimeSiteUrl')}</strong> <code>{siteUrl}</code>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong>{t('runtimeApiBase')}</strong> <code>{apiBase}</code>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<strong>Version</strong> <code>{appVersion}</code>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="panel latest-panel">
|
<section className="panel latest-panel">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
58
app/pricing/page.tsx
Normal file
58
app/pricing/page.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useI18n } from '../components/I18nProvider';
|
||||||
|
|
||||||
|
const pricing = [
|
||||||
|
{
|
||||||
|
keyTitle: 'pricingMonthly',
|
||||||
|
price: '10€',
|
||||||
|
interval: 'pricingPerMonth',
|
||||||
|
keyBody: 'pricingMonthlyBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keyTitle: 'pricingAnnual',
|
||||||
|
price: '100€',
|
||||||
|
interval: 'pricingPerYear',
|
||||||
|
keyBody: 'pricingAnnualBody',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PricingPage() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<section className="panel">
|
||||||
|
<div className="breadcrumb">
|
||||||
|
<Link href="/">{t('homeCrumb')}</Link> / <span>{t('pricingTitle')}</span>
|
||||||
|
</div>
|
||||||
|
<h1>{t('pricingTitle')}</h1>
|
||||||
|
<p style={{ marginTop: 8 }}>{t('pricingLead')}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="cards" style={{ marginTop: 18 }}>
|
||||||
|
{pricing.map((item) => (
|
||||||
|
<div key={item.keyTitle} className="panel">
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<h3 className="card-title" style={{ margin: 0 }}>
|
||||||
|
{t(item.keyTitle as any)}
|
||||||
|
</h3>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div style={{ fontSize: 26, fontWeight: 700 }}>{item.price}</div>
|
||||||
|
<div style={{ color: '#cbd5e1' }}>{t(item.interval as any)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style={{ marginTop: 10 }}>{t(item.keyBody as any)}</p>
|
||||||
|
<p style={{ color: '#cbd5e1', marginTop: 6 }}>{t('pricingPerListing')}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="panel" style={{ marginTop: 18 }}>
|
||||||
|
<h2 className="card-title">{t('pricingNotesTitle')}</h2>
|
||||||
|
<p style={{ marginTop: 8 }}>{t('pricingNotesBody')}</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
lib/i18n.ts
34
lib/i18n.ts
|
|
@ -27,9 +27,26 @@ const allMessages = {
|
||||||
highlightApiTitle: 'API-friendly',
|
highlightApiTitle: 'API-friendly',
|
||||||
highlightApiBody: 'Structured data so you can surface listings wherever you need them.',
|
highlightApiBody: 'Structured data so you can surface listings wherever you need them.',
|
||||||
runtimeConfigTitle: 'Runtime configuration',
|
runtimeConfigTitle: 'Runtime configuration',
|
||||||
|
runtimeConfigLead: 'Build-time and runtime values used by the service.',
|
||||||
runtimeAppEnv: 'APP_ENV',
|
runtimeAppEnv: 'APP_ENV',
|
||||||
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
||||||
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
|
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
|
||||||
|
aboutTitle: 'About lomavuokraus.fi',
|
||||||
|
aboutLead: 'A focused marketplace for Finnish holiday rentals with fast browsing, clear moderation, and a simple host experience.',
|
||||||
|
pricingTitle: 'Pricing',
|
||||||
|
pricingLead: 'Straightforward pricing for hosts with no surprises.',
|
||||||
|
pricingMonthly: 'Monthly',
|
||||||
|
pricingAnnual: 'Annual',
|
||||||
|
pricingPerMonth: 'per month',
|
||||||
|
pricingPerYear: 'per year (save 20%)',
|
||||||
|
pricingMonthlyBody: 'Start with a monthly plan at 10€ per listing.',
|
||||||
|
pricingAnnualBody: 'Pay annually for 100€ per listing and keep your costs predictable.',
|
||||||
|
pricingPerListing: 'Pricing is per active listing. Cancel anytime.',
|
||||||
|
pricingNotesTitle: 'Notes',
|
||||||
|
pricingNotesBody: 'We keep pricing simple while we build out hosting tools, messaging, and integrations. All current features are included.',
|
||||||
|
footerAbout: 'About',
|
||||||
|
footerPricing: 'Pricing',
|
||||||
|
footerPrivacy: 'Privacy & cookies',
|
||||||
loginTitle: 'Login',
|
loginTitle: 'Login',
|
||||||
emailLabel: 'Email',
|
emailLabel: 'Email',
|
||||||
passwordLabel: 'Password',
|
passwordLabel: 'Password',
|
||||||
|
|
@ -244,9 +261,26 @@ const allMessages = {
|
||||||
highlightApiTitle: 'API-ystävällinen',
|
highlightApiTitle: 'API-ystävällinen',
|
||||||
highlightApiBody: 'Strukturoitu data, jotta löydät kohteet kaikissa kanavissa.',
|
highlightApiBody: 'Strukturoitu data, jotta löydät kohteet kaikissa kanavissa.',
|
||||||
runtimeConfigTitle: 'Ajoaikainen konfiguraatio',
|
runtimeConfigTitle: 'Ajoaikainen konfiguraatio',
|
||||||
|
runtimeConfigLead: 'Ajo- ja rakennusaikaiset arvot, joita palvelu käyttää.',
|
||||||
runtimeAppEnv: 'APP_ENV',
|
runtimeAppEnv: 'APP_ENV',
|
||||||
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
||||||
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
|
runtimeApiBase: 'NEXT_PUBLIC_API_BASE',
|
||||||
|
aboutTitle: 'Tietoja lomavuokraus.fi:stä',
|
||||||
|
aboutLead: 'Keskittynyt suomalainen loma-asuntopalvelu: nopea selaus, selkeä moderointi ja mutkaton kokemus vuokraajalle.',
|
||||||
|
pricingTitle: 'Hinnasto',
|
||||||
|
pricingLead: 'Selkeä hinnoittelu vuokraajille ilman yllätyksiä.',
|
||||||
|
pricingMonthly: 'Kuukausi',
|
||||||
|
pricingAnnual: 'Vuosimaksu',
|
||||||
|
pricingPerMonth: 'kuukaudessa',
|
||||||
|
pricingPerYear: 'vuodessa (20 % säästö)',
|
||||||
|
pricingMonthlyBody: 'Aloita kuukausihinnalla 10 € per kohde.',
|
||||||
|
pricingAnnualBody: 'Maksa vuodessa 100 € per kohde ja pidä kulut ennustettavina.',
|
||||||
|
pricingPerListing: 'Hinta on per aktiivinen kohde. Voit perua milloin vain.',
|
||||||
|
pricingNotesTitle: 'Huomiot',
|
||||||
|
pricingNotesBody: 'Pidämme hinnoittelun yksinkertaisena samalla kun rakennamme uusia isäntätyökaluja, viestejä ja integraatioita. Kaikki nykyiset ominaisuudet sisältyvät.',
|
||||||
|
footerAbout: 'Tietoa',
|
||||||
|
footerPricing: 'Hinnasto',
|
||||||
|
footerPrivacy: 'Tietosuoja ja evästeet',
|
||||||
loginTitle: 'Kirjaudu sisään',
|
loginTitle: 'Kirjaudu sisään',
|
||||||
emailLabel: 'Sähköposti',
|
emailLabel: 'Sähköposti',
|
||||||
passwordLabel: 'Salasana',
|
passwordLabel: 'Salasana',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue