feat: build juozas auto site
This commit is contained in:
43
src/components/CarCard.astro
Normal file
43
src/components/CarCard.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { lt } from '../i18n/lt';
|
||||
import { formatMileage, formatPrice } from '../lib/format';
|
||||
|
||||
type Props = {
|
||||
car: CollectionEntry<'cars'>;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
const { car, compact = false } = Astro.props;
|
||||
const title = `${car.data.year} ${car.data.make} ${car.data.model}`;
|
||||
---
|
||||
|
||||
<a
|
||||
href={`/automobiliai/${car.slug}`}
|
||||
class:list={[
|
||||
'group block rounded-[2rem] bg-paper transition-transform duration-300 ease-out-quart focus-visible:outline-burgundy-600',
|
||||
compact ? 'opacity-75' : 'hover:-translate-y-1',
|
||||
]}
|
||||
aria-label={`${lt.actions.viewCar}: ${title}`}
|
||||
>
|
||||
<div class="overflow-hidden rounded-[1.5rem] bg-wash">
|
||||
<Image
|
||||
src={car.data.photos[0]}
|
||||
alt={`${car.data.make} ${car.data.model} automobilio nuotrauka`}
|
||||
width={720}
|
||||
height={540}
|
||||
loading="lazy"
|
||||
format="avif"
|
||||
class="aspect-[4/3] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<p class="text-sm text-muted">{car.data.city}</p>
|
||||
<h3 class="mt-1 text-xl font-semibold tracking-[-0.04em] text-ink">{title}</h3>
|
||||
<p class="mt-3 text-2xl font-semibold tracking-[-0.05em] text-burgundy-700 tabular">{formatPrice(car.data.price)}</p>
|
||||
<p class="mt-2 text-sm text-muted tabular">
|
||||
{formatMileage(car.data.mileage)} · {car.data.fuel} · {car.data.transmission}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
46
src/components/CarGallery.astro
Normal file
46
src/components/CarGallery.astro
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
type Props = {
|
||||
photos: ImageMetadata[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
const { photos, title } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="relative" aria-label="Automobilio nuotraukos" data-gallery>
|
||||
<div class="no-scrollbar flex snap-x snap-mandatory gap-3 overflow-x-auto rounded-[2rem] scroll-smooth bg-wash" data-gallery-track>
|
||||
{photos.map((photo, index) => (
|
||||
<Image
|
||||
src={photo}
|
||||
alt={`${title} nuotrauka ${index + 1}`}
|
||||
width={960}
|
||||
height={720}
|
||||
loading={index < 2 ? 'eager' : 'lazy'}
|
||||
format="avif"
|
||||
class="aspect-[4/3] min-w-full snap-center object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p class="absolute bottom-4 right-4 rounded-full bg-ink/80 px-3 py-1 text-xs font-semibold text-paper tabular" data-gallery-counter>
|
||||
1 / {photos.length}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
document.querySelectorAll('[data-gallery]').forEach((gallery) => {
|
||||
const track = gallery.querySelector('[data-gallery-track]');
|
||||
const counter = gallery.querySelector('[data-gallery-counter]');
|
||||
if (!track || !counter) return;
|
||||
const total = track.children.length;
|
||||
track.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
const index = Math.round(track.scrollLeft / track.clientWidth) + 1;
|
||||
counter.textContent = `${Math.min(Math.max(index, 1), total)} / ${total}`;
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
});
|
||||
</script>
|
||||
47
src/components/ContactButtons.astro
Normal file
47
src/components/ContactButtons.astro
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import { lt } from '../i18n/lt';
|
||||
import { PHONE_E164, PHONE_WA, telUrl, whatsappUrl } from '../lib/contact';
|
||||
|
||||
type Props = {
|
||||
make?: string;
|
||||
model?: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
const { make, model, compact = false } = Astro.props;
|
||||
const baseClass = compact ? 'px-4 py-3 text-sm' : 'px-5 py-4 text-base';
|
||||
---
|
||||
|
||||
<div class="grid w-full max-w-full grid-cols-1 gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]" data-contact-buttons>
|
||||
<a
|
||||
class:list={[
|
||||
'min-w-0 rounded-full bg-burgundy-700 text-center font-semibold text-paper shadow-soft transition-transform duration-200 ease-out-quart hover:-translate-y-0.5 active:translate-y-0 tabular',
|
||||
baseClass,
|
||||
]}
|
||||
href={telUrl(PHONE_E164)}
|
||||
data-contact-event
|
||||
>
|
||||
{lt.actions.call}
|
||||
</a>
|
||||
<a
|
||||
class:list={[
|
||||
'min-w-0 rounded-full border border-line bg-paper text-center font-semibold text-ink transition-colors duration-200 ease-out-quart hover:border-burgundy-600 hover:text-burgundy-700 tabular',
|
||||
baseClass,
|
||||
]}
|
||||
href={whatsappUrl(PHONE_WA, make, model)}
|
||||
data-contact-event
|
||||
>
|
||||
{lt.actions.whatsapp}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
if (!window.__juozasContactEventsBound) {
|
||||
window.__juozasContactEventsBound = true;
|
||||
document.addEventListener('click', (event) => {
|
||||
const link = event.target.closest('[data-contact-event]');
|
||||
if (!link) return;
|
||||
if (window.fbq) window.fbq('track', 'Contact');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
18
src/components/Footer.astro
Normal file
18
src/components/Footer.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import { EMAIL, HOURS, PHONE_DISPLAY } from '../lib/contact';
|
||||
---
|
||||
|
||||
<footer class="mt-24 border-t border-line bg-wash/60">
|
||||
<div class="mx-auto grid max-w-7xl gap-8 px-5 py-10 text-sm text-muted sm:px-8 md:grid-cols-[1fr_auto] lg:px-10">
|
||||
<div>
|
||||
<p class="font-semibold text-ink">Juozas Auto</p>
|
||||
<p class="mt-2 max-w-md">Atrinkti automobiliai Lietuvoje. Sąžiningos kainos, tikri automobiliai, be paslėptų mokesčių.</p>
|
||||
</div>
|
||||
<address class="not-italic md:text-right">
|
||||
<a class="block font-semibold text-ink" href="tel:+37061234567">{PHONE_DISPLAY}</a>
|
||||
<a class="mt-1 block" href={`mailto:${EMAIL}`}>{EMAIL}</a>
|
||||
<p class="mt-1">{HOURS}</p>
|
||||
</address>
|
||||
<p class="text-xs text-muted md:col-span-2">© 2026 Juozas Auto. Informacija svetainėje nėra viešas pasiūlymas.</p>
|
||||
</div>
|
||||
</footer>
|
||||
14
src/components/Header.astro
Normal file
14
src/components/Header.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import { lt } from '../i18n/lt';
|
||||
---
|
||||
|
||||
<header class="mx-auto flex w-full max-w-7xl flex-col items-start gap-4 px-5 py-5 sm:flex-row sm:items-center sm:justify-between sm:px-8 lg:px-10">
|
||||
<a href="/" class="text-lg font-semibold tracking-[-0.04em]" aria-label="Juozas Auto pradžia">Juozas Auto</a>
|
||||
<nav aria-label="Pagrindinė navigacija">
|
||||
<ul class="flex items-center gap-5 text-sm text-muted sm:gap-7">
|
||||
<li><a class="transition-colors duration-200 ease-out-quart hover:text-ink" href="/#automobiliai">{lt.nav.cars}</a></li>
|
||||
<li><a class="transition-colors duration-200 ease-out-quart hover:text-ink" href="/apie">{lt.nav.about}</a></li>
|
||||
<li><a class="transition-colors duration-200 ease-out-quart hover:text-ink" href="/kontaktai">{lt.nav.contact}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
9
src/components/JsonLd.astro
Normal file
9
src/components/JsonLd.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
type Props = {
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const { data } = Astro.props;
|
||||
---
|
||||
|
||||
<script is:inline type="application/ld+json" set:html={JSON.stringify(data)} />
|
||||
26
src/components/MetaPixel.astro
Normal file
26
src/components/MetaPixel.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
const pixelId = import.meta.env.META_PIXEL_ID;
|
||||
---
|
||||
|
||||
{pixelId && (
|
||||
<script is:inline define:vars={{ pixelId }}>
|
||||
!(function (f, b, e, v, n, t, s) {
|
||||
if (f.fbq) return;
|
||||
n = f.fbq = function () {
|
||||
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
|
||||
};
|
||||
if (!f._fbq) f._fbq = n;
|
||||
n.push = n;
|
||||
n.loaded = true;
|
||||
n.version = '2.0';
|
||||
n.queue = [];
|
||||
t = b.createElement(e);
|
||||
t.async = true;
|
||||
t.src = v;
|
||||
s = b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', pixelId);
|
||||
fbq('track', 'PageView');
|
||||
</script>
|
||||
)}
|
||||
28
src/components/SpecStrip.astro
Normal file
28
src/components/SpecStrip.astro
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import { lt } from '../i18n/lt';
|
||||
import { formatMileage } from '../lib/format';
|
||||
|
||||
type Props = {
|
||||
year: number;
|
||||
mileage: number;
|
||||
fuel: string;
|
||||
transmission: string;
|
||||
};
|
||||
|
||||
const { year, mileage, fuel, transmission } = Astro.props;
|
||||
const specs = [
|
||||
[lt.labels.year, String(year)],
|
||||
[lt.labels.mileage, formatMileage(mileage)],
|
||||
[lt.labels.fuel, fuel],
|
||||
[lt.labels.gearbox, transmission],
|
||||
];
|
||||
---
|
||||
|
||||
<dl class="grid w-full min-w-0 grid-cols-2 overflow-hidden rounded-[1.75rem] border border-line bg-paper sm:grid-cols-4">
|
||||
{specs.map(([label, value]) => (
|
||||
<div class="min-w-0 border-line px-4 py-4 odd:border-r sm:border-r sm:last:border-r-0">
|
||||
<dt class="text-xs uppercase tracking-[0.12em] text-muted">{label}</dt>
|
||||
<dd class="mt-1 font-semibold text-ink tabular">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
BIN
src/content/cars/_photos/bmw-320d-2018-vilnius/01.jpg
Normal file
BIN
src/content/cars/_photos/bmw-320d-2018-vilnius/01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
BIN
src/content/cars/_photos/bmw-320d-2018-vilnius/02.jpg
Normal file
BIN
src/content/cars/_photos/bmw-320d-2018-vilnius/02.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
BIN
src/content/cars/_photos/toyota-rav4-2020-vilnius/01.jpg
Normal file
BIN
src/content/cars/_photos/toyota-rav4-2020-vilnius/01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
BIN
src/content/cars/_photos/toyota-rav4-2020-vilnius/02.jpg
Normal file
BIN
src/content/cars/_photos/toyota-rav4-2020-vilnius/02.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 349 KiB |
BIN
src/content/cars/_photos/vw-passat-2016-kaunas/01.jpg
Normal file
BIN
src/content/cars/_photos/vw-passat-2016-kaunas/01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
BIN
src/content/cars/_photos/vw-passat-2016-kaunas/02.jpg
Normal file
BIN
src/content/cars/_photos/vw-passat-2016-kaunas/02.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
29
src/content/cars/bmw-320d-2018-vilnius.md
Normal file
29
src/content/cars/bmw-320d-2018-vilnius.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
make: BMW
|
||||
model: 320d
|
||||
year: 2018
|
||||
price: 12500
|
||||
mileage: 145000
|
||||
fuel: dyzelinas
|
||||
transmission: automatinė
|
||||
bodyType: sedanas
|
||||
color: Pilka
|
||||
city: Vilnius
|
||||
drivetrain: galiniai
|
||||
power: 140
|
||||
engineSize: 2.0
|
||||
firstRegistration: 2018-05
|
||||
vin: WBA8C91010A000000
|
||||
photos:
|
||||
- ./_photos/bmw-320d-2018-vilnius/01.jpg
|
||||
- ./_photos/bmw-320d-2018-vilnius/02.jpg
|
||||
sold: false
|
||||
featured: true
|
||||
publishedAt: 2026-04-15
|
||||
---
|
||||
|
||||
Tvarkingas BMW 320d su automatine pavarų dėže, aiškia istorija ir prižiūrėtu salonu. Automobilis paruoštas apžiūrai Vilniuje.
|
||||
|
||||
Pakeisti pagrindiniai eksploataciniai mazgai, važiuoklė standi, variklis dirba tolygiai. Kaina nurodyta be paslėptų mokesčių.
|
||||
|
||||
Nuotraukos: Unsplash placeholder, savininkas pakeis realiomis automobilio nuotraukomis.
|
||||
28
src/content/cars/toyota-rav4-2020-vilnius.md
Normal file
28
src/content/cars/toyota-rav4-2020-vilnius.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
make: Toyota
|
||||
model: RAV4
|
||||
year: 2020
|
||||
price: 23900
|
||||
mileage: 82000
|
||||
fuel: hibridas
|
||||
transmission: automatinė
|
||||
bodyType: visureigis
|
||||
color: Balta
|
||||
city: Vilnius
|
||||
drivetrain: visi
|
||||
power: 160
|
||||
engineSize: 2.5
|
||||
firstRegistration: 2020-03
|
||||
photos:
|
||||
- ./_photos/toyota-rav4-2020-vilnius/01.jpg
|
||||
- ./_photos/toyota-rav4-2020-vilnius/02.jpg
|
||||
sold: false
|
||||
featured: true
|
||||
publishedAt: 2026-04-18
|
||||
---
|
||||
|
||||
Erdvus hibridinis RAV4 su automatine pavarų dėže ir visais varomais ratais. Tinka šeimai, miestui ir ilgesniems savaitgalio maršrutams.
|
||||
|
||||
Automobilis parduodamas su aiškia komplektacija ir be papildomų administravimo mokesčių.
|
||||
|
||||
Nuotraukos: Unsplash placeholder, savininkas pakeis realiomis automobilio nuotraukomis.
|
||||
28
src/content/cars/vw-passat-2016-kaunas.md
Normal file
28
src/content/cars/vw-passat-2016-kaunas.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
make: Volkswagen
|
||||
model: Passat
|
||||
year: 2016
|
||||
price: 9800
|
||||
mileage: 178000
|
||||
fuel: dyzelinas
|
||||
transmission: mechaninė
|
||||
bodyType: universalas
|
||||
color: Juoda
|
||||
city: Kaunas
|
||||
drivetrain: priekiniai
|
||||
power: 110
|
||||
engineSize: 2.0
|
||||
firstRegistration: 2016-09
|
||||
photos:
|
||||
- ./_photos/vw-passat-2016-kaunas/01.jpg
|
||||
- ./_photos/vw-passat-2016-kaunas/02.jpg
|
||||
sold: false
|
||||
featured: false
|
||||
publishedAt: 2026-04-10
|
||||
---
|
||||
|
||||
Praktiškas Passat universalas kasdienai ir ilgesnėms kelionėms. Ekonomiškas dyzelinis variklis, tvarkingas kėbulas ir švarus salonas.
|
||||
|
||||
Automobilį galima apžiūrėti Kaune iš anksto suderinus laiką telefonu arba WhatsApp žinute.
|
||||
|
||||
Nuotraukos: Unsplash placeholder, savininkas pakeis realiomis automobilio nuotraukomis.
|
||||
29
src/content/config.ts
Normal file
29
src/content/config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const cars = defineCollection({
|
||||
type: 'content',
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
make: z.string(),
|
||||
model: z.string(),
|
||||
year: z.number().int().min(1990).max(2030),
|
||||
price: z.number().int().positive(),
|
||||
mileage: z.number().int().nonnegative(),
|
||||
fuel: z.enum(['benzinas', 'dyzelinas', 'hibridas', 'elektra', 'dujos']),
|
||||
transmission: z.enum(['mechaninė', 'automatinė']),
|
||||
bodyType: z.enum(['sedanas', 'universalas', 'hečbekas', 'visureigis', 'kupė', 'kabrioletas', 'vienatūris']),
|
||||
color: z.string(),
|
||||
city: z.string(),
|
||||
drivetrain: z.enum(['priekiniai', 'galiniai', 'visi']).optional(),
|
||||
power: z.number().int().optional(),
|
||||
engineSize: z.number().optional(),
|
||||
firstRegistration: z.string().optional(),
|
||||
vin: z.string().optional(),
|
||||
photos: z.array(image()).min(1),
|
||||
sold: z.boolean().default(false),
|
||||
featured: z.boolean().default(false),
|
||||
publishedAt: z.date(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { cars };
|
||||
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
35
src/i18n/lt.ts
Normal file
35
src/i18n/lt.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export const lt = {
|
||||
nav: {
|
||||
cars: 'Automobiliai',
|
||||
about: 'Apie',
|
||||
contact: 'Kontaktai',
|
||||
},
|
||||
actions: {
|
||||
call: 'Skambinti',
|
||||
whatsapp: 'WhatsApp',
|
||||
viewCar: 'Peržiūrėti automobilį',
|
||||
send: 'Siųsti užklausą',
|
||||
},
|
||||
labels: {
|
||||
year: 'Metai',
|
||||
mileage: 'Rida',
|
||||
fuel: 'Kuras',
|
||||
gearbox: 'Pavaros',
|
||||
city: 'Miestas',
|
||||
bodyType: 'Kėbulas',
|
||||
color: 'Spalva',
|
||||
drivetrain: 'Varantieji ratai',
|
||||
power: 'Galia',
|
||||
engineSize: 'Variklis',
|
||||
firstRegistration: 'Pirma registracija',
|
||||
vin: 'VIN',
|
||||
},
|
||||
sections: {
|
||||
currentCars: 'Automobiliai',
|
||||
soldCars: 'Parduoti automobiliai',
|
||||
specs: 'Specifikacija',
|
||||
location: 'Vieta',
|
||||
similar: 'Panašūs automobiliai',
|
||||
description: 'Aprašymas',
|
||||
},
|
||||
};
|
||||
51
src/layouts/BaseLayout.astro
Normal file
51
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
import MetaPixel from '../components/MetaPixel.astro';
|
||||
import { site } from '../site';
|
||||
import '../styles/global.css';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
ogTitle?: string;
|
||||
description?: string;
|
||||
canonicalPath?: string;
|
||||
image?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
const {
|
||||
title = site.name,
|
||||
ogTitle = title,
|
||||
description = site.description,
|
||||
canonicalPath = Astro.url.pathname,
|
||||
image,
|
||||
type = 'website',
|
||||
} = Astro.props;
|
||||
|
||||
const canonicalUrl = new URL(canonicalPath, site.url).toString();
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="lt">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:locale" content="lt_LT" />
|
||||
{image && <meta property="og:image" content={image} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={ogTitle} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
{image && <meta name="twitter:image" content={image} />}
|
||||
<MetaPixel />
|
||||
</head>
|
||||
<body class="min-h-screen text-ink antialiased">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
14
src/lib/contact.ts
Normal file
14
src/lib/contact.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const PHONE_DISPLAY = '+370 612 34 567';
|
||||
export const PHONE_E164 = '+37061234567';
|
||||
export const PHONE_WA = '37061234567';
|
||||
export const EMAIL = 'info@juozas.lt';
|
||||
export const CITY = 'Vilnius';
|
||||
export const HOURS = 'I-V 9:00-18:00, VI 10:00-14:00';
|
||||
|
||||
export const telUrl = (phone = PHONE_E164) => `tel:${phone.replace(/\s/g, '')}`;
|
||||
|
||||
export const whatsappUrl = (phone: string, make?: string, model?: string) => {
|
||||
const subject = [make, model].filter(Boolean).join(' ').trim();
|
||||
const text = subject ? `Sveiki, domina ${subject}` : 'Sveiki, domina automobilis';
|
||||
return `https://wa.me/${phone.replace(/\D/g, '')}?text=${encodeURIComponent(text)}`;
|
||||
};
|
||||
52
src/lib/format.ts
Normal file
52
src/lib/format.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
const lithuanianMap: Record<string, string> = {
|
||||
ą: 'a',
|
||||
č: 'c',
|
||||
ę: 'e',
|
||||
ė: 'e',
|
||||
į: 'i',
|
||||
š: 's',
|
||||
ų: 'u',
|
||||
ū: 'u',
|
||||
ž: 'z',
|
||||
};
|
||||
|
||||
export type SlugCar = {
|
||||
make: string;
|
||||
model: string;
|
||||
year: number;
|
||||
city: string;
|
||||
};
|
||||
|
||||
export const groupNumber = (value: number) => new Intl.NumberFormat('lt-LT').format(value).replace(/\u00a0/g, ' ');
|
||||
|
||||
export const formatPrice = (price: number) => `${groupNumber(price)}\u00a0€`;
|
||||
|
||||
export const formatMileage = (mileage: number) => `${groupNumber(mileage)} km`;
|
||||
|
||||
export const formatPower = (kw: number) => `${kw} kW (${Math.round(kw * 1.35962)} AG)`;
|
||||
|
||||
export const formatEngineSize = (liters: number) => `${liters.toFixed(1).replace('.', ',')} l`;
|
||||
|
||||
export const slugify = (value: string) =>
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[ąčęėįšųūž]/g, (letter) => lithuanianMap[letter] ?? letter)
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
|
||||
export const slugifyCar = (car: SlugCar) => slugify(`${car.make}-${car.model}-${car.year}-${car.city}`);
|
||||
|
||||
export const stripMarkdown = (value: string) =>
|
||||
value
|
||||
.replace(/```[\s\S]*?```/g, ' ')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||
.replace(/[*_>#~-]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
export const excerpt = (value: string, limit = 155) => stripMarkdown(value).slice(0, limit);
|
||||
50
src/lib/jsonLd.ts
Normal file
50
src/lib/jsonLd.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export type VehicleSchemaCar = {
|
||||
make: string;
|
||||
model: string;
|
||||
year: number;
|
||||
price: number;
|
||||
mileage: number;
|
||||
fuel: string;
|
||||
transmission: string;
|
||||
bodyType: string;
|
||||
color: string;
|
||||
sold?: boolean;
|
||||
vin?: string;
|
||||
};
|
||||
|
||||
type VehicleJsonLdInput = {
|
||||
car: VehicleSchemaCar;
|
||||
canonicalUrl: string;
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
export const vehicleJsonLd = ({ car, canonicalUrl, imageUrl }: VehicleJsonLdInput) => ({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Vehicle',
|
||||
name: `${car.year} ${car.make} ${car.model}`,
|
||||
brand: {
|
||||
'@type': 'Brand',
|
||||
name: car.make,
|
||||
},
|
||||
model: car.model,
|
||||
vehicleModelDate: car.year,
|
||||
mileageFromOdometer: {
|
||||
'@type': 'QuantitativeValue',
|
||||
value: car.mileage,
|
||||
unitCode: 'KMT',
|
||||
},
|
||||
fuelType: car.fuel,
|
||||
vehicleTransmission: car.transmission,
|
||||
bodyType: car.bodyType,
|
||||
color: car.color,
|
||||
...(car.vin ? { vehicleIdentificationNumber: car.vin } : {}),
|
||||
image: imageUrl,
|
||||
url: canonicalUrl,
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: car.price,
|
||||
priceCurrency: 'EUR',
|
||||
availability: car.sold ? 'https://schema.org/SoldOut' : 'https://schema.org/InStock',
|
||||
url: canonicalUrl,
|
||||
},
|
||||
});
|
||||
10
src/pages/api/meta-capi.ts
Normal file
10
src/pages/api/meta-capi.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// TODO: Add Meta Conversions API forwarding when the site moves beyond static-only output.
|
||||
// Keep the public site static for now. A future Vercel Function can accept Contact events,
|
||||
// hash user data server-side, and forward them to Meta with a server-only access token.
|
||||
|
||||
export function GET() {
|
||||
return new Response('Meta Conversions API stub. Configure a serverless function before use.\n', {
|
||||
status: 501,
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
45
src/pages/apie.astro
Normal file
45
src/pages/apie.astro
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Apie - Juozas Auto" description="Trumpai apie Juozas Auto principus, automobilio pardavimo pagalbą ir darbo būdą." canonicalPath="/apie">
|
||||
<Header />
|
||||
<main class="mx-auto max-w-5xl px-5 py-10 sm:px-8 lg:px-10">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-burgundy-700">Apie</p>
|
||||
<h1 class="mt-4 max-w-3xl text-3xl font-semibold tracking-[-0.06em]">Mažas inventorius, daugiau dėmesio kiekvienam automobiliui.</h1>
|
||||
<p class="mt-7 max-w-2xl text-lg leading-8 text-muted">
|
||||
Juozas Auto yra vieno pardavėjo projektas. Čia nėra pirkėjų paskyrų, paslėptų mokesčių ar triukšmingų skelbimų blokų. Tik keli automobiliai ir aiški informacija sprendimui priimti.
|
||||
</p>
|
||||
|
||||
<section class="mt-16 grid gap-8 md:grid-cols-3" aria-labelledby="process-heading">
|
||||
<h2 id="process-heading" class="sr-only">Kaip tai veikia</h2>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-burgundy-700">01</p>
|
||||
<h3 class="mt-3 text-xl font-semibold tracking-[-0.04em]">Atranka</h3>
|
||||
<p class="mt-3 text-muted">Į svetainę patenka tik keli automobiliai, kuriuos galima ramiai apžiūrėti ir palyginti.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-burgundy-700">02</p>
|
||||
<h3 class="mt-3 text-xl font-semibold tracking-[-0.04em]">Pateikimas</h3>
|
||||
<p class="mt-3 text-muted">Nuotraukos, kaina ir specifikacija pateikiami be perteklinių blokų ar dirbtinės skubos.</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-burgundy-700">03</p>
|
||||
<h3 class="mt-3 text-xl font-semibold tracking-[-0.04em]">Kontaktas</h3>
|
||||
<p class="mt-3 text-muted">Skambutis arba WhatsApp žinutė veda tiesiai pas pardavėją. Savininkas tekstą pakeis tikra istorija.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-16 rounded-[2rem] bg-wash p-8">
|
||||
<p class="text-sm uppercase tracking-[0.18em] text-muted">Pasitikėjimas</p>
|
||||
<div class="mt-6 grid gap-8 sm:grid-cols-3">
|
||||
<div><p class="text-3xl font-semibold tabular">8+</p><p class="mt-1 text-sm text-muted">metai patirties</p></div>
|
||||
<div><p class="text-3xl font-semibold tabular">120+</p><p class="mt-1 text-sm text-muted">parduotų automobilių</p></div>
|
||||
<div><p class="text-3xl font-semibold tabular">1:1</p><p class="mt-1 text-sm text-muted">tiesioginis bendravimas</p></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
128
src/pages/automobiliai/[slug].astro
Normal file
128
src/pages/automobiliai/[slug].astro
Normal file
@@ -0,0 +1,128 @@
|
||||
---
|
||||
import { getImage } from 'astro:assets';
|
||||
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||
import CarCard from '../../components/CarCard.astro';
|
||||
import CarGallery from '../../components/CarGallery.astro';
|
||||
import ContactButtons from '../../components/ContactButtons.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import Header from '../../components/Header.astro';
|
||||
import JsonLd from '../../components/JsonLd.astro';
|
||||
import SpecStrip from '../../components/SpecStrip.astro';
|
||||
import { lt } from '../../i18n/lt';
|
||||
import { EMAIL, PHONE_DISPLAY } from '../../lib/contact';
|
||||
import { excerpt, formatEngineSize, formatMileage, formatPower, formatPrice } from '../../lib/format';
|
||||
import { vehicleJsonLd } from '../../lib/jsonLd';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { site } from '../../site';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cars = await getCollection('cars');
|
||||
return cars.map((car) => ({ params: { slug: car.slug }, props: { car } }));
|
||||
}
|
||||
|
||||
const { car } = Astro.props as { car: CollectionEntry<'cars'> };
|
||||
const allCars = await getCollection('cars');
|
||||
const similarCars = allCars.filter((item) => item.id !== car.id && !item.data.sold).slice(0, 3);
|
||||
const { Content } = await car.render();
|
||||
const carName = `${car.data.make} ${car.data.model}`;
|
||||
const title = `${car.data.year} ${car.data.make} ${car.data.model} — ${formatPrice(car.data.price)} — Juozas Auto`;
|
||||
const ogTitle = `${car.data.year} ${car.data.make} ${car.data.model} — ${formatPrice(car.data.price)}`;
|
||||
const description = excerpt(car.body);
|
||||
const canonicalPath = `/automobiliai/${car.slug}`;
|
||||
const canonicalUrl = new URL(canonicalPath, site.url).toString();
|
||||
const ogImage = await getImage({ src: car.data.photos[0], width: 1200, height: 630, format: 'jpg' });
|
||||
const ogImageUrl = new URL(ogImage.src, site.url).toString();
|
||||
const specs = [
|
||||
[lt.labels.year, String(car.data.year)],
|
||||
[lt.labels.mileage, formatMileage(car.data.mileage)],
|
||||
[lt.labels.fuel, car.data.fuel],
|
||||
[lt.labels.gearbox, car.data.transmission],
|
||||
[lt.labels.bodyType, car.data.bodyType],
|
||||
[lt.labels.color, car.data.color],
|
||||
car.data.drivetrain ? [lt.labels.drivetrain, car.data.drivetrain] : null,
|
||||
car.data.power ? [lt.labels.power, formatPower(car.data.power)] : null,
|
||||
car.data.engineSize ? [lt.labels.engineSize, formatEngineSize(car.data.engineSize)] : null,
|
||||
car.data.firstRegistration ? [lt.labels.firstRegistration, car.data.firstRegistration] : null,
|
||||
car.data.vin ? [lt.labels.vin, car.data.vin] : null,
|
||||
].filter(Boolean) as [string, string][];
|
||||
|
||||
const jsonLd = vehicleJsonLd({ car: car.data, canonicalUrl, imageUrl: ogImageUrl });
|
||||
---
|
||||
|
||||
<BaseLayout title={title} ogTitle={ogTitle} description={description} canonicalPath={canonicalPath} image={ogImageUrl} type="product">
|
||||
<Header />
|
||||
<JsonLd data={jsonLd} />
|
||||
<main class="mx-auto max-w-7xl px-5 pb-8 sm:px-8 lg:px-10">
|
||||
<div class="grid min-w-0 gap-10 lg:grid-cols-[minmax(0,1.08fr)_minmax(380px,0.72fr)] lg:items-start">
|
||||
<div class="min-w-0 lg:sticky lg:top-6">
|
||||
<CarGallery photos={car.data.photos} title={carName} />
|
||||
</div>
|
||||
|
||||
<aside class="w-full max-w-full min-w-0 overflow-hidden lg:sticky lg:top-6">
|
||||
<p class="text-sm text-muted">{car.data.year} · {car.data.city}</p>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-[-0.06em] text-ink sm:text-3xl">{carName}</h1>
|
||||
<p class="mt-5 text-3xl font-semibold tracking-[-0.07em] text-burgundy-700 tabular">{formatPrice(car.data.price)}</p>
|
||||
<div class="mt-6 lg:hidden">
|
||||
<ContactButtons make={car.data.make} model={car.data.model} compact />
|
||||
</div>
|
||||
<div class="mt-7">
|
||||
<SpecStrip year={car.data.year} mileage={car.data.mileage} fuel={car.data.fuel} transmission={car.data.transmission} />
|
||||
</div>
|
||||
<div class="mt-7 hidden lg:block">
|
||||
<ContactButtons make={car.data.make} model={car.data.model} />
|
||||
<p class="mt-4 text-sm text-muted">Tiesioginis kontaktas: {PHONE_DISPLAY}, {EMAIL}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid gap-12 lg:grid-cols-[minmax(0,1fr)_380px]">
|
||||
<article class="prose-lite measure">
|
||||
<h2 class="mb-5 text-2xl font-semibold tracking-[-0.05em] text-ink">{lt.sections.description}</h2>
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<section aria-labelledby="specs-heading">
|
||||
<h2 id="specs-heading" class="text-2xl font-semibold tracking-[-0.05em]">{lt.sections.specs}</h2>
|
||||
<dl class="mt-5 divide-y divide-line rounded-[1.75rem] border border-line bg-paper">
|
||||
{specs.map(([label, value]) => (
|
||||
<div class="grid grid-cols-[1fr_1.25fr] gap-4 px-5 py-4 text-sm">
|
||||
<dt class="text-muted">{label}</dt>
|
||||
<dd class="font-semibold text-ink tabular">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="mt-16 rounded-[2rem] bg-wash p-6 sm:p-8" aria-labelledby="location-heading">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{lt.sections.location}</p>
|
||||
<h2 id="location-heading" class="mt-2 text-2xl font-semibold tracking-[-0.05em]">{car.data.city}</h2>
|
||||
<p class="mt-3 max-w-2xl text-muted">Apžiūra derinama telefonu. Tikslus adresas pateikiamas susitarus dėl laiko.</p>
|
||||
</section>
|
||||
|
||||
{similarCars.length > 0 && (
|
||||
<section class="mt-20" aria-labelledby="similar-heading">
|
||||
<h2 id="similar-heading" class="text-2xl font-semibold tracking-[-0.05em]">{lt.sections.similar}</h2>
|
||||
<div class="mt-7 grid gap-x-7 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{similarCars.map((item) => <CarCard car={item} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<div class="fixed inset-x-0 bottom-0 z-40 translate-y-full border-t border-line bg-paper/95 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3 shadow-soft lg:hidden" data-sticky-contact>
|
||||
<ContactButtons make={car.data.make} model={car.data.model} compact />
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
|
||||
<script is:inline>
|
||||
const sticky = document.querySelector('[data-sticky-contact]');
|
||||
if (sticky) {
|
||||
const toggle = () => sticky.classList.toggle('translate-y-full', window.scrollY < 200);
|
||||
sticky.classList.add('transition-transform', 'duration-300', 'ease-out-quart');
|
||||
toggle();
|
||||
window.addEventListener('scroll', toggle, { passive: true });
|
||||
}
|
||||
</script>
|
||||
81
src/pages/index.astro
Normal file
81
src/pages/index.astro
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import CarCard from '../components/CarCard.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { lt } from '../i18n/lt';
|
||||
import { site } from '../site';
|
||||
|
||||
const allCars = await getCollection('cars');
|
||||
const currentCars = allCars.filter((car) => !car.data.sold).sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
|
||||
const soldCars = allCars.filter((car) => car.data.sold);
|
||||
const heroCar = currentCars.find((car) => car.data.featured) ?? currentCars[0];
|
||||
---
|
||||
|
||||
<BaseLayout title="Juozas Auto" description={site.description} canonicalPath="/">
|
||||
<Header />
|
||||
<main>
|
||||
<section class="mx-auto grid max-w-7xl gap-10 px-5 pb-16 pt-8 sm:px-8 lg:grid-cols-[0.9fr_1.1fr] lg:items-end lg:px-10 lg:pb-24 lg:pt-14">
|
||||
<div class="max-w-3xl">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-burgundy-700">Naudoti automobiliai Lietuvoje</p>
|
||||
<h1 class="mt-5 text-3xl font-semibold text-ink sm:text-3xl">
|
||||
Atrinkti automobiliai. Be triukšmo, be paslėptų mokesčių.
|
||||
</h1>
|
||||
<p class="mt-6 max-w-xl text-lg text-muted">
|
||||
Mažas inventorius, aiškios kainos ir tiesioginis kontaktas su pardavėju. Kiekvienas automobilis pateiktas taip, kad sprendimą priimtumėte ramiai.
|
||||
</p>
|
||||
</div>
|
||||
{heroCar && (
|
||||
<a href={`/automobiliai/${heroCar.slug}`} class="group block" aria-label={`Peržiūrėti ${heroCar.data.make} ${heroCar.data.model}`}>
|
||||
<div class="overflow-hidden rounded-[2.5rem] bg-wash shadow-soft">
|
||||
<Image
|
||||
src={heroCar.data.photos[0]}
|
||||
alt={`${heroCar.data.make} ${heroCar.data.model} automobilio nuotrauka`}
|
||||
width={1040}
|
||||
height={780}
|
||||
loading="eager"
|
||||
format="avif"
|
||||
class="aspect-[4/3] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-4 flex items-center justify-between text-sm text-muted">
|
||||
<span>Naujausias pasirinkimas</span>
|
||||
<span class="font-semibold text-burgundy-700">{heroCar.data.year} {heroCar.data.make} {heroCar.data.model}</span>
|
||||
</p>
|
||||
</a>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section id="automobiliai" class="mx-auto max-w-7xl px-5 sm:px-8 lg:px-10">
|
||||
<div class="mb-8 flex items-end justify-between gap-6">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{currentCars.length} vnt.</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.05em]">{lt.sections.currentCars}</h2>
|
||||
</div>
|
||||
<a class="hidden text-sm font-semibold text-burgundy-700 sm:block" href="/kontaktai">Norite parduoti automobilį?</a>
|
||||
</div>
|
||||
{currentCars.length > 0 ? (
|
||||
<div class="grid gap-x-7 gap-y-12 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{currentCars.map((car) => <CarCard car={car} />)}
|
||||
</div>
|
||||
) : (
|
||||
<div class="rounded-[2rem] border border-line bg-paper p-8">
|
||||
<p class="text-xl font-semibold">Šiuo metu aktyvių automobilių nėra.</p>
|
||||
<p class="mt-2 text-muted">Susisiekite, jei ieškote konkretaus modelio arba norite parduoti savo automobilį.</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{site.showSoldCars && soldCars.length > 0 && (
|
||||
<section class="mx-auto mt-20 max-w-7xl px-5 sm:px-8 lg:px-10">
|
||||
<h2 class="text-xl font-semibold tracking-[-0.04em]">{lt.sections.soldCars}</h2>
|
||||
<div class="mt-6 grid gap-x-7 gap-y-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{soldCars.map((car) => <CarCard car={car} compact />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
46
src/pages/kontaktai.astro
Normal file
46
src/pages/kontaktai.astro
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
import ContactButtons from '../components/ContactButtons.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { EMAIL, HOURS, PHONE_DISPLAY } from '../lib/contact';
|
||||
import { site } from '../site';
|
||||
---
|
||||
|
||||
<BaseLayout title="Kontaktai - Juozas Auto" description="Susisiekite telefonu, el. paštu arba WhatsApp dėl automobilio apžiūros Lietuvoje." canonicalPath="/kontaktai">
|
||||
<Header />
|
||||
<main class="mx-auto grid max-w-7xl gap-12 px-5 py-10 sm:px-8 lg:grid-cols-[0.8fr_1.2fr] lg:px-10">
|
||||
<section>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-burgundy-700">Kontaktai</p>
|
||||
<h1 class="mt-4 text-3xl font-semibold tracking-[-0.06em]">Susitarkime dėl apžiūros.</h1>
|
||||
<p class="mt-5 max-w-xl text-lg text-muted">Greičiausias būdas susisiekti yra skambutis arba WhatsApp žinutė. Atsakome darbo valandomis.</p>
|
||||
<div class="mt-8 max-w-md">
|
||||
<ContactButtons />
|
||||
</div>
|
||||
<dl class="mt-10 space-y-4 text-sm">
|
||||
<div><dt class="text-muted">Telefonas</dt><dd class="mt-1 font-semibold tabular">{PHONE_DISPLAY}</dd></div>
|
||||
<div><dt class="text-muted">El. paštas</dt><dd class="mt-1 font-semibold">{EMAIL}</dd></div>
|
||||
<div><dt class="text-muted">Darbo laikas</dt><dd class="mt-1 font-semibold">{HOURS}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="rounded-[2rem] border border-line bg-paper p-6 shadow-soft sm:p-8" aria-labelledby="sell-heading">
|
||||
<h2 id="sell-heading" class="text-2xl font-semibold tracking-[-0.05em]">Norite parduoti automobilį?</h2>
|
||||
<p class="mt-3 text-muted">Palikite kontaktą ir trumpą informaciją. Nuotraukas galėsite atsiųsti atsakius į užklausą.</p>
|
||||
{!site.formAction && (
|
||||
<p class="mt-5 rounded-2xl bg-burgundy-50 px-4 py-3 text-sm text-burgundy-900">
|
||||
Forma paruošta prijungimui. Iki tol greičiausias kontaktas yra telefonas arba WhatsApp.
|
||||
</p>
|
||||
)}
|
||||
<form class="mt-7 grid gap-4" action={site.formAction || undefined} method="post">
|
||||
<label class="grid gap-2 text-sm font-semibold">Vardas<input class="rounded-2xl border border-line bg-paper px-4 py-3 font-normal" name="name" autocomplete="name" required /></label>
|
||||
<label class="grid gap-2 text-sm font-semibold">Telefonas<input class="rounded-2xl border border-line bg-paper px-4 py-3 font-normal" name="phone" autocomplete="tel" required /></label>
|
||||
<label class="grid gap-2 text-sm font-semibold">Automobilis<input class="rounded-2xl border border-line bg-paper px-4 py-3 font-normal" name="car" placeholder="BMW 320d, 2018" required /></label>
|
||||
<label class="grid gap-2 text-sm font-semibold">Pastaba<textarea class="min-h-32 rounded-2xl border border-line bg-paper px-4 py-3 font-normal" name="message" placeholder="Trumpai apie komplektaciją, ridą, būklę ir nuotraukas." /></label>
|
||||
<p class="text-sm text-muted">Nuotraukų įkėlimas šiame etape nenaudojamas dėl paprasto ir greito statinio puslapio.</p>
|
||||
<button class="rounded-full bg-burgundy-700 px-5 py-4 font-semibold text-paper transition-transform duration-200 ease-out-quart hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60" type="submit" disabled={!site.formAction}>Siųsti užklausą</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
7
src/pages/robots.txt.ts
Normal file
7
src/pages/robots.txt.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { site } from '../site';
|
||||
|
||||
export function GET() {
|
||||
return new Response(`User-agent: *\nAllow: /\nSitemap: ${site.url}/sitemap-index.xml\n`, {
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
7
src/site.ts
Normal file
7
src/site.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const site = {
|
||||
name: 'Juozas Auto',
|
||||
url: 'https://auto.juozas.lt',
|
||||
description: 'Atrinkti naudoti automobiliai Lietuvoje. Be triukšmo, be paslėptų mokesčių.',
|
||||
showSoldCars: false,
|
||||
formAction: import.meta.env.PUBLIC_FORMSPREE_ENDPOINT ?? '',
|
||||
};
|
||||
83
src/styles/global.css
Normal file
83
src/styles/global.css
Normal file
@@ -0,0 +1,83 @@
|
||||
@import '@fontsource/geist-sans/400.css';
|
||||
@import '@fontsource/geist-sans/600.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background: theme('colors.paper');
|
||||
color: theme('colors.ink');
|
||||
font-kerning: normal;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, oklch(93% 0.03 24), transparent 34rem),
|
||||
theme('colors.paper');
|
||||
font-family: theme('fontFamily.sans');
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid theme('colors.burgundy.600');
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.measure {
|
||||
max-width: 68ch;
|
||||
}
|
||||
|
||||
.tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.prose-lite {
|
||||
color: theme('colors.ink');
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose-lite p + p {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user