Compare commits
No commits in common. "main" and "darkmode" have entirely different histories.
@ -1,61 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>atmos · Konzeptionelle Düfte aus der Schweiz</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="atmos — konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter. Sechs Düfte als Discovery Set oder 50 ml Flakon. Made in Switzerland."
|
||||
/>
|
||||
<meta name="theme-color" content="#262626" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
|
||||
<!-- TODO: replace https://atmos.example with the real production domain -->
|
||||
<link rel="canonical" href="https://atmos.example/" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="atmos" />
|
||||
<meta property="og:title" content="atmos · Konzeptionelle Düfte" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter. Made in Switzerland."
|
||||
/>
|
||||
<meta property="og:url" content="https://atmos.example/" />
|
||||
<meta property="og:image" content="https://atmos.example/og-image.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content="de_CH" />
|
||||
|
||||
<!-- Twitter / X -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="atmos · Konzeptionelle Düfte" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Konzeptionelle Nischendüfte zwischen Materialität, Raum und Charakter."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://atmos.example/og-image.jpg" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/questrial/questrial-latin.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<!-- Preload LCP image of the landing hero -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="image"
|
||||
href="/images/hero/768/blasse-seide-hero-product.webp"
|
||||
imagesrcset="/images/hero/480/blasse-seide-hero-product.webp 480w, /images/hero/768/blasse-seide-hero-product.webp 768w, /images/hero/960/blasse-seide-hero-product.webp 960w, /blasse-seide-hero-product.webp 1078w"
|
||||
imagesizes="(max-width: 760px) 92vw, (max-width: 1180px) 45vw, 768px"
|
||||
fetchpriority="high"
|
||||
href="https://fonts.googleapis.com/css2?family=Questrial&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>parfum-shop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
32
parfum-shop/package-lock.json
generated
@ -9,7 +9,6 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"gsap": "^3.14.2",
|
||||
"lenis": "^1.3.23",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router": "^7.14.0"
|
||||
@ -1779,37 +1778,6 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lenis": {
|
||||
"version": "1.3.23",
|
||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz",
|
||||
"integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"playground",
|
||||
"playground/*"
|
||||
],
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/darkroomengineering"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": ">=3.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"vue": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"gsap": "^3.14.2",
|
||||
"lenis": "^1.3.23",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router": "^7.14.0"
|
||||
|
||||
BIN
parfum-shop/public/BLASSE SEIDE.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
parfum-shop/public/DISCOVERYSET.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
parfum-shop/public/HERO.jpeg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
parfum-shop/public/KALTER BETON.png
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
parfum-shop/public/NASSER MARMOR.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
parfum-shop/public/SCHWARZES BENZIN.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
parfum-shop/public/VERBRANNTES CHROM.png
Normal file
|
After Width: | Height: | Size: 446 KiB |
BIN
parfum-shop/public/WEISSE ASCHE.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
parfum-shop/public/atmos-discovery-set-thumbnail.png
Executable file
|
After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 111 KiB |
@ -1,3 +0,0 @@
|
||||
<svg width="1386" height="344" viewBox="0 0 1386 344" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M121 343.5C96.6667 343.5 75.3333 337.833 57 326.5C39 314.833 25 299.167 15 279.5C5 259.833 9.23872e-07 238.167 9.23872e-07 214.5C9.23872e-07 190.5 5 168.833 15 149.5C25 129.833 39 114.333 57 103C75.3333 91.3333 96.6667 85.5 121 85.5C141.667 85.5 159.333 89.5 174 97.5C189 105.5 201.167 116.333 210.5 130V89.5H248V339.5H210.5V299.5C201.167 312.833 189 323.5 174 331.5C159.333 339.5 141.667 343.5 121 343.5ZM126 309.5C144.667 309.5 160.333 305.167 173 296.5C186 287.833 195.833 276.333 202.5 262C209.167 247.333 212.5 231.5 212.5 214.5C212.5 197.167 209.167 181.333 202.5 167C195.833 152.667 186 141.167 173 132.5C160.333 123.833 144.667 119.5 126 119.5C107.667 119.5 91.8333 123.833 78.5 132.5C65.1667 141.167 55 152.667 48 167C41 181.333 37.5 197.167 37.5 214.5C37.5 231.5 41 247.333 48 262C55 276.333 65.1667 287.833 78.5 296.5C91.8333 305.167 107.667 309.5 126 309.5ZM339.863 339.5V122.5H288.863V89.5H339.863V1.43051e-06H377.363V89.5H434.863V122.5H377.363V339.5H339.863ZM475.949 339.5V89.5H513.449V127.5C519.116 117.5 528.116 108 540.449 99C552.783 90 569.116 85.5 589.449 85.5C606.116 85.5 621.449 90 635.449 99C649.783 107.667 660.949 120 668.949 136C672.283 130.333 677.449 123.5 684.449 115.5C691.783 107.5 701.283 100.5 712.949 94.5C724.616 88.5 738.616 85.5 754.949 85.5C770.949 85.5 785.949 89.6667 799.949 98C813.949 106.333 825.283 118.333 833.949 134C842.949 149.333 847.449 167.5 847.449 188.5V339.5H809.949V189.5C809.949 168.5 803.949 151.667 791.949 139C780.283 126 765.616 119.5 747.949 119.5C729.616 119.5 713.783 125.667 700.449 138C687.116 150.333 680.449 167.667 680.449 190V339.5H642.949V189.5C642.949 168.5 636.949 151.667 624.949 139C613.283 126 598.616 119.5 580.949 119.5C562.616 119.5 546.783 125.667 533.449 138C520.116 150.333 513.449 167.667 513.449 190V339.5H475.949ZM1018.53 343.5C993.198 343.5 971.198 337.833 952.531 326.5C933.865 314.833 919.531 299.167 909.531 279.5C899.531 259.833 894.531 238.167 894.531 214.5C894.531 190.5 899.531 168.833 909.531 149.5C919.531 129.833 933.865 114.333 952.531 103C971.198 91.3333 993.198 85.5 1018.53 85.5C1044.2 85.5 1066.2 91.3333 1084.53 103C1103.2 114.333 1117.53 129.833 1127.53 149.5C1137.53 168.833 1142.53 190.5 1142.53 214.5C1142.53 238.167 1137.53 259.833 1127.53 279.5C1117.53 299.167 1103.2 314.833 1084.53 326.5C1066.2 337.833 1044.2 343.5 1018.53 343.5ZM1018.53 309.5C1037.2 309.5 1052.86 305.167 1065.53 296.5C1078.53 287.833 1088.36 276.333 1095.03 262C1101.7 247.333 1105.03 231.5 1105.03 214.5C1105.03 197.167 1101.7 181.333 1095.03 167C1088.36 152.667 1078.53 141.167 1065.53 132.5C1052.86 123.833 1037.2 119.5 1018.53 119.5C1000.2 119.5 984.531 123.833 971.531 132.5C958.531 141.167 948.698 152.667 942.031 167C935.365 181.333 932.031 197.167 932.031 214.5C932.031 231.5 935.365 247.333 942.031 262C948.698 276.333 958.531 287.833 971.531 296.5C984.531 305.167 1000.2 309.5 1018.53 309.5ZM1283.26 343.5C1260.26 343.5 1240.76 339.667 1224.76 332C1209.09 324.333 1197.09 314.333 1188.76 302C1180.42 289.333 1175.76 275.667 1174.76 261H1213.76C1214.76 269.333 1217.59 277.5 1222.26 285.5C1227.26 293.167 1234.76 299.5 1244.76 304.5C1254.76 309.167 1267.76 311.5 1283.76 311.5C1288.76 311.5 1294.92 311 1302.26 310C1309.59 309 1316.59 307.167 1323.26 304.5C1330.26 301.833 1336.09 297.833 1340.76 292.5C1345.42 287.167 1347.76 280.333 1347.76 272C1347.76 261.667 1343.76 253.667 1335.76 248C1327.76 242.333 1317.42 238 1304.76 235C1292.09 231.667 1278.59 228.5 1264.26 225.5C1250.26 222.5 1236.92 218.667 1224.26 214C1211.59 209 1201.26 202.167 1193.26 193.5C1185.26 184.5 1181.26 172.333 1181.26 157C1181.26 134.333 1189.42 116.833 1205.76 104.5C1222.42 91.8333 1246.92 85.5 1279.26 85.5C1301.26 85.5 1319.09 89 1332.76 96C1346.76 102.667 1357.26 111.333 1364.26 122C1371.59 132.667 1375.92 144.167 1377.26 156.5H1339.26C1337.92 145.833 1332.59 136.667 1323.26 129C1314.26 121.333 1299.26 117.5 1278.26 117.5C1238.59 117.5 1218.76 129.5 1218.76 153.5C1218.76 163.5 1222.76 171.167 1230.76 176.5C1238.76 181.833 1249.09 186.167 1261.76 189.5C1274.42 192.5 1287.76 195.5 1301.76 198.5C1316.09 201.167 1329.59 205 1342.26 210C1354.92 215 1365.26 222.167 1373.26 231.5C1381.26 240.5 1385.26 252.833 1385.26 268.5C1385.26 292.833 1375.92 311.5 1357.26 324.5C1338.92 337.167 1314.26 343.5 1283.26 343.5Z" fill="#EAEAEA"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 88 KiB |
BIN
parfum-shop/public/blasse-seide-product-image.png
Normal file
|
After Width: | Height: | Size: 952 KiB |
|
Before Width: | Height: | Size: 47 KiB |
BIN
parfum-shop/public/blasse-seide-product-sample-image.png
Executable file
|
After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 85 KiB |
@ -1,93 +0,0 @@
|
||||
Copyright 2011 The Questrial Project Authors (https://github.com/googlefonts/questrial)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
Before Width: | Height: | Size: 3.5 MiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eaeaea" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H59.31l58.35,58.34a8,8,0,0,1-11.32,11.32l-72-72a8,8,0,0,1,0-11.32l72-72a8,8,0,0,1,11.32,11.32L59.31,120H216A8,8,0,0,1,224,128Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 273 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eaeaea" viewBox="0 0 256 256"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.84,15.84,0,0,0,9.25,14.5A16.05,16.05,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H80a8,8,0,0,0-5.23,1.95L40,224V64H216Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 355 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#eaeaea" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 22 KiB |
BIN
parfum-shop/public/kalter-beton-product-image.png
Normal file
|
After Width: | Height: | Size: 957 KiB |
|
Before Width: | Height: | Size: 49 KiB |
BIN
parfum-shop/public/kalter-beton-product-sample-image.png
Executable file
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 29 KiB |
BIN
parfum-shop/public/kalter-beton-product.png
Normal file
|
After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 175 KiB |
BIN
parfum-shop/public/nasser-marmor-product-image.png
Normal file
|
After Width: | Height: | Size: 952 KiB |
|
Before Width: | Height: | Size: 47 KiB |
BIN
parfum-shop/public/nasser-marmor-product-sample-image.png
Executable file
|
After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 122 KiB |
@ -1,8 +0,0 @@
|
||||
# robots.txt — atmos parfum-shop
|
||||
# TODO: replace https://atmos.example with the real production domain
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /api/
|
||||
|
||||
Sitemap: https://atmos.example/sitemap.xml
|
||||
BIN
parfum-shop/public/schwarzes-benzin-product-image.png
Normal file
|
After Width: | Height: | Size: 933 KiB |
|
Before Width: | Height: | Size: 48 KiB |
BIN
parfum-shop/public/schwarzes-benzin-product-sample-image.png
Executable file
|
After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 141 KiB |
@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://atmos.ch/</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/about</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/discovery-set</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/small-batch</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/support</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.4</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/impressum</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.2</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/datenschutz</loc>
|
||||
<changefreq>yearly</changefreq>
|
||||
<priority>0.2</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/kalter-beton</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/nasser-marmor</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/blasse-seide</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/weisse-asche</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/verbranntes-chrom</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://atmos.ch/duft/schwarzes-benzin</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
BIN
parfum-shop/public/verbranntes-chrom-product-image.png
Normal file
|
After Width: | Height: | Size: 951 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 202 KiB |
BIN
parfum-shop/public/verbrannteschrom-product-sample-image.png
Executable file
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 30 KiB |
BIN
parfum-shop/public/weisse-asche-product-image.png
Normal file
|
After Width: | Height: | Size: 932 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
parfum-shop/public/weisse-asche-product-sample-image.png
Executable file
|
After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 220 KiB |
@ -11,7 +11,7 @@ export const catalogProducts = [
|
||||
slug: "discovery-set",
|
||||
name: "Discovery Set",
|
||||
kind: "discovery_set",
|
||||
size_label: "6 x 2 ml",
|
||||
size_label: "6 x 2ml",
|
||||
price_cents: 4800,
|
||||
discovery_credit_cents: 4800,
|
||||
},
|
||||
@ -19,18 +19,18 @@ export const catalogProducts = [
|
||||
{
|
||||
id: `${perfume.slug}-sample`,
|
||||
slug: perfume.slug,
|
||||
name: `${perfume.name} Probe`,
|
||||
name: `${perfume.name} Sample`,
|
||||
kind: "sample",
|
||||
size_label: "2 ml Probe",
|
||||
size_label: "2ml",
|
||||
price_cents: parsePriceCents(perfume.prices.sample),
|
||||
discovery_credit_cents: 0,
|
||||
},
|
||||
{
|
||||
id: `${perfume.slug}-full`,
|
||||
slug: perfume.slug,
|
||||
name: `${perfume.name} 50 ml Flakon`,
|
||||
name: `${perfume.name} Full Size`,
|
||||
kind: "full_size",
|
||||
size_label: "50 ml Flakon",
|
||||
size_label: "50ml",
|
||||
price_cents: parsePriceCents(perfume.prices.full),
|
||||
discovery_credit_cents: 0,
|
||||
},
|
||||
|
||||
@ -1,11 +1,4 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
const viteBin = path.join(
|
||||
"node_modules",
|
||||
".bin",
|
||||
process.platform === "win32" ? "vite.cmd" : "vite"
|
||||
);
|
||||
|
||||
const run = (name, command, args, env = {}) => {
|
||||
const child = spawn(command, args, {
|
||||
@ -29,7 +22,7 @@ const run = (name, command, args, env = {}) => {
|
||||
};
|
||||
|
||||
const api = run("api", "node", ["server/index.js"], { API_PORT: "4174" });
|
||||
const vite = run("vite", viteBin, []);
|
||||
const vite = run("vite", "node_modules/.bin/vite", []);
|
||||
|
||||
const stop = () => {
|
||||
api.kill("SIGTERM");
|
||||
|
||||
@ -25,7 +25,7 @@ const readBody = async (req) =>
|
||||
req.on("data", (chunk) => {
|
||||
raw += chunk;
|
||||
if (raw.length > 1_000_000) {
|
||||
reject(new Error("Anfrage ist zu gross."));
|
||||
reject(new Error("Request body too large"));
|
||||
req.destroy();
|
||||
}
|
||||
});
|
||||
@ -37,7 +37,7 @@ const readBody = async (req) =>
|
||||
try {
|
||||
resolve(JSON.parse(raw));
|
||||
} catch {
|
||||
reject(new Error("Ungültiges JSON."));
|
||||
reject(new Error("Invalid JSON"));
|
||||
}
|
||||
});
|
||||
req.on("error", reject);
|
||||
@ -130,7 +130,7 @@ const authenticate = (req) => {
|
||||
const requireAuth = (req, res) => {
|
||||
const auth = authenticate(req);
|
||||
if (!auth) {
|
||||
json(res, 401, { error: "Bitte melde dich an, um fortzufahren." });
|
||||
json(res, 401, { error: "Please log in to continue." });
|
||||
return null;
|
||||
}
|
||||
return auth;
|
||||
@ -175,7 +175,7 @@ const getAvailableDiscounts = (userId, rows) => {
|
||||
type: "discovery",
|
||||
creditId: discovery.id,
|
||||
amount_cents: Math.min(discovery.amount_cents, fullTotal),
|
||||
label: "Discovery-Set-Gutschrift",
|
||||
label: "Discovery Set credit",
|
||||
});
|
||||
}
|
||||
|
||||
@ -194,7 +194,7 @@ const getAvailableDiscounts = (userId, rows) => {
|
||||
slug: row.slug,
|
||||
product_id: row.product_id,
|
||||
amount_cents: Math.min(sample.amount_cents, row.price_cents * row.quantity),
|
||||
label: `Proben-Gutschrift für ${row.name.replace(" 50 ml Flakon", "")}`,
|
||||
label: `${row.name.replace(" Full Size", "")} sample credit`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -355,13 +355,13 @@ const register = async (req, res) => {
|
||||
const surname = String(body.surname || "").trim();
|
||||
|
||||
if (!firstName || !surname || !email || !password) {
|
||||
json(res, 400, { error: "Vorname, Nachname, E-Mail und Passwort sind erforderlich." });
|
||||
json(res, 400, { error: "First name, surname, email, and password are required." });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email);
|
||||
if (existing) {
|
||||
json(res, 409, { error: "Mit dieser E-Mail existiert bereits ein Konto." });
|
||||
json(res, 409, { error: "An account with this email already exists." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -389,7 +389,7 @@ const login = async (req, res) => {
|
||||
const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email);
|
||||
|
||||
if (!user || !verifyPassword(password, user.password_salt, user.password_hash)) {
|
||||
json(res, 401, { error: "E-Mail oder Passwort ist ungültig." });
|
||||
json(res, 401, { error: "Invalid email or password." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -415,7 +415,7 @@ const patchProfile = async (req, res, user) => {
|
||||
});
|
||||
|
||||
if (!firstName || !surname) {
|
||||
json(res, 400, { error: "Vorname und Nachname sind erforderlich." });
|
||||
json(res, 400, { error: "First name and surname are required." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -459,7 +459,7 @@ const addCartItem = async (req, res, user) => {
|
||||
const quantity = Math.max(1, Number(body.quantity || 1));
|
||||
const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId);
|
||||
if (!product) {
|
||||
json(res, 404, { error: "Produkt nicht gefunden." });
|
||||
json(res, 404, { error: "Product not found." });
|
||||
return;
|
||||
}
|
||||
const timestamp = now();
|
||||
@ -472,7 +472,7 @@ const addCartItem = async (req, res, user) => {
|
||||
).run(user.id, productId, quantity, timestamp, timestamp);
|
||||
json(res, 200, {
|
||||
cart: getCart(user.id),
|
||||
message: `${quantity} x ${product.name} wurde in den Warenkorb gelegt.`,
|
||||
message: `${quantity} x ${product.name} added.`,
|
||||
});
|
||||
};
|
||||
|
||||
@ -480,7 +480,7 @@ const patchCartItem = async (req, res, user, productId) => {
|
||||
const body = await readBody(req);
|
||||
const quantity = Number(body.quantity);
|
||||
if (!Number.isFinite(quantity)) {
|
||||
json(res, 400, { error: "Die Menge ist erforderlich." });
|
||||
json(res, 400, { error: "Quantity is required." });
|
||||
return;
|
||||
}
|
||||
if (quantity <= 0) {
|
||||
@ -502,7 +502,7 @@ const checkout = async (req, res, user) => {
|
||||
const body = await readBody(req);
|
||||
const rows = getCartRows(user.id);
|
||||
if (rows.length === 0) {
|
||||
json(res, 400, { error: "Dein Warenkorb ist leer." });
|
||||
json(res, 400, { error: "Your cart is empty." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -514,11 +514,11 @@ const checkout = async (req, res, user) => {
|
||||
};
|
||||
const paymentMethod = String(body.payment_method || body.paymentMethod || "").trim();
|
||||
if (!addressFields.street_name || !addressFields.house_number || !addressFields.zip_code || !addressFields.city) {
|
||||
json(res, 400, { error: "Strasse, Hausnummer, PLZ und Ort sind erforderlich." });
|
||||
json(res, 400, { error: "Street name, house number, ZIP code, and city are required." });
|
||||
return;
|
||||
}
|
||||
if (!["Bill", "Card", "Twint", "PayPal"].includes(paymentMethod)) {
|
||||
json(res, 400, { error: "Wähle eine Zahlungsmethode." });
|
||||
json(res, 400, { error: "Choose a payment method." });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -611,7 +611,7 @@ const subscribeProduct = async (req, res, user) => {
|
||||
const type = String(body.type || "restock").trim();
|
||||
const product = db.prepare("SELECT * FROM products WHERE id = ?").get(productId);
|
||||
if (!product) {
|
||||
json(res, 404, { error: "Produkt nicht gefunden." });
|
||||
json(res, 404, { error: "Product not found." });
|
||||
return;
|
||||
}
|
||||
db.prepare(
|
||||
@ -620,7 +620,7 @@ const subscribeProduct = async (req, res, user) => {
|
||||
).run(user.id, productId, type, now());
|
||||
json(res, 200, {
|
||||
ok: true,
|
||||
message: `${product.name}: Benachrichtigung gespeichert.`,
|
||||
message: `${product.name} ${type} subscription saved.`,
|
||||
subscriptions: getProductSubscriptions(user.id),
|
||||
});
|
||||
};
|
||||
@ -719,13 +719,13 @@ const route = async (req, res) => {
|
||||
return deleteCartItem(res, user, decodeURIComponent(itemMatch[1]));
|
||||
}
|
||||
|
||||
json(res, 404, { error: "Route nicht gefunden." });
|
||||
json(res, 404, { error: "Route not found." });
|
||||
};
|
||||
|
||||
createServer((req, res) => {
|
||||
route(req, res).catch((error) => {
|
||||
console.error(error);
|
||||
json(res, 500, { error: "Serverfehler." });
|
||||
json(res, 500, { error: "Server error." });
|
||||
});
|
||||
}).listen(PORT, () => {
|
||||
console.log(`Shop API listening on http://localhost:${PORT}`);
|
||||
|
||||
@ -1,90 +1,35 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: "Questrial", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#root {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-text);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--theme-bg);
|
||||
}
|
||||
|
||||
main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: var(--container-wide);
|
||||
margin: 0 auto;
|
||||
padding: 0 0 clamp(2.2rem, 6vw, 5rem);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.page,
|
||||
.detail-page,
|
||||
.discovery-page,
|
||||
.about-page,
|
||||
.support-page,
|
||||
.small-page,
|
||||
.impressum-page,
|
||||
.datenschutz-page {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.detail-page .navbar--light,
|
||||
.discovery-page .navbar--light,
|
||||
.about-page .navbar--light,
|
||||
.support-page .navbar--light,
|
||||
.small-page .navbar--light,
|
||||
.impressum-page .navbar--light,
|
||||
.datenschutz-page .navbar--light {
|
||||
position: fixed;
|
||||
top: clamp(0.75rem, 2.1vw, 1.4rem);
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-nav);
|
||||
margin-bottom: 0;
|
||||
padding-top: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.detail-page .navbar--light .nav-pill,
|
||||
.discovery-page .navbar--light .nav-pill,
|
||||
.about-page .navbar--light .nav-pill,
|
||||
.support-page .navbar--light .nav-pill,
|
||||
.small-page .navbar--light .nav-pill,
|
||||
.impressum-page .navbar--light .nav-pill,
|
||||
.datenschutz-page .navbar--light .nav-pill {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.navbar--light .nav-pill,
|
||||
.navbar--light .nav-link,
|
||||
.shell,
|
||||
.page,
|
||||
.detail-page,
|
||||
.discovery-page,
|
||||
.about-page,
|
||||
.support-page,
|
||||
.small-page,
|
||||
.impressum-page,
|
||||
.datenschutz-page,
|
||||
[class*="-page"],
|
||||
[class*="-shell"],
|
||||
[class*="-card"],
|
||||
[class*="-panel"],
|
||||
[class*="-box"],
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
transition:
|
||||
background-color var(--duration-med) var(--ease-out),
|
||||
border-color var(--duration-med) var(--ease-out),
|
||||
color var(--duration-med) var(--ease-out),
|
||||
box-shadow var(--duration-med) var(--ease-out);
|
||||
textarea {
|
||||
transition: background-color 0.24s ease, border-color 0.24s ease, color 0.24s ease;
|
||||
}
|
||||
|
||||
body.theme-dark .navbar--light .nav-pill {
|
||||
|
||||
@ -13,11 +13,7 @@ import SupportChatbot from "./components/SupportChatbot";
|
||||
import ScrollToTop from "./components/ScrollToTop";
|
||||
import ShopDrawer from "./components/ShopDrawer";
|
||||
import CartToast from "./components/CartToast";
|
||||
import { ProductTransitionProvider } from "./components/ProductTransition";
|
||||
import { PageTransitionProvider } from "./transitions/PageTransition";
|
||||
import useLenisSmoothScroll from "./hooks/useLenisSmoothScroll";
|
||||
import useScrollTextReveal from "./hooks/useScrollTextReveal";
|
||||
import useButtonInteractions from "./hooks/useButtonInteractions";
|
||||
import { ThemeProvider } from "./theme/ThemeContext";
|
||||
import "./style/textReveal.css";
|
||||
|
||||
@ -33,11 +29,8 @@ function App() {
|
||||
});
|
||||
const shouldFlushFooter =
|
||||
location.pathname === "/" || location.pathname.startsWith("/duft/");
|
||||
const showSupportChatbot = location.pathname === "/";
|
||||
|
||||
useLenisSmoothScroll(location.pathname);
|
||||
useScrollTextReveal(routeContentRef, location.pathname);
|
||||
useButtonInteractions();
|
||||
useScrollTextReveal(routeContentRef, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
@ -59,33 +52,27 @@ function App() {
|
||||
|
||||
return (
|
||||
<ThemeProvider value={{ theme, isLight, toggleTheme }}>
|
||||
<ProductTransitionProvider>
|
||||
<PageTransitionProvider>
|
||||
<ScrollToTop />
|
||||
<>
|
||||
<ScrollToTop />
|
||||
|
||||
<a href="#main-content" className="skip-link">
|
||||
Zum Inhalt springen
|
||||
</a>
|
||||
<div ref={routeContentRef}>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/duft/:perfumeSlug" element={<ProductDetailPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/impressum" element={<ImpressumPage />} />
|
||||
<Route path="/datenschutz" element={<DatenschutzPage />} />
|
||||
<Route path="/support" element={<SupportPage />} />
|
||||
<Route path="/discovery-set" element={<DiscoverySetPage />} />
|
||||
<Route path="/small-batch" element={<SmallBatchPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
<div ref={routeContentRef} data-route-content>
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/duft/:perfumeSlug" element={<ProductDetailPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
<Route path="/impressum" element={<ImpressumPage />} />
|
||||
<Route path="/datenschutz" element={<DatenschutzPage />} />
|
||||
<Route path="/support" element={<SupportPage />} />
|
||||
<Route path="/discovery-set" element={<DiscoverySetPage />} />
|
||||
<Route path="/small-batch" element={<SmallBatchPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
<ShopDrawer />
|
||||
<CartToast />
|
||||
<Footer flushTop={shouldFlushFooter} />
|
||||
{showSupportChatbot && <SupportChatbot />}
|
||||
</PageTransitionProvider>
|
||||
</ProductTransitionProvider>
|
||||
<ShopDrawer />
|
||||
<CartToast />
|
||||
<Footer flushTop={shouldFlushFooter} />
|
||||
<SupportChatbot />
|
||||
</>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,23 +22,14 @@ function CartToast() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cart-toast" role="status" aria-live="polite">
|
||||
<button
|
||||
type="button"
|
||||
className="atmos-btn atmos-btn--outline atmos-btn--icon atmos-btn--sm cart-toast-close"
|
||||
onClick={dismissToast}
|
||||
aria-label="Hinweis schliessen"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
<div className="cart-toast">
|
||||
<button className="cart-toast-close" type="button" onClick={dismissToast}>
|
||||
x
|
||||
</button>
|
||||
<strong>{cartToast.title}</strong>
|
||||
<p>{cartToast.message}</p>
|
||||
{cartToast.actionLabel && (
|
||||
<button
|
||||
type="button"
|
||||
className="atmos-btn atmos-btn--primary atmos-btn--block atmos-btn--sm"
|
||||
onClick={runAction}
|
||||
>
|
||||
<button type="button" onClick={runAction}>
|
||||
{cartToast.actionLabel}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@ -1,121 +1,84 @@
|
||||
.site-footer {
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 82% 10%, rgba(var(--theme-accent-rgb) / 0.15), transparent 22rem),
|
||||
var(--footer-bg);
|
||||
color: var(--footer-text);
|
||||
border-top: 1px solid var(--footer-border);
|
||||
margin-top: 40px;
|
||||
background: #1f1f1f;
|
||||
color: #f5f5f5;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.site-footer--flush {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.site-footer::before {
|
||||
content: "ATMOS";
|
||||
position: absolute;
|
||||
right: var(--page-x);
|
||||
bottom: -0.16em;
|
||||
color: var(--footer-watermark);
|
||||
font-size: clamp(5.5rem, 18vw, 20rem);
|
||||
line-height: 0.8;
|
||||
letter-spacing: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.site-footer__inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: var(--container-wide);
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: clamp(2.4rem, 7vw, 6.5rem) 0 clamp(2.2rem, 5vw, 4.8rem);
|
||||
padding: 28px 20px 32px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(12rem, 0.65fr) minmax(12rem, 0.75fr);
|
||||
gap: var(--gap-lg);
|
||||
align-items: start;
|
||||
grid-template-columns: 1.4fr 1fr 1fr;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.site-footer__brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.site-footer__logo {
|
||||
width: fit-content;
|
||||
color: var(--theme-accent-contrast);
|
||||
font-size: clamp(1rem, 1.5vw, 1.2rem);
|
||||
letter-spacing: 0.22em;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.22em;
|
||||
}
|
||||
|
||||
.site-footer__text {
|
||||
max-width: 32rem;
|
||||
margin: 0;
|
||||
color: var(--footer-text-muted);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.65;
|
||||
max-width: 320px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.site-footer__nav-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
padding-top: 0.2rem;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.site-footer__heading {
|
||||
color: var(--footer-text-faint);
|
||||
font-size: var(--text-xs);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.site-footer__nav {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.site-footer__nav ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.72rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.site-footer__nav a {
|
||||
width: fit-content;
|
||||
color: var(--footer-text);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
color var(--duration-med) var(--ease-out),
|
||||
opacity var(--duration-med) var(--ease-out),
|
||||
transform var(--duration-med) var(--ease-out);
|
||||
color: #f5f5f5;
|
||||
font-size: 14px;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.site-footer__nav a:hover,
|
||||
.site-footer__nav a:focus-visible {
|
||||
color: var(--theme-accent);
|
||||
transform: translateX(0.25rem);
|
||||
.site-footer__nav a:hover {
|
||||
opacity: 0.7;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
@media (max-width: 900px) {
|
||||
.site-footer__inner {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.site-footer__brand {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 640px) {
|
||||
.site-footer__inner {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 24px 16px 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,28 +1,6 @@
|
||||
import { Link } from "react-router";
|
||||
import "./Footer.css";
|
||||
|
||||
const footerLinkGroups = [
|
||||
{
|
||||
heading: "Navigation",
|
||||
ariaLabel: "Footer Navigation",
|
||||
links: [
|
||||
{ to: "/", label: "Startseite" },
|
||||
{ to: "/#dufte", label: "D\u00FCfte" },
|
||||
{ to: "/discovery-set", label: "Discovery Set" },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "Rechtliches & Info",
|
||||
ariaLabel: "Footer Rechtliches und Info",
|
||||
links: [
|
||||
{ to: "/about", label: "About Us" },
|
||||
{ to: "/support", label: "Support" },
|
||||
{ to: "/impressum", label: "Impressum" },
|
||||
{ to: "/datenschutz", label: "Datenschutz" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function Footer({ flushTop = false }) {
|
||||
return (
|
||||
<footer className={`site-footer${flushTop ? " site-footer--flush" : ""}`}>
|
||||
@ -32,24 +10,28 @@ function Footer({ flushTop = false }) {
|
||||
ATMOS
|
||||
</Link>
|
||||
<p className="site-footer__text">
|
||||
{"Konzeptuelle D\u00FCfte zwischen Materialit\u00E4t, Raum und Charakter."}
|
||||
Konzeptuelle Düfte zwischen Materialität, Raum und Charakter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{footerLinkGroups.map((group) => (
|
||||
<div className="site-footer__nav-group" key={group.heading}>
|
||||
<span className="site-footer__heading">{group.heading}</span>
|
||||
<nav className="site-footer__nav" aria-label={group.ariaLabel}>
|
||||
<ul>
|
||||
{group.links.map((link) => (
|
||||
<li key={link.to}>
|
||||
<Link to={link.to}>{link.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
))}
|
||||
<div className="site-footer__nav-group">
|
||||
<span className="site-footer__heading">Navigation</span>
|
||||
<nav className="site-footer__nav">
|
||||
<Link to="/">Startseite</Link>
|
||||
<Link to="/#dufte">Düfte</Link>
|
||||
<Link to="/discovery-set">Discovery Set</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="site-footer__nav-group">
|
||||
<span className="site-footer__heading">Rechtliches & Info</span>
|
||||
<nav className="site-footer__nav">
|
||||
<Link to="/about">About Us</Link>
|
||||
<Link to="/support">Support</Link>
|
||||
<Link to="/impressum">Impressum</Link>
|
||||
<Link to="/datenschutz">Datenschutz</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||