Overhaul content management
Some checks failed
Build and deploy updated apps / Build & deploy (push) Failing after 1m7s
Some checks failed
Build and deploy updated apps / Build & deploy (push) Failing after 1m7s
This commit is contained in:
parent
1025cc0786
commit
73083ded58
@ -1,170 +1,3 @@
|
||||
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 * 3
|
||||
},
|
||||
variant: {
|
||||
default: 'winter',
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const business: BusinessInfo = {
|
||||
uid: 'panoramablick-saalbach.at',
|
||||
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: {
|
||||
@ -173,646 +6,4 @@ export default defineAppConfig({
|
||||
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',
|
||||
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: {
|
||||
prompt: {
|
||||
de: 'Ihr Name',
|
||||
en: 'Your name'
|
||||
},
|
||||
invalid: {
|
||||
de: 'Name zu kurz',
|
||||
en: 'Name too short'
|
||||
}
|
||||
},
|
||||
email: {
|
||||
prompt: {
|
||||
de: 'Ihre E-Mail',
|
||||
en: 'Your email address'
|
||||
},
|
||||
invalid: {
|
||||
de: 'E-Mail Adresse nicht gültig',
|
||||
en: 'Email address not valid'
|
||||
}
|
||||
},
|
||||
subject: {
|
||||
prompt: {
|
||||
de: 'Betreff',
|
||||
en: 'Subject'
|
||||
},
|
||||
invalid: {
|
||||
de: 'Betreff zu kurz',
|
||||
en: 'Subject too short'
|
||||
}
|
||||
},
|
||||
message: {
|
||||
prompt: {
|
||||
de: 'Ihre Anfrage',
|
||||
en: 'Your inquiry'
|
||||
},
|
||||
invalid: {
|
||||
de: 'Nachricht zu kurz',
|
||||
en: 'Message too short'
|
||||
}
|
||||
},
|
||||
send: {
|
||||
de: 'Senden',
|
||||
en: 'Send'
|
||||
},
|
||||
sent: {
|
||||
title: {
|
||||
de: 'Nachricht gesendet',
|
||||
en: 'Message sent'
|
||||
},
|
||||
description: {
|
||||
de: 'Vielen Dank - wir melden uns bald.',
|
||||
en: 'Thank you - we\'ll be in touch soon.'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: {
|
||||
de: 'Fehler',
|
||||
en: 'Error'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Nachricht konnte nicht versendet werden - bitte versuchen Sie es erneut.',
|
||||
en: 'The message could not be sent - please try again.'
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -5,15 +5,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { c } = useContent()
|
||||
const { l } = useContentInjected()
|
||||
const { add } = useToast()
|
||||
const shown = useCookie('cookie-toast-shown', { maxAge: 60 * 60 * 24 * 7 })
|
||||
|
||||
onMounted(() => {
|
||||
if (!shown.value) {
|
||||
add({
|
||||
title: c.value.cookies.title,
|
||||
description: c.value.cookies.description,
|
||||
title: l.value.cookies.title,
|
||||
description: l.value.cookies.description,
|
||||
timeout: 8000,
|
||||
color: 'primary',
|
||||
icon: 'i-heroicons-shield-check',
|
||||
|
||||
7
apps/panoramablick-saalbach.at/app.yaml
Normal file
7
apps/panoramablick-saalbach.at/app.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
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!
|
||||
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
This is an auto-imported component
|
||||
</div>
|
||||
</template>
|
||||
@ -36,7 +36,7 @@
|
||||
variant="outline"
|
||||
trailing-icon="i-heroicons-envelope"
|
||||
>
|
||||
{{ c.button.contact }}
|
||||
{{ g.button.contact }}
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
@ -46,7 +46,7 @@
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-calendar-days"
|
||||
>
|
||||
{{ c.button.book }}
|
||||
{{ g.button.book }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -56,6 +56,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
defineProps<{ apartment: Apartment }>()
|
||||
const {g, p} = useContentInjected()
|
||||
defineProps<{ apartment: Apartment, index: number }>()
|
||||
</script>
|
||||
@ -7,11 +7,11 @@
|
||||
gap-4 py-4 border-b border-neutral-300"
|
||||
>
|
||||
<!-- avatar + copy -->
|
||||
<AppHero :src="c.footer.image"
|
||||
<AppHero :src="l.image"
|
||||
alt="Ihre Gastgeberin Monika"
|
||||
:size="16"
|
||||
:title="c.footer.questions"
|
||||
:description="c.footer.prompt">
|
||||
:title="l.questions"
|
||||
:description="l.prompt">
|
||||
<!-- Contact shortcuts -->
|
||||
<div class="mt-2 space-y-1">
|
||||
<!-- Phone (phone + WhatsApp icons) -->
|
||||
@ -20,16 +20,16 @@
|
||||
<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 :href="`tel:${g.business.phone.replace(/\s+/g, '')}`" class="hover:underline text-sm text-neutral-600">
|
||||
{{ g.business.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 :href="`mailto:${g.business.email}`" class="hover:underline text-sm text-neutral-600">
|
||||
{{ g.business.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,7 +43,7 @@
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-envelope"
|
||||
>
|
||||
{{ c.button.contact }}
|
||||
{{ g.button.contact }}
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
@ -53,7 +53,7 @@
|
||||
variant="solid"
|
||||
trailing-icon="i-heroicons-calendar-days"
|
||||
>
|
||||
{{ c.button.book }}
|
||||
{{ g.button.book }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
@ -61,11 +61,11 @@
|
||||
|
||||
<!-- © line -->
|
||||
<div class="pt-4 text-center text-sm text-neutral-600 flex flex-col py-4">
|
||||
<span>© {{ year }} Panoramablick Saalbach</span>
|
||||
<span>© {{ year }} {{ g.business.name }}</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>
|
||||
<NuxtLink :to="p('legal#imprint')" class="underline ml-2">{{ l.imprint }}</NuxtLink>
|
||||
<NuxtLink :to="p('legal#privacy')" class="underline ml-2">{{ l.privacy }}</NuxtLink>
|
||||
<NuxtLink :to="p('legal#accessibility')" class="underline ml-2">{{ l.accessibility }}</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</AppStripe>
|
||||
@ -73,6 +73,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
const {g, l, p} = useContentInjected()
|
||||
const year = new Date().getFullYear()
|
||||
</script>
|
||||
|
||||
16
apps/panoramablick-saalbach.at/components/AppFooter.yaml
Normal file
16
apps/panoramablick-saalbach.at/components/AppFooter.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
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
|
||||
@ -3,11 +3,11 @@
|
||||
<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">
|
||||
<NuxtLink :to="p('[locale]/[variant]')" class="text-xl font-semibold flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-home" />
|
||||
|
||||
<span class="hidden md:inline">
|
||||
{{ c.header.home }}
|
||||
{{ l.home }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
:to="p('apartments')"
|
||||
class="block sm:hidden"
|
||||
>
|
||||
{{ c.header.apartments.split(' ')[0] }}
|
||||
{{ l.apartments.split(' ')[0] }}
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Desktop: Show full text -->
|
||||
@ -28,14 +28,14 @@
|
||||
:to="p('apartments')"
|
||||
class="hidden sm:block"
|
||||
>
|
||||
{{ c.header.apartments }}
|
||||
{{ l.apartments }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink :to="p('book')">{{ c.header.book }}</NuxtLink>
|
||||
<NuxtLink :to="p('book')">{{ l.book }}</NuxtLink>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink :to="p('contact')">{{ c.header.contact }}</NuxtLink>
|
||||
<NuxtLink :to="p('contact')">{{ l.contact }}</NuxtLink>
|
||||
</li>
|
||||
<LocaleSwitcher/>
|
||||
<VariantSwitcher/>
|
||||
@ -46,5 +46,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
const {l, p} = useContentInjected()
|
||||
</script>
|
||||
|
||||
10
apps/panoramablick-saalbach.at/components/AppHeader.yaml
Normal file
10
apps/panoramablick-saalbach.at/components/AppHeader.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
home: ${business.name}
|
||||
apartments:
|
||||
$de: Apartments & Preise
|
||||
$en: Apartments & Prices
|
||||
book:
|
||||
$de: Buchen
|
||||
$en: Book
|
||||
contact:
|
||||
$de: Kontakt
|
||||
$en: Contact
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<AppFeaturesGrid :features="l"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {l} = useContentInjected()
|
||||
</script>
|
||||
96
apps/panoramablick-saalbach.at/components/AppHighlights.yaml
Normal file
96
apps/panoramablick-saalbach.at/components/AppHighlights.yaml
Normal file
@ -0,0 +1,96 @@
|
||||
- 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
|
||||
@ -1,22 +0,0 @@
|
||||
// ~/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}
|
||||
}
|
||||
26
apps/panoramablick-saalbach.at/composables/useSeoLinking.ts
Normal file
26
apps/panoramablick-saalbach.at/composables/useSeoLinking.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export function useSeoLinking() {
|
||||
const config = useContentConfig()
|
||||
const locales = config.locale?.list.map(l => l.code) ?? []
|
||||
|
||||
const {locale} = useContentInjected()
|
||||
|
||||
const route = useRoute()
|
||||
const base = 'https://panoramablick-saalbach.at'
|
||||
|
||||
const links = computed(() => {
|
||||
const links = []
|
||||
|
||||
for (const l of locales) {
|
||||
const path = '/' + l + route.path.slice(3)
|
||||
links.push({rel: 'alternate', hreflang: l, href: base + path})
|
||||
|
||||
if (l === locale.value) {
|
||||
links.push({rel: 'canonical', href: base + path})
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
})
|
||||
|
||||
useHead(() => ({link: links.value}))
|
||||
}
|
||||
47
apps/panoramablick-saalbach.at/content.global.mts
Normal file
47
apps/panoramablick-saalbach.at/content.global.mts
Normal file
@ -0,0 +1,47 @@
|
||||
class Text {
|
||||
constructor(readonly $de: string, readonly $en: string) {}
|
||||
}
|
||||
|
||||
class Price {
|
||||
readonly text: Text
|
||||
|
||||
constructor(readonly from: number) {
|
||||
this.text = new Text(
|
||||
`Ab ${this.from},- pro Tag`,
|
||||
`Starting at ${this.from},- per day`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Capacity {
|
||||
readonly text: Text
|
||||
|
||||
constructor(readonly from: number, readonly to: number) {
|
||||
this.text = new Text(
|
||||
`${this.from} bis ${this.to} Personen`,
|
||||
`${this.from} to ${this.to} persons`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Apartment {
|
||||
readonly id: string
|
||||
readonly title: string
|
||||
readonly price: Price
|
||||
readonly capacity: Capacity
|
||||
|
||||
constructor(number: number, readonly size: number, price: number, from: number, to: number) {
|
||||
this.id = `apartment-${number}`
|
||||
this.title = `Apartment ${number}`
|
||||
this.price = new Price(price)
|
||||
this.capacity = new Capacity(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
apartments: [
|
||||
new Apartment(1, 38, 98, 2, 4),
|
||||
new Apartment(2, 50, 155, 2, 5),
|
||||
new Apartment(3, 50, 155, 2, 6)
|
||||
]
|
||||
}
|
||||
30
apps/panoramablick-saalbach.at/content.global.yaml
Normal file
30
apps/panoramablick-saalbach.at/content.global.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
business:
|
||||
uid: panoramablick-saalbach.at
|
||||
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
|
||||
|
||||
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
|
||||
@ -29,12 +29,5 @@ export default defineNuxtConfig({
|
||||
icon: {
|
||||
provider: 'server',
|
||||
serverBundle: 'local'
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
crawlLinks: true,
|
||||
routes: ["/de/summer"],
|
||||
failOnError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -14,6 +14,7 @@
|
||||
"@nuxt/fonts": "^0.11.4",
|
||||
"@nuxt/icon": "^2.0.0",
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxt/kit": "^4.1.3",
|
||||
"@nuxt/ui": "^4.0.1",
|
||||
"nuxt": "^4.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
class Text {
|
||||
constructor(readonly $de: string, readonly $en: string) {
|
||||
}
|
||||
}
|
||||
|
||||
class Feature {
|
||||
readonly label: Text | string
|
||||
|
||||
constructor(readonly icon: string, readonly de: string, readonly en?: string) {
|
||||
this.label = en ? new Text(de, en) : de
|
||||
}
|
||||
}
|
||||
|
||||
const features = [
|
||||
new Feature('shower-head', 'Badezimmer mit Dusche', 'Bathroom with shower'),
|
||||
new Feature('toilet', 'Bad und WC getrennt', 'Bathroom and toilet in separate rooms'),
|
||||
new Feature('wifi', 'Schnelles WLAN', 'High-speed WiFi'),
|
||||
new Feature('tv', 'Fernseher', 'TV'),
|
||||
new Feature('microwave', 'Mikrowelle', 'Microwave'),
|
||||
new Feature('microwave', 'Backrohr', 'Oven'),
|
||||
new Feature('bubbles', 'Geschirrspüler', 'Dishwasher'),
|
||||
new Feature('coffee', 'Kaffeemaschine', 'Coffee maker'),
|
||||
new Feature('coffee', 'Wasserkocher', 'Electric water boiler'),
|
||||
new Feature('bed', 'Bettwäsche', 'Bed sheets'),
|
||||
new Feature('layers', 'Handtücher', 'Towels'),
|
||||
new Feature('wind', 'Haarföhn', 'Hairdryer')
|
||||
]
|
||||
|
||||
class Apartment {
|
||||
readonly title: string
|
||||
readonly subtitle: Text
|
||||
readonly features: Feature[]
|
||||
|
||||
constructor(index: number, readonly de: string, readonly en: string, specifics: Feature[], readonly images: string[]) {
|
||||
this.title = '${apartments[' + index.toString() + '].title}'
|
||||
this.subtitle = new Text(de, en)
|
||||
|
||||
this.features = [
|
||||
new Feature('coins', '${apartments[' + index.toString() + '].price.text}'),
|
||||
new Feature('users', '${apartments[' + index.toString() + '].capacity.text}'),
|
||||
new Feature('scaling', '${apartments[' + index.toString() + '].size} m²'),
|
||||
...specifics,
|
||||
...features
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
list: [
|
||||
new Apartment(
|
||||
0,
|
||||
'${apartments[0].size} m² Urlaubsvergnügen! Dieses gemütliche Appartement bietet Ihnen Platz für bis zu ${apartments[0].capacity.to} Personen. Freuen Sie sich auf 1 Schlafzimmer, eine Wohnküche mit ausziehbarer Couch, Badezimmer, eine kleine Terrasse und ein getrenntes WC.',
|
||||
'${apartments[0].size} m² of holiday comfort! This cozy apartment comfortably accommodates up to ${apartments[0].capacity.to} 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.',
|
||||
[
|
||||
new Feature('mountain', 'Terrasse mit idyllischem Ausblick', 'Terrace with idyllic view'),
|
||||
new Feature('bed-double', 'Schlafzimmer mit Doppelbett', 'Bedroom with double bed'),
|
||||
new Feature('sofa', 'Wohnküche mit ausziehbarem Schlafsofa', 'Living/kitchen area with sofa bed'),
|
||||
],
|
||||
[
|
||||
'/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'
|
||||
]
|
||||
),
|
||||
new Apartment(
|
||||
1,
|
||||
'Wohnkomfort auf ${apartments[1].size} m²! Dieses großzügige Appartement bietet Ihnen Platz für bis zu ${apartments[1].capacity.to} Personen und bietet 2 Schlafzimmer, eine Wohnküche, Badezimmer, einen Balkon und ein getrenntes WC.',
|
||||
'Enjoy ${apartments[1].size} m² of comfortable living! This large apartment sleeps up to ${apartments[1].capacity.to} people and includes 2 bedrooms, a combined kitchen and living room, a bathroom, a balcony, and a separate toilet.',
|
||||
[
|
||||
new Feature('mountain', 'Balkon mit idyllischem Ausblick', 'Balcony with idyllic view'),
|
||||
new Feature('bed-double', 'Schlafzimmer 1 mit Doppelbett', 'Bedroom 1 with double bed'),
|
||||
new Feature('bed-double', 'Schlafzimmer 2 mit Doppelbett & Schlafsofa', 'Bedroom 2 with double bed & sofa bed'),
|
||||
new Feature('sofa', 'Wohnküche', 'Living/kitchen area'),
|
||||
],
|
||||
[
|
||||
'/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'
|
||||
]
|
||||
),
|
||||
new Apartment(
|
||||
2,
|
||||
'Wohlfühlen auf ${apartments[2].size} m²! Dieses geräumige Appartement bietet Ihnen Platz für bis zu ${apartments[2].capacity.to} Personen und bietet 2 Schlafzimmer, eine Wohnküche, Badezimmer, einen Balkon und ein getrenntes WC.',
|
||||
'Spacious ${apartments[2].size} m² apartment for up to ${apartments[2].capacity.to} guests, with 2 bedrooms, a living kitchen, bathroom, balcony, and separate toilet.',
|
||||
[
|
||||
new Feature('mountain', 'Balkon mit idyllischem Ausblick', 'Balcony with idyllic view'),
|
||||
new Feature('bed-double', 'Schlafzimmer 1 mit Doppelbett', 'Bedroom 1 with double bed'),
|
||||
new Feature('bed-double', 'Schlafzimmer 2 mit Doppelbett & Schlafsofa', 'Bedroom 2 with double bed & sofa bed'),
|
||||
new Feature('sofa', 'Wohnküche', 'Living/kitchen area'),
|
||||
],
|
||||
[
|
||||
'/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'
|
||||
]
|
||||
)
|
||||
]
|
||||
}
|
||||
@ -4,27 +4,33 @@
|
||||
<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">
|
||||
<NuxtImg :src="c.apartments.highlight.image.left" alt="Image 1" class="w-40 h-60 object-cover"/>
|
||||
<NuxtImg :src="c.apartments.highlight.image.right" class="w-40 h-60 object-cover mt-6" />
|
||||
<NuxtImg :src="l.highlight.image.left" alt="Image 1" class="w-40 h-60 object-cover"/>
|
||||
<NuxtImg :src="l.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 :title="l.highlight.title" :text="l.highlight.description">
|
||||
<AppHighlights/>
|
||||
</AppTitleText>
|
||||
</div>
|
||||
</AppFlatSection>
|
||||
|
||||
<AppCardSection
|
||||
v-for="(apartment, index) in c.apartments.list"
|
||||
:key="apartment.id"
|
||||
v-for="(apartment, index) in l.list"
|
||||
:key="index"
|
||||
:padTop="index === 0"
|
||||
|
||||
>
|
||||
<AppApartment :apartment="apartment" :id="apartment.id"/>
|
||||
<AppApartment :apartment="apartment" :index="index" :id="g.apartments[index].id"/>
|
||||
</AppCardSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {c} = useContent()
|
||||
useSeoLinking()
|
||||
const {g, l} = useContentInjected()
|
||||
|
||||
useSeoMeta({
|
||||
title: () => l.value.meta.title,
|
||||
description: () => l.value.meta.description,
|
||||
})
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
meta:
|
||||
title:
|
||||
$de$summer: ${business.name} Saalbach – Unsere Sommer-Apartments
|
||||
$de$winter: ${business.name} Saalbach – Unsere Winter-Apartments
|
||||
$en$summer: ${business.name} Saalbach – Our Summer Apartments
|
||||
$en$winter: ${business.name} Saalbach – Our Winter Apartments
|
||||
|
||||
description:
|
||||
$de$summer: Entdecken Sie unsere gemütlichen Sommer-Apartments in Saalbach mit Balkon oder Terrasse, WLAN, Sauna und Panoramablick – unweit der Gondeln, ideal zum Biken.
|
||||
$de$winter: Entdecken Sie unsere komfortablen Winter-Apartments in Saalbach mit Sauna, Skikeller, WLAN und Panoramablick – unweit der Gondeln, ideal zum Skifahren.
|
||||
$en$summer: Discover our cozy summer apartments in Saalbach with balcony or terrace, Wi-Fi, sauna, and mountain views – not far from the gondolas, perfect for biking.
|
||||
$en$winter: Discover our comfortable winter apartments in Saalbach with sauna, ski storage, Wi-Fi, and mountain views – not far from the gondolas, perfect for skiing.
|
||||
|
||||
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
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="absolute inset-0 z-0">
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
:items="c.landing.welcome.images"
|
||||
:items="l.welcome.images"
|
||||
:ui="{item: 'basis-full h-full ps-0', container: 'flex items-stretch h-full'}"
|
||||
:autoplay="{ delay: 5000 }"
|
||||
:loop="true"
|
||||
@ -24,28 +24,28 @@
|
||||
<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>
|
||||
<h1 class="text-5xl max-w-4xl font-bold">{{ l.welcome.title }}</h1>
|
||||
<p class="mt-4 text-lg">
|
||||
{{
|
||||
c.landing.welcome.description
|
||||
l.welcome.description
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-8 flex gap-4 flex-wrap">
|
||||
<UButton :to="p('apartments')" color="primary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-arrow-right">{{ c.button.apartments }}
|
||||
trailing-icon="i-heroicons-arrow-right">{{ g.button.apartments }}
|
||||
</UButton>
|
||||
<UButton :to="p('book')" color="secondary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-calendar-days">
|
||||
{{ c.button.book }}
|
||||
{{ g.button.book }}
|
||||
</UButton>
|
||||
<UButton :to="p('contact')" color="secondary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-envelope">
|
||||
{{ c.button.contact }}
|
||||
{{ g.button.contact }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="c.landing.welcome.joker === true" class="flex items-center justify-center p-8">
|
||||
<div v-if="l.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"
|
||||
@ -64,16 +64,16 @@
|
||||
<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">
|
||||
<NuxtImg :src="c.landing.highlight.image.left" alt="Image 1" class="w-40 h-60 object-cover"/>
|
||||
<NuxtImg :src="c.landing.highlight.image.right" class="w-40 h-60 object-cover mt-6" />
|
||||
<NuxtImg :src="l.highlight.image.left" alt="Image 1" class="w-40 h-60 object-cover"/>
|
||||
<NuxtImg :src="l.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"
|
||||
<AppTitleText :title="l.highlight.title" :text="l.highlight.description">
|
||||
<UButton :to="p('contact#hosts')" color="primary" variant="solid" size="xl"
|
||||
trailing-icon="i-heroicons-arrow-right">
|
||||
{{
|
||||
c.button.hosts
|
||||
g.button.hosts
|
||||
}}
|
||||
</UButton>
|
||||
</AppTitleText>
|
||||
@ -84,10 +84,10 @@
|
||||
<div class="flex flex-col md:flex-row w-full items-stretch overflow-hidden">
|
||||
<!-- Left: 40% width on desktop, full on mobile -->
|
||||
<div class="w-full md:w-[40%] flex flex-col justify-center pr">
|
||||
<AppTitleText :title="c.landing.location.title" :text="c.landing.location.description">
|
||||
<AppTitleText :title="l.location.title" :text="l.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 }}
|
||||
{{ g.button.map }}
|
||||
</UButton>
|
||||
</AppTitleText>
|
||||
</div>
|
||||
@ -98,7 +98,7 @@
|
||||
aria-label="Google Maps öffnen"
|
||||
>
|
||||
<NuxtImg
|
||||
:src="c.landing.location.image"
|
||||
:src="l.location.image"
|
||||
alt="Vorschaubild der Lage in Google Maps"
|
||||
class="w-full h-64 md:h-full object-cover transition-transform duration-300 ease-out group-hover:scale-110"
|
||||
/>
|
||||
@ -109,20 +109,20 @@
|
||||
</AppCardSection>
|
||||
|
||||
<AppFlatSection>
|
||||
<AppTitleText :title="c.landing.apartments.title" :text="c.landing.apartments.description">
|
||||
<AppTitleText :title="l.apartments.title" :text="l.apartments.description">
|
||||
<div class="flex flex-wrap gap-4 justify-center">
|
||||
|
||||
<NuxtLink v-for="apartment in c.apartments.list" :to="p('apartments', apartment.id)"
|
||||
<NuxtLink v-for="(thumbnail, index) in l.apartments.thumbnails" :to="p('apartments#' + g.apartments[index].id)"
|
||||
class="relative group block w-60 h-80 overflow-hidden">
|
||||
<NuxtImg :src="apartment.thumbnail" alt="Image 1"
|
||||
<NuxtImg :src="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>
|
||||
<li class="text-sm">{{ g.apartments[index].title }}</li>
|
||||
<li class="text-sm">{{ g.apartments[index].capacity.text }}</li>
|
||||
<li class="text-sm">{{ g.apartments[index].price.text }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -134,19 +134,19 @@
|
||||
<AppCardSection>
|
||||
<div class="flex-1 pr">
|
||||
<h2 class="text-2xl font-bold mb-4">
|
||||
{{ c.landing.features.title }}
|
||||
{{ l.features.title }}
|
||||
</h2>
|
||||
|
||||
<p class="text-lg pb-4">
|
||||
{{ c.landing.features.description1 }}
|
||||
{{ l.features.description1 }}
|
||||
<UButton to="https://www.bruendl.at/" variant="outline" trailing-icon="i-heroicons-arrow-right">{{
|
||||
c.landing.features.description2
|
||||
l.features.description2
|
||||
}}
|
||||
</UButton>
|
||||
{{ c.landing.features.description3 }}
|
||||
{{ l.features.description3 }}
|
||||
</p>
|
||||
|
||||
<AppFeaturesGrid :features="c.landing.features.list"/>
|
||||
<AppHighlights/>
|
||||
</div>
|
||||
</AppCardSection>
|
||||
</div>
|
||||
@ -160,7 +160,11 @@
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {p, c} = useContent()
|
||||
useSeoLinking()
|
||||
const {g, l, p} = useContentInjected()
|
||||
|
||||
useSeoMeta({
|
||||
title: () => l.value.meta.title,
|
||||
description: () => l.value.meta.description,
|
||||
})
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@ -0,0 +1,83 @@
|
||||
meta:
|
||||
title:
|
||||
$de$summer: ${business.name} Saalbach – Apartments für Ihren Sommerurlaub
|
||||
$de$winter: ${business.name} Saalbach – Apartments für Ihren Winterurlaub
|
||||
$en$summer: ${business.name} Saalbach – Apartments for Your Summer Holiday
|
||||
$en$winter: ${business.name} Saalbach – Apartments for Your Winter Holiday
|
||||
description:
|
||||
$de$summer: Gemütliche Apartments in Saalbach mit Panoramablick. Genießen Sie Wandern, Radfahren, Sauna und herzliche Gastfreundschaft im Sommerurlaub.
|
||||
$de$winter: Gemütliche Apartments in bester Lage in Saalbach. Genießen Sie Ihren Winterurlaub mit Sauna, Panoramablick und herzlicher Gastfreundschaft.
|
||||
$en$summer: Cozy apartments in Saalbach with panoramic mountain views. Enjoy hiking, biking, the sauna, and warm hospitality during your summer stay.
|
||||
$en$winter: Cozy apartments in the best location in Saalbach. Enjoy your winter holiday with a sauna, panoramic mountain views, and warm hospitality.
|
||||
|
||||
welcome:
|
||||
title:
|
||||
$de: Willkommen im ${business.name} in Saalbach
|
||||
$en: 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 Durchschlafen 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.
|
||||
thumbnails:
|
||||
- /apartments/2/4.webp
|
||||
- /apartments/2/8.webp
|
||||
- /apartments/2/7.webp
|
||||
|
||||
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.
|
||||
@ -27,7 +27,7 @@
|
||||
<div>
|
||||
<AppFlatSection>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<span class="text-lg text-neutral-600">Coming Soon</span>
|
||||
<span class="text-lg text-neutral-600">{{l.coming}}</span>
|
||||
</div>
|
||||
|
||||
<!-- <!– SSR-safe: iframe appears only in the browser –>-->
|
||||
@ -43,5 +43,13 @@
|
||||
</AppFlatSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useSeoLinking()
|
||||
const {l} = useContentInjected()
|
||||
|
||||
useSeoMeta({
|
||||
title: () => l.value.meta.title,
|
||||
description: () => l.value.meta.description,
|
||||
})
|
||||
</script>
|
||||
12
apps/panoramablick-saalbach.at/pages/[locale]/book.yaml
Normal file
12
apps/panoramablick-saalbach.at/pages/[locale]/book.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
meta:
|
||||
title:
|
||||
$de: ${business.name} Saalbach – Online Buchen & Anfragen
|
||||
$en: ${business.name} Saalbach – Online Booking & Inquiries
|
||||
|
||||
description:
|
||||
$de: Buchen Sie Ihren Urlaub im ${business.name} Saalbach bald ganz einfach online. Bis dahin erreichen Sie uns über das Kontaktformular für Ihre Anfrage.
|
||||
$en: Soon you’ll be able to book your stay at ${business.name} Saalbach online. Until then, please contact us via the inquiry form.
|
||||
|
||||
coming:
|
||||
$de: Demnächst verfügbar! Bitte verwenden Sie vorerst das Kontaktformular.
|
||||
$en: Coming soon! Please use the contact form for now.
|
||||
@ -6,11 +6,11 @@
|
||||
<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>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold mb-4">{{ l.title }}</h1>
|
||||
|
||||
<!-- Extended intro -->
|
||||
<p class="text-neutral-600 mb-4 max-w-prose">
|
||||
{{ c.contact.description }}
|
||||
{{ l.description }}
|
||||
</p>
|
||||
|
||||
<!-- Contact shortcuts -->
|
||||
@ -21,51 +21,51 @@
|
||||
<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 :href="`tel:${g.business.phone.replace(/\s+/g, '')}`" class="hover:underline text-neutral-600">
|
||||
{{ g.business.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 :href="`mailto:${g.business.email}`" class="hover:underline text-neutral-600">
|
||||
{{ g.business.email }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-neutral-600 mb-4 max-w-prose">
|
||||
{{ c.contact.online1 }}
|
||||
{{ l.online1 }}
|
||||
<UButton :to="p('book')" variant="outline" trailing-icon="i-heroicons-calendar-days">{{
|
||||
c.contact.online2
|
||||
l.online2
|
||||
}}
|
||||
</UButton>
|
||||
{{ c.contact.online3 }}
|
||||
{{ l.online3 }}
|
||||
</p>
|
||||
|
||||
<!-- Form -->
|
||||
<UForm :state="state" :schema="schema" 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.prompt"/>
|
||||
<UInput v-model="state.name" class="w-full" :placeholder="l.form.name.prompt"/>
|
||||
</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.prompt"/>
|
||||
:placeholder="l.form.email.prompt"/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="subject" label="Betreff" :ui="{ label: 'sr-only' }">
|
||||
<UInput v-model="state.subject" class="w-full" :placeholder="c.contact.form.subject.prompt"/>
|
||||
<UInput v-model="state.subject" class="w-full" :placeholder="l.form.subject.prompt"/>
|
||||
</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.prompt"/>
|
||||
:placeholder="l.form.message.prompt"/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" color="primary" size="lg" class="w-full sm:w-auto" trailing-icon="i-heroicons-paper-airplane">
|
||||
{{ c.contact.form.send }}
|
||||
{{ l.form.send }}
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
@ -74,21 +74,21 @@
|
||||
<!-- ─── 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"
|
||||
:src="l.heroes.parents.image"
|
||||
:alt="l.heroes.parents.title"
|
||||
image-side="left"
|
||||
:size="50"
|
||||
:title="c.contact.heroes.parents.title"
|
||||
:description="c.contact.heroes.parents.description"
|
||||
:title="l.heroes.parents.title"
|
||||
:description="l.heroes.parents.description"
|
||||
/>
|
||||
|
||||
<AppHero
|
||||
:src="c.contact.heroes.children.image"
|
||||
:alt="c.contact.heroes.children.title"
|
||||
:src="l.heroes.children.image"
|
||||
:alt="l.heroes.children.title"
|
||||
image-side="right"
|
||||
:size="50"
|
||||
:title="c.contact.heroes.children.title"
|
||||
:description="c.contact.heroes.children.description"
|
||||
:title="l.heroes.children.title"
|
||||
:description="l.heroes.children.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -97,17 +97,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
useSeoLinking()
|
||||
const {g, l, p} = useContentInjected()
|
||||
|
||||
useSeoMeta({
|
||||
title: () => l.value.meta.title,
|
||||
description: () => l.value.meta.description,
|
||||
})
|
||||
|
||||
/*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 = computed(() => v.object({
|
||||
name: v.pipe(v.string(), v.minLength(2, c.value.contact.form.name.invalid)),
|
||||
email: v.pipe(v.string(), v.email(c.value.contact.form.email.invalid)),
|
||||
subject: v.pipe(v.string(), v.minLength(3, c.value.contact.form.subject.invalid)),
|
||||
message: v.pipe(v.string(), v.minLength(10, c.value.contact.form.message.invalid))
|
||||
name: v.pipe(v.string(), v.minLength(2, l.value.form.name.invalid)),
|
||||
email: v.pipe(v.string(), v.email(l.value.form.email.invalid)),
|
||||
subject: v.pipe(v.string(), v.minLength(3, l.value.form.subject.invalid)),
|
||||
message: v.pipe(v.string(), v.minLength(10, l.value.form.message.invalid))
|
||||
}))
|
||||
//type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
@ -120,8 +128,7 @@ const toast = useToast()
|
||||
|
||||
/* ───── submit handler ───── */
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
const config = useAppConfig()
|
||||
const hotelId = config.content.business.uid ?? ''
|
||||
const hotelId = g.value.business.uid ?? ''
|
||||
|
||||
try {
|
||||
await $fetch('https://api.dominikmilacher.com/contact', {
|
||||
@ -134,8 +141,8 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
})
|
||||
|
||||
toast.add({
|
||||
title: c.value.contact.form.sent.title,
|
||||
description: c.value.contact.form.sent.description,
|
||||
title: l.value.form.sent.title,
|
||||
description: l.value.form.sent.description,
|
||||
color: 'primary'
|
||||
})
|
||||
|
||||
@ -145,11 +152,10 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
//console.log(err?.data?.detail)
|
||||
//console.log(err?.message)
|
||||
toast.add({
|
||||
title: c.value.contact.form.error.title,
|
||||
description: c.value.contact.form.error.description,
|
||||
title: l.value.form.error.title,
|
||||
description: l.value.form.error.description,
|
||||
color: 'primary'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
82
apps/panoramablick-saalbach.at/pages/[locale]/contact.yaml
Normal file
82
apps/panoramablick-saalbach.at/pages/[locale]/contact.yaml
Normal file
@ -0,0 +1,82 @@
|
||||
meta:
|
||||
title:
|
||||
$de: ${business.name} Saalbach – Kontakt & Anfrage
|
||||
$en: ${business.name} Saalbach – Contact & Inquiry
|
||||
description:
|
||||
$de: Nehmen Sie Kontakt mit uns auf! Monika $ Norbert freuen sich auf Ihre Anfrage – per Telefon, E-Mail oder Formular direkt im Landhaus Panoramablick.
|
||||
$en: Get in touch with us! Monika $ Norbert look forward to your inquiry – by phone, email, or contact form at Landhaus Panoramablick Saalbach.
|
||||
|
||||
title:
|
||||
$de: Kontakt aufnehmen
|
||||
$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.
|
||||
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:
|
||||
prompt:
|
||||
$de: Ihr Name
|
||||
$en: Your name
|
||||
invalid:
|
||||
$de: Name zu kurz
|
||||
$en: Name too short
|
||||
email:
|
||||
prompt:
|
||||
$de: Ihre E-Mail
|
||||
$en: Your email address
|
||||
invalid:
|
||||
$de: E-Mail Adresse nicht gültig
|
||||
$en: Email address not valid
|
||||
subject:
|
||||
prompt:
|
||||
$de: Betreff
|
||||
$en: Subject
|
||||
invalid:
|
||||
$de: Betreff zu kurz
|
||||
$en: Subject too short
|
||||
message:
|
||||
prompt:
|
||||
$de: Ihre Anfrage
|
||||
$en: Your inquiry
|
||||
invalid:
|
||||
$de: Nachricht zu kurz
|
||||
$en: Message too short
|
||||
send:
|
||||
$de: Senden
|
||||
$en: Send
|
||||
sent:
|
||||
title:
|
||||
$de: Nachricht gesendet
|
||||
$en: Message sent
|
||||
description:
|
||||
$de: Vielen Dank - wir melden uns bald.
|
||||
$en: Thank you - we'll be in touch soon.
|
||||
error:
|
||||
title:
|
||||
$de: Fehler
|
||||
$en: Error
|
||||
description:
|
||||
$de: Die Nachricht konnte nicht versendet werden - bitte versuchen Sie es erneut.
|
||||
$en: The message could not be sent - please try again.
|
||||
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
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppFlatSection>
|
||||
<section v-for="block in c.legal"
|
||||
<section v-for="block in l.blocks"
|
||||
:key="block.anchor"
|
||||
:id="block.anchor"
|
||||
class="prose mx-auto max-w-3xl p-4 lg:p-8">
|
||||
@ -29,5 +29,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {c} = useContent()
|
||||
useSeoLinking()
|
||||
const {l} = useContentInjected()
|
||||
useSeoMeta({
|
||||
title: () => l.value.meta.title,
|
||||
description: () => l.value.meta.description,
|
||||
})
|
||||
</script>
|
||||
146
apps/panoramablick-saalbach.at/pages/[locale]/legal.yaml
Normal file
146
apps/panoramablick-saalbach.at/pages/[locale]/legal.yaml
Normal file
@ -0,0 +1,146 @@
|
||||
meta:
|
||||
title:
|
||||
$de: ${business.name} Saalbach – Impressum & Datenschutz
|
||||
$en: ${business.name} Saalbach – Legal Notice & Privacy Policy
|
||||
description:
|
||||
$de: Rechtliche Informationen, Impressum und Datenschutzerklärung für das ${business.name} Saalbach.
|
||||
$en: Legal information, imprint, and privacy policy for ${business.name} Saalbach.
|
||||
|
||||
blocks:
|
||||
- anchor: imprint
|
||||
title:
|
||||
$de: Impressum
|
||||
$en: Imprint
|
||||
sections:
|
||||
- title: ${business.name}
|
||||
paragraphs:
|
||||
- $de: 'Informationen und Offenlegung gemäß §5 (1) ECG, § 25 MedienG, § 63 GewO und § 14 UGB:'
|
||||
$en: 'Information and disclosure according to §5 (1) ECG, § 25 Media Act, § 63 Trade Regulation Act, and § 14 Commercial Code (UGB):'
|
||||
- $de: |-
|
||||
Betreiber: ${business.operator}
|
||||
Anschrift: ${business.address}
|
||||
Telefon: ${business.phone}
|
||||
Email: ${business.email}
|
||||
$en: |-
|
||||
Operator: ${business.operator}
|
||||
Address: ${business.address}
|
||||
Phone: ${business.phone}
|
||||
Email: ${business.email}
|
||||
- $de: |-
|
||||
UID-Nr: ${business.vat}
|
||||
Gewerbeaufsichtbehörde: ${business.authority}
|
||||
Mitgliedschaft: ${business.membership}
|
||||
Rechtsvorschrift: ${business.regulation}
|
||||
$en: |-
|
||||
VAT ID: ${business.vat}
|
||||
Supervisory authority: ${business.authority}
|
||||
Chamber membership: ${business.membership}
|
||||
Legal regulations: ${business.regulation}
|
||||
- title:
|
||||
$de: Verweise und Links
|
||||
$en: External Links
|
||||
paragraphs:
|
||||
- $de: Trotz sorgfältiger inhaltlicher Kontrolle übernimmt der Betreiber dieser Website keine Verantwortung für Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich. Sollten Sie dennoch auf einen ausgehenden Link stoßen, der auf eine Website mit rechtswidrigen Inhalten oder Tätigkeiten verweist, bitten wir um einen entsprechenden Hinweis. In einem solchen Fall wird der Link gemäß § 17 Abs. 2 ECG umgehend entfernt.
|
||||
$en: Despite careful content control, the operator of this website assumes no responsibility for the content of external links. The operators of the linked websites are solely responsible for their content. If you come across a link that leads to unlawful content or activity, we kindly ask for your notification. Such links will be removed promptly in accordance with § 17 (2) ECG.
|
||||
- $de: Der Betreiber dieser Website achtet die Urheberrechte Dritter mit größtmöglicher Sorgfalt. Falls Sie dennoch eine Urheberrechtsverletzung bemerken, ersuchen wir ebenfalls um Mitteilung. Bei Bekanntwerden einer entsprechenden Rechtsverletzung werden die betroffenen Inhalte unverzüglich entfernt.
|
||||
$en: The operator of this website takes great care to respect the copyrights of third parties. If you notice any copyright infringement, please notify us. Upon becoming aware of such a violation, the affected content will be removed without delay.
|
||||
- title:
|
||||
$de: Haftungsausschluss
|
||||
$en: Disclaimer
|
||||
paragraphs:
|
||||
- $de: Der Betreiber übernimmt keine Gewähr für die Aktualität, Richtigkeit, Vollständigkeit oder Qualität der bereitgestellten Informationen. Haftungsansprüche jeglicher Art, die durch die Nutzung oder Nichtnutzung der angebotenen Inhalte bzw. durch die Nutzung fehlerhafter oder unvollständiger Informationen verursacht werden, sind ausgeschlossen – sofern kein nachweislich vorsätzliches oder grob fahrlässiges Verschulden vorliegt. Alle Angebote sind freibleibend und unverbindlich. Der Betreiber behält sich ausdrücklich vor, Teile der Seiten oder das gesamte Angebot ohne gesonderte Ankündigung zu verändern, zu ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig einzustellen.
|
||||
$en: The operator assumes no liability for the accuracy, completeness, or quality of the information provided. Claims for damages of any kind arising from the use or non-use of the information presented, or from the use of incorrect or incomplete content, are excluded unless caused by demonstrably intentional or grossly negligent behavior. All offers are non-binding. The operator expressly reserves the right to modify, supplement, delete, or temporarily or permanently cease publication of parts of the site or the entire offer without prior notice.
|
||||
- $de: Aufgrund der technischen Gegebenheiten des Internets kann keine Gewähr für die ständige Verfügbarkeit, Authentizität oder Fehlerfreiheit der Website übernommen werden. Jegliche Haftung für unmittelbare oder mittelbare Schäden, die durch die Nutzung oder vorübergehende Nichtverfügbarkeit der Website entstehen, ist – soweit gesetzlich zulässig – ausgeschlossen.
|
||||
$en: Due to the technical nature of the internet, no guarantee can be given for uninterrupted availability, authenticity, or accuracy of the website. Liability for any direct or indirect damages resulting from the use or temporary unavailability of this website is excluded to the extent permitted by law.
|
||||
- title:
|
||||
$de: Urheberrecht und Nutzung
|
||||
$en: Copyright and Use
|
||||
paragraphs:
|
||||
- $de: Die Inhalte dieser Website (Texte, Bilder, Grafiken, Videos etc.) sind urheberrechtlich geschützt und ausschließlich für den persönlichen Gebrauch bestimmt. Eine darüber hinausgehende Nutzung – insbesondere die Vervielfältigung, Speicherung in Datenbanken, gewerbliche Nutzung oder Weitergabe an Dritte – ist ohne ausdrückliche Zustimmung des Betreibers nicht gestattet. Die Einbindung einzelner Seiten in fremde Frames ist unzulässig.
|
||||
$en: The content of this website (texts, images, graphics, videos, etc.) is protected by copyright and intended solely for personal use. Any further use — including reproduction, storage in databases, commercial use, or distribution to third parties — is not permitted without the explicit consent of the operator. Embedding individual pages of this website in external frames is prohibited.
|
||||
- $de: Der Betreiber ist bemüht, in allen Publikationen Urheberrechte Dritter zu beachten oder auf lizenzfreie bzw. eigene Inhalte zurückzugreifen. Sollte dennoch eine Urheberrechtsverletzung vorliegen, bitten wir um einen Hinweis. Bei Bekanntwerden werden entsprechende Inhalte umgehend entfernt.
|
||||
$en: The operator makes every effort to respect the copyrights of third parties or to use either self-created or license-free content. If you suspect a copyright violation, please inform us. In such cases, the relevant content will be removed promptly.
|
||||
|
||||
- anchor: privacy
|
||||
title:
|
||||
$de: Datenschutz
|
||||
$en: Privacy Policy
|
||||
sections:
|
||||
- title:
|
||||
$de: Einleitung
|
||||
$en: Introduction
|
||||
paragraphs:
|
||||
- $de: Der Schutz Ihrer persönlichen Daten ist uns ein wichtiges Anliegen. Wir behandeln Ihre Daten vertraulich und entsprechend der geltenden Datenschutzvorschriften (DSGVO, TKG 2003). Diese Erklärung informiert Sie darüber, welche Daten wir im Rahmen unseres Webauftritts erfassen, speichern und wie wir mit ihnen umgehen.
|
||||
$en: Protecting your personal data is very important to us. We treat your data confidentially and in accordance with applicable data protection laws (GDPR, TKG 2003). This policy explains what data is collected when you use our website, how it is processed, and how it is protected.
|
||||
- title:
|
||||
$de: Umgang mit personenbezogenen Daten
|
||||
$en: Handling of Personal Data
|
||||
paragraphs:
|
||||
- $de: Wenn Sie über unsere Website oder per E-Mail Kontakt mit uns aufnehmen, verarbeiten wir die von Ihnen bereitgestellten personenbezogenen Daten ausschließlich zur Bearbeitung Ihrer Anfrage oder für damit zusammenhängende Zwecke.
|
||||
$en: If you contact us via our website or by email, we will use the personal data you provide exclusively to process your inquiry or for related purposes.
|
||||
- $de: Die Daten werden nur so lange gespeichert, wie es für die Erfüllung des jeweiligen Zwecks notwendig ist oder rechtliche Aufbewahrungspflichten dies vorsehen. Eine Weitergabe an Dritte erfolgt nicht, es sei denn, dies ist gesetzlich erforderlich oder Sie haben ausdrücklich zugestimmt.
|
||||
$en: Your data will only be stored as long as necessary to fulfill the intended purpose or as required by legal retention obligations. No data will be shared with third parties unless required by law or explicitly consented to by you.
|
||||
- title:
|
||||
$de: Serverprotokolle
|
||||
$en: Server Logs
|
||||
paragraphs:
|
||||
- $de: 'Beim Aufruf unserer Website werden automatisch Informationen durch den Webserver protokolliert (sogenannte Server-Logfiles). Dazu gehören unter anderem:'
|
||||
$en: 'When visiting our website, technical information is automatically recorded by the web server (so-called server log files). This includes, for example:'
|
||||
- $de: |-
|
||||
• IP-Adresse
|
||||
• Datum und Uhrzeit des Zugriffs
|
||||
• besuchte Seiten
|
||||
• verwendeter Browser und Betriebssystem
|
||||
• Spracheinstellungen
|
||||
• ggf. Referrer-URL
|
||||
$en: |-
|
||||
• IP address
|
||||
• Date and time of access
|
||||
• Visited pages
|
||||
• Browser and operating system used
|
||||
• Language settings
|
||||
• Referrer URL (if applicable)
|
||||
- $de: Diese Daten sind technisch notwendig, um den sicheren und stabilen Betrieb der Website zu gewährleisten. Sie lassen keinen direkten Rückschluss auf Ihre Person zu und werden nicht mit anderen Datenquellen zusammengeführt.
|
||||
$en: These logs are essential for the safe and stable operation of the site. The data does not allow for direct identification of users and is not merged with other data sources.
|
||||
- title: Cookies
|
||||
paragraphs:
|
||||
- $de: Unsere Website verwendet ausschließlich technisch notwendige Cookies. Wir setzen keine Tracking-Technologien ein und analysieren keine Nutzerverhalten.
|
||||
$en: This website only uses technically necessary cookies. No tracking or analytics tools are in use.
|
||||
- $de: Das einzige gesetzte Cookie dient der Unterscheidung zwischen Sommer- und Winterdarstellung der Website. Es enthält keine personenbezogenen Informationen und wird nur für diesen funktionalen Zweck verwendet.
|
||||
$en: The only cookie stored is used to toggle between summer and winter versions of the website. This cookie does not contain any personal information and is used solely for this visual adjustment.
|
||||
- title:
|
||||
$de: Ihre Rechte
|
||||
$en: Your Rights
|
||||
paragraphs:
|
||||
- $de: Sie haben das Recht auf Auskunft über Ihre bei uns gespeicherten personenbezogenen Daten sowie auf Berichtigung, Löschung oder Einschränkung der Verarbeitung. Sie können der Datenverarbeitung jederzeit widersprechen und – sofern zutreffend – Ihr Recht auf Datenübertragbarkeit geltend machen.
|
||||
$en: You have the right to access your stored personal data, request correction or deletion, and restrict or object to processing. If applicable, you may also request data portability.
|
||||
- $de: Wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer Daten gegen geltendes Datenschutzrecht verstößt, haben Sie das Recht, sich bei der zuständigen Datenschutzbehörde zu beschweren oder uns direkt zu kontaktieren.
|
||||
$en: If you believe that your data is being processed in violation of data protection laws, you have the right to file a complaint with the relevant data protection authority or contact us directly.
|
||||
|
||||
- anchor: accessibility
|
||||
title:
|
||||
$de: Barrierefreiheit
|
||||
$en: Accessibility
|
||||
sections:
|
||||
- title:
|
||||
$de: Allgemeines
|
||||
$en: General Information
|
||||
paragraphs:
|
||||
- $de: Der Betreiber dieser Website ist bemüht, die Inhalte möglichst barrierefrei bereitzustellen. Auch wenn das österreichische Barrierefreiheitsgesetz (BaFG) für Kleinstunternehmen nicht verpflichtend ist, orientiert sich der Betreiber freiwillig an den anerkannten Richtlinien für barrierefreie Webinhalte (WCAG).
|
||||
$en: The operator of this website strives to make the content as accessible as possible. While the Austrian Accessibility Act (BaFG) does not apply to microenterprises, the operator voluntarily follows the recognized guidelines for accessible web content (WCAG).
|
||||
- title:
|
||||
$de: Rechtlicher Rahmen
|
||||
$en: Legal Framework
|
||||
paragraphs:
|
||||
- $de: Laut österreichischem Barrierefreiheitsgesetz (BaFG) sind Kleinstunternehmen – also Betriebe mit weniger als zehn Mitarbeitenden und einem Jahresumsatz oder einer Bilanzsumme unter 2 Millionen Euro – von den gesetzlichen Verpflichtungen zur digitalen Barrierefreiheit ausgenommen, sofern ausschließlich Dienstleistungen online angeboten werden.
|
||||
$en: According to the Austrian Accessibility Act (BaFG), microenterprises — defined as businesses with fewer than ten employees and annual revenue or balance sheet totals under 2 million euros — are exempt from the legal obligations of digital accessibility, provided they only offer online services.
|
||||
- $de: Der Betreiber fällt unter diese gesetzliche Ausnahme und ist daher nicht verpflichtet, eine formale Barrierefreiheitserklärung zu veröffentlichen.
|
||||
$en: The operator qualifies as such a microenterprise and is therefore not legally required to publish a formal accessibility statement.
|
||||
- $de: Trotzdem wurde bei der Gestaltung der Website auf eine möglichst barrierearme Umsetzung geachtet. Dazu zählen unter anderem gut lesbare Schriftarten, ausreichende Farbkontraste, eine klare Inhaltsstruktur und die Bedienbarkeit ohne Maus. Ziel ist es, die Nutzung der Website für möglichst viele Menschen zugänglich zu machen.
|
||||
$en: Nevertheless, efforts have been made to design this website in a way that minimizes barriers. This includes readable fonts, sufficient color contrast, a clear content structure, and keyboard navigability. The goal is to make the website usable for as many people as possible.
|
||||
- title:
|
||||
$de: Kontakt bei Barrieren
|
||||
$en: Reporting Accessibility Issues
|
||||
paragraphs:
|
||||
- $de: Sollten Inhalte oder Funktionen dieser Website nicht barrierefrei zugänglich sein, wird um Rückmeldung gebeten. Hinweise können jederzeit per E-Mail an ${business.email} übermittelt werden.
|
||||
$en: If you encounter any inaccessible content or functionality on this website, you are encouraged to report it. Please send your feedback via email to ${business.email}.
|
||||
@ -1,7 +1 @@
|
||||
export default defineAppConfig({
|
||||
content: {
|
||||
matrix: undefined,
|
||||
business: undefined,
|
||||
legal: undefined,
|
||||
}
|
||||
})
|
||||
export default defineAppConfig({})
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
const { locale, localeCodes, matrixPath } = useContent()
|
||||
const { preferLocale } = useContentPreference()
|
||||
const config = useContentConfig()
|
||||
const {preferLocale} = useContentPreference()
|
||||
const {locale, path} = useContentInjected()
|
||||
|
||||
const candidates = computed(() => localeCodes.filter(l => l !== locale.value))
|
||||
const locales = config.locale?.list ?? []
|
||||
const candidates = computed(() => locales.filter(l => l.code !== locale.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink v-for="candidate in candidates" :key="candidate" :to="{ path: matrixPath({locale: candidate}), query: { freeze: 'true' } }" @click="preferLocale(candidate)">
|
||||
{{ candidate.toUpperCase() }}
|
||||
<NuxtLink
|
||||
v-for="candidate in candidates"
|
||||
:key="candidate.code"
|
||||
:to="{ path: path(undefined, {locale: candidate.code}).value, query: { freeze: 'true' }}"
|
||||
@click="preferLocale(candidate.code)">
|
||||
{{ candidate.code.toUpperCase() }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
const { variant, variants, matrixPath } = useContent()
|
||||
const { preferVariant } = useContentPreference()
|
||||
const config = useContentConfig()
|
||||
const {preferVariant} = useContentPreference()
|
||||
const {variant, path} = useContentInjected()
|
||||
|
||||
const variants = config.variant?.list ?? []
|
||||
const candidates = computed(() => variants.filter(v => v.code !== variant.value))
|
||||
</script>
|
||||
|
||||
@ -9,7 +11,7 @@ const candidates = computed(() => variants.filter(v => v.code !== variant.value)
|
||||
<NuxtLink
|
||||
v-for="candidate in candidates"
|
||||
:key="candidate.code"
|
||||
:to="{ path: matrixPath({variant: candidate.code}), query: { freeze: 'true' }}"
|
||||
:to="{ path: path(undefined, {variant: candidate.code}).value, query: { freeze: 'true' }}"
|
||||
class="flex items-center justify-center"
|
||||
@click="preferVariant(candidate.code)">
|
||||
<Icon :name="candidate.icon"/>
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
import { isTemplate, expandTemplate } from '../utils/content-template'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from '#app'
|
||||
|
||||
const specializations: Record<string, object> = {}
|
||||
|
||||
function buildSpecialization(locale: string, variant: string): object {
|
||||
const config = useAppConfig()
|
||||
const locales = config.content.matrix.locale.list.map(l => l.code)
|
||||
const variants = config.content.matrix.variant.list.map(v => v.code)
|
||||
const allTags = new Set([...locales, ...variants])
|
||||
|
||||
function split(key: string): {base?: string; tags: string[]} {
|
||||
const parts = key.split('@')
|
||||
|
||||
if (allTags.has(parts[0])) {
|
||||
return {base: undefined, tags: parts}
|
||||
}
|
||||
|
||||
return {base: parts[0], tags: parts.slice(1)}
|
||||
}
|
||||
|
||||
function relevant(tags: string[]): boolean {
|
||||
if (tags.length < 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
if (tag !== locale && tag !== variant) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function filterObject(object: any): any {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(e => filterObject(e)).filter(e => e !== undefined)
|
||||
}
|
||||
|
||||
if (typeof object !== 'object') {
|
||||
return object
|
||||
}
|
||||
|
||||
const copy: Record<string, any> = {}
|
||||
let anonymous: [number, any][] = []
|
||||
|
||||
for (const key in object) {
|
||||
const {base, tags} = split(key)
|
||||
|
||||
if (!relevant(tags)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const value = filterObject(object[key])
|
||||
|
||||
if (base === undefined) {
|
||||
anonymous.push([tags.length, value])
|
||||
} else {
|
||||
copy[base] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(copy).length > 0 && anonymous.length > 0) {
|
||||
console.warn(`Invalid mixing of keys in object ${object}`)
|
||||
}
|
||||
|
||||
if (Object.keys(copy).length > 0) {
|
||||
return copy
|
||||
}
|
||||
|
||||
if (anonymous.length < 1) {
|
||||
console.warn("Unable to parse object", object)
|
||||
}
|
||||
|
||||
anonymous.sort((a, b) => b[0] - a[0])
|
||||
|
||||
if (anonymous[0][0] > 2) {
|
||||
console.warn(`More than two tags per key in object ${object}`)
|
||||
}
|
||||
|
||||
return anonymous[0][1]
|
||||
}
|
||||
|
||||
const filtered = filterObject(config.content)
|
||||
|
||||
function deepReplace(obj: any, predicate: (node: any) => boolean, replacer: (node: any) => any): void {
|
||||
if (Array.isArray(obj)) {
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
if (predicate(obj[i])) {
|
||||
obj[i] = replacer(obj[i]);
|
||||
} else if (typeof obj[i] === 'object' && obj[i] !== null) {
|
||||
deepReplace(obj[i], predicate, replacer);
|
||||
}
|
||||
}
|
||||
} else if (typeof obj === 'object' && obj !== null) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (predicate(obj[key])) {
|
||||
obj[key] = replacer(obj[key]);
|
||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
deepReplace(obj[key], predicate, replacer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deepReplace(filtered, (node: any) => isTemplate(node), (node: any) => expandTemplate(node, filtered))
|
||||
return filtered
|
||||
}
|
||||
|
||||
function getSpecialization(locale: string, variant: string): object {
|
||||
const key = locale + '/' + variant
|
||||
|
||||
if (!(key in specializations)) {
|
||||
specializations[key] = buildSpecialization(locale, variant)
|
||||
}
|
||||
|
||||
return specializations[key]
|
||||
}
|
||||
|
||||
export function useContent() {
|
||||
const config = useAppConfig()
|
||||
const route = useRoute()
|
||||
const {getLocaleVariant, buildPrefix} = useContentPrefix()
|
||||
|
||||
const locales = config.content.matrix.locale.list.map(l => l.code)
|
||||
const variants = config.content.matrix.variant.list.map(v => v.code)
|
||||
|
||||
const localeVariant = computed(() => getLocaleVariant(route.path)!)
|
||||
const currentLocale = computed(() => localeVariant.value[0])
|
||||
const currentVariant = computed(() => localeVariant.value[1])
|
||||
|
||||
function matrixPath(options: { page?: string; anchor?: string; locale?: string; variant?: string }): string {
|
||||
let page = options.page
|
||||
|
||||
if (page && !page.startsWith('/')) {
|
||||
page = '/' + page
|
||||
}
|
||||
|
||||
if (page === undefined) {
|
||||
page = route.fullPath
|
||||
|
||||
const [locale, variant] = getLocaleVariant(page)!
|
||||
const length = buildPrefix(locale, variant).length
|
||||
|
||||
page = page.slice(length)
|
||||
}
|
||||
|
||||
const prefix = buildPrefix(options.locale ?? currentLocale.value, options.variant ?? currentVariant.value)
|
||||
|
||||
let base = prefix + page
|
||||
|
||||
if (base.endsWith('/')) {
|
||||
base = base.slice(0, -1)
|
||||
}
|
||||
|
||||
return options.anchor ? `${base}#${options.anchor}` : base
|
||||
}
|
||||
|
||||
function p(page: string, anchor?: string): string {
|
||||
return matrixPath({page: page, anchor: anchor})
|
||||
}
|
||||
|
||||
const specialization = computed<any>(() => getSpecialization(currentLocale.value, currentVariant.value))
|
||||
|
||||
return {
|
||||
locale: currentLocale,
|
||||
locales: config.content.matrix.locale.list,
|
||||
localeCodes: locales,
|
||||
variant: currentVariant,
|
||||
variants: config.content.matrix.variant.list,
|
||||
variantCodes: variants,
|
||||
matrixPath: matrixPath,
|
||||
p: p,
|
||||
c: specialization,
|
||||
}
|
||||
}
|
||||
6
packages/layers/content/composables/useContentConfig.ts
Normal file
6
packages/layers/content/composables/useContentConfig.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {type Configuration} from '../utils/content-config'
|
||||
import config from 'virtual:content/content.config'
|
||||
|
||||
export function useContentConfig() {
|
||||
return config as Configuration
|
||||
}
|
||||
98
packages/layers/content/composables/useContentInjected.ts
Normal file
98
packages/layers/content/composables/useContentInjected.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import {type JsonValue} from '../utils/content-canonical'
|
||||
import {buildContent, expandContent} from '../utils/content-reduced'
|
||||
import {useContentPreference} from "./useContentPreference"
|
||||
import config from 'virtual:content/content.config'
|
||||
import shortcuts from 'virtual:content/shortcuts'
|
||||
|
||||
export function useContentInjected(slug?: string, global?: JsonValue, local?: JsonValue) {
|
||||
if (!slug) {
|
||||
throw new Error('useContentInjected called without arguments, there might be a problem with the Vite plugin')
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const {preferredLocale, preferredVariant} = useContentPreference()
|
||||
|
||||
const lo = computed(() => (route.params.locale as string | undefined) ?? preferredLocale.value)
|
||||
const va = computed(() => (route.params.variant as string | undefined) ?? preferredVariant.value)
|
||||
|
||||
const g = computed(() => getContent('content.global', lo.value, va.value, global, local))
|
||||
const l = computed(() => getContent(slug, lo.value, va.value, global, local))
|
||||
|
||||
function path(page?: string, options?: {locale?: string, variant?: string}) {
|
||||
return computed(() => {
|
||||
let result: string // don't touch page, this will mess with reactivity
|
||||
|
||||
if (page === undefined) {
|
||||
result = route.fullPath
|
||||
|
||||
function replace(what: string, value: string) { // replaces only the first occurrence
|
||||
const expression = new RegExp(`/${what}(?=[/?#]|$)`)
|
||||
return result.replace(expression, `/${value}`)
|
||||
}
|
||||
|
||||
result = lo.value ? replace(lo.value, '[locale]') : result
|
||||
result = va.value ? replace(va.value, '[variant]') : result
|
||||
} else {
|
||||
const index = page.search(/[?#]/)
|
||||
const path = index === -1 ? page : page.slice(0, index)
|
||||
|
||||
if (path in shortcuts) {
|
||||
result = shortcuts[path] + page.slice(index === -1 ? page.length : index)
|
||||
} else {
|
||||
result = page
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.startsWith('/')) {
|
||||
result = '/' + result
|
||||
}
|
||||
|
||||
result = lo.value ? result.replace('[locale]', options?.locale ?? lo.value ?? '') : result
|
||||
result = va.value ? result.replace('[variant]', options?.variant ?? va.value ?? '') : result
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
locale: lo,
|
||||
variant: va,
|
||||
global: g,
|
||||
local: l,
|
||||
g: g,
|
||||
l: l,
|
||||
path: path,
|
||||
p: path
|
||||
}
|
||||
}
|
||||
|
||||
const cache = new Map<string, JsonValue | undefined>()
|
||||
|
||||
function getContent(slug: string, locale?: string, variant?: string, global?: JsonValue, local?: JsonValue): JsonValue | undefined {
|
||||
const key = `${slug}${locale}${variant}`
|
||||
|
||||
const hit = cache.get(key)
|
||||
if (hit !== undefined) {
|
||||
return hit
|
||||
}
|
||||
|
||||
const locales: string[] = config.locale?.list.map(l => l.code) ?? []
|
||||
const variants: string[] = config.variant?.list.map(v => v.code) ?? []
|
||||
|
||||
const content = slug === 'content.global' ? global : local
|
||||
|
||||
let [reduced, expand] =
|
||||
content === undefined ? [undefined, false] : buildContent(locale, locales, variant, variants, content)
|
||||
|
||||
if (reduced && expand) {
|
||||
if (slug === 'content.global') {
|
||||
reduced = expandContent(reduced, [reduced])
|
||||
} else {
|
||||
const g = getContent('content.global', locale, variant, global, local)
|
||||
reduced = expandContent(reduced, g ? [reduced, g] : [reduced])
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(key, reduced)
|
||||
return reduced
|
||||
}
|
||||
@ -1,19 +1,18 @@
|
||||
import type {CookieRef} from "nuxt/app"
|
||||
import type {CookieRef} from 'nuxt/app'
|
||||
import config from 'virtual:content/content.config'
|
||||
|
||||
export function useContentPreference() {
|
||||
const config = useAppConfig()
|
||||
|
||||
const locales = config.content.matrix.locale.list.map(l => l.code)
|
||||
const variants = config.content.matrix.variant.list.map(v => v.code)
|
||||
const locales = config.locale?.list.map(l => l.code) ?? []
|
||||
const variants = config.variant?.list.map(v => v.code) ?? []
|
||||
|
||||
const localeCookie = useCookie<string>('locale', {
|
||||
sameSite: 'lax',
|
||||
maxAge: config.content.matrix.locale.cookieMaxAge
|
||||
maxAge: config.locale?.cookieMaxAge ?? 0
|
||||
})
|
||||
|
||||
const variantCookie = useCookie<string>('variant', {
|
||||
sameSite: 'lax',
|
||||
maxAge: config.content.matrix.variant.cookieMaxAge
|
||||
maxAge: config.variant?.cookieMaxAge ?? 0
|
||||
})
|
||||
|
||||
function prefer(list: string[], cookie: CookieRef<string>) {
|
||||
@ -24,9 +23,9 @@ export function useContentPreference() {
|
||||
}
|
||||
}
|
||||
|
||||
function preferred(list: string[], cookie: CookieRef<string | undefined>, fallback: string) {
|
||||
function preferred(list: string[], cookie: CookieRef<string | undefined>, fallback: string | undefined) {
|
||||
return computed(() => {
|
||||
if (cookie.value!==undefined && list.includes(cookie.value)) {
|
||||
if (cookie.value !== undefined && list.includes(cookie.value)) {
|
||||
return cookie.value
|
||||
} else if (cookie.value !== undefined) {
|
||||
cookie.value = undefined
|
||||
@ -37,9 +36,9 @@ export function useContentPreference() {
|
||||
}
|
||||
|
||||
return {
|
||||
preferredLocale: preferred(locales, localeCookie, config.content.matrix.locale.default),
|
||||
preferredLocale: preferred(locales, localeCookie, config.locale?.default),
|
||||
preferLocale: prefer(locales, localeCookie),
|
||||
preferredVariant: preferred(variants, variantCookie, config.content.matrix.variant.default),
|
||||
preferredVariant: preferred(variants, variantCookie, config.variant?.default),
|
||||
preferVariant: prefer(variants, variantCookie)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
export function useContentPrefix() {
|
||||
const config = useAppConfig()
|
||||
|
||||
const locales: string[] = config.content.matrix.locale.list.map(l => l.code)
|
||||
const variants: string[] = config.content.matrix.variant.list.map(v => v.code)
|
||||
|
||||
type Prefix = [string, string, string]
|
||||
let prefixes: Prefix[] = []
|
||||
|
||||
if (locales.length>1 && variants.length>1) {
|
||||
prefixes = locales.flatMap(l => variants.map(v => [`/${l}/${v}`, l, v] as Prefix))
|
||||
} else if (locales.length>1 && variants.length===1) {
|
||||
prefixes = locales.map(l => [`/${l}`, l, variants[0]!])
|
||||
} else if (locales.length===1 && variants.length>1) {
|
||||
prefixes = variants.map(v => [`/${v}`, locales[0]!, v])
|
||||
} else if (locales.length===1 && variants.length===1) {
|
||||
prefixes = [['', locales[0]!, variants[0]!]]
|
||||
} else {
|
||||
console.warn('Missing locales and variants in AppConfig')
|
||||
}
|
||||
|
||||
const terminators = new Set(['/', '#', '?'])
|
||||
|
||||
function getLocaleVariant(path: string): [string, string] | undefined {
|
||||
for (const [p, l, v] of prefixes) {
|
||||
if (path.startsWith(p) && (path.length===p.length || terminators.has(path[p.length]))) {
|
||||
return [l, v]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function buildPrefix(locale: string, variant: string): string {
|
||||
let segments: string[] = []
|
||||
|
||||
if (locales.length > 1) {
|
||||
segments.push(locale)
|
||||
}
|
||||
|
||||
if (variants.length > 1) {
|
||||
segments.push(variant)
|
||||
}
|
||||
|
||||
return (segments.length ? '/' : '') + segments.join('/')
|
||||
}
|
||||
|
||||
return {
|
||||
prefixes: prefixes.map(p => p[0]),
|
||||
getLocaleVariant: getLocaleVariant,
|
||||
buildPrefix: buildPrefix
|
||||
}
|
||||
}
|
||||
18
packages/layers/content/content.config.ts
Normal file
18
packages/layers/content/content.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export default {
|
||||
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 * 3
|
||||
},
|
||||
variant: {
|
||||
default: 'winter',
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// important: routes do not exist in middleware, never (in)directly use e.g. useRoute
|
||||
|
||||
if (!import.meta.client) return
|
||||
|
||||
if (to.matched.length) {
|
||||
const config = useContentConfig()
|
||||
const {preferredLocale, preferredVariant} = useContentPreference()
|
||||
|
||||
const locales = config.locale?.list.map(l => l.code) ?? []
|
||||
if (to.params.locale !== undefined && !locales.includes(to.params.locale)) {
|
||||
return navigateTo('/' + preferredLocale.value + to.fullPath.slice(to.params.locale.length + 1))
|
||||
}
|
||||
|
||||
const variants = config.variant?.list.map(v => v.code) ?? []
|
||||
if (to.params.variant !== undefined && !variants.includes(to.params.variant)) {
|
||||
const prefix = to.params.locale === undefined ? '/' : '/' + to.params.locale + '/'
|
||||
return navigateTo(prefix + preferredVariant.value + to.fullPath.slice(prefix.length + to.params.variant.length))
|
||||
}
|
||||
} else {
|
||||
const {preferredLocale, preferredVariant} = useContentPreference()
|
||||
return navigateTo('/' + [preferredLocale.value, preferredVariant.value].filter(p => !!p).join('/'))
|
||||
}
|
||||
})
|
||||
@ -1,16 +0,0 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// important: routes do not exist in middleware, never (in)directly use e.g. useRoute
|
||||
|
||||
const {getLocaleVariant, buildPrefix} = useContentPrefix()
|
||||
const localeVariant = getLocaleVariant(to.path)
|
||||
|
||||
if (to.matched.length && localeVariant) return
|
||||
|
||||
if (!to.matched.length && localeVariant) {
|
||||
const [locale, variant] = localeVariant
|
||||
return navigateTo(buildPrefix(locale, variant))
|
||||
}
|
||||
|
||||
const {preferredLocale, preferredVariant} = useContentPreference()
|
||||
return navigateTo(buildPrefix(preferredLocale.value, preferredVariant.value))
|
||||
})
|
||||
94
packages/layers/content/modules/content-files.ts
Normal file
94
packages/layers/content/modules/content-files.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import {addVitePlugin, defineNuxtModule, getLayerDirectories} from '@nuxt/kit'
|
||||
import {getLayerFiles, getLayerContent} from '../utils/content-files'
|
||||
import {getRoutes, getExpandedRoutes, getRouteShortcuts, getConfig} from '../utils/content-routes'
|
||||
import {join, relative, dirname, basename, extname} from 'path'
|
||||
import {existsSync} from 'fs'
|
||||
|
||||
/* Rationale:
|
||||
1) Every project file containing useInjectedContent() is transformed and virtual modules are imported
|
||||
2) Upon request the virtual modules are compiled from ts/yaml files
|
||||
3) If any of the files that back a virtual module change the module is invalidated
|
||||
*/
|
||||
|
||||
export default defineNuxtModule({
|
||||
setup() {
|
||||
addVitePlugin(() => ({
|
||||
name: 'content-import-injector',
|
||||
resolveId(id) {
|
||||
if (id.startsWith('virtual:content/')) {
|
||||
return '\0' + id // convention, hide from further processing
|
||||
}
|
||||
},
|
||||
async load(id) {
|
||||
if (id.startsWith('\0' + 'virtual:content/')) {
|
||||
const slug = id.slice(17)
|
||||
|
||||
if (slug === 'shortcuts') {
|
||||
const routes = await getRoutes()
|
||||
const shortcuts = getRouteShortcuts(routes)
|
||||
|
||||
return `export default ${JSON.stringify(shortcuts)}`
|
||||
} else if (slug === 'content.config') {
|
||||
const file = getLayerDirectories().map(d => join(d.root, 'content.config.ts')).find(f => existsSync(f))
|
||||
|
||||
if (file) {
|
||||
this.addWatchFile(file)
|
||||
return `export {default} from '${file}'`
|
||||
}
|
||||
} else {
|
||||
const files = getLayerFiles(slug)
|
||||
const content = await getLayerContent(files)
|
||||
|
||||
files.filter(f => existsSync(f)).forEach(f => this.addWatchFile(f))
|
||||
|
||||
return `export const content = ${JSON.stringify(content)}`
|
||||
}
|
||||
}
|
||||
},
|
||||
hotUpdate({file, server}) {
|
||||
const root = getLayerDirectories().map(d => d.root).find(r => file.startsWith(r))
|
||||
|
||||
if (root !== undefined) {
|
||||
const path = relative(root, file)
|
||||
const slug = join(dirname(path), basename(path, extname(path)))
|
||||
|
||||
const module = server.moduleGraph.getModuleById('\0' + `virtual:content/${slug}`)
|
||||
|
||||
if (module) {
|
||||
server.moduleGraph.invalidateModule(module)
|
||||
this.environment.hot.send({ type: 'full-reload' })
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
transform(code, id) {
|
||||
let slug: string | undefined = undefined
|
||||
|
||||
for (const root of getLayerDirectories().map(d => d.root)) {
|
||||
if (id.startsWith(root) && !id.includes('/node_modules/')) {
|
||||
const path = relative(root, id).split('?')[0]!
|
||||
slug = join(dirname(path), basename(path, extname(path)))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (slug === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const expression = /\buseContentInjected\s*\(\s*\)/g
|
||||
if (!expression.test(code)) return null
|
||||
|
||||
const transformed = code.replace(expression,
|
||||
`useContentInjected('${slug}', a23631da455de90c5a945da16897d0bd, d61397115e513f2bb1d8e872b7e7788f)`)
|
||||
|
||||
return `
|
||||
import {content as a23631da455de90c5a945da16897d0bd} from 'virtual:content/content.global';
|
||||
import {content as d61397115e513f2bb1d8e872b7e7788f} from 'virtual:content/${slug}';
|
||||
${transformed}
|
||||
`
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
@ -1,8 +1,22 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
|
||||
import {getConfig, getExpandedRoutes} from './utils/content-routes'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
$meta: {
|
||||
name: 'content', // creates alias #layers/content
|
||||
},
|
||||
devtools: { enabled: true },
|
||||
devtools: {enabled: true},
|
||||
hooks: {
|
||||
async 'prerender:routes'(context) {
|
||||
context.routes.clear()
|
||||
|
||||
const config = await getConfig()
|
||||
const routes = await getExpandedRoutes(config)
|
||||
|
||||
for (const route of routes) {
|
||||
context.routes.add(`/${route}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -11,7 +11,12 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"vue-router": "^4.5.1"
|
||||
"esbuild": "^0.25.11",
|
||||
"vue-router": "^4.5.1",
|
||||
"yaml": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": "^4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "latest",
|
||||
|
||||
381
packages/layers/content/utils/content-canonical.ts
Normal file
381
packages/layers/content/utils/content-canonical.ts
Normal file
@ -0,0 +1,381 @@
|
||||
export type Primitive = string | number | boolean | null
|
||||
|
||||
export type JsonObject = { [key: string]: JsonValue }
|
||||
export type JsonArray = JsonValue[]
|
||||
export type JsonValue = Primitive | JsonObject | JsonArray
|
||||
|
||||
export type CanonicalValue = CanonicalPrimitive | CanonicalObject | CanonicalArray
|
||||
|
||||
// We do not use Map or Set as properties, they cant be serialized to JSON properly
|
||||
// Instead we use objects and arrays under the hood, and optimize speed/memory for runtime, not build time
|
||||
// TODO: In fromCanonicalJson the .x are not set (not critical actually)
|
||||
|
||||
export class CanonicalBase {
|
||||
protected readonly x: number = 0
|
||||
private readonly t?: string[]
|
||||
|
||||
constructor(tags: string[]) {
|
||||
const clean = cleanTags(tags)
|
||||
this.t = clean.length ? clean : undefined
|
||||
}
|
||||
|
||||
get tags(): readonly string[] {
|
||||
return this.t ?? []
|
||||
}
|
||||
|
||||
get delete(): boolean {
|
||||
return this.t?.includes('delete') ?? false
|
||||
}
|
||||
|
||||
get replace(): boolean {
|
||||
return this.t?.includes('replace') ?? false
|
||||
}
|
||||
|
||||
static fromCanonicalJson(json: JsonObject): CanonicalBase {
|
||||
const base = Object.create(CanonicalBase.prototype)
|
||||
base.t = json.t
|
||||
|
||||
return base as CanonicalBase
|
||||
}
|
||||
|
||||
mergeInto(other: CanonicalBase): CanonicalBase {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
dropTags(except: string[]): CanonicalBase {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): any {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
export class CanonicalPrimitive extends CanonicalBase {
|
||||
protected override readonly x: number = 1
|
||||
private v: Primitive | string[]
|
||||
private e?: boolean
|
||||
|
||||
get value(): Readonly<Primitive | string[]> {
|
||||
return this.v
|
||||
}
|
||||
|
||||
get expand(): boolean {
|
||||
return this.e ?? false
|
||||
}
|
||||
|
||||
constructor(tags: string[], value: Primitive | string[], expand?: boolean) {
|
||||
super(tags)
|
||||
|
||||
this.v = value
|
||||
this.e = expand
|
||||
}
|
||||
|
||||
static fromInputJson(outerTags: string[], primitive: Primitive): CanonicalPrimitive {
|
||||
let v: Primitive | string[]
|
||||
let e: boolean | undefined = undefined
|
||||
|
||||
if (typeof primitive === 'string') {
|
||||
const segments = primitive.split(/\$\{(.*?)\}/)
|
||||
|
||||
if (segments.length > 1) {
|
||||
v = segments
|
||||
e = true
|
||||
} else {
|
||||
v = primitive
|
||||
}
|
||||
} else {
|
||||
v = primitive
|
||||
}
|
||||
|
||||
return new CanonicalPrimitive(outerTags, v, e)
|
||||
}
|
||||
|
||||
static override fromCanonicalJson(json: JsonObject): CanonicalPrimitive {
|
||||
const primitive = super.fromCanonicalJson(json) as CanonicalPrimitive
|
||||
Object.setPrototypeOf(primitive, CanonicalPrimitive.prototype)
|
||||
|
||||
primitive.v = json.v as Primitive | string[]
|
||||
primitive.e = json.e as boolean | undefined
|
||||
|
||||
return primitive
|
||||
}
|
||||
|
||||
override mergeInto(other: CanonicalPrimitive): CanonicalPrimitive {
|
||||
return this
|
||||
}
|
||||
|
||||
override dropTags(except: string[]): CanonicalPrimitive {
|
||||
return new CanonicalPrimitive(this.tags.filter(t => except.includes(t)), this.v, this.e)
|
||||
}
|
||||
|
||||
override reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): CanonicalPrimitive | Primitive | undefined {
|
||||
if (!options.predicate(this.tags)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.expand) {
|
||||
options.expand = true
|
||||
return this
|
||||
}
|
||||
|
||||
return this.v as Primitive
|
||||
}
|
||||
|
||||
resolve(resolver: (key: string) => JsonValue): string {
|
||||
return (this.v as string[]).map((p, i) => i % 2 ? resolver(p) : p).join('')
|
||||
}
|
||||
}
|
||||
|
||||
export class CanonicalObject extends CanonicalBase {
|
||||
protected override readonly x: number = 2
|
||||
private n?: Record<string, CanonicalValue>
|
||||
private a?: CanonicalValue[]
|
||||
|
||||
constructor(
|
||||
tags: string[],
|
||||
named: Record<string, CanonicalValue>,
|
||||
anonymous: CanonicalValue[],
|
||||
) {
|
||||
super(tags)
|
||||
|
||||
if (Object.keys(named).length) {
|
||||
this.n = named
|
||||
}
|
||||
|
||||
if (anonymous.length) {
|
||||
this.a = anonymous
|
||||
}
|
||||
}
|
||||
|
||||
get named(): Readonly<Record<string, CanonicalValue>> {
|
||||
return this.n ?? {}
|
||||
}
|
||||
|
||||
get anonymous(): readonly CanonicalValue[] {
|
||||
return this.a ?? []
|
||||
}
|
||||
|
||||
static fromInputJson(outerTags: string[], json: JsonObject): CanonicalObject {
|
||||
let n: Record<string, CanonicalValue> = {}
|
||||
let a: CanonicalValue[] = []
|
||||
|
||||
for (const key in json) {
|
||||
if (key === '$tags') continue
|
||||
|
||||
let [name, ...tags] = key.split('$')
|
||||
tags = cleanTags(tags)
|
||||
|
||||
if (name) {
|
||||
n[name] = fromInputJson(tags, json[key] as JsonValue)
|
||||
} else if (tags.length) {
|
||||
a.push(fromInputJson(tags, json[key] as JsonValue))
|
||||
} else {
|
||||
console.error(`No name and no tags found in key '${key}'`)
|
||||
}
|
||||
}
|
||||
|
||||
return new CanonicalObject([...outerTags, ...getInnerTags(json)], n, a)
|
||||
}
|
||||
|
||||
static override fromCanonicalJson(json: JsonObject): CanonicalObject {
|
||||
const object = super.fromCanonicalJson(json) as CanonicalObject
|
||||
Object.setPrototypeOf(object, CanonicalObject.prototype)
|
||||
|
||||
if (json.n) {
|
||||
object.n = {}
|
||||
|
||||
for (const [k, v] of Object.entries(json.n)) {
|
||||
object.n[k] = fromCanonicalJson(v)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.a) {
|
||||
object.a = (json.a as JsonValue[]).map(i => fromCanonicalJson(i))
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
override mergeInto(other: CanonicalObject): CanonicalObject {
|
||||
const n: Record<string, CanonicalValue> = {}
|
||||
|
||||
for (const key of new Set([...Object.keys(this.named), ...Object.keys(other.named)])) {
|
||||
let merged: CanonicalValue | undefined = undefined
|
||||
|
||||
if (key in this.named && key in other.named) {
|
||||
merged = mergeInto(this.named[key]!, other.named[key]!)
|
||||
} else {
|
||||
merged = key in this.named ? this.named[key] : other.named[key]
|
||||
}
|
||||
|
||||
if (merged) {
|
||||
n[key] = merged
|
||||
}
|
||||
}
|
||||
|
||||
return new CanonicalObject([...this.tags], n, [...this.anonymous])
|
||||
}
|
||||
|
||||
override dropTags(except: string[]): CanonicalObject {
|
||||
const n = Object.fromEntries(Object.entries(this.named).map(([k, v]) => [k, v.dropTags(except)]))
|
||||
const a = this.anonymous.map(a => a.dropTags(except))
|
||||
|
||||
return new CanonicalObject(this.tags.filter(t => except.includes(t)), n, a)
|
||||
}
|
||||
|
||||
override reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): any | undefined {
|
||||
if (!options.predicate(this.tags)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const n = Object.fromEntries(Object.entries(this.named).map(([k, v]) => [k, v.reduce(options)]).filter(([k, v]) => v !== undefined))
|
||||
|
||||
if (Object.keys(n).length) {
|
||||
return n
|
||||
}
|
||||
|
||||
const a = this.anonymous.map(a => a.reduce(options)).filter(a => a !== undefined)
|
||||
|
||||
if (a.length) {
|
||||
if (a.length > 1) {
|
||||
console.error(`Multiple anonymous object properties: ${JSON.stringify(a)}`)
|
||||
}
|
||||
|
||||
return a[0]
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class CanonicalArray extends CanonicalBase {
|
||||
protected override readonly x: number = 3
|
||||
private i?: CanonicalValue[]
|
||||
|
||||
constructor(tags: string[], items: CanonicalValue[]) {
|
||||
super(tags)
|
||||
|
||||
if (items.length) {
|
||||
this.i = items
|
||||
}
|
||||
}
|
||||
|
||||
get items(): readonly CanonicalValue[] {
|
||||
return this.i ?? []
|
||||
}
|
||||
|
||||
static fromInputJson(outerTags: string[], json: JsonArray): CanonicalArray {
|
||||
const tags = [...outerTags]
|
||||
|
||||
json = json.filter(item => {
|
||||
if (typeof item === 'object' && item !== null && Object.keys(item).length === 1 && '$tags' in item) {
|
||||
tags.push(...getInnerTags(item))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return new CanonicalArray(tags, json.map(i => fromInputJson([], i)))
|
||||
}
|
||||
|
||||
static override fromCanonicalJson(json: JsonObject): CanonicalArray {
|
||||
const array = super.fromCanonicalJson(json) as CanonicalArray
|
||||
Object.setPrototypeOf(array, CanonicalArray.prototype)
|
||||
|
||||
if (json.i) {
|
||||
array.i = (json.i as JsonValue[]).map(i => fromCanonicalJson(i))
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
override mergeInto(other: CanonicalArray): CanonicalArray {
|
||||
console.error("Merging arrays not yet implemented")
|
||||
return this // TODO
|
||||
}
|
||||
|
||||
override dropTags(except: string[]): CanonicalArray {
|
||||
const items = (this.i ?? []).map(i => i.dropTags(except))
|
||||
return new CanonicalArray(this.tags.filter(t => except.includes(t)), items)
|
||||
}
|
||||
|
||||
override reduce(options: { predicate: (tags?: string[]) => boolean; expand?: boolean }): any | undefined {
|
||||
if (!options.predicate(this.tags)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.items.map(i => i.reduce(options)).filter(i => i !== undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export function fromInputJson(outerTags: string[], json: JsonValue): CanonicalValue {
|
||||
if (Array.isArray(json)) {
|
||||
return CanonicalArray.fromInputJson(outerTags, json)
|
||||
} else if (typeof json === 'object' && json !== null) {
|
||||
return CanonicalObject.fromInputJson(outerTags, json)
|
||||
}
|
||||
|
||||
return CanonicalPrimitive.fromInputJson(outerTags, json)
|
||||
}
|
||||
|
||||
export function fromCanonicalJson(json: JsonValue): CanonicalValue {
|
||||
if (typeof json === 'object' && json !== null) {
|
||||
json = json as JsonObject
|
||||
|
||||
if (json.x === 1) {
|
||||
return CanonicalPrimitive.fromCanonicalJson(json)
|
||||
} else if (json.x == 2) {
|
||||
return CanonicalObject.fromCanonicalJson(json)
|
||||
}
|
||||
|
||||
return CanonicalArray.fromCanonicalJson(json)
|
||||
}
|
||||
|
||||
throw new Error(`Not a valid canonical object: ${json}`)
|
||||
}
|
||||
|
||||
function mergeInto(self: CanonicalValue, other: CanonicalValue): CanonicalValue | undefined {
|
||||
if (self instanceof CanonicalBase) {
|
||||
if (self.delete) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (self.replace) {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
if (self instanceof CanonicalPrimitive && other instanceof CanonicalPrimitive
|
||||
|| self instanceof CanonicalObject && other instanceof CanonicalObject
|
||||
|| self instanceof CanonicalArray && other instanceof CanonicalArray) {
|
||||
return self.mergeInto(other)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
export function merge(items: CanonicalValue[]): CanonicalValue | undefined {
|
||||
if (!items.length) {
|
||||
return undefined
|
||||
} else if (items.length === 1) {
|
||||
return items[0]
|
||||
}
|
||||
|
||||
return items.reduceRight((acc, cur) => mergeInto(cur, acc))
|
||||
}
|
||||
|
||||
function getInnerTags(json: JsonObject): string[] {
|
||||
const tags = json['$tags']
|
||||
|
||||
if (typeof tags === 'string') {
|
||||
return cleanTags(tags.split(' '))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function cleanTags(tags: string[]): string[] {
|
||||
return [...new Set(tags)].map(t => t.trim()).filter(t => !!t).sort()
|
||||
}
|
||||
18
packages/layers/content/utils/content-config.ts
Normal file
18
packages/layers/content/utils/content-config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export type Translation = string | Record<string, string>
|
||||
|
||||
export interface Option {
|
||||
code: string
|
||||
icon: string
|
||||
name: Translation
|
||||
}
|
||||
|
||||
export interface Specification {
|
||||
default: string
|
||||
list: Option[]
|
||||
cookieMaxAge: number
|
||||
}
|
||||
|
||||
export interface Configuration {
|
||||
locale?: Specification
|
||||
variant?: Specification
|
||||
}
|
||||
81
packages/layers/content/utils/content-files.ts
Normal file
81
packages/layers/content/utils/content-files.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import {getLayerDirectories} from '@nuxt/kit'
|
||||
import {existsSync, lstatSync} from 'fs'
|
||||
import {readFile} from 'fs/promises'
|
||||
import {join} from 'path'
|
||||
import {parse} from 'yaml'
|
||||
import {fromInputJson, merge, type CanonicalValue} from './content-canonical'
|
||||
import {type Plugin, build} from 'esbuild'
|
||||
|
||||
export function getLayerFiles(slug: string): string[] {
|
||||
return getLayerDirectories().flatMap(d => ['mts', 'yaml'].map(e => join(d.root, `${slug}.${e}`)))
|
||||
}
|
||||
|
||||
export async function getLayerContent(files: string[]): Promise<CanonicalValue | undefined> {
|
||||
const ts = await mergeTsFiles(files.filter(f => f.endsWith('.mts') && existsSync(f) && lstatSync(f).isFile()))
|
||||
const yaml = await mergeYamlFiles(files.filter(f => f.endsWith('.yaml') && existsSync(f) && lstatSync(f).isFile()))
|
||||
|
||||
const merged = merge([ts, yaml].filter(c => c !== undefined))
|
||||
return merged?.dropTags(['de', 'en', 'summer', 'winter']) // TODO
|
||||
}
|
||||
|
||||
async function mergeTsFiles(files: string[]): Promise<CanonicalValue | undefined> {
|
||||
if (!files.length) return undefined
|
||||
|
||||
try {
|
||||
const loader: Plugin = {
|
||||
name: 'file-loader',
|
||||
setup(build) {
|
||||
build.onLoad({filter: /\.mts$/}, async (args) => {
|
||||
let code = await readFile(args.path, 'utf-8')
|
||||
const index = files.indexOf(args.path)
|
||||
|
||||
if (index < files.length - 1) {
|
||||
code = `
|
||||
export * from '${files[index + 1]}';
|
||||
import * as parent from '${files[index + 1]}';
|
||||
${code}
|
||||
`
|
||||
}
|
||||
|
||||
return {contents: code, loader: 'ts'}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const bundle = await build({
|
||||
entryPoints: [files[0]!],
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
target: 'node22',
|
||||
write: false,
|
||||
sourcemap: 'inline',
|
||||
plugins: [loader]
|
||||
})
|
||||
|
||||
const url = 'data:text/javascript;base64,' + Buffer.from(bundle.outputFiles[0]!.text).toString('base64')
|
||||
|
||||
const module = await import(url)
|
||||
return fromInputJson([], module.default)
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
console.error(`Unable to load merged ts: ${message}`)
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeYamlFiles(files: string[]): Promise<CanonicalValue | undefined> {
|
||||
const objects = await Promise.all(files.map(async f => {
|
||||
try {
|
||||
return fromInputJson([], parse(await readFile(f, 'utf-8')))
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e)
|
||||
console.error(`Unable to load content file ${f}: ${message}`)
|
||||
|
||||
return undefined
|
||||
}
|
||||
}))
|
||||
|
||||
return merge(objects.filter(o => !!o))
|
||||
}
|
||||
@ -1,295 +0,0 @@
|
||||
import type { Translations } from './content-types'
|
||||
import { t } from './content-template'
|
||||
|
||||
export interface LegalSection {
|
||||
title: Translations,
|
||||
paragraphs: Translations[]
|
||||
}
|
||||
|
||||
export interface LegalBlock {
|
||||
anchor: string,
|
||||
title: Translations,
|
||||
sections: LegalSection[]
|
||||
}
|
||||
|
||||
export type Legal = LegalBlock[]
|
||||
|
||||
export class LegalPageBuilder {
|
||||
static build(): Legal {
|
||||
return [
|
||||
this.buildImprint(),
|
||||
this.buildPrivacy(),
|
||||
this.buildAccessibility()
|
||||
]
|
||||
}
|
||||
|
||||
protected static buildImprint(): LegalBlock {
|
||||
return {
|
||||
anchor: 'imprint',
|
||||
title: {
|
||||
de: 'Impressum',
|
||||
en: 'Imprint'
|
||||
},
|
||||
sections: [
|
||||
this.buildImprintBusiness(),
|
||||
this.buildImprintLinks(),
|
||||
this.buildImprintDisclaimer(),
|
||||
this.buildImprintCopyright()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildImprintBusiness(): LegalSection {
|
||||
return {
|
||||
title: t`${'business.name'}`,
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Informationen und Offenlegung gemäß §5 (1) ECG, § 25 MedienG, § 63 GewO und § 14 UGB:',
|
||||
en: 'Information and disclosure according to §5 (1) ECG, § 25 Media Act, § 63 Trade Regulation Act, and § 14 Commercial Code (UGB):'
|
||||
},
|
||||
{
|
||||
de: t`Betreiber: ${'business.operator'}\nAnschrift: ${'business.address'}\nTelefon: ${'business.phone'}\nEmail: ${'business.email'}`,
|
||||
en: t`Operator: ${'business.operator'}\nAddress: ${'business.address'}\nPhone: ${'business.phone'}\nEmail: ${'business.email'}`
|
||||
},
|
||||
{
|
||||
de: t`UID-Nr: ${'business.vat'}\nGewerbeaufsichtbehörde: ${'business.authority'}\nMitgliedschaft: ${'business.membership'}\nRechtsvorschrift: ${'business.regulation'}`,
|
||||
en: t`VAT ID: ${'business.vat'}\nSupervisory authority: ${'business.authority'}\nChamber membership: ${'business.membership'}\nLegal regulations: ${'business.regulation'}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildImprintLinks(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Verweise und Links',
|
||||
en: 'External Links'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Trotz sorgfältiger inhaltlicher Kontrolle übernimmt der Betreiber dieser Website keine Verantwortung für Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich. Sollten Sie dennoch auf einen ausgehenden Link stoßen, der auf eine Website mit rechtswidrigen Inhalten oder Tätigkeiten verweist, bitten wir um einen entsprechenden Hinweis. In einem solchen Fall wird der Link gemäß § 17 Abs. 2 ECG umgehend entfernt.',
|
||||
en: 'Despite careful content control, the operator of this website assumes no responsibility for the content of external links. The operators of the linked websites are solely responsible for their content. If you come across a link that leads to unlawful content or activity, we kindly ask for your notification. Such links will be removed promptly in accordance with § 17 (2) ECG.'
|
||||
},
|
||||
{
|
||||
de: 'Der Betreiber dieser Website achtet die Urheberrechte Dritter mit größtmöglicher Sorgfalt. Falls Sie dennoch eine Urheberrechtsverletzung bemerken, ersuchen wir ebenfalls um Mitteilung. Bei Bekanntwerden einer entsprechenden Rechtsverletzung werden die betroffenen Inhalte unverzüglich entfernt.',
|
||||
en: 'The operator of this website takes great care to respect the copyrights of third parties. If you notice any copyright infringement, please notify us. Upon becoming aware of such a violation, the affected content will be removed without delay.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildImprintDisclaimer(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Haftungsausschluss',
|
||||
en: 'Disclaimer'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Der Betreiber übernimmt keine Gewähr für die Aktualität, Richtigkeit, Vollständigkeit oder Qualität der bereitgestellten Informationen. Haftungsansprüche jeglicher Art, die durch die Nutzung oder Nichtnutzung der angebotenen Inhalte bzw. durch die Nutzung fehlerhafter oder unvollständiger Informationen verursacht werden, sind ausgeschlossen – sofern kein nachweislich vorsätzliches oder grob fahrlässiges Verschulden vorliegt. Alle Angebote sind freibleibend und unverbindlich. Der Betreiber behält sich ausdrücklich vor, Teile der Seiten oder das gesamte Angebot ohne gesonderte Ankündigung zu verändern, zu ergänzen, zu löschen oder die Veröffentlichung zeitweise oder endgültig einzustellen.',
|
||||
en: 'The operator assumes no liability for the accuracy, completeness, or quality of the information provided. Claims for damages of any kind arising from the use or non-use of the information presented, or from the use of incorrect or incomplete content, are excluded unless caused by demonstrably intentional or grossly negligent behavior. All offers are non-binding. The operator expressly reserves the right to modify, supplement, delete, or temporarily or permanently cease publication of parts of the site or the entire offer without prior notice.'
|
||||
},
|
||||
{
|
||||
de: 'Aufgrund der technischen Gegebenheiten des Internets kann keine Gewähr für die ständige Verfügbarkeit, Authentizität oder Fehlerfreiheit der Website übernommen werden. Jegliche Haftung für unmittelbare oder mittelbare Schäden, die durch die Nutzung oder vorübergehende Nichtverfügbarkeit der Website entstehen, ist – soweit gesetzlich zulässig – ausgeschlossen.',
|
||||
en: 'Due to the technical nature of the internet, no guarantee can be given for uninterrupted availability, authenticity, or accuracy of the website. Liability for any direct or indirect damages resulting from the use or temporary unavailability of this website is excluded to the extent permitted by law.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildImprintCopyright(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Urheberrecht und Nutzung',
|
||||
en: 'Copyright and Use'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Die Inhalte dieser Website (Texte, Bilder, Grafiken, Videos etc.) sind urheberrechtlich geschützt und ausschließlich für den persönlichen Gebrauch bestimmt. Eine darüber hinausgehende Nutzung – insbesondere die Vervielfältigung, Speicherung in Datenbanken, gewerbliche Nutzung oder Weitergabe an Dritte – ist ohne ausdrückliche Zustimmung des Betreibers nicht gestattet. Die Einbindung einzelner Seiten in fremde Frames ist unzulässig.',
|
||||
en: 'The content of this website (texts, images, graphics, videos, etc.) is protected by copyright and intended solely for personal use. Any further use — including reproduction, storage in databases, commercial use, or distribution to third parties — is not permitted without the explicit consent of the operator. Embedding individual pages of this website in external frames is prohibited.'
|
||||
},
|
||||
{
|
||||
de: 'Der Betreiber ist bemüht, in allen Publikationen Urheberrechte Dritter zu beachten oder auf lizenzfreie bzw. eigene Inhalte zurückzugreifen. Sollte dennoch eine Urheberrechtsverletzung vorliegen, bitten wir um einen Hinweis. Bei Bekanntwerden werden entsprechende Inhalte umgehend entfernt.',
|
||||
en: 'The operator makes every effort to respect the copyrights of third parties or to use either self-created or license-free content. If you suspect a copyright violation, please inform us. In such cases, the relevant content will be removed promptly.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildPrivacy(): LegalBlock {
|
||||
return {
|
||||
anchor: 'privacy',
|
||||
title: {
|
||||
de: 'Datenschutz',
|
||||
en: 'Privacy Policy'
|
||||
},
|
||||
sections: [
|
||||
this.buildPrivacyIntroduction(),
|
||||
this.buildPrivacyData(),
|
||||
this.buildPrivacyLogs(),
|
||||
this.buildPrivacyCookies(),
|
||||
this.buildPrivacyRights()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildPrivacyIntroduction(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Einleitung',
|
||||
en: 'Introduction'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Der Schutz Ihrer persönlichen Daten ist uns ein wichtiges Anliegen. Wir behandeln Ihre Daten vertraulich und entsprechend der geltenden Datenschutzvorschriften (DSGVO, TKG 2003). Diese Erklärung informiert Sie darüber, welche Daten wir im Rahmen unseres Webauftritts erfassen, speichern und wie wir mit ihnen umgehen.',
|
||||
en: 'Protecting your personal data is very important to us. We treat your data confidentially and in accordance with applicable data protection laws (GDPR, TKG 2003). This policy explains what data is collected when you use our website, how it is processed, and how it is protected.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildPrivacyData(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Umgang mit personenbezogenen Daten',
|
||||
en: 'Handling of Personal Data'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Wenn Sie über unsere Website oder per E-Mail Kontakt mit uns aufnehmen, verarbeiten wir die von Ihnen bereitgestellten personenbezogenen Daten ausschließlich zur Bearbeitung Ihrer Anfrage oder für damit zusammenhängende Zwecke.',
|
||||
en: 'If you contact us via our website or by email, we will use the personal data you provide exclusively to process your inquiry or for related purposes.'
|
||||
},
|
||||
{
|
||||
de: 'Die Daten werden nur so lange gespeichert, wie es für die Erfüllung des jeweiligen Zwecks notwendig ist oder rechtliche Aufbewahrungspflichten dies vorsehen. Eine Weitergabe an Dritte erfolgt nicht, es sei denn, dies ist gesetzlich erforderlich oder Sie haben ausdrücklich zugestimmt.',
|
||||
en: 'Your data will only be stored as long as necessary to fulfill the intended purpose or as required by legal retention obligations. No data will be shared with third parties unless required by law or explicitly consented to by you.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildPrivacyLogs(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Serverprotokolle',
|
||||
en: 'Server Logs'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Beim Aufruf unserer Website werden automatisch Informationen durch den Webserver protokolliert (sogenannte Server-Logfiles). Dazu gehören unter anderem:',
|
||||
en: 'When visiting our website, technical information is automatically recorded by the web server (so-called server log files). This includes, for example:'
|
||||
},
|
||||
{
|
||||
de: '• IP-Adresse\n• Datum und Uhrzeit des Zugriffs\n• besuchte Seiten\n• verwendeter Browser und Betriebssystem\n• Spracheinstellungen\n• ggf. Referrer-URL',
|
||||
en: '• IP address\n• Date and time of access\n• Visited pages\n• Browser and operating system used\n• Language settings\n• Referrer URL (if applicable)'
|
||||
},
|
||||
{
|
||||
de: 'Diese Daten sind technisch notwendig, um den sicheren und stabilen Betrieb der Website zu gewährleisten. Sie lassen keinen direkten Rückschluss auf Ihre Person zu und werden nicht mit anderen Datenquellen zusammengeführt.',
|
||||
en: 'These logs are essential for the safe and stable operation of the site. The data does not allow for direct identification of users and is not merged with other data sources.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildPrivacyCookies(): LegalSection {
|
||||
return {
|
||||
title: 'Cookies',
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Unsere Website verwendet ausschließlich technisch notwendige Cookies. Wir setzen keine Tracking-Technologien ein und analysieren keine Nutzerverhalten.',
|
||||
en: 'This website only uses technically necessary cookies. No tracking or analytics tools are in use.'
|
||||
},
|
||||
{
|
||||
de: 'Das einzige gesetzte Cookie dient der Unterscheidung zwischen Sommer- und Winterdarstellung der Website. Es enthält keine personenbezogenen Informationen und wird nur für diesen funktionalen Zweck verwendet.',
|
||||
en: 'The only cookie stored is used to toggle between summer and winter versions of the website. This cookie does not contain any personal information and is used solely for this visual adjustment.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildPrivacyRights(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Ihre Rechte',
|
||||
en: 'Your Rights'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Sie haben das Recht auf Auskunft über Ihre bei uns gespeicherten personenbezogenen Daten sowie auf Berichtigung, Löschung oder Einschränkung der Verarbeitung. Sie können der Datenverarbeitung jederzeit widersprechen und – sofern zutreffend – Ihr Recht auf Datenübertragbarkeit geltend machen.',
|
||||
en: 'You have the right to access your stored personal data, request correction or deletion, and restrict or object to processing. If applicable, you may also request data portability.'
|
||||
},
|
||||
{
|
||||
de: 'Wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer Daten gegen geltendes Datenschutzrecht verstößt, haben Sie das Recht, sich bei der zuständigen Datenschutzbehörde zu beschweren oder uns direkt zu kontaktieren.',
|
||||
en: 'If you believe that your data is being processed in violation of data protection laws, you have the right to file a complaint with the relevant data protection authority or contact us directly.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildAccessibility(): LegalBlock {
|
||||
return {
|
||||
anchor: 'accessibility',
|
||||
title: {
|
||||
de: 'Barrierefreiheit',
|
||||
en: 'Accessibility'
|
||||
},
|
||||
sections: [
|
||||
this.buildAccessibilityGeneral(),
|
||||
this.buildAccessibilityFramework(),
|
||||
this.buildAccessibilityContact(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildAccessibilityGeneral(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Allgemeines',
|
||||
en: 'General Information'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Der Betreiber dieser Website ist bemüht, die Inhalte möglichst barrierefrei bereitzustellen. Auch wenn das österreichische Barrierefreiheitsgesetz (BaFG) für Kleinstunternehmen nicht verpflichtend ist, orientiert sich der Betreiber freiwillig an den anerkannten Richtlinien für barrierefreie Webinhalte (WCAG).',
|
||||
en: 'The operator of this website strives to make the content as accessible as possible. While the Austrian Accessibility Act (BaFG) does not apply to microenterprises, the operator voluntarily follows the recognized guidelines for accessible web content (WCAG).'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildAccessibilityFramework(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Rechtlicher Rahmen',
|
||||
en: 'Legal Framework'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: 'Laut österreichischem Barrierefreiheitsgesetz (BaFG) sind Kleinstunternehmen – also Betriebe mit weniger als zehn Mitarbeitenden und einem Jahresumsatz oder einer Bilanzsumme unter 2 Millionen Euro – von den gesetzlichen Verpflichtungen zur digitalen Barrierefreiheit ausgenommen, sofern ausschließlich Dienstleistungen online angeboten werden.',
|
||||
en: 'According to the Austrian Accessibility Act (BaFG), microenterprises — defined as businesses with fewer than ten employees and annual revenue or balance sheet totals under 2 million euros — are exempt from the legal obligations of digital accessibility, provided they only offer online services.'
|
||||
},
|
||||
{
|
||||
de: 'Der Betreiber fällt unter diese gesetzliche Ausnahme und ist daher nicht verpflichtet, eine formale Barrierefreiheitserklärung zu veröffentlichen.',
|
||||
en: 'The operator qualifies as such a microenterprise and is therefore not legally required to publish a formal accessibility statement.'
|
||||
},
|
||||
{
|
||||
de: 'Trotzdem wurde bei der Gestaltung der Website auf eine möglichst barrierearme Umsetzung geachtet. Dazu zählen unter anderem gut lesbare Schriftarten, ausreichende Farbkontraste, eine klare Inhaltsstruktur und die Bedienbarkeit ohne Maus. Ziel ist es, die Nutzung der Website für möglichst viele Menschen zugänglich zu machen.',
|
||||
en: 'Nevertheless, efforts have been made to design this website in a way that minimizes barriers. This includes readable fonts, sufficient color contrast, a clear content structure, and keyboard navigability. The goal is to make the website usable for as many people as possible.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
protected static buildAccessibilityContact(): LegalSection {
|
||||
return {
|
||||
title: {
|
||||
de: 'Kontakt bei Barrieren',
|
||||
en: 'Reporting Accessibility Issues'
|
||||
},
|
||||
paragraphs: [
|
||||
{
|
||||
de: t`Sollten Inhalte oder Funktionen dieser Website nicht barrierefrei zugänglich sein, wird um Rückmeldung gebeten. Hinweise können jederzeit per E-Mail an ${'business.email'} übermittelt werden.`,
|
||||
en: t`If you encounter any inaccessible content or functionality on this website, you are encouraged to report it. Please send your feedback via email to ${'business.email'}.`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
121
packages/layers/content/utils/content-reduced.ts
Normal file
121
packages/layers/content/utils/content-reduced.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import {type JsonValue, fromCanonicalJson, CanonicalPrimitive} from "./content-canonical";
|
||||
|
||||
export function buildContent(
|
||||
locale: string | undefined,
|
||||
locales: string[],
|
||||
variant: string | undefined,
|
||||
variants: string[],
|
||||
content: JsonValue
|
||||
): [JsonValue, boolean] {
|
||||
function check(all: string[], specified: string[], query: string): boolean {
|
||||
const related = specified.filter(t => all.includes(t))
|
||||
all = related.length ? related : all
|
||||
return all.includes(query)
|
||||
}
|
||||
|
||||
function predicate(tags?: string[]) {
|
||||
if (!tags || !tags.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (locale && !check(locales, tags, locale)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !variant || check(variants, tags, variant)
|
||||
}
|
||||
|
||||
const options = {predicate: predicate, expand: false}
|
||||
|
||||
const canonical = fromCanonicalJson(content)
|
||||
const reduced = canonical.reduce(options)
|
||||
|
||||
return [reduced, options.expand]
|
||||
}
|
||||
|
||||
export function expandContent(content: JsonValue, sources: JsonValue[]): JsonValue {
|
||||
function resolve(key: string): JsonValue {
|
||||
for (const source of sources) {
|
||||
const value = getPath(source, key)
|
||||
|
||||
if (value !== undefined) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return `$\{${key}\}`
|
||||
}
|
||||
|
||||
function traverse(item: JsonValue): JsonValue {
|
||||
if (item !== null) {
|
||||
if (item instanceof CanonicalPrimitive) {
|
||||
return item.resolve(resolve)
|
||||
} else if (Array.isArray(item)) {
|
||||
return item.map(i => traverse(i))
|
||||
} else if (typeof item === 'object') {
|
||||
const record: Record<string, JsonValue> = {}
|
||||
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
record[k] = traverse(v)
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
return traverse(content)
|
||||
}
|
||||
|
||||
// function getPath(obj: any, path: string, sep = '.'): any { // TODO REFACTOR THIS
|
||||
// const keys = path.split(sep)
|
||||
// let cur = obj
|
||||
//
|
||||
// for (const key of keys) {
|
||||
// if (cur == null || typeof cur !== 'object') return undefined
|
||||
// cur = cur[key]
|
||||
// }
|
||||
//
|
||||
// return cur
|
||||
// }
|
||||
|
||||
|
||||
function toPath(path: string): Array<string | number> {
|
||||
// Matches:
|
||||
// - bare tokens: foo, bar
|
||||
// - [123] numeric indices
|
||||
// - ["str"] or ['str'] quoted keys
|
||||
const re = /[^.[\]]+|\[(?:([0-9]+)|(["'])(.*?)\2)\]/g;
|
||||
const tokens: Array<string | number> = [];
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = re.exec(path))) {
|
||||
if (m[1] !== undefined) {
|
||||
tokens.push(Number(m[1])); // [123]
|
||||
} else if (m[3] !== undefined) {
|
||||
tokens.push(m[3]); // ["key"] / ['key']
|
||||
} else {
|
||||
tokens.push(m[0]); // bare token
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function getPath(obj: unknown, path: string): JsonValue | undefined {
|
||||
const steps = toPath(path);
|
||||
let cur: any = obj;
|
||||
|
||||
for (const step of steps) {
|
||||
if (cur == null) return undefined;
|
||||
|
||||
if (typeof step === 'number') {
|
||||
if (!Array.isArray(cur) || step < 0 || step >= cur.length) return undefined;
|
||||
cur = cur[step];
|
||||
} else {
|
||||
cur = cur[step];
|
||||
}
|
||||
}
|
||||
return cur as JsonValue;
|
||||
}
|
||||
92
packages/layers/content/utils/content-routes.ts
Normal file
92
packages/layers/content/utils/content-routes.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import {type Configuration} from './content-config'
|
||||
import {getLayerDirectories} from '@nuxt/kit'
|
||||
import {join, relative} from 'path'
|
||||
import {existsSync} from 'fs'
|
||||
import {readdir} from 'fs/promises'
|
||||
|
||||
// this solely exists because in prerender:routes composables or virtual modules are not available yet
|
||||
export async function getConfig(): Promise<Configuration> {
|
||||
const config = getLayerDirectories().map(d => join(d.root, 'content.config.ts')).find(f => existsSync(f))
|
||||
|
||||
if (config) {
|
||||
const module = await import(config)
|
||||
return module.default
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export async function getRoutes(): Promise<Set<string>> {
|
||||
async function find(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true , recursive: true })
|
||||
return entries.filter(e => e.isFile() && e.name.endsWith('.vue')).map(e => relative(directory, join(e.parentPath, e.name)))
|
||||
}
|
||||
|
||||
const files = await Promise.all(getLayerDirectories().map(d => find(d.appPages)))
|
||||
return new Set(files.flatMap(fs => fs.map(f => f.slice(0, -4))))
|
||||
}
|
||||
|
||||
export function getRouteShortcuts(routes: Set<string>): Record<string, string> {
|
||||
const shorten = (r: string) => r.replace('[locale]/', '').replace('[variant]/', '')
|
||||
|
||||
const shortcuts = [...routes].map(r => [shorten(r), r] as [string, string])
|
||||
const matches: Record<string, string[]> = {}
|
||||
|
||||
for (const [shortcut, route] of shortcuts) {
|
||||
const list = matches[shortcut] ?? []
|
||||
list.push(route)
|
||||
matches[shortcut] = list
|
||||
}
|
||||
|
||||
const unique: Record<string, string> = {}
|
||||
|
||||
for (const [shortcut, routes] of Object.entries(matches)) {
|
||||
if (routes.length === 1) {
|
||||
unique[shortcut] = routes[0]!
|
||||
}
|
||||
}
|
||||
|
||||
return unique
|
||||
}
|
||||
|
||||
export async function getExpandedRoutes(config: Configuration): Promise<Set<string>> {
|
||||
const locales = config.locale?.list.map(i => i.code) ?? []
|
||||
const variants = config.variant?.list.map(i => i.code) ?? []
|
||||
|
||||
let combinations: [string | undefined, string | undefined][]
|
||||
|
||||
if (!locales.length && !variants.length) {
|
||||
combinations = []
|
||||
} else if (!locales.length) {
|
||||
combinations = variants.map(v => [undefined, v])
|
||||
} else if (!variants.length) {
|
||||
combinations = locales.map(l => [l, undefined])
|
||||
} else {
|
||||
combinations = locales.flatMap(l => variants.map(v => [l, v] as [string, string]))
|
||||
}
|
||||
|
||||
function expand(route: string, locale?: string, variant?: string): string {
|
||||
let result = route
|
||||
|
||||
if (locale) {
|
||||
result = result.replace('[locale]', locale)
|
||||
}
|
||||
|
||||
if (variant) {
|
||||
result = result.replace('[variant]', variant)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const result = new Set<string>()
|
||||
|
||||
for (const unexpanded of await getRoutes()) {
|
||||
for (const [locale, variant] of combinations) {
|
||||
let expanded = expand(unexpanded, locale, variant)
|
||||
result.add(expanded.endsWith('/index') ? expanded.slice(0, -6) : expanded)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
const MAGIC = 0x7f5a08f5
|
||||
|
||||
export interface TemplateString {
|
||||
__magic: number
|
||||
strings: TemplateStringsArray
|
||||
variables: string[]
|
||||
}
|
||||
|
||||
export function t(strings: TemplateStringsArray, ...variables: string[]): TemplateString {
|
||||
return { __magic: MAGIC, strings: strings, variables: variables }
|
||||
}
|
||||
|
||||
export function isTemplate(template: any): boolean {
|
||||
return template !== null && typeof template === 'object' && template.__magic === MAGIC
|
||||
}
|
||||
|
||||
export function expandTemplate(template: TemplateString, content: any): string {
|
||||
const values = template.variables.map(path =>
|
||||
path.split('.').reduce((acc, key) => {
|
||||
if (acc && typeof acc === 'object' && key in acc) {
|
||||
return acc[key]
|
||||
}
|
||||
|
||||
return undefined
|
||||
}, content)
|
||||
)
|
||||
|
||||
let result = ''
|
||||
|
||||
for (let i = 0; i < template.strings.length; i++) {
|
||||
result += template.strings[i]
|
||||
|
||||
if (i < values.length) {
|
||||
result += values[i] ?? `\${${template.variables[i]}}`
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import type { TemplateString } from './content-template'
|
||||
|
||||
export type Translation = string | TemplateString
|
||||
export type Translations = Translation | Record<string, Translation>
|
||||
|
||||
export interface Locale {
|
||||
code: string
|
||||
name: Translations
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface Variant {
|
||||
code: string
|
||||
name: Translations
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export interface ContentMatrix {
|
||||
locale: {
|
||||
default: string
|
||||
list: Locale[]
|
||||
cookieMaxAge: number
|
||||
}
|
||||
|
||||
variant: {
|
||||
default: string
|
||||
list: Variant[]
|
||||
cookieMaxAge: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface BusinessInfo {
|
||||
uid: string
|
||||
name: Translations
|
||||
operator: Translations
|
||||
address: Translations
|
||||
phone: Translations
|
||||
email: Translations
|
||||
vat: Translations
|
||||
authority: Translations
|
||||
membership: Translations
|
||||
regulation: Translations
|
||||
}
|
||||
346
pnpm-lock.yaml
generated
346
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ importers:
|
||||
'@nuxt/image':
|
||||
specifier: 1.11.0
|
||||
version: 1.11.0(db0@0.3.4)(ioredis@5.8.0)(magicast@0.3.5)
|
||||
'@nuxt/kit':
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.3(magicast@0.3.5)
|
||||
'@nuxt/ui':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(@babel/parser@7.28.4)(change-case@5.4.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.8.0)(magicast@0.3.5)(typescript@5.9.2)(valibot@1.1.0(typescript@5.9.2))(vite@7.1.9(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.2)))(vue@3.5.22(typescript@5.9.2))(zod@4.1.11)
|
||||
@ -53,9 +56,18 @@ importers:
|
||||
|
||||
packages/layers/content:
|
||||
dependencies:
|
||||
'@nuxt/kit':
|
||||
specifier: ^4.1.3
|
||||
version: 4.1.3(magicast@0.3.5)
|
||||
esbuild:
|
||||
specifier: ^0.25.11
|
||||
version: 0.25.11
|
||||
vue-router:
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1(vue@3.5.22(typescript@5.9.2))
|
||||
yaml:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
devDependencies:
|
||||
'@nuxt/eslint':
|
||||
specifier: latest
|
||||
@ -274,156 +286,312 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.11':
|
||||
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.10':
|
||||
resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.11':
|
||||
resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.10':
|
||||
resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.11':
|
||||
resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.10':
|
||||
resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.11':
|
||||
resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.10':
|
||||
resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.11':
|
||||
resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.10':
|
||||
resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.11':
|
||||
resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.10':
|
||||
resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.11':
|
||||
resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.10':
|
||||
resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.11':
|
||||
resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.10':
|
||||
resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.11':
|
||||
resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.10':
|
||||
resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.11':
|
||||
resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.10':
|
||||
resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.11':
|
||||
resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.10':
|
||||
resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.11':
|
||||
resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.10':
|
||||
resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.11':
|
||||
resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.10':
|
||||
resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.11':
|
||||
resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.10':
|
||||
resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.11':
|
||||
resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.10':
|
||||
resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.11':
|
||||
resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.10':
|
||||
resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.11':
|
||||
resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.10':
|
||||
resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.11':
|
||||
resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.10':
|
||||
resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.11':
|
||||
resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0':
|
||||
resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@ -679,6 +847,10 @@ packages:
|
||||
resolution: {integrity: sha512-P5q41xeEOa6ZQC0PvIP7TSBmOAMxXK4qihDcCbYIJq8RcVsEPbGZVlidmxE6EOw1ucSyodq9nbV31FAKwoL4NQ==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
'@nuxt/kit@4.1.3':
|
||||
resolution: {integrity: sha512-WK0yPIqcb3GQ8r4GutF6p/2fsyXnmmmkuwVLzN4YaJHrpA2tjEagjbxdjkWYeHW8o4XIKJ4micah4wPOVK49Mg==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
'@nuxt/schema@4.1.2':
|
||||
resolution: {integrity: sha512-uFr13C6c52OFbF3hZVIV65KvhQRyrwp1GlAm7EVNGjebY8279QEel57T4R9UA1dn2Et6CBynBFhWoFwwo97Pig==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
@ -2478,6 +2650,11 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.25.11:
|
||||
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
@ -3972,6 +4149,7 @@ packages:
|
||||
|
||||
semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.2:
|
||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||
@ -5021,81 +5199,159 @@ snapshots:
|
||||
'@esbuild/aix-ppc64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.10':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.11':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.0(eslint@9.37.0(jiti@2.6.1))':
|
||||
dependencies:
|
||||
eslint: 9.37.0(jiti@2.6.1)
|
||||
@ -5125,11 +5381,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@nodelib/fs.walk': 3.0.1
|
||||
ansis: 4.2.0
|
||||
bundle-require: 5.1.0(esbuild@0.25.10)
|
||||
bundle-require: 5.1.0(esbuild@0.25.11)
|
||||
cac: 6.7.14
|
||||
chokidar: 4.0.3
|
||||
debug: 4.4.1
|
||||
esbuild: 0.25.10
|
||||
esbuild: 0.25.11
|
||||
eslint: 9.37.0(jiti@2.6.1)
|
||||
find-up: 7.0.0
|
||||
get-port-please: 3.2.0
|
||||
@ -5485,7 +5741,7 @@ snapshots:
|
||||
'@nuxt/devtools-kit': 2.6.5(magicast@0.3.5)(vite@7.1.9(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||
'@nuxt/eslint-config': 1.9.0(@typescript-eslint/utils@8.40.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.2))(@vue/compiler-sfc@3.5.22)(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.2)
|
||||
'@nuxt/eslint-plugin': 1.9.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.2)
|
||||
'@nuxt/kit': 4.1.2(magicast@0.3.5)
|
||||
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||
chokidar: 4.0.3
|
||||
eslint: 9.37.0(jiti@2.6.1)
|
||||
eslint-flat-config-utils: 2.1.4
|
||||
@ -5560,7 +5816,7 @@ snapshots:
|
||||
'@iconify/utils': 3.0.2
|
||||
'@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.2))
|
||||
'@nuxt/devtools-kit': 2.6.5(magicast@0.3.5)(vite@7.1.9(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||
'@nuxt/kit': 4.1.2(magicast@0.3.5)
|
||||
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||
consola: 3.4.2
|
||||
local-pkg: 1.1.2
|
||||
mlly: 1.8.0
|
||||
@ -5668,6 +5924,33 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/kit@4.1.3(magicast@0.3.5)':
|
||||
dependencies:
|
||||
c12: 3.3.0(magicast@0.3.5)
|
||||
consola: 3.4.2
|
||||
defu: 6.1.4
|
||||
destr: 2.0.5
|
||||
errx: 0.1.0
|
||||
exsolve: 1.0.7
|
||||
ignore: 7.0.5
|
||||
jiti: 2.6.1
|
||||
klona: 2.0.6
|
||||
mlly: 1.8.0
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
pkg-types: 2.3.0
|
||||
rc9: 2.1.2
|
||||
scule: 1.3.0
|
||||
semver: 7.7.2
|
||||
std-env: 3.9.0
|
||||
tinyglobby: 0.2.15
|
||||
ufo: 1.6.1
|
||||
unctx: 2.4.1
|
||||
unimport: 5.4.1
|
||||
untyped: 2.0.0
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/schema@4.1.2':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.22
|
||||
@ -5703,7 +5986,7 @@ snapshots:
|
||||
'@internationalized/number': 3.6.5
|
||||
'@nuxt/fonts': 0.11.4(db0@0.3.4)(ioredis@5.8.0)(magicast@0.3.5)(vite@7.1.9(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))
|
||||
'@nuxt/icon': 2.0.0(magicast@0.3.5)(vite@7.1.9(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.2))
|
||||
'@nuxt/kit': 4.1.2(magicast@0.3.5)
|
||||
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||
'@nuxt/schema': 4.1.2
|
||||
'@nuxtjs/color-mode': 3.5.2(magicast@0.3.5)
|
||||
'@standard-schema/spec': 1.0.0
|
||||
@ -5739,8 +6022,8 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
typescript: 5.9.2
|
||||
unplugin: 2.3.10
|
||||
unplugin-auto-import: 20.2.0(@nuxt/kit@4.1.2(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.2)))
|
||||
unplugin-vue-components: 29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.2(magicast@0.3.5))(vue@3.5.22(typescript@5.9.2))
|
||||
unplugin-auto-import: 20.2.0(@nuxt/kit@4.1.3(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.2)))
|
||||
unplugin-vue-components: 29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.3(magicast@0.3.5))(vue@3.5.22(typescript@5.9.2))
|
||||
vaul-vue: 0.4.1(reka-ui@2.5.1(typescript@5.9.2)(vue@3.5.22(typescript@5.9.2)))(vue@3.5.22(typescript@5.9.2))
|
||||
vue-component-type-helpers: 3.1.0
|
||||
optionalDependencies:
|
||||
@ -5799,7 +6082,7 @@ snapshots:
|
||||
consola: 3.4.2
|
||||
cssnano: 7.1.1(postcss@8.5.6)
|
||||
defu: 6.1.4
|
||||
esbuild: 0.25.10
|
||||
esbuild: 0.25.11
|
||||
escape-string-regexp: 5.0.0
|
||||
exsolve: 1.0.7
|
||||
get-port-please: 3.2.0
|
||||
@ -6975,9 +7258,9 @@ snapshots:
|
||||
dependencies:
|
||||
run-applescript: 7.1.0
|
||||
|
||||
bundle-require@5.1.0(esbuild@0.25.10):
|
||||
bundle-require@5.1.0(esbuild@0.25.11):
|
||||
dependencies:
|
||||
esbuild: 0.25.10
|
||||
esbuild: 0.25.11
|
||||
load-tsconfig: 0.2.5
|
||||
|
||||
c12@3.3.0(magicast@0.3.5):
|
||||
@ -7420,6 +7703,35 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.25.10
|
||||
'@esbuild/win32-x64': 0.25.10
|
||||
|
||||
esbuild@0.25.11:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.11
|
||||
'@esbuild/android-arm': 0.25.11
|
||||
'@esbuild/android-arm64': 0.25.11
|
||||
'@esbuild/android-x64': 0.25.11
|
||||
'@esbuild/darwin-arm64': 0.25.11
|
||||
'@esbuild/darwin-x64': 0.25.11
|
||||
'@esbuild/freebsd-arm64': 0.25.11
|
||||
'@esbuild/freebsd-x64': 0.25.11
|
||||
'@esbuild/linux-arm': 0.25.11
|
||||
'@esbuild/linux-arm64': 0.25.11
|
||||
'@esbuild/linux-ia32': 0.25.11
|
||||
'@esbuild/linux-loong64': 0.25.11
|
||||
'@esbuild/linux-mips64el': 0.25.11
|
||||
'@esbuild/linux-ppc64': 0.25.11
|
||||
'@esbuild/linux-riscv64': 0.25.11
|
||||
'@esbuild/linux-s390x': 0.25.11
|
||||
'@esbuild/linux-x64': 0.25.11
|
||||
'@esbuild/netbsd-arm64': 0.25.11
|
||||
'@esbuild/netbsd-x64': 0.25.11
|
||||
'@esbuild/openbsd-arm64': 0.25.11
|
||||
'@esbuild/openbsd-x64': 0.25.11
|
||||
'@esbuild/openharmony-arm64': 0.25.11
|
||||
'@esbuild/sunos-x64': 0.25.11
|
||||
'@esbuild/win32-arm64': 0.25.11
|
||||
'@esbuild/win32-ia32': 0.25.11
|
||||
'@esbuild/win32-x64': 0.25.11
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
@ -8364,7 +8676,7 @@ snapshots:
|
||||
defu: 6.1.4
|
||||
destr: 2.0.5
|
||||
dot-prop: 9.0.0
|
||||
esbuild: 0.25.10
|
||||
esbuild: 0.25.11
|
||||
escape-string-regexp: 5.0.0
|
||||
etag: 1.8.1
|
||||
exsolve: 1.0.7
|
||||
@ -8505,7 +8817,7 @@ snapshots:
|
||||
destr: 2.0.5
|
||||
devalue: 5.3.2
|
||||
errx: 0.1.0
|
||||
esbuild: 0.25.10
|
||||
esbuild: 0.25.11
|
||||
escape-string-regexp: 5.0.0
|
||||
estree-walker: 3.0.3
|
||||
exsolve: 1.0.7
|
||||
@ -9628,7 +9940,7 @@ snapshots:
|
||||
unplugin: 2.3.10
|
||||
unplugin-utils: 0.3.0
|
||||
|
||||
unplugin-auto-import@20.2.0(@nuxt/kit@4.1.2(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.2))):
|
||||
unplugin-auto-import@20.2.0(@nuxt/kit@4.1.3(magicast@0.3.5))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.2))):
|
||||
dependencies:
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.19
|
||||
@ -9637,7 +9949,7 @@ snapshots:
|
||||
unplugin: 2.3.10
|
||||
unplugin-utils: 0.3.0
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 4.1.2(magicast@0.3.5)
|
||||
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||
'@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.2))
|
||||
|
||||
unplugin-utils@0.2.5:
|
||||
@ -9650,7 +9962,7 @@ snapshots:
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
|
||||
unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.2(magicast@0.3.5))(vue@3.5.22(typescript@5.9.2)):
|
||||
unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(@nuxt/kit@4.1.3(magicast@0.3.5))(vue@3.5.22(typescript@5.9.2)):
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.3
|
||||
@ -9663,7 +9975,7 @@ snapshots:
|
||||
vue: 3.5.22(typescript@5.9.2)
|
||||
optionalDependencies:
|
||||
'@babel/parser': 7.28.4
|
||||
'@nuxt/kit': 4.1.2(magicast@0.3.5)
|
||||
'@nuxt/kit': 4.1.3(magicast@0.3.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -9863,7 +10175,7 @@ snapshots:
|
||||
|
||||
vite@7.1.9(@types/node@22.15.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.10
|
||||
esbuild: 0.25.11
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user