Add About and Pricing pages with footer links

This commit is contained in:
Tero Halla-aho 2025-11-27 11:21:46 +02:00
parent 4d5c7eedf8
commit 2a835d9875
8 changed files with 183 additions and 57 deletions

6
AGENTS.md Normal file
View 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.

View file

@ -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
View 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>
);
}

View 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>
);
}

View file

@ -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>

View file

@ -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
View 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>
);
}

View file

@ -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',