Compare commits

..

No commits in common. "main" and "darkmode" have entirely different histories.

148 changed files with 5036 additions and 8849 deletions

View File

@ -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>

View File

@ -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",

View File

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

View File

@ -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,
},

View File

@ -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");

View File

@ -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}`);

View File

@ -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 {

View File

@ -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>
);
}

View File

@ -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>
)}

View File

@ -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;
}
}

View File

@ -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>
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More