Integrate panoramablick-saalbach.at
136
README.md
@ -1,135 +1,3 @@
|
||||
# Turborepo starter
|
||||
## Good to know
|
||||
|
||||
This Turborepo starter is maintained by the Turborepo core team.
|
||||
|
||||
## Using this example
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
npx create-turbo@latest
|
||||
```
|
||||
|
||||
## What's inside?
|
||||
|
||||
This Turborepo includes the following packages/apps:
|
||||
|
||||
### Apps and Packages
|
||||
|
||||
- `docs`: a [Next.js](https://nextjs.org/) app
|
||||
- `web`: another [Next.js](https://nextjs.org/) app
|
||||
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
|
||||
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
|
||||
- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo
|
||||
|
||||
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
|
||||
|
||||
### Utilities
|
||||
|
||||
This Turborepo has some additional tools already setup for you:
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/) for static type checking
|
||||
- [ESLint](https://eslint.org/) for code linting
|
||||
- [Prettier](https://prettier.io) for code formatting
|
||||
|
||||
### Build
|
||||
|
||||
To build all apps and packages, run the following command:
|
||||
|
||||
```
|
||||
cd my-turborepo
|
||||
|
||||
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
|
||||
turbo build
|
||||
|
||||
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
|
||||
npx turbo build
|
||||
yarn dlx turbo build
|
||||
pnpm exec turbo build
|
||||
```
|
||||
|
||||
You can build a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters):
|
||||
|
||||
```
|
||||
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
|
||||
turbo build --filter=docs
|
||||
|
||||
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
|
||||
npx turbo build --filter=docs
|
||||
yarn exec turbo build --filter=docs
|
||||
pnpm exec turbo build --filter=docs
|
||||
```
|
||||
|
||||
### Develop
|
||||
|
||||
To develop all apps and packages, run the following command:
|
||||
|
||||
```
|
||||
cd my-turborepo
|
||||
|
||||
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
|
||||
turbo dev
|
||||
|
||||
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
|
||||
npx turbo dev
|
||||
yarn exec turbo dev
|
||||
pnpm exec turbo dev
|
||||
```
|
||||
|
||||
You can develop a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters):
|
||||
|
||||
```
|
||||
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
|
||||
turbo dev --filter=web
|
||||
|
||||
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
|
||||
npx turbo dev --filter=web
|
||||
yarn exec turbo dev --filter=web
|
||||
pnpm exec turbo dev --filter=web
|
||||
```
|
||||
|
||||
### Remote Caching
|
||||
|
||||
> [!TIP]
|
||||
> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache).
|
||||
|
||||
Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
|
||||
|
||||
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands:
|
||||
|
||||
```
|
||||
cd my-turborepo
|
||||
|
||||
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
|
||||
turbo login
|
||||
|
||||
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
|
||||
npx turbo login
|
||||
yarn exec turbo login
|
||||
pnpm exec turbo login
|
||||
```
|
||||
|
||||
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
|
||||
|
||||
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
|
||||
|
||||
```
|
||||
# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended)
|
||||
turbo link
|
||||
|
||||
# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager
|
||||
npx turbo link
|
||||
yarn exec turbo link
|
||||
pnpm exec turbo link
|
||||
```
|
||||
|
||||
## Useful Links
|
||||
|
||||
Learn more about the power of Turborepo:
|
||||
|
||||
- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks)
|
||||
- [Caching](https://turborepo.com/docs/crafting-your-repository/caching)
|
||||
- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching)
|
||||
- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters)
|
||||
- [Configuration Options](https://turborepo.com/docs/reference/configuration)
|
||||
- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference)
|
||||
- Using custom tags directly inside the <template> tag will fuck up transitions, always wrap within an extra div
|
||||
774
apps/panoramablick-saalbach.at/app.config.ts
Normal file
@ -0,0 +1,774 @@
|
||||
import { LegalPageBuilder } from '#layers/content/utils/content-legal'
|
||||
import type { BusinessInfo, ContentMatrix } from '#layers/content/utils/content-types'
|
||||
import { t } from '#layers/content/utils/content-template'
|
||||
|
||||
const matrix: ContentMatrix = {
|
||||
locale: {
|
||||
default: 'de',
|
||||
list: [
|
||||
{ code: 'de', name: { de: 'Deutsch', en: 'German' }, icon: 'german' },
|
||||
{ code: 'en', name: { de: 'Englisch', en: 'English' }, icon: 'english' },
|
||||
],
|
||||
cookieMaxAge: 60 * 60 * 24 * 365
|
||||
},
|
||||
variant: {
|
||||
default: 'summer',
|
||||
list: [
|
||||
{ code: 'summer', name: { de: 'Sommer', en: 'Summer' }, icon: 'i-lucide-sun' },
|
||||
{ code: 'winter', name: { de: 'Winter', en: 'Winter' }, icon: 'i-lucide-snowflake' },
|
||||
],
|
||||
cookieMaxAge: 60 * 60 * 24 * 365
|
||||
}
|
||||
}
|
||||
|
||||
const business: BusinessInfo = {
|
||||
name: {
|
||||
de: 'Landhaus Panoramablick',
|
||||
en: 'Guesthouse Panoramablick'
|
||||
},
|
||||
operator: 'Monika & Norbert Pail',
|
||||
address: 'Unterer Ronachweg 731, 5753 Saalbach, AT',
|
||||
phone: '+43 664 7904775',
|
||||
email: 'info@panoramablick-saalbach.at',
|
||||
vat: 'ATU 62062608',
|
||||
authority: 'Bezirkshauptmannschaft Zell am See',
|
||||
membership: 'Wirtschaftskammer Salzburg',
|
||||
regulation: 'www.ris.bka.gv.at'
|
||||
}
|
||||
|
||||
const apartments = {
|
||||
1: {
|
||||
price: 98,
|
||||
capacity: {
|
||||
min: 2,
|
||||
max: 4
|
||||
},
|
||||
size: 38
|
||||
},
|
||||
2: {
|
||||
price: 155,
|
||||
capacity: {
|
||||
min: 2,
|
||||
max: 5
|
||||
},
|
||||
size: 50
|
||||
},
|
||||
3: {
|
||||
price: 155,
|
||||
capacity: {
|
||||
min: 2,
|
||||
max: 6
|
||||
},
|
||||
size: 50
|
||||
}
|
||||
}
|
||||
|
||||
function price(apartment: number) {
|
||||
return {
|
||||
de: `Ab ${apartments[apartment].price},- pro Tag`,
|
||||
en: `Starting at ${apartments[apartment].price},- per day`
|
||||
}
|
||||
}
|
||||
|
||||
function capacity(apartment: number) {
|
||||
return {
|
||||
de: `${apartments[apartment].capacity.min} bis ${apartments[apartment].capacity.max} Personen`,
|
||||
en: `${apartments[apartment].capacity.min} to ${apartments[apartment].capacity.max} persons`,
|
||||
}
|
||||
}
|
||||
|
||||
const apartment_features = [
|
||||
{
|
||||
icon: 'shower-head',
|
||||
label: {
|
||||
de: 'Badezimmer mit Dusche',
|
||||
en: 'Bathroom with shower'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'toilet',
|
||||
label: {
|
||||
de: 'Bad und WC getrennt',
|
||||
en: 'Bathroom and toilet in separate rooms'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'wifi',
|
||||
label: {
|
||||
de: 'Schnelles WLAN',
|
||||
en: 'High-speed WiFi'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'tv',
|
||||
label: {
|
||||
de: 'Fernseher',
|
||||
en: 'TV'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'microwave',
|
||||
label: {
|
||||
de: 'Mikrowelle',
|
||||
en: 'Microwave'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'microwave',
|
||||
label: {
|
||||
de: 'Backrohr',
|
||||
en: 'Oven'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bubbles',
|
||||
label: {
|
||||
de: 'Geschirrspüler',
|
||||
en: 'Dishwasher'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'coffee',
|
||||
label: {
|
||||
de: 'Kaffeemaschine',
|
||||
en: 'Coffee maker'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'coffee',
|
||||
label: {
|
||||
de: 'Wasserkocher',
|
||||
en: 'Electric water boiler'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed',
|
||||
label: {
|
||||
de: 'Bettwäsche',
|
||||
en: 'Bed sheets'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'layers',
|
||||
label: {
|
||||
de: 'Handtücher',
|
||||
en: 'Towels'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'wind',
|
||||
label: {
|
||||
de: 'Haarföhn',
|
||||
en: 'Hairdryer'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'gimblet',
|
||||
secondary: 'stone',
|
||||
neutral: 'sandstone'
|
||||
}
|
||||
},
|
||||
|
||||
content: {
|
||||
matrix: matrix,
|
||||
business: business,
|
||||
legal: LegalPageBuilder.build(),
|
||||
|
||||
cookies: {
|
||||
title: {
|
||||
de: 'Ihre Privatsphäre ist uns wichtig',
|
||||
en: 'We value your privacy'
|
||||
},
|
||||
description: {
|
||||
de: 'Wir verwenden keine Tracking-Cookies. Genießen Sie Ihren Aufenthalt auf unserer Website!',
|
||||
en: 'We do not use any tracking cookies. Enjoy your stay on our website!'
|
||||
}
|
||||
},
|
||||
header: {
|
||||
home: business.name,
|
||||
apartments: {
|
||||
de: 'Apartments & Preise',
|
||||
en: 'Apartments & Prices'
|
||||
},
|
||||
book: {
|
||||
de: 'Buchen',
|
||||
en: 'Book'
|
||||
},
|
||||
contact: {
|
||||
de: 'Kontakt',
|
||||
en: 'Contact'
|
||||
}
|
||||
},
|
||||
footer: {
|
||||
image: '/host.webp',
|
||||
questions: {
|
||||
de: 'Fragen oder Wünsche?',
|
||||
en: 'Questions or requests?'
|
||||
},
|
||||
prompt: {
|
||||
de: 'Monika freut sich von Ihnen zu hören!',
|
||||
en: 'Monika is looking forward to hearing from you!'
|
||||
},
|
||||
imprint: {
|
||||
de: 'Impressum',
|
||||
en: 'Imprint'
|
||||
},
|
||||
privacy: {
|
||||
de: 'Datenschutz',
|
||||
en: 'Privacy'
|
||||
},
|
||||
accessibility: {
|
||||
de: 'Barrierefreiheit',
|
||||
en: 'Accessibility'
|
||||
}
|
||||
},
|
||||
button: {
|
||||
apartments: {
|
||||
de: 'Apartments',
|
||||
en: 'Apartments'
|
||||
},
|
||||
book: {
|
||||
de: 'Buchen',
|
||||
en: 'Book'
|
||||
},
|
||||
contact: {
|
||||
de: 'Kontakt',
|
||||
en: 'Contact'
|
||||
},
|
||||
hosts: {
|
||||
de: 'Die Gastgeber',
|
||||
en: 'The Hosts'
|
||||
},
|
||||
map: {
|
||||
de: 'Maps Öffnen',
|
||||
en: 'Open Maps'
|
||||
}
|
||||
},
|
||||
landing: {
|
||||
welcome: {
|
||||
title: {
|
||||
de: t`Willkommen im ${'business.name'} in Saalbach`,
|
||||
en: t`Welcome to ${'business.name'} in Saalbach`
|
||||
},
|
||||
description: {
|
||||
de: 'Genießen Sie erholsame Tage inmitten der Salzburger Natur mit herrlichem Panoramablick auf die Berge von Saalbach. Unser Landhaus bietet gemütliche Apartments und herzliche Gastfreundschaft in perfekter Lage.',
|
||||
en: 'Enjoy relaxing days amidst the Salzburg nature with a stunning panoramic view of the Saalbach mountains. Our guesthouse offers cozy apartments and warm hospitality in a perfect location.'
|
||||
},
|
||||
images: {
|
||||
summer: [
|
||||
'/landing/2.webp',
|
||||
'/landing/1.webp',
|
||||
'/landing/3.webp',
|
||||
'/landing/4.webp'
|
||||
],
|
||||
winter: [
|
||||
'/landing/w1.webp',
|
||||
'/landing/w2.webp'
|
||||
]
|
||||
},
|
||||
joker: {
|
||||
'summer': true,
|
||||
'winter': false
|
||||
}
|
||||
},
|
||||
highlight: {
|
||||
title: {
|
||||
de: 'Erleben Sie Natur hautnah',
|
||||
en: 'Experience nature close up'
|
||||
},
|
||||
description: {
|
||||
'de@summer': 'Unser Landhaus liegt eingebettet in der idyllischen Salzburger Landschaft mit atemberaubendem Panoramablick auf Saalbach. Ob Wandern, Radfahren, die Umgebung erkunden, oder Entspannte Momente in der Sauna – bei uns finden Sie alles, was das Sommerherz begehrt.',
|
||||
'de@winter': 'Unser Landhaus liegt eingebettet in der idyllischen Salzburger Landschaft mit atemberaubendem Panoramablick auf Saalbach. Ob Schifahren, Winterwandern, die Umgebung erkunden, oder Entspannte Momente in der Sauna – bei uns finden Sie alles, was das Winterherz begehrt.',
|
||||
'en@summer': 'Our guesthouse is located in the idyllic Salzburg countryside with breathtaking panoramic views of Saalbach. Whether hiking, cycling, exploring the surroundings, or relaxing moments in the sauna – here you’ll find everything your heart desires.',
|
||||
'en@winter': 'Our guesthouse is located in the idyllic Salzburg countryside with breathtaking panoramic views of Saalbach. Whether skiing, winter hiking, exploring the surroundings, or enjoying relaxing moments in the sauna – here you’ll find everything your heart desires.'
|
||||
},
|
||||
image: {
|
||||
left: '/ap.webp',
|
||||
right: '/sauna.webp'
|
||||
}
|
||||
},
|
||||
location: {
|
||||
title: {
|
||||
de: 'Logieren Sie in bester Lage',
|
||||
en: 'Stay at the best location'
|
||||
},
|
||||
description: {
|
||||
'de@summer': 'Unser Landhaus liegt mitten in Saalbach. In wenigen Gehminuten sind Sie bei den Gondeln, im Bikepark, sowie im wunderschönen Ortskern von Saalbach. Lassen Sie Ihr Auto den Urlaub über stehen und genießen Sie die Zeit bei uns.',
|
||||
'de@winter': 'Unser Landhaus liegt mitten in Saalbach. In wenigen Gehminuten sind sie an der Skipiste sowie im wunderschönen Ortskern von Saalbach. Lassen Sie Ihr Auto den Urlaub über stehen und genießen Sie die Zeit bei uns.',
|
||||
'en@summer': 'Our guesthouse is located right in the heart of Saalbach. Within a few minutes\' walk, you\'ll reach the gondolas, the bike park, and the beautiful town center of Saalbach. Leave your car parked for the entire vacation and enjoy your time with us.',
|
||||
'en@winter': 'Our guesthouse is located right in the heart of Saalbach. Within a few minutes\' walk, you\'ll be at the ski slope and the beautiful town center of Saalbach. Leave your car parked for the entire vacation and enjoy your time with us.'
|
||||
},
|
||||
image: '/maps.webp'
|
||||
},
|
||||
apartments: {
|
||||
title: {
|
||||
de: 'Unsere Apartments',
|
||||
en: 'Our apartments'
|
||||
},
|
||||
description: {
|
||||
'de@summer': 'Wählen Sie aus unseren neu renovierten Appartements. Die Wohnungen sind großzügig gestaltet und bieten alle einen herrlichen Blick in die Saalbacher Berglandschaft. Dank einer raffinierter Bauweise genießen Sie auch an den heißesten Sommerabenden angenehme Temperaturen in unseren Zimmern, ganz ohne Klimaanlage. Konstante Wohlfühltemperaturen laden zum druchschlafen und erholen ein.',
|
||||
'de@winter': 'Wählen Sie aus unseren neu renovierten Appartements. Die Wohnungen sind großzügig gestaltet und bieten alle einen herrlichen Blick in die Saalbacher Berglandschaft. Freuen Sie sich auf gemütlich warme Winterabende in unseren drei Wohlfühlappartements.',
|
||||
'en@summer': 'Choose from our newly renovated apartments. The units are spaciously designed and all offer a wonderful view of the Saalbach mountain landscape. Thanks to clever construction, you can enjoy comfortable temperatures in our rooms even on the hottest summer evenings—completely without air conditioning. Consistent, pleasant temperatures invite you to sleep well and relax.',
|
||||
'en@winter': 'Choose from our newly renovated apartments. The units are spaciously designed and all offer a wonderful view of the Saalbach mountain landscape. Look forward to cozy, warm winter evenings in our three comfort apartments.'
|
||||
}
|
||||
},
|
||||
features: {
|
||||
title: {
|
||||
de: 'Darauf können Sie sich freuen!',
|
||||
en: 'That’s something to look forward to!'
|
||||
},
|
||||
description1: {
|
||||
de: 'Was Sie in Ihrem Urlaub im Landhaus Appartement Panoramablick erwartet. Unser Tipp:',
|
||||
en: 'What awaits you during your stay at Landhaus Appartement Panoramablick. Our insider tip:'
|
||||
},
|
||||
description2: {
|
||||
de: 'Reservieren',
|
||||
en: 'Book'
|
||||
},
|
||||
description3: {
|
||||
de: 'Sie online beim Sportgeschäft in nächster Nähe und sichern sich dabei exklusive Rabatte.',
|
||||
en: 'online at the nearby sports shop and secure exclusive discounts.'
|
||||
},
|
||||
list: [
|
||||
{
|
||||
icon: 'heart-handshake',
|
||||
label: {
|
||||
de: 'Herzliche Gastgeber',
|
||||
en: 'Warm-hearted hosts'
|
||||
},
|
||||
detail: {
|
||||
de: 'Persönliche Tipps & herzlicher Service direkt im Haus',
|
||||
en: 'Personalized tips & warm service right in the house'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'heater',
|
||||
label: {
|
||||
de: 'Sauna im Haus',
|
||||
en: 'Sauna available'
|
||||
},
|
||||
detail: {
|
||||
de: 'Sauna inklusive Infrarotkabine und Dampfsauna',
|
||||
en: 'Sauna including infrared cabin and steam sauna'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'wifi',
|
||||
label: {
|
||||
de: 'Schnelles WLAN',
|
||||
en: 'Fast WiFi'
|
||||
},
|
||||
detail: {
|
||||
de: 'Professionelles WLAN Equipment in jedem Appartement',
|
||||
en: 'Professional Wi-Fi equipment in every apartment'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'car-front',
|
||||
label: {
|
||||
de: 'Kostenlos parken',
|
||||
en: 'Free parking'
|
||||
},
|
||||
detail: {
|
||||
de: 'Kostenlose Stellplätze direkt vor der Tür',
|
||||
en: 'Free parking spaces right outside the door'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
'summer': 'bike',
|
||||
'winter': 'lock'
|
||||
},
|
||||
label: {
|
||||
'de@summer': 'Bikekeller',
|
||||
'de@winter': 'Skikeller',
|
||||
'en@summer': 'Bike storage room',
|
||||
'en@winter': 'Ski storage room'
|
||||
},
|
||||
detail: {
|
||||
de: 'Platz für Ihr Urlaubsequipment',
|
||||
en: 'Space for your holiday equipment'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'flame-kindling',
|
||||
label: {
|
||||
de: 'Grillplatz',
|
||||
en: 'Barbecue'
|
||||
},
|
||||
detail: {
|
||||
de: 'Kugelgrill und Smoker leihbar',
|
||||
en: 'Kettle grill and smoker available for rent'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'mountain',
|
||||
label: {
|
||||
de: 'Exzellente Lage',
|
||||
en: 'Excellent location'
|
||||
},
|
||||
detail: {
|
||||
de: '5 Min. zum Ortskern',
|
||||
en: '5 minutes to the town center'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'cable-car',
|
||||
label: {
|
||||
'de@summer': 'Bei den Gondeln',
|
||||
'de@winter': 'An der Skipiste',
|
||||
'en@summer': 'Right next to the gondolas',
|
||||
'en@winter': 'Ski-in/ski-out location'
|
||||
},
|
||||
detail: {
|
||||
'de@summer': 'Direkter Einstieg in den Bikepark Saalbach',
|
||||
'de@winter': 'Direkter Einstieg in den Skizirkus Saalbach',
|
||||
'en@summer': 'Direct access to the Saalbach bike park',
|
||||
'en@winter': 'The house is located next to the ski slope'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'id-card-lanyard',
|
||||
label: {
|
||||
'summer': 'Joker Card',
|
||||
'winter': 'Guest Mobility Ticket'
|
||||
},
|
||||
detail: {
|
||||
'de@summer': 'Kostenlose Aktivitäten von der ersten bis zur letzten Urlaubsminute inklusive!',
|
||||
'de@winter': 'Kostenlose Fahrten mit öffentlichen Verkehrsmitteln im Bundesland Salzburg',
|
||||
'en@summer': 'Free activities included from the first to the last minute of your vacation',
|
||||
'en@winter': 'Free rides on public transportation throughout Salzburg'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed',
|
||||
label: {
|
||||
de: 'Erholsamer Schlaf',
|
||||
en: 'Restful sleep'
|
||||
},
|
||||
detail: {
|
||||
'de@summer': 'Angenehme Zimmertemperaturen auch an heißen Sommertagen',
|
||||
'de@winter': 'Kuschelige Wohlfühlbetten für einen erholsamen Schlaf',
|
||||
'en@summer': 'Comfortable room temperatures even on hot summer days',
|
||||
'en@winter': 'Cozy comfort beds for a restful sleep'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'trees',
|
||||
label: {
|
||||
de: 'Atemberaubendes Panorama',
|
||||
en: 'Breathtaking panorama'
|
||||
},
|
||||
detail: {
|
||||
de: 'Genießen Sie die Aussicht über das Home of Lässig',
|
||||
en: 'Enjoy the wonderful view over the Home of Lässig'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'store',
|
||||
label: {
|
||||
de: 'Sportgeschäft',
|
||||
en: 'Sports shop'
|
||||
},
|
||||
detail: {
|
||||
de: 'Exklusive Rabatte bei Sport Bründl bei Vorabreservierung',
|
||||
en: 'Exclusive discounts at Sport Bründl with advance reservation'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
contact: {
|
||||
title: {
|
||||
de: 'Kontakt aufnehmen',
|
||||
'de@winter': 'Winterkontakt',
|
||||
en: 'Contact us'
|
||||
},
|
||||
description: {
|
||||
de: 'Wir freuen uns auf Ihre Anfrage – Sie erreichen uns per Telefon, SMS, WhatsApp, E-Mail oder über das untenstehende Formular.',
|
||||
en: 'We’re happy to hear from you – contact us by phone, SMS, WhatsApp, email, or through the form below.'
|
||||
},
|
||||
phone: business.phone,
|
||||
email: business.email,
|
||||
online1: {
|
||||
de: 'Sie wollen direkt online buchen?',
|
||||
en: 'Ready to book online?'
|
||||
},
|
||||
online2: {
|
||||
de: 'Hier',
|
||||
en: 'Here'
|
||||
},
|
||||
online3: {
|
||||
de: 'geht\'s lang!',
|
||||
en: 'you go!'
|
||||
},
|
||||
form: {
|
||||
name: {
|
||||
de: 'Ihr Name',
|
||||
en: 'Your name'
|
||||
},
|
||||
email: {
|
||||
de: 'Ihre E-Mail',
|
||||
en: 'Your email address'
|
||||
},
|
||||
subject: {
|
||||
de: 'Betreff',
|
||||
en: 'Subject'
|
||||
},
|
||||
message: {
|
||||
de: 'Ihre Anfrage',
|
||||
en: 'Your inquiry'
|
||||
},
|
||||
send: {
|
||||
de: 'Senden',
|
||||
en: 'Send'
|
||||
}
|
||||
},
|
||||
heroes: {
|
||||
parents: {
|
||||
title: 'Monika & Norbert',
|
||||
description: {
|
||||
de: 'Herzliche Gastgeber – immer mit einem Tipp zur Region parat.',
|
||||
en: 'Genuine hospitality – and always a great tip for exploring the area.'
|
||||
},
|
||||
image: '/family/monika-norbert.webp'
|
||||
},
|
||||
children: {
|
||||
title: 'Sabrina & Daniel',
|
||||
description: {
|
||||
de: 'Jung, aktiv und voller Energie – Sohn Daniel und Tochter Sabrina bringen Schwung ins Haus.',
|
||||
en: 'Energetic and full of life – Daniel and Sabrina keep the house buzzing.'
|
||||
},
|
||||
image: '/family/sabrina-daniel.webp'
|
||||
}
|
||||
}
|
||||
},
|
||||
apartments: {
|
||||
'highlight': {
|
||||
title: {
|
||||
de: 'Darauf können Sie sich freuen!',
|
||||
en: 'Here’s what you can look forward to!'
|
||||
},
|
||||
description: {
|
||||
de: 'Was Sie in Ihrem Urlaub im Landhaus Appartement Panoramablick erwartet.',
|
||||
en: 'What to expect during your vacation at Landhaus Appartement Panoramablick.'
|
||||
},
|
||||
image: {
|
||||
left: '/ap.webp',
|
||||
right: '/sauna.webp'
|
||||
}
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: 'apartment-1',
|
||||
title: 'Apartment 1',
|
||||
subtitle: {
|
||||
de: `${apartments[1].size} m² Urlaubsvergnügen! Dieses gemütliche Appartement bietet Ihnen Platz für bis zu ${apartments[1].capacity.max} Personen. Freuen Sie sich auf 1 Schlafzimmer, eine Wohnküche mit ausziehbarer Couch, Badezimmer, eine kleine Terrasse und ein getrenntes WC.`,
|
||||
en: `${apartments[1].size} m² of holiday comfort! This cozy apartment comfortably accommodates up to ${apartments[1].capacity.max} guests. It features one bedroom, an open-plan kitchen and living area with a pull-out sofa, a bathroom, a small terrace, and a separate toilet.`
|
||||
},
|
||||
thumbnail: '/apartments/2/4.webp',
|
||||
highlights: [
|
||||
capacity(1),
|
||||
price(1)
|
||||
],
|
||||
images: [
|
||||
'/apartments/1/1.webp',
|
||||
'/apartments/1/2.webp',
|
||||
'/apartments/1/3.webp',
|
||||
'/apartments/1/4.webp',
|
||||
'/apartments/1/5.webp',
|
||||
'/apartments/1/6.webp',
|
||||
'/apartments/1/7.webp',
|
||||
'/apartments/1/8.webp',
|
||||
'/apartments/1/9.webp',
|
||||
'/apartments/1/10.webp',
|
||||
'/apartments/1/11.webp'
|
||||
],
|
||||
features: [
|
||||
{
|
||||
icon: 'coins',
|
||||
label: price(1)
|
||||
},
|
||||
{
|
||||
icon: 'users',
|
||||
label: capacity(1),
|
||||
},
|
||||
{
|
||||
icon: 'scaling',
|
||||
label: `${apartments[1].size} m²`
|
||||
},
|
||||
{
|
||||
icon: 'mountain',
|
||||
label: {
|
||||
de: 'Terrasse mit idyllischem Ausblick',
|
||||
en: 'Terrace with idyllic view'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed-double',
|
||||
label: {
|
||||
de: 'Schlafzimmer mit Doppelbett',
|
||||
en: 'Bedroom with double bed'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'sofa',
|
||||
label: {
|
||||
de: 'Wohnküche mit ausziehbarem Schlafsofa',
|
||||
en: 'Living/kitchen area with sofa bed'
|
||||
}
|
||||
},
|
||||
...apartment_features
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'apartment-2',
|
||||
title: 'Apartment 2',
|
||||
subtitle: {
|
||||
de: `Wohnkomfort auf ${apartments[2].size} m²! Dieses großzügige Appartement bietet Ihnen Platz für bis zu ${apartments[2].capacity.max} Personen und bietet 2 Schlafzimmer, eine Wohnküche, Badezimmer, einen Balkon und ein getrenntes WC.`,
|
||||
en: `Enjoy ${apartments[2].size} m² of comfortable living! This large apartment sleeps up to ${apartments[2].capacity.max} people and includes 2 bedrooms, a combined kitchen and living room, a bathroom, a balcony, and a separate toilet.`
|
||||
},
|
||||
thumbnail: '/apartments/2/8.webp',
|
||||
highlights: [
|
||||
capacity(2),
|
||||
price(2)
|
||||
],
|
||||
images: [
|
||||
'/apartments/2/1.webp',
|
||||
'/apartments/2/2.webp',
|
||||
'/apartments/2/3.webp',
|
||||
'/apartments/2/4.webp',
|
||||
'/apartments/2/5.webp',
|
||||
'/apartments/2/6.webp',
|
||||
'/apartments/2/7.webp',
|
||||
'/apartments/2/8.webp',
|
||||
'/apartments/2/9.webp',
|
||||
'/apartments/2/10.webp',
|
||||
'/apartments/2/11.webp',
|
||||
'/apartments/2/12.webp',
|
||||
'/apartments/2/13.webp'
|
||||
],
|
||||
features: [
|
||||
{
|
||||
icon: 'coins',
|
||||
label: price(2)
|
||||
},
|
||||
{
|
||||
icon: 'users',
|
||||
label: capacity(2),
|
||||
},
|
||||
{
|
||||
icon: 'scaling',
|
||||
label: `${apartments[2].size} m²`
|
||||
},
|
||||
{
|
||||
icon: 'mountain',
|
||||
label: {
|
||||
de: 'Balkon mit idyllischem Ausblick',
|
||||
en: 'Balcony with idyllic view'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed-double',
|
||||
label: {
|
||||
de: 'Schlafzimmer 1 mit Doppelbett',
|
||||
en: 'Bedroom 1 with double bed'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed-double',
|
||||
label: {
|
||||
de: 'Schlafzimmer 2 mit Doppelbett & Schlafsofa',
|
||||
en: 'Bedroom 2 with double bed and sofa bed'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'sofa',
|
||||
label: {
|
||||
de: 'Wohnküche',
|
||||
en: 'Living/kitchen area'
|
||||
}
|
||||
},
|
||||
...apartment_features
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'apartment-3',
|
||||
title: 'Apartment 3',
|
||||
subtitle: {
|
||||
de: `Wohlfühlen auf ${apartments[3].size} m²! Dieses geräumige Appartement bietet Ihnen Platz für bis zu ${apartments[3].capacity.max} Personen und bietet 2 Schlafzimmer, eine Wohnküche, Badezimmer, einen Balkon und ein getrenntes WC.`,
|
||||
en: `Spacious ${apartments[3].size} m² apartment for up to ${apartments[3].capacity.max} guests, with 2 bedrooms, a living kitchen, bathroom, balcony, and separate toilet.`
|
||||
},
|
||||
thumbnail: '/apartments/2/7.webp',
|
||||
highlights: [
|
||||
capacity(3),
|
||||
price(3)
|
||||
],
|
||||
images: [
|
||||
'/apartments/3/1.webp',
|
||||
'/apartments/3/2.webp',
|
||||
'/apartments/3/3.webp',
|
||||
'/apartments/3/4.webp',
|
||||
'/apartments/3/5.webp',
|
||||
'/apartments/3/6.webp',
|
||||
'/apartments/3/7.webp',
|
||||
'/apartments/3/8.webp',
|
||||
'/apartments/3/9.webp',
|
||||
'/apartments/3/10.webp',
|
||||
'/apartments/3/11.webp',
|
||||
'/apartments/3/12.webp',
|
||||
'/apartments/3/13.webp',
|
||||
'/apartments/3/14.webp'
|
||||
],
|
||||
features: [
|
||||
{
|
||||
icon: 'coins',
|
||||
label: price(3)
|
||||
},
|
||||
{
|
||||
icon: 'users',
|
||||
label: capacity(3)
|
||||
},
|
||||
{
|
||||
icon: 'scaling',
|
||||
label: `${apartments[3].size} m²`
|
||||
},
|
||||
{
|
||||
icon: 'mountain',
|
||||
label: {
|
||||
de: 'Balkon mit idyllischem Ausblick',
|
||||
en: 'Balcony with idyllic view'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed-double',
|
||||
label: {
|
||||
de: 'Schlafzimmer 1 mit Doppelbett',
|
||||
en: 'Bedroom 1 with double bed'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'bed-double',
|
||||
label: {
|
||||
de: 'Schlafzimmer 2 mit Doppelbett & Schlafsofa',
|
||||
en: 'Bedroom 2 with double bed and sofa bed'
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: 'sofa',
|
||||
label: {
|
||||
de: 'Wohnküche',
|
||||
en: 'Living/kitchen area'
|
||||
}
|
||||
},
|
||||
...apartment_features
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
16
apps/panoramablick-saalbach.at/app.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.page-enter-from,
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<NuxtPage/>
|
||||
</template>
|
||||
@ -1,8 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Hello World!</h1>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LocaleSwitcher/>
|
||||
<br/>
|
||||
<VariantSwitcher/>
|
||||
</template>
|
||||
110
apps/panoramablick-saalbach.at/assets/css/main.css
Normal file
@ -0,0 +1,110 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
.red {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.green {
|
||||
border: 1px solid green;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-outline {
|
||||
-webkit-text-stroke: 1px black;
|
||||
color: gold;
|
||||
}
|
||||
|
||||
.pxr {
|
||||
@apply px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.pyr {
|
||||
@apply py-4 sm:py-6 lg:py-8;
|
||||
}
|
||||
|
||||
.pr {
|
||||
@apply p-4 sm:p-6 lg:p-8;
|
||||
}
|
||||
}
|
||||
|
||||
@theme static {
|
||||
--color-gimblet: oklch(74.37% 0.06969 91.48);
|
||||
--color-gimblet-50: oklch(97.851% 0.00669 97.351);
|
||||
--color-gimblet-100: oklch(95.198% 0.01238 91.52);
|
||||
--color-gimblet-200: oklch(90.103% 0.02653 90.109);
|
||||
--color-gimblet-300: oklch(84.858% 0.04176 91.748);
|
||||
--color-gimblet-400: oklch(79.678% 0.056 91.049);
|
||||
--color-gimblet-500: oklch(74.37% 0.06969 91.48);
|
||||
--color-gimblet-600: oklch(66.9% 0.08657 90.713);
|
||||
--color-gimblet-700: oklch(55.892% 0.07041 91.887);
|
||||
--color-gimblet-800: oklch(44.164% 0.05385 91.131);
|
||||
--color-gimblet-900: oklch(31.471% 0.0355 91.688);
|
||||
|
||||
--color-driftwood: oklch(72% 0.065 228);
|
||||
--color-driftwood-50: oklch(97.5% 0.008 228);
|
||||
--color-driftwood-100: oklch(94.5% 0.015 228);
|
||||
--color-driftwood-200: oklch(88.8% 0.028 228);
|
||||
--color-driftwood-300: oklch(83% 0.042 228);
|
||||
--color-driftwood-400: oklch(77% 0.056 228);
|
||||
--color-driftwood-500: oklch(72% 0.065 228);
|
||||
--color-driftwood-600: oklch(63% 0.07 228);
|
||||
--color-driftwood-700: oklch(50% 0.06 228);
|
||||
--color-driftwood-800: oklch(38% 0.045 228);
|
||||
--color-driftwood-900: oklch(27% 0.03 228);
|
||||
|
||||
|
||||
--color-sapphire-50: oklch(97.5% 0.005 260);
|
||||
--color-sapphire-100: oklch(95.2% 0.01 260);
|
||||
--color-sapphire-200: oklch(90% 0.02 260);
|
||||
--color-sapphire-300: oklch(84% 0.035 260);
|
||||
--color-sapphire-400: oklch(78% 0.05 260);
|
||||
--color-sapphire-500: oklch(72% 0.065 260);
|
||||
--color-sapphire-600: oklch(65% 0.075 260);
|
||||
--color-sapphire-700: oklch(54% 0.06 260);
|
||||
--color-sapphire-800: oklch(42% 0.045 260);
|
||||
--color-sapphire-900: oklch(30% 0.03 260);
|
||||
|
||||
--color-stone-50: oklch(98% 0.002 260); /* lightest gray */
|
||||
--color-stone-100: oklch(94% 0.004 260);
|
||||
--color-stone-200: oklch(88% 0.006 260);
|
||||
--color-stone-300: oklch(80% 0.008 260);
|
||||
--color-stone-400: oklch(70% 0.01 260);
|
||||
--color-stone-500: oklch(60% 0.012 260); /* true neutral gray */
|
||||
--color-stone-600: oklch(48% 0.012 260);
|
||||
--color-stone-700: oklch(36% 0.01 260);
|
||||
--color-stone-800: oklch(26% 0.008 260);
|
||||
--color-stone-900: oklch(16% 0.006 260); /* darkest gray */
|
||||
|
||||
|
||||
--color-clay-50: oklch(96% 0.008 35);
|
||||
--color-clay-100: oklch(92% 0.015 35);
|
||||
--color-clay-200: oklch(86% 0.03 35);
|
||||
--color-clay-300: oklch(80% 0.05 35);
|
||||
--color-clay-400: oklch(74% 0.07 35);
|
||||
--color-clay-500: oklch(70% 0.08 35);
|
||||
--color-clay-600: oklch(62% 0.09 35);
|
||||
--color-clay-700: oklch(50% 0.07 35);
|
||||
--color-clay-800: oklch(38% 0.05 35);
|
||||
--color-clay-900: oklch(28% 0.035 35);
|
||||
|
||||
|
||||
--color-sandstone-50: oklch(98% 0.003 85);
|
||||
--color-sandstone-100: oklch(96% 0.006 85);
|
||||
--color-sandstone-200: oklch(92% 0.012 85);
|
||||
--color-sandstone-300: oklch(87% 0.02 85);
|
||||
--color-sandstone-400: oklch(80% 0.03 85);
|
||||
--color-sandstone-500: oklch(72% 0.035 85);
|
||||
--color-sandstone-600: oklch(63% 0.03 85);
|
||||
--color-sandstone-700: oklch(50% 0.025 85);
|
||||
--color-sandstone-800: oklch(38% 0.02 85);
|
||||
--color-sandstone-900: oklch(26% 0.015 85);
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-radius: 0rem;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
5
apps/panoramablick-saalbach.at/components/AppAlert.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
This is an auto-imported component
|
||||
</div>
|
||||
</template>
|
||||
61
apps/panoramablick-saalbach.at/components/AppApartment.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<section class="w-full">
|
||||
<!-- ❶ two-column card ----------------------------->
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<!-- ◀ left half : image carousel -->
|
||||
<AppThumbnailCarousel :images="apartment.images" class="basis-1/3"></AppThumbnailCarousel>
|
||||
|
||||
<!-- ▶ right half : text (top) & icons (bottom) -->
|
||||
<div class="basis-2/3 flex flex-col h-full p-4 gap-4">
|
||||
<!-- title + intro copy -->
|
||||
<div class="flex-1 flex flex-col gap-4">
|
||||
<!-- title -->
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ apartment.title }}
|
||||
</h2>
|
||||
|
||||
<!-- description (optional) -->
|
||||
<p
|
||||
v-if="true"
|
||||
class="text-lg pb-4">
|
||||
{{
|
||||
apartment.subtitle
|
||||
}}
|
||||
</p>
|
||||
|
||||
<!-- anything the parent puts in here -->
|
||||
<AppFeaturesGrid :features="apartment.features"/>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-row gap-4 ">
|
||||
<UButton
|
||||
:to="p('contact')"
|
||||
size="md"
|
||||
color="primary"
|
||||
variant="outline"
|
||||
trailing-icon="i-heroicons-envelope"
|
||||
>
|
||||
{{ c.button.contact }}
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
:to="p('book')"
|
||||
size="md"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-calendar-days"
|
||||
>
|
||||
{{ c.button.book }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
defineProps<{ apartment: Apartment }>()
|
||||
</script>
|
||||
24
apps/panoramablick-saalbach.at/components/AppCardSection.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
id: {type: String, default: undefined},
|
||||
|
||||
padTop: {type: Boolean, default: true},
|
||||
padBottom: {type: Boolean, default: true}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- bind the id → renders only when props.id is truthy -->
|
||||
<section :id="props.id" class="w-full">
|
||||
<AppStripe>
|
||||
<div :class="[
|
||||
props.padTop && 'pt-12',
|
||||
props.padBottom && 'pb-12'
|
||||
]">
|
||||
<div class="bg-neutral-100 border border-neutral-200">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</AppStripe>
|
||||
</section>
|
||||
</template>
|
||||
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid gap-y-8 gap-x-10
|
||||
grid-cols-[repeat(auto-fit,minmax(12rem,1fr))]">
|
||||
|
||||
<div
|
||||
v-for="(feat, idx) in features"
|
||||
:key="idx"
|
||||
class="flex items-start gap-4">
|
||||
|
||||
<UIcon
|
||||
:name="`i-lucide-${feat.icon}`"
|
||||
class="w-6 h-6 shrink-0 text-primary-600 mt-0.5"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold text-neutral-700 leading-tight">
|
||||
{{ feat.label }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="feat.detail"
|
||||
class="text-sm text-neutral-600">
|
||||
{{ feat.detail }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Feature {
|
||||
icon: string
|
||||
label: string
|
||||
detail?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
features: Feature[]
|
||||
}>()
|
||||
</script>
|
||||
11
apps/panoramablick-saalbach.at/components/AppFlatSection.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<section class="w-full bg-neutral-100 border-t border-b border-neutral-200">
|
||||
<AppStripe>
|
||||
<div class="py-12">
|
||||
<slot/>
|
||||
</div>
|
||||
</AppStripe>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
78
apps/panoramablick-saalbach.at/components/AppFooter.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<footer class="bg-neutral-100 border-t border-neutral-200">
|
||||
<AppStripe>
|
||||
<!-- 📌 contact block -->
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between
|
||||
gap-4 py-4 border-b border-neutral-300"
|
||||
>
|
||||
<!-- avatar + copy -->
|
||||
<AppHero :src="c.footer.image"
|
||||
alt="Ihre Gastgeberin Monika"
|
||||
:size="16"
|
||||
:title="c.footer.questions"
|
||||
:description="c.footer.prompt">
|
||||
<!-- Contact shortcuts -->
|
||||
<div class="mt-2 space-y-1">
|
||||
<!-- Phone (phone + WhatsApp icons) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Heroicons phone -->
|
||||
<UIcon name="i-heroicons-phone" class="w-4 h-4 text-sm text-neutral-600"/>
|
||||
<!-- WhatsApp icon (any Iconify set you use) -->
|
||||
<UIcon name="i-uil-whatsapp" class="w-4 h-4 text-sm text-neutral-600"/>
|
||||
<a :href="`tel:${c.contact.phone.replace(/\s+/g, '')}`" class="hover:underline text-sm text-neutral-600">
|
||||
{{ c.contact.phone }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- E-mail -->
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-envelope" class="w-4 h-4 text-sm text-neutral-600"/>
|
||||
<a :href="`mailto:${c.contact.email}`" class="hover:underline text-sm text-neutral-600">
|
||||
{{ c.contact.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</AppHero>
|
||||
|
||||
<div class="flex flex-col gap-4 ">
|
||||
<UButton
|
||||
:to="p('contact')"
|
||||
size="md"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-envelope"
|
||||
>
|
||||
{{ c.button.contact }}
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
:to="p('book')"
|
||||
size="md"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-calendar-days"
|
||||
>
|
||||
{{ c.button.book }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- © line -->
|
||||
<div class="pt-4 text-center text-sm text-neutral-600 flex flex-col py-4">
|
||||
<span>© {{ year }} Panoramablick Saalbach</span>
|
||||
<div>
|
||||
<NuxtLink :to="p('legal', 'imprint')" class="underline ml-2">{{ c.footer.imprint }}</NuxtLink>
|
||||
<NuxtLink :to="p('legal', 'privacy')" class="underline ml-2">{{ c.footer.privacy }}</NuxtLink>
|
||||
<NuxtLink :to="p('legal', 'accessibility')" class="underline ml-2">{{ c.footer.accessibility }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</AppStripe>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
const year = new Date().getFullYear()
|
||||
</script>
|
||||
36
apps/panoramablick-saalbach.at/components/AppHeader.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<header class="sticky top-0 z-50 bg-neutral-200 shadow text-neutral-700">
|
||||
<AppStripe>
|
||||
<nav class="mx-auto py-4 flex items-center justify-between">
|
||||
<!-- your logo / home link -->
|
||||
<NuxtLink :to="p('/')" class="text-xl font-semibold flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-home" />
|
||||
|
||||
<span class="hidden md:inline">
|
||||
{{ c.header.home }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
|
||||
|
||||
<!-- nav links -->
|
||||
<ul class="flex space-x-6">
|
||||
<li>
|
||||
<NuxtLink :to="p('apartments')">{{ c.header.apartments }}</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink :to="p('book')">{{ c.header.book }}</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink :to="p('contact')">{{ c.header.contact }}</NuxtLink>
|
||||
</li>
|
||||
<LocaleSwitcher/>
|
||||
<VariantSwitcher/>
|
||||
</ul>
|
||||
</nav>
|
||||
</AppStripe>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
</script>
|
||||
55
apps/panoramablick-saalbach.at/components/AppHero.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
/* Path or URL for the portrait image */
|
||||
src: {type: String, required: true},
|
||||
/* Alt text for the image */
|
||||
alt: {type: String, default: ''},
|
||||
/**
|
||||
* Tailwind size token (e.g. 14 → w-14 h-14 or "full" → w-full h-full).
|
||||
* Defaults to 14 (= 3.5 rem = 56 px).
|
||||
*/
|
||||
size: {type: [Number, String], default: 14},
|
||||
/* Heading / description */
|
||||
title: {type: String, required: true},
|
||||
description: {type: String, required: true},
|
||||
/* Image left or right */
|
||||
imageSide: {type: String as () => 'left' | 'right', default: 'left'},
|
||||
/* Extra wrapper classes */
|
||||
wrapperClass: {type: String, default: ''}
|
||||
})
|
||||
|
||||
/* Tailwind width/height classes */
|
||||
const sizeClasses = computed(() => `w-${props.size} h-${props.size}`)
|
||||
|
||||
/* If the token is numeric, convert to px for NuxtImg width/height props */
|
||||
const numericSize = computed(() => {
|
||||
const n = Number(props.size)
|
||||
return Number.isFinite(n) ? n * 4 /* Tailwind token ×4 px */ : undefined
|
||||
})
|
||||
|
||||
const wrapperFlex = computed(() =>
|
||||
props.imageSide === 'right' ? 'flex-row-reverse text-right' : 'flex-row text-left'
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['flex items-center gap-4', wrapperFlex, wrapperClass]">
|
||||
<NuxtImg
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:width="numericSize"
|
||||
:height="numericSize"
|
||||
class="rounded-full object-cover shrink-0"
|
||||
:class="sizeClasses"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="font-semibold text-neutral-700">{{ title }}</p>
|
||||
<p class="text-sm text-neutral-600">{{ description }}</p>
|
||||
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
12
apps/panoramablick-saalbach.at/components/AppStripe.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({inheritAttrs: false})
|
||||
const {innerClass = 'px-4 sm:px-6 lg:px-8'} = defineProps<{ innerClass?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex justify-center">
|
||||
<div :class="['w-full max-w-6xl', innerClass]" v-bind="$attrs">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
// import {ref} from 'vue'
|
||||
// const {t, tm, rt} = useVariantData()
|
||||
// import {useTemplateRef} from '#imports'
|
||||
//
|
||||
interface Props {
|
||||
images?: string[] // ❶ Fragezeichen = optional
|
||||
}
|
||||
//
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
images: () => [
|
||||
'https://picsum.photos/200/300'
|
||||
]
|
||||
})
|
||||
|
||||
/* ───────── shared slider state ───────── */
|
||||
const carousel = useTemplateRef('carousel')
|
||||
const activeIndex = ref(0)
|
||||
|
||||
function onSelect(index: number) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
|
||||
function select(index: number) {
|
||||
activeIndex.value = index
|
||||
carousel.value?.emblaApi?.scrollTo(index)
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-start p-4">
|
||||
<!-- main slider -->
|
||||
<UCarousel
|
||||
ref="carousel"
|
||||
auto-height
|
||||
arrows
|
||||
prev-icon="i-lucide-chevron-left"
|
||||
next-icon="i-lucide-chevron-right"
|
||||
v-slot="{ item }"
|
||||
:items="props.images"
|
||||
:autoplay="{ delay: 5_000 }"
|
||||
:ui="{item: 'ps-0',
|
||||
controls: 'absolute top-6 inset-x-15',
|
||||
dots: '-top-7',
|
||||
dot: 'w-1 h-1'}"
|
||||
loop
|
||||
class="relative w-full"
|
||||
@select="onSelect"
|
||||
>
|
||||
<img :src="item" class="object-cover" alt="Bild"/>
|
||||
</UCarousel>
|
||||
|
||||
<!-- thumbnails -->
|
||||
<!-- <div class="flex flex-wrap gap-2 pt-4">-->
|
||||
<!-- <button-->
|
||||
<!-- v-for="(thumb, idx) in props.images"-->
|
||||
<!-- :key="idx"-->
|
||||
<!-- @click="select(idx)"-->
|
||||
<!-- :class="[-->
|
||||
<!-- 'size-16 rounded-lg overflow-hidden opacity-40 hover:opacity-100',-->
|
||||
<!-- 'transition-opacity focus:outline-none focus-visible:ring',-->
|
||||
<!-- activeIndex === idx && 'opacity-100 ring-2 ring-primary-500'-->
|
||||
<!-- ]"-->
|
||||
<!-- >-->
|
||||
<!-- <img :src="thumb" alt="" class="w-full h-full object-cover"/>-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</template>
|
||||
27
apps/panoramablick-saalbach.at/components/AppTitleText.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<!-- components/TransBlock.vue -->
|
||||
<template>
|
||||
<div class="flex-1">
|
||||
<!-- title -->
|
||||
<h2 class="text-2xl font-bold mb-4">
|
||||
{{ title }}
|
||||
</h2>
|
||||
|
||||
<!-- description (optional) -->
|
||||
<p
|
||||
v-if="text"
|
||||
class="text-lg pb-4">
|
||||
{{ text }}
|
||||
</p>
|
||||
|
||||
<!-- anything the parent puts in here -->
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
title: string,
|
||||
text?: string
|
||||
}>()
|
||||
|
||||
</script>
|
||||
22
apps/panoramablick-saalbach.at/composables/useApartments.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// ~/composables/useApartments.ts
|
||||
import {computed} from 'vue' // ① explizit importieren
|
||||
import {useI18n} from 'apps/panoramablick-saalbach.at/.nuxt/imports'
|
||||
|
||||
export interface Apartment {
|
||||
title: string
|
||||
subtitle: string
|
||||
images: string[]
|
||||
features: { icon: string; label: string }[]
|
||||
}
|
||||
|
||||
export function useApartments() {
|
||||
const {t} = useI18n()
|
||||
|
||||
const list = computed(() =>
|
||||
t('apartments') as unknown as Apartment[]
|
||||
)
|
||||
|
||||
const lst = list//[1, 2, 3]
|
||||
|
||||
return {lst}
|
||||
}
|
||||
16
apps/panoramablick-saalbach.at/layouts/default.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<!-- <UApp :toaster="{ position: 'top-center' }">-->
|
||||
<div class="flex flex-col min-h-dvh bg-neutral-50">
|
||||
|
||||
<AppHeader/>
|
||||
|
||||
<main class="flex-1">
|
||||
<slot/>
|
||||
</main>
|
||||
|
||||
<AppFooter/>
|
||||
</div>
|
||||
<!-- </UApp>-->
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@ -2,5 +2,18 @@
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
extends: [ '@layers/content' ]
|
||||
})
|
||||
extends: [ '@layers/content' ],
|
||||
modules: [
|
||||
"@nuxt/ui",
|
||||
"@nuxt/image",
|
||||
"@nuxt/fonts",
|
||||
"@nuxt/icon"
|
||||
],
|
||||
css: [ '~/assets/css/main.css' ],
|
||||
ui: {
|
||||
colorMode: false
|
||||
},
|
||||
app: {
|
||||
pageTransition: { name: 'page', mode: 'out-in' }
|
||||
},
|
||||
})
|
||||
@ -10,9 +10,15 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@layers/content": "workspace:0.0.0",
|
||||
"@nuxt/fonts": "^0.11.4",
|
||||
"@nuxt/icon": "^2.0.0",
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxt/ui": "^4.0.1",
|
||||
"nuxt": "^4.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"valibot": "^1.1.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1",
|
||||
"@layers/content": "workspace:0.0.0"
|
||||
"vue-router": "^4.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
<!-- /error.vue (Nuxt automatically serves this for 404 routes) -->
|
||||
<template>
|
||||
<section
|
||||
class="flex min-h-screen flex-col items-center justify-center
|
||||
bg-neutral-100 px-4 text-center"
|
||||
>
|
||||
<!-- Icon -->
|
||||
<UIcon
|
||||
name="i-heroicons-face-frown"
|
||||
class="h-16 w-16 text-primary-600 mb-6"
|
||||
/>
|
||||
|
||||
<!-- Headline -->
|
||||
<h1 class="text-4xl font-bold text-neutral-900 mb-2">
|
||||
Seite nicht gefunden
|
||||
</h1>
|
||||
|
||||
<!-- Sub-copy -->
|
||||
<p class="max-w-md text-neutral-600 mb-8">
|
||||
Die gewünschte Seite existiert leider nicht (mehr) oder der Link war
|
||||
fehlerhaft. Versuchen Sie es über die Startseite noch einmal.
|
||||
</p>
|
||||
|
||||
<!-- CTA -->
|
||||
<UButton
|
||||
to="/"
|
||||
size="lg"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-arrow-uturn-left"
|
||||
>
|
||||
Zur Startseite
|
||||
</UButton>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({layout: false, statusCode: 404})
|
||||
</script>
|
||||
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="w-full h-full bg-neutral-50">
|
||||
<AppFlatSection>
|
||||
<div class="flex flex-col md:flex-row items-center gap-8">
|
||||
<!-- Left: Image Group -->
|
||||
<div class="flex flex-row gap-4 justify-center relative">
|
||||
<img :src="c.apartments.highlight.image.left" alt="Image 1" class="w-40 h-60 object-cover"/>
|
||||
<img :src="c.apartments.highlight.image.right" class="w-40 h-60 object-cover mt-6" />
|
||||
</div>
|
||||
|
||||
<AppTitleText :title="c.apartments.highlight.title" :text="c.apartments.highlight.description">
|
||||
<AppFeaturesGrid :features="c.landing.features.list"/>
|
||||
</AppTitleText>
|
||||
</div>
|
||||
</AppFlatSection>
|
||||
|
||||
<AppCardSection
|
||||
v-for="(apartment, index) in c.apartments.list"
|
||||
:key="apartment.id"
|
||||
:padTop="index === 0"
|
||||
|
||||
>
|
||||
<AppApartment :apartment="apartment" :id="apartment.id"/>
|
||||
</AppCardSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {c} = useContent()
|
||||
</script>
|
||||
@ -0,0 +1,47 @@
|
||||
<!--<script setup lang="ts">-->
|
||||
<!--/* Inject the external assets into <head> */-->
|
||||
<!--useHead({-->
|
||||
<!-- link: [-->
|
||||
<!-- {-->
|
||||
<!-- rel: 'stylesheet',-->
|
||||
<!-- href: 'https://mainframe.capcorn.net/ressourcen/newUI/css/capcorn.css'-->
|
||||
<!-- }-->
|
||||
<!-- ],-->
|
||||
<!-- script: [-->
|
||||
<!-- {-->
|
||||
<!-- src: 'https://mainframe.capcorn.net/ressourcen/newUI/js/jquery.js',-->
|
||||
<!-- tagPosition: 'bodyClose', // load at the end of <body>-->
|
||||
<!-- defer: true // optional-->
|
||||
<!-- },-->
|
||||
<!-- {-->
|
||||
<!-- src: 'https://mainframe.capcorn.net/ressourcen/newUI/js/capcorn.js',-->
|
||||
<!-- tagPosition: 'bodyClose',-->
|
||||
<!-- defer: true-->
|
||||
<!-- }-->
|
||||
<!-- ]-->
|
||||
<!--})-->
|
||||
<!--</script>-->
|
||||
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AppFlatSection>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<span class="text-lg text-neutral-600">Coming Soon</span>
|
||||
</div>
|
||||
|
||||
<!-- <!– SSR-safe: iframe appears only in the browser –>-->
|
||||
<!-- <!– <ClientOnly>-->
|
||||
<!-- <iframe-->
|
||||
<!-- id="iframeCapCorn"-->
|
||||
<!-- src="https://www.capcorn.net/MasterReq?MB=1487&FL=17&LG=0"-->
|
||||
<!-- frameborder="0"-->
|
||||
<!-- width="100%"-->
|
||||
<!-- scrolling="auto"-->
|
||||
<!-- />-->
|
||||
<!-- </ClientOnly> –>-->
|
||||
</AppFlatSection>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Contact‑centric layout -->
|
||||
<AppFlatSection>
|
||||
<!-- Grid: form (fixed max‑width) | host snapshots -->
|
||||
<div class="flex flex-col md:flex-row gap-8 items-center">
|
||||
<!-- ─── Contact form ─── -->
|
||||
<div class="w-full md:w-auto max-w-xl mx-auto lg:mx-0">
|
||||
<h1 class="text-3xl sm:text-4xl font-bold mb-4">{{ c.contact.title }}</h1>
|
||||
|
||||
<!-- Extended intro -->
|
||||
<p class="text-neutral-600 mb-4 max-w-prose">
|
||||
{{ c.contact.description }}
|
||||
</p>
|
||||
|
||||
<!-- Contact shortcuts -->
|
||||
<div class="mb-8 space-y-1 text-sm">
|
||||
<!-- Phone (phone + WhatsApp icons) -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Heroicons phone -->
|
||||
<UIcon name="i-heroicons-phone" class="w-4 h-4 text-neutral-600"/>
|
||||
<!-- WhatsApp icon (any Iconify set you use) -->
|
||||
<UIcon name="i-uil-whatsapp" class="w-4 h-4 text-neutral-600"/>
|
||||
<a :href="`tel:${c.contact.phone.replace(/\s+/g, '')}`" class="hover:underline text-neutral-600">
|
||||
{{ c.contact.phone }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- E-mail -->
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-envelope" class="w-4 h-4 text-neutral-600"/>
|
||||
<a :href="`mailto:${c.contact.email}`" class="hover:underline text-neutral-600">
|
||||
{{ c.contact.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-neutral-600 mb-4 max-w-prose">
|
||||
{{ c.contact.online1 }}
|
||||
<UButton :to="p('book')" variant="outline" trailing-icon="i-heroicons-calendar-days">{{
|
||||
c.contact.online2
|
||||
}}
|
||||
</UButton>
|
||||
{{ c.contact.online3 }}
|
||||
</p>
|
||||
|
||||
<!-- Form -->
|
||||
<UForm :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormField name="name" label="Name" :ui="{ label: 'sr-only' }">
|
||||
<UInput v-model="state.name" class="w-full" :placeholder="c.contact.form.name"/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="email" label="E-Mail" :ui="{ label: 'sr-only' }">
|
||||
<UInput v-model="state.email" type="email" class="w-full"
|
||||
:placeholder="c.contact.form.email"/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="subject" label="Betreff" :ui="{ label: 'sr-only' }">
|
||||
<UInput v-model="state.subject" class="w-full" :placeholder="c.contact.form.subject"/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="message" label="Nachricht" :ui="{ label: 'sr-only' }">
|
||||
<UTextarea v-model="state.message" :rows="6" class="w-full"
|
||||
:placeholder="c.contact.form.message"/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" color="primary" size="lg" class="w-full sm:w-auto" trailing-icon="i-heroicons-paper-airplane">
|
||||
{{ c.contact.form.send }}
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ─── Decorative host snapshots ─── -->
|
||||
<div id="hosts" class="flex flex-col gap-10 lg:self-center w-full md:w-auto">
|
||||
<AppHero
|
||||
:src="c.contact.heroes.parents.image"
|
||||
:alt="c.contact.heroes.parents.title"
|
||||
image-side="left"
|
||||
:size="50"
|
||||
:title="c.contact.heroes.parents.title"
|
||||
:description="c.contact.heroes.parents.description"
|
||||
/>
|
||||
|
||||
<AppHero
|
||||
:src="c.contact.heroes.children.image"
|
||||
:alt="c.contact.heroes.children.title"
|
||||
image-side="right"
|
||||
:size="50"
|
||||
:title="c.contact.heroes.children.title"
|
||||
:description="c.contact.heroes.children.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppFlatSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/*TODO form should contain link to privacy statement?*/
|
||||
const {p, c} = useContent()
|
||||
import * as v from 'valibot'
|
||||
import type {FormSubmitEvent} from '@nuxt/ui'
|
||||
|
||||
/* ───── validation schema ───── */
|
||||
const schema = v.object({
|
||||
name: v.pipe(v.string(), v.minLength(2, 'Bitte Namen eingeben')),
|
||||
email: v.pipe(v.string(), v.email('Ungültige E-Mail')),
|
||||
subject: v.pipe(v.string(), v.minLength(3, 'Betreff fehlt')),
|
||||
message: v.pipe(v.string(), v.minLength(10, 'Nachricht ist zu kurz'))
|
||||
})
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
/* ───── reactive form state ───── */
|
||||
const state = reactive<Schema>({
|
||||
name: '', email: '', subject: '', message: ''
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
/* ───── submit handler ───── */
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
const config = useRuntimeConfig()
|
||||
const hotelId = config.public.hotelId
|
||||
|
||||
if (!hotelId) {
|
||||
toast.add({
|
||||
title: 'Fehler',
|
||||
description: 'Hotel-ID ist nicht konfiguriert!',
|
||||
color: 'primary'
|
||||
})
|
||||
throw new Error('Hotel ID not configured')
|
||||
}
|
||||
|
||||
try {
|
||||
await $fetch('https://api.dominikmilacher.com/contact', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: {
|
||||
...event.data,
|
||||
hotel: hotelId
|
||||
}
|
||||
})
|
||||
|
||||
toast.add({
|
||||
title: 'Nachricht gesendet',
|
||||
description: 'Vielen Dank – wir melden uns bald.',
|
||||
color: 'primary'
|
||||
})
|
||||
|
||||
/* reset fields */
|
||||
Object.assign(state, {name: '', email: '', subject: '', message: ''})
|
||||
} catch (err: any) {
|
||||
toast.add({
|
||||
title: 'Fehler',
|
||||
description: err?.data?.detail ?? err?.message ?? 'Nachricht konnte nicht gesendet werden.',
|
||||
color: 'primary'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-0 z-0">
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
:items="c.landing.welcome.images"
|
||||
:ui="{item: 'basis-full h-full ps-0', container: 'flex items-stretch h-full'}"
|
||||
:autoplay="{ delay: 5000 }"
|
||||
:loop="true"
|
||||
:fade="true"
|
||||
:duration="150"
|
||||
class="w-full h-full custom-carousel">
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<img :src="item" class="w-full h-full object-cover" alt="Demo Picture"/>
|
||||
</div>
|
||||
</UCarousel>
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-0 z-5 bg-black/60"/>
|
||||
|
||||
<div class="relative z-10">
|
||||
<AppStripe class="text-white py-4 sm:py-8 lg:py-16">
|
||||
<div class="flex flex-col md:flex-row py-6 gap-4 sm:gap-8 lg:gap-16">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<h1 class="text-5xl max-w-4xl font-bold">{{ c.landing.welcome.title }}</h1>
|
||||
<p class="mt-4 text-lg">
|
||||
{{
|
||||
c.landing.welcome.description
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-8 flex gap-4">
|
||||
<UButton :to="p('apartments')" color="primary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-arrow-right">{{ c.button.apartments }}
|
||||
</UButton>
|
||||
<UButton :to="p('book')" color="secondary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-calendar-days">
|
||||
{{ c.button.book }}
|
||||
</UButton>
|
||||
<UButton :to="p('contact')" color="secondary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-envelope">
|
||||
{{ c.button.contact }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="c.landing.welcome.joker === true" class="flex items-center justify-center p-8">
|
||||
<a href="https://www.saalbach.com/de/sommer/joker-card">
|
||||
<NuxtImg
|
||||
src="/joker-card.webp"
|
||||
alt="Joker Card"
|
||||
class="w-30 md:w-50 transform rotate-15"
|
||||
style="border-radius: .5rem"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</AppStripe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppFlatSection>
|
||||
<div class="flex flex-col md:flex-row items-center gap-4">
|
||||
<!-- Left: Image Group -->
|
||||
<div class="flex flex-row gap-4 justify-center relative">
|
||||
<img :src="c.landing.highlight.image.left" alt="Image 1" class="w-40 h-60 object-cover"/>
|
||||
<img :src="c.landing.highlight.image.right" class="w-40 h-60 object-cover mt-6" />
|
||||
</div>
|
||||
|
||||
<!-- Right: Text Content -->
|
||||
<AppTitleText :title="c.landing.highlight.title" :text="c.landing.highlight.description">
|
||||
<UButton :to="p('contact', 'hosts')" color="primary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-arrow-right">
|
||||
{{
|
||||
c.button.hosts
|
||||
}}
|
||||
</UButton>
|
||||
</AppTitleText>
|
||||
</div>
|
||||
</AppFlatSection>
|
||||
|
||||
<AppCardSection>
|
||||
<div class="flex flex-col md:flex-row items-center gap-4">
|
||||
|
||||
<!-- Left: Text (40%) -->
|
||||
<div class="basis-2/5 pr">
|
||||
<AppTitleText :title="c.landing.location.title" :text="c.landing.location.description">
|
||||
<UButton to="https://maps.app.goo.gl/FtTC8ervoBCnfU3j7" color="primary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-arrow-right">
|
||||
{{ c.button.map }}
|
||||
</UButton>
|
||||
</AppTitleText>
|
||||
</div>
|
||||
|
||||
<!-- Right: Image (60%) -->
|
||||
<a
|
||||
href="https://maps.app.goo.gl/FtTC8ervoBCnfU3j7"
|
||||
class="basis-3/5 flex justify-center overflow-hidden group"
|
||||
aria-label="Google Maps öffnen"
|
||||
>
|
||||
<img
|
||||
:src="c.landing.location.image"
|
||||
alt="Vorschaubild der Lage in Google Maps"
|
||||
class="w-full h-80 object-cover transition-transform duration-300 ease-out group-hover:scale-110"
|
||||
/>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</AppCardSection>
|
||||
|
||||
<AppFlatSection>
|
||||
<AppTitleText :title="c.landing.apartments.title" :text="c.landing.apartments.description">
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
|
||||
<NuxtLink v-for="apartment in c.apartments.list" :to="p('apartments', apartment.id)"
|
||||
class="relative group block w-60 h-80 overflow-hidden">
|
||||
<img :src="apartment.thumbnail" alt="Image 1"
|
||||
class="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"/>
|
||||
<div class="absolute inset-0 bg-black/50"></div>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center text-white text-lg font-semibold drop-shadow-md">
|
||||
<div class="text-center">
|
||||
<h2>{{ apartment.title }}</h2>
|
||||
<ul>
|
||||
<li v-for="highlight in apartment.highlights" class="text-sm">{{ highlight }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</AppTitleText>
|
||||
</AppFlatSection>
|
||||
|
||||
<AppCardSection>
|
||||
<div class="flex-1 pr">
|
||||
<h2 class="text-2xl font-bold mb-4">
|
||||
{{ c.landing.features.title }}
|
||||
</h2>
|
||||
|
||||
<p class="text-lg pb-4">
|
||||
{{ c.landing.features.description1 }}
|
||||
<UButton to="https://www.bruendl.at/" variant="outline" trailing-icon="i-heroicons-arrow-right">{{
|
||||
c.landing.features.description2
|
||||
}}
|
||||
</UButton>
|
||||
{{ c.landing.features.description3 }}
|
||||
</p>
|
||||
|
||||
<AppFeaturesGrid :features="c.landing.features.list"/>
|
||||
</div>
|
||||
</AppCardSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* TODO this is somewhat hacky, but currently no other way to style carousel inner wrapper */
|
||||
:deep(.custom-carousel > .overflow-hidden) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppFlatSection>
|
||||
<section v-for="block in c.legal"
|
||||
:key="block.anchor"
|
||||
:id="block.anchor"
|
||||
class="prose mx-auto max-w-3xl p-4 lg:p-8">
|
||||
<article
|
||||
|
||||
class="mb-12"
|
||||
>
|
||||
<h1 class="text-3xl font-bold mb-6 whitespace-pre-line">{{ block.title }}</h1>
|
||||
|
||||
<section
|
||||
v-for="s in block.sections"
|
||||
:key="s.title"
|
||||
class="mb-8 whitespace-pre-line"
|
||||
>
|
||||
<h2 class="text-2xl font-semibold mb-4">{{ s.title }}</h2>
|
||||
|
||||
<p v-for="p in s.paragraphs" :key="p" class="mb-3 leading-relaxed whitespace-pre-line">
|
||||
{{ p }}
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
</AppFlatSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {c} = useContent()
|
||||
</script>
|
||||
BIN
apps/panoramablick-saalbach.at/public/ap.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/1.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/10.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/11.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/2.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/3.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/4.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/5.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/6.webp
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/7.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/8.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/1/9.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 803 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 980 KiB |
|
After Width: | Height: | Size: 775 KiB |
|
After Width: | Height: | Size: 891 KiB |
|
After Width: | Height: | Size: 931 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 517 KiB |
|
After Width: | Height: | Size: 1005 KiB |
|
After Width: | Height: | Size: 695 KiB |
|
After Width: | Height: | Size: 800 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/1.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/10.webp
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/11.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/12.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/13.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/2.webp
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/3.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/4.webp
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/5.webp
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/6.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/7.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/8.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/2/9.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 896 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 749 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 695 KiB |
|
After Width: | Height: | Size: 935 KiB |
|
After Width: | Height: | Size: 987 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 886 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
After Width: | Height: | Size: 658 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/1.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/10.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/11.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/12.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/13.webp
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/14.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/2.webp
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/3.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/4.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/5.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/6.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/7.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/8.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
apps/panoramablick-saalbach.at/public/apartments/3/9.webp
Normal file
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 710 KiB |
|
After Width: | Height: | Size: 695 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
After Width: | Height: | Size: 538 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 748 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 788 KiB |
|
After Width: | Height: | Size: 937 KiB |