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.
|
||||
- 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.
|
||||
- Added About and Pricing pages (FI/EN), moved highlights/runtime config to About, and linked footer navigation.
|
||||
|
||||
To resume:
|
||||
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 NavBar from './components/NavBar';
|
||||
import { I18nProvider } from './components/I18nProvider';
|
||||
import SiteFooter from './components/SiteFooter';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Lomavuokraus.fi',
|
||||
|
|
@ -13,24 +14,13 @@ export default function RootLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const version = process.env.NEXT_PUBLIC_VERSION || 'dev';
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<I18nProvider>
|
||||
<NavBar />
|
||||
{children}
|
||||
<footer className="site-footer">
|
||||
<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>
|
||||
<SiteFooter />
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
45
app/page.tsx
45
app/page.tsx
|
|
@ -18,27 +18,8 @@ type LatestListing = {
|
|||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const highlights = [
|
||||
{
|
||||
keyTitle: 'highlightQualityTitle',
|
||||
keyBody: 'highlightQualityBody',
|
||||
},
|
||||
{
|
||||
keyTitle: 'highlightLocalTitle',
|
||||
keyBody: 'highlightLocalBody',
|
||||
},
|
||||
{
|
||||
keyTitle: 'highlightApiTitle',
|
||||
keyBody: 'highlightApiBody',
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
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 [activeIndex, setActiveIndex] = useState(0);
|
||||
const [loadingLatest, setLoadingLatest] = useState(false);
|
||||
|
|
@ -91,32 +72,6 @@ export default function HomePage() {
|
|||
</div>
|
||||
</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">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
|
||||
<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',
|
||||
highlightApiBody: 'Structured data so you can surface listings wherever you need them.',
|
||||
runtimeConfigTitle: 'Runtime configuration',
|
||||
runtimeConfigLead: 'Build-time and runtime values used by the service.',
|
||||
runtimeAppEnv: 'APP_ENV',
|
||||
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
||||
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',
|
||||
emailLabel: 'Email',
|
||||
passwordLabel: 'Password',
|
||||
|
|
@ -244,9 +261,26 @@ const allMessages = {
|
|||
highlightApiTitle: 'API-ystävällinen',
|
||||
highlightApiBody: 'Strukturoitu data, jotta löydät kohteet kaikissa kanavissa.',
|
||||
runtimeConfigTitle: 'Ajoaikainen konfiguraatio',
|
||||
runtimeConfigLead: 'Ajo- ja rakennusaikaiset arvot, joita palvelu käyttää.',
|
||||
runtimeAppEnv: 'APP_ENV',
|
||||
runtimeSiteUrl: 'NEXT_PUBLIC_SITE_URL',
|
||||
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',
|
||||
emailLabel: 'Sähköposti',
|
||||
passwordLabel: 'Salasana',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue