Compare commits
8 Commits
1d4584e5d9
...
goodboy
| Author | SHA1 | Date | |
|---|---|---|---|
| 346ec484fa | |||
|
|
217bbdc6a7 | ||
|
|
7e8d40878b | ||
|
|
2dc5401179 | ||
|
|
3b9e35a447 | ||
|
|
a95932cd79 | ||
|
|
01102ef3f7 | ||
| 22d641e4e5 |
17
index.html
17
index.html
@@ -1,21 +1,20 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<!-- TODO: Set the document title to the name of your application -->
|
<title>WEBklar</title>
|
||||||
<title>Lovable App</title>
|
<meta name="description" content="WEBklar – Ihre Webagentur" />
|
||||||
<meta name="description" content="Lovable Generated Project" />
|
<meta name="author" content="WEBklar" />
|
||||||
<meta name="author" content="Lovable" />
|
|
||||||
|
|
||||||
<!-- TODO: Update og:title to match your application name -->
|
<meta property="og:title" content="WEBklar" />
|
||||||
<meta property="og:title" content="Lovable App" />
|
<meta property="og:description" content="WEBklar – Ihre Webagentur" />
|
||||||
<meta property="og:description" content="Lovable Generated Project" />
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@Lovable" />
|
<meta name="twitter:site" content="@WEBklar" />
|
||||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
714
package-lock.json
generated
714
package-lock.json
generated
@@ -39,6 +39,7 @@
|
|||||||
"@react-three/fiber": "^8.18.0",
|
"@react-three/fiber": "^8.18.0",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"appwrite": "^22.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"motion": "^12.29.2",
|
"motion": "^12.29.2",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"ogl": "^1.0.11",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -70,7 +72,7 @@
|
|||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/react": "^18.3.23",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
@@ -120,6 +122,174 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/compat-data": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/core": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.0",
|
||||||
|
"@babel/generator": "^7.29.0",
|
||||||
|
"@babel/helper-compilation-targets": "^7.28.6",
|
||||||
|
"@babel/helper-module-transforms": "^7.28.6",
|
||||||
|
"@babel/helpers": "^7.28.6",
|
||||||
|
"@babel/parser": "^7.29.0",
|
||||||
|
"@babel/template": "^7.28.6",
|
||||||
|
"@babel/traverse": "^7.29.0",
|
||||||
|
"@babel/types": "^7.29.0",
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"gensync": "^1.0.0-beta.2",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/babel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/core/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/generator": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.29.0",
|
||||||
|
"@babel/types": "^7.29.0",
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
|
"jsesc": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/compat-data": "^7.28.6",
|
||||||
|
"@babel/helper-validator-option": "^7.27.1",
|
||||||
|
"browserslist": "^4.24.0",
|
||||||
|
"lru-cache": "^5.1.1",
|
||||||
|
"semver": "^6.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-globals": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-module-imports": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/traverse": "^7.28.6",
|
||||||
|
"@babel/types": "^7.28.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-module-transforms": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-module-imports": "^7.28.6",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5",
|
||||||
|
"@babel/traverse": "^7.28.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.28.5",
|
"version": "7.28.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
@@ -130,6 +300,78 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-validator-option": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helpers": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/template": "^7.28.6",
|
||||||
|
"@babel/types": "^7.28.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.29.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.28.2",
|
"version": "7.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
|
||||||
@@ -139,6 +381,54 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/template": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.28.6",
|
||||||
|
"@babel/parser": "^7.28.6",
|
||||||
|
"@babel/types": "^7.28.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/traverse": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.29.0",
|
||||||
|
"@babel/generator": "^7.29.0",
|
||||||
|
"@babel/helper-globals": "^7.28.0",
|
||||||
|
"@babel/parser": "^7.29.0",
|
||||||
|
"@babel/template": "^7.28.6",
|
||||||
|
"@babel/types": "^7.29.0",
|
||||||
|
"debug": "^4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
|
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
@@ -849,17 +1139,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/remapping": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
@@ -871,15 +1168,6 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/set-array": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
@@ -887,9 +1175,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -2593,232 +2881,6 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@swc/core": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/counter": "^0.1.3",
|
|
||||||
"@swc/types": "^0.1.23"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/swc"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@swc/core-darwin-arm64": "1.13.2",
|
|
||||||
"@swc/core-darwin-x64": "1.13.2",
|
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.13.2",
|
|
||||||
"@swc/core-linux-arm64-gnu": "1.13.2",
|
|
||||||
"@swc/core-linux-arm64-musl": "1.13.2",
|
|
||||||
"@swc/core-linux-x64-gnu": "1.13.2",
|
|
||||||
"@swc/core-linux-x64-musl": "1.13.2",
|
|
||||||
"@swc/core-win32-arm64-msvc": "1.13.2",
|
|
||||||
"@swc/core-win32-ia32-msvc": "1.13.2",
|
|
||||||
"@swc/core-win32-x64-msvc": "1.13.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@swc/helpers": ">=0.5.17"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@swc/helpers": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-darwin-arm64": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-darwin-x64": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-linux-arm64-musl": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-linux-x64-gnu": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-linux-x64-musl": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/core-win32-x64-msvc": {
|
|
||||||
"version": "1.13.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.2.tgz",
|
|
||||||
"integrity": "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/counter": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
|
||||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/@swc/types": {
|
|
||||||
"version": "0.1.23",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz",
|
|
||||||
"integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/counter": "^0.1.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tabler/icons": {
|
"node_modules/@tabler/icons": {
|
||||||
"version": "3.36.1",
|
"version": "3.36.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
|
||||||
@@ -2993,6 +3055,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/babel__core": {
|
||||||
|
"version": "7.20.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.20.7",
|
||||||
|
"@babel/types": "^7.20.7",
|
||||||
|
"@types/babel__generator": "*",
|
||||||
|
"@types/babel__template": "*",
|
||||||
|
"@types/babel__traverse": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__generator": {
|
||||||
|
"version": "7.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
||||||
|
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__template": {
|
||||||
|
"version": "7.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
||||||
|
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.1.0",
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__traverse": {
|
||||||
|
"version": "7.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
||||||
|
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.28.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -3401,18 +3508,25 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react-swc": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "3.11.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
"integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==",
|
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.28.0",
|
||||||
|
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.27",
|
"@rolldown/pluginutils": "1.0.0-beta.27",
|
||||||
"@swc/core": "^1.12.11"
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"react-refresh": "^0.17.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^4 || ^5 || ^6 || ^7"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
@@ -3662,6 +3776,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/appwrite": {
|
||||||
|
"version": "22.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-22.0.0.tgz",
|
||||||
|
"integrity": "sha512-iFlfshYttuQheIyar6m789+Z/gvfKWQxWQCDhHzH9cEkFkn+laJZV8nMvGRH+1rTYNfAcFuycWKBGZiEDFxXug==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"bignumber.js": "9.0.0",
|
||||||
|
"json-bigint": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
@@ -3778,6 +3902,15 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bignumber.js": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -4098,6 +4231,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5089,6 +5229,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gensync": {
|
||||||
|
"version": "1.0.0-beta.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
@@ -5608,6 +5758,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsesc": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jsesc": "bin/jsesc"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-bigint": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bignumber.js": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
@@ -5629,6 +5801,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json5": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"json5": "lib/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -6443,6 +6628,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ogl": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -6974,6 +7165,16 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-refresh": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
||||||
@@ -8496,6 +8697,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"@react-three/fiber": "^8.18.0",
|
"@react-three/fiber": "^8.18.0",
|
||||||
"@tabler/icons-react": "^3.36.1",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"appwrite": "^22.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"motion": "^12.29.2",
|
"motion": "^12.29.2",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"ogl": "^1.0.11",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/react": "^18.3.23",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@vitejs/plugin-react-swc": "^3.11.0",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
|||||||
BIN
public/backgroud_effect.png
Normal file
BIN
public/backgroud_effect.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 974 KiB |
BIN
public/backgroud_effect1.png
Normal file
BIN
public/backgroud_effect1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
8
public/favicon.svg
Normal file
8
public/favicon.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||||
|
<rect x="48" y="48" width="416" height="416" rx="92" fill="#FFFFFF"/>
|
||||||
|
<ellipse cx="256" cy="256" rx="210" ry="150" fill="#111111" transform="rotate(-28 256 256)"/>
|
||||||
|
<circle cx="356" cy="172" r="18" fill="#FFFFFF"/>
|
||||||
|
<g transform="translate(256 256) rotate(-28)">
|
||||||
|
<path d="M 0,-170 C 22,-105 60,-72 120,-56 C 60,-40 22,-8 0,64 C -22,-8 -60,-40 -120,-56 C -60,-72 -22,-105 0,-170 Z" fill="#FFFFFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 510 B |
BIN
public/project pics/emailsorter.png
Normal file
BIN
public/project pics/emailsorter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
64
scripts/create-appwrite-collection.mjs
Normal file
64
scripts/create-appwrite-collection.mjs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Script zum Erstellen der Appwrite-Collection für Kontaktformulare.
|
||||||
|
* Einmalig ausführen mit: node scripts/create-appwrite-collection.mjs
|
||||||
|
*
|
||||||
|
* Benötigt: API-Key aus Appwrite Console (Settings > API Keys)
|
||||||
|
* Umgebungsvariable: APPWRITE_API_KEY=your-secret-key
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Client, Databases, Permission, Role } from "appwrite";
|
||||||
|
|
||||||
|
const ENDPOINT = "https://appwrite.webklar.com/v1";
|
||||||
|
const PROJECT_ID = "696b82270034001dab69";
|
||||||
|
const DATABASE_ID = "698124a20035e8f6dc42";
|
||||||
|
const COLLECTION_ID = "contact_submissions";
|
||||||
|
|
||||||
|
const apiKey = process.env.APPWRITE_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error(
|
||||||
|
"Fehler: APPWRITE_API_KEY Umgebungsvariable fehlt.\n" +
|
||||||
|
"Beispiel: $env:APPWRITE_API_KEY='your-key'; node scripts/create-appwrite-collection.mjs"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new Client().setEndpoint(ENDPOINT).setProject(PROJECT_ID).setKey(apiKey);
|
||||||
|
const databases = new Databases(client);
|
||||||
|
|
||||||
|
async function createCollection() {
|
||||||
|
try {
|
||||||
|
// 1. Collection erstellen
|
||||||
|
// Permission: "any" darf Dokumente erstellen (für öffentliches Kontaktformular)
|
||||||
|
await databases.createCollection(
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTION_ID,
|
||||||
|
"Kontaktanfragen",
|
||||||
|
[Permission.create(Role.any())],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
console.log("✓ Collection erstellt:", COLLECTION_ID);
|
||||||
|
|
||||||
|
// 2. Attribute hinzufügen (Appwrite erfordert kleine Wartezeit zwischen den Schritten)
|
||||||
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "name", 256, true);
|
||||||
|
await delay(500);
|
||||||
|
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "email", 512, true);
|
||||||
|
await delay(500);
|
||||||
|
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "company", 256, false);
|
||||||
|
await delay(500);
|
||||||
|
await databases.createStringAttribute(DATABASE_ID, COLLECTION_ID, "message", 4096, true);
|
||||||
|
|
||||||
|
console.log("✓ Attribute erstellt: name, email, company, message");
|
||||||
|
console.log("\nCollection ist bereit. Kontaktformular kann jetzt genutzt werden.");
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 409) {
|
||||||
|
console.log("Collection existiert bereits:", COLLECTION_ID);
|
||||||
|
} else {
|
||||||
|
console.error("Fehler:", err.message || err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createCollection();
|
||||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
import ContactPage from "./pages/Contact";
|
import ContactPage from "./pages/Contact";
|
||||||
|
import AGBPage from "./pages/AGB";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -18,6 +19,7 @@ const App = () => (
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/kontakt" element={<ContactPage />} />
|
<Route path="/kontakt" element={<ContactPage />} />
|
||||||
|
<Route path="/agb" element={<AGBPage />} />
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ const Contact = () => {
|
|||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<div className="divider mb-12" />
|
<div className="divider mb-12" />
|
||||||
<div className="flex flex-col sm:flex-row gap-8 text-muted-foreground">
|
<div className="flex flex-col sm:flex-row gap-8 text-muted-foreground">
|
||||||
<a href="mailto:hello@webklar.de" className="flex items-center gap-3 hover:text-foreground transition-colors group">
|
<a href="mailto:support@webklar.com" className="flex items-center gap-3 hover:text-foreground transition-colors group">
|
||||||
<Mail className="w-5 h-5" />
|
<Mail className="w-5 h-5" />
|
||||||
<span>hello@webklar.de</span>
|
<span>support@webklar.com</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="tel:+4912345678" className="flex items-center gap-3 hover:text-foreground transition-colors group">
|
<a href="tel:+491704969375" className="flex items-center gap-3 hover:text-foreground transition-colors group">
|
||||||
<Phone className="w-5 h-5" />
|
<Phone className="w-5 h-5" />
|
||||||
<span>+49 123 456 78</span>
|
<span>0170 4969375</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
128
src/components/CountUp.tsx
Normal file
128
src/components/CountUp.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { useInView, useMotionValue, useSpring } from "motion/react";
|
||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface CountUpProps {
|
||||||
|
to: number;
|
||||||
|
from?: number;
|
||||||
|
direction?: "up" | "down";
|
||||||
|
delay?: number;
|
||||||
|
duration?: number;
|
||||||
|
className?: string;
|
||||||
|
startWhen?: boolean;
|
||||||
|
separator?: string;
|
||||||
|
suffix?: string;
|
||||||
|
prefix?: string;
|
||||||
|
padMinLength?: number;
|
||||||
|
onStart?: () => void;
|
||||||
|
onEnd?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CountUp({
|
||||||
|
to,
|
||||||
|
from = 0,
|
||||||
|
direction = "up",
|
||||||
|
delay = 0,
|
||||||
|
duration = 2,
|
||||||
|
className = "",
|
||||||
|
startWhen = true,
|
||||||
|
separator = "",
|
||||||
|
suffix = "",
|
||||||
|
prefix = "",
|
||||||
|
padMinLength,
|
||||||
|
onStart,
|
||||||
|
onEnd,
|
||||||
|
}: CountUpProps) {
|
||||||
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
|
const containerRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const motionValue = useMotionValue(direction === "down" ? to : from);
|
||||||
|
|
||||||
|
const damping = 20 + 40 * (1 / duration);
|
||||||
|
const stiffness = 100 * (1 / duration);
|
||||||
|
|
||||||
|
const springValue = useSpring(motionValue, {
|
||||||
|
damping,
|
||||||
|
stiffness,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isInView = useInView(containerRef, { once: true, margin: "0px" });
|
||||||
|
|
||||||
|
const getDecimalPlaces = (num: number) => {
|
||||||
|
const str = num.toString();
|
||||||
|
if (str.includes(".")) {
|
||||||
|
const decimals = str.split(".")[1];
|
||||||
|
if (decimals && parseInt(decimals, 10) !== 0) {
|
||||||
|
return decimals.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxDecimals = Math.max(getDecimalPlaces(from), getDecimalPlaces(to));
|
||||||
|
|
||||||
|
const formatValue = useCallback(
|
||||||
|
(latest: number) => {
|
||||||
|
if (padMinLength != null) {
|
||||||
|
const n = Math.round(latest);
|
||||||
|
return n.toString().padStart(padMinLength, "0");
|
||||||
|
}
|
||||||
|
const hasDecimals = maxDecimals > 0;
|
||||||
|
const options: Intl.NumberFormatOptions = {
|
||||||
|
useGrouping: !!separator,
|
||||||
|
minimumFractionDigits: hasDecimals ? maxDecimals : 0,
|
||||||
|
maximumFractionDigits: hasDecimals ? maxDecimals : 0,
|
||||||
|
};
|
||||||
|
let formattedNumber = Intl.NumberFormat("de-DE", options).format(latest);
|
||||||
|
if (separator) {
|
||||||
|
formattedNumber = formattedNumber.replace(/\s/g, separator);
|
||||||
|
}
|
||||||
|
return formattedNumber;
|
||||||
|
},
|
||||||
|
[maxDecimals, separator, padMinLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.textContent = formatValue(direction === "down" ? to : from);
|
||||||
|
}
|
||||||
|
}, [from, to, direction, formatValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInView && startWhen) {
|
||||||
|
if (typeof onStart === "function") onStart();
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
motionValue.set(direction === "down" ? from : to);
|
||||||
|
}, delay * 1000);
|
||||||
|
|
||||||
|
const durationTimeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
if (typeof onEnd === "function") onEnd();
|
||||||
|
},
|
||||||
|
delay * 1000 + duration * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
clearTimeout(durationTimeoutId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isInView, startWhen, motionValue, direction, from, to, delay, onStart, onEnd, duration]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = springValue.on("change", (latest) => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.textContent = formatValue(latest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [springValue, formatValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={containerRef} className={className}>
|
||||||
|
{prefix && <span>{prefix}</span>}
|
||||||
|
<span ref={ref} />
|
||||||
|
{suffix && <span>{suffix}</span>}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import CountUp from "@/components/CountUp";
|
||||||
|
|
||||||
const DifferentiationSection = () => {
|
const DifferentiationSection = () => {
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +16,9 @@ const DifferentiationSection = () => {
|
|||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-8 mb-12">
|
<div className="grid md:grid-cols-3 gap-8 mb-12">
|
||||||
<div className="p-6 border border-border rounded-lg bg-background">
|
<div className="p-6 border border-border rounded-lg bg-background">
|
||||||
<div className="text-4xl font-display font-medium text-foreground mb-2">01</div>
|
<div className="text-4xl font-display font-medium text-foreground mb-2">
|
||||||
|
<CountUp from={0} to={1} duration={1} padMinLength={2} startWhen={true} />
|
||||||
|
</div>
|
||||||
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
|
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
|
||||||
Alles aus einer Hand
|
Alles aus einer Hand
|
||||||
</h3>
|
</h3>
|
||||||
@@ -25,7 +28,9 @@ const DifferentiationSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 border border-border rounded-lg bg-background">
|
<div className="p-6 border border-border rounded-lg bg-background">
|
||||||
<div className="text-4xl font-display font-medium text-foreground mb-2">02</div>
|
<div className="text-4xl font-display font-medium text-foreground mb-2">
|
||||||
|
<CountUp from={0} to={2} duration={1} padMinLength={2} startWhen={true} />
|
||||||
|
</div>
|
||||||
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
|
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
|
||||||
Systeme statt Inseln
|
Systeme statt Inseln
|
||||||
</h3>
|
</h3>
|
||||||
@@ -35,7 +40,9 @@ const DifferentiationSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 border border-border rounded-lg bg-background">
|
<div className="p-6 border border-border rounded-lg bg-background">
|
||||||
<div className="text-4xl font-display font-medium text-foreground mb-2">03</div>
|
<div className="text-4xl font-display font-medium text-foreground mb-2">
|
||||||
|
<CountUp from={0} to={3} duration={1} padMinLength={2} startWhen={true} />
|
||||||
|
</div>
|
||||||
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
|
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
|
||||||
Langfristige Partnerschaft
|
Langfristige Partnerschaft
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Logo from '@/components/Logo';
|
||||||
|
|
||||||
const DevStudio: React.FC = () => {
|
const DevStudio: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +16,7 @@ const Footer: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mr-0 mb-4 md:mr-4 md:flex">
|
<div className="mr-0 mb-4 md:mr-4 md:flex">
|
||||||
<a className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-white" href="/">
|
<a className="relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-white" href="/">
|
||||||
<img alt="logo" width="30" height="30" src="https://assets.aceternity.com/logo-dark.png" />
|
<Logo width={30} height={30} />
|
||||||
<span className="font-medium text-white">WEBklar</span>
|
<span className="font-medium text-white">WEBklar</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MobileNavToggle,
|
MobileNavToggle,
|
||||||
MobileNavMenu,
|
MobileNavMenu,
|
||||||
} from "@/components/ui/resizable-navbar";
|
} from "@/components/ui/resizable-navbar";
|
||||||
|
import Logo from "@/components/Logo";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -30,6 +31,7 @@ const Header = () => {
|
|||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<NavBody>
|
<NavBody>
|
||||||
<NavbarLogo href="#">
|
<NavbarLogo href="#">
|
||||||
|
<Logo width={30} height={30} />
|
||||||
<span className="font-display text-lg font-medium tracking-tight">
|
<span className="font-display text-lg font-medium tracking-tight">
|
||||||
Webklar
|
Webklar
|
||||||
</span>
|
</span>
|
||||||
@@ -39,8 +41,7 @@ const Header = () => {
|
|||||||
<Link to="/kontakt">
|
<Link to="/kontakt">
|
||||||
<NavbarButton
|
<NavbarButton
|
||||||
as="span"
|
as="span"
|
||||||
variant="primary"
|
variant="dark"
|
||||||
className="!text-black"
|
|
||||||
>
|
>
|
||||||
Kontakt
|
Kontakt
|
||||||
</NavbarButton>
|
</NavbarButton>
|
||||||
@@ -52,6 +53,7 @@ const Header = () => {
|
|||||||
<MobileNav>
|
<MobileNav>
|
||||||
<MobileNavHeader>
|
<MobileNavHeader>
|
||||||
<NavbarLogo href="#">
|
<NavbarLogo href="#">
|
||||||
|
<Logo width={30} height={30} />
|
||||||
<span className="font-display text-lg font-medium tracking-tight">
|
<span className="font-display text-lg font-medium tracking-tight">
|
||||||
Webklar
|
Webklar
|
||||||
</span>
|
</span>
|
||||||
@@ -82,8 +84,8 @@ const Header = () => {
|
|||||||
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
|
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
|
||||||
<NavbarButton
|
<NavbarButton
|
||||||
as="span"
|
as="span"
|
||||||
variant="primary"
|
variant="dark"
|
||||||
className="block w-full text-center !text-black"
|
className="block w-full text-center"
|
||||||
>
|
>
|
||||||
Kontakt
|
Kontakt
|
||||||
</NavbarButton>
|
</NavbarButton>
|
||||||
|
|||||||
@@ -1,13 +1,79 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import Silk from "@/components/Silk";
|
import Silk from "@/components/Silk";
|
||||||
|
import CountUp from "@/components/CountUp";
|
||||||
|
|
||||||
|
const SPARKLE_SVG = (
|
||||||
|
<svg className="btn-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
function DemoButtonLetters({ text }: { text: string }) {
|
||||||
|
// #region agent log
|
||||||
|
const chars = text.split("");
|
||||||
|
const spaceIndex = chars.findIndex((c) => c === " ");
|
||||||
|
const lastIndex = chars.length - 1;
|
||||||
|
const lastChar = chars[lastIndex];
|
||||||
|
fetch("http://127.0.0.1:7244/ingest/72f53105-0a54-4d4c-a295-fb93aa72afcc", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
location: "Hero.tsx:DemoButtonLetters",
|
||||||
|
message: "Letter split for button text",
|
||||||
|
data: { text, len: chars.length, spaceIndex, spaceChar: spaceIndex >= 0 ? chars[spaceIndex] : null, lastIndex, lastChar },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
sessionId: "debug-session",
|
||||||
|
hypothesisId: "A,C",
|
||||||
|
}),
|
||||||
|
}).catch(() => {});
|
||||||
|
// #endregion
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{chars.map((char, i) => (
|
||||||
|
<span key={i} className={char === " " ? "btn-letter btn-letter-space" : "btn-letter"}>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
|
const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [companyAge, setCompanyAge] = useState("");
|
const [companyAge, setCompanyAge] = useState("");
|
||||||
|
const secondBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = secondBtnRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const firstTxtWrapper = el.querySelector(".txt-wrapper");
|
||||||
|
const letters = firstTxtWrapper ? firstTxtWrapper.querySelectorAll(".btn-letter") : [];
|
||||||
|
const spaceIdx = 8;
|
||||||
|
const lastIdx = 16;
|
||||||
|
const wSpace = letters[spaceIdx]?.getBoundingClientRect?.()?.width ?? -1;
|
||||||
|
const wLast = letters[lastIdx]?.getBoundingClientRect?.()?.width ?? -1;
|
||||||
|
fetch("http://127.0.0.1:7244/ingest/72f53105-0a54-4d4c-a295-fb93aa72afcc", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
location: "Hero.tsx:useEffect:measure",
|
||||||
|
message: "Measured btn-letter widths (space + last)",
|
||||||
|
data: { letterCount: letters.length, wSpace, wLast, spaceIdx, lastIdx },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
sessionId: "debug-session",
|
||||||
|
runId: "post-fix",
|
||||||
|
hypothesisId: "B,D,E",
|
||||||
|
}),
|
||||||
|
}).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const calculateAge = () => {
|
const calculateAge = () => {
|
||||||
@@ -40,7 +106,7 @@ const Hero = () => {
|
|||||||
<Silk
|
<Silk
|
||||||
speed={3}
|
speed={3}
|
||||||
scale={0.5}
|
scale={0.5}
|
||||||
color="#373737"
|
color="#6a6a6a"
|
||||||
noiseIntensity={4
|
noiseIntensity={4
|
||||||
}
|
}
|
||||||
rotation={0}
|
rotation={0}
|
||||||
@@ -66,23 +132,54 @@ const Hero = () => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
{/* CTA Buttons */}
|
||||||
<div className="flex flex-col sm:flex-row gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}>
|
<div className="flex flex-col sm:flex-row flex-nowrap items-stretch sm:items-center gap-3 sm:gap-4 mb-6 animate-fade-in" style={{ animationDelay: '0.5s' }}>
|
||||||
<Link to="/kontakt">
|
<div className="btn-wrapper shrink-0 w-full sm:w-auto">
|
||||||
<Button
|
<button
|
||||||
size="lg"
|
type="button"
|
||||||
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
|
className="btn btn-primary w-full sm:w-auto justify-center"
|
||||||
|
onClick={() => navigate("/kontakt")}
|
||||||
|
aria-label="Kostenlose Potenzialanalyse sichern"
|
||||||
>
|
>
|
||||||
Kostenlose Potenzialanalyse sichern
|
<ArrowRight className="btn-icon" size={24} strokeWidth={2} aria-hidden />
|
||||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
<div className="txt-wrapper">
|
||||||
</Button>
|
<span className="txt-width-helper" aria-hidden="true">
|
||||||
</Link>
|
<DemoButtonLetters text="Kostenlose Potenzialanalyse sichern" />
|
||||||
<Button
|
</span>
|
||||||
size="lg"
|
<div className="txt-1">
|
||||||
variant="outline"
|
<DemoButtonLetters text="Kostenlose Potenzialanalyse sichern" />
|
||||||
className="btn-outline rounded-full px-8 py-6 text-base font-medium"
|
</div>
|
||||||
>
|
<div className="txt-2">
|
||||||
System-Demo anfordern
|
<DemoButtonLetters text="Wird weitergeleitet..." />
|
||||||
</Button>
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="btn-wrapper w-full sm:w-auto">
|
||||||
|
<button
|
||||||
|
ref={secondBtnRef}
|
||||||
|
type="button"
|
||||||
|
className="btn w-full sm:w-auto justify-center"
|
||||||
|
onClick={() => {
|
||||||
|
const el = document.getElementById("projects");
|
||||||
|
if (el) el.scrollIntoView({ behavior: "smooth" });
|
||||||
|
else navigate("/#projects");
|
||||||
|
}}
|
||||||
|
aria-label="Projekte ansehen"
|
||||||
|
>
|
||||||
|
{SPARKLE_SVG}
|
||||||
|
<div className="txt-wrapper">
|
||||||
|
<span className="txt-width-helper" aria-hidden="true">
|
||||||
|
<DemoButtonLetters text="Projekte ansehen" />
|
||||||
|
</span>
|
||||||
|
<div className="txt-1">
|
||||||
|
<DemoButtonLetters text="Projekte ansehen" />
|
||||||
|
</div>
|
||||||
|
<div className="txt-2">
|
||||||
|
<DemoButtonLetters text="Wird geladen..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trust Line */}
|
{/* Trust Line */}
|
||||||
@@ -97,7 +194,17 @@ const Hero = () => {
|
|||||||
<div className="divider mb-12" />
|
<div className="divider mb-12" />
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
||||||
<div className="animate-fade-in" style={{ animationDelay: '0.6s' }}>
|
<div className="animate-fade-in" style={{ animationDelay: '0.6s' }}>
|
||||||
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">10+</div>
|
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">
|
||||||
|
<CountUp
|
||||||
|
from={0}
|
||||||
|
to={10}
|
||||||
|
direction="up"
|
||||||
|
duration={1.2}
|
||||||
|
className="count-up-text"
|
||||||
|
startWhen={true}
|
||||||
|
suffix="+"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="label-tag">Projekte</div>
|
<div className="label-tag">Projekte</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="animate-fade-in" style={{ animationDelay: '0.7s' }}>
|
<div className="animate-fade-in" style={{ animationDelay: '0.7s' }}>
|
||||||
@@ -105,7 +212,17 @@ const Hero = () => {
|
|||||||
<div className="label-tag">Am Markt</div>
|
<div className="label-tag">Am Markt</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="animate-fade-in" style={{ animationDelay: '0.8s' }}>
|
<div className="animate-fade-in" style={{ animationDelay: '0.8s' }}>
|
||||||
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">99,9%</div>
|
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">
|
||||||
|
<CountUp
|
||||||
|
from={0}
|
||||||
|
to={99.9}
|
||||||
|
direction="up"
|
||||||
|
duration={1.5}
|
||||||
|
className="count-up-text"
|
||||||
|
startWhen={true}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="label-tag">Systemverfügbarkeit</div>
|
<div className="label-tag">Systemverfügbarkeit</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="animate-fade-in" style={{ animationDelay: '0.9s' }}>
|
<div className="animate-fade-in" style={{ animationDelay: '0.9s' }}>
|
||||||
|
|||||||
16
src/components/LightRays.css
Normal file
16
src/components/LightRays.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.light-rays-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-rays-fallback {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
486
src/components/LightRays.tsx
Normal file
486
src/components/LightRays.tsx
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
// @ts-expect-error ogl has no type definitions
|
||||||
|
import { Renderer, Program, Triangle, Mesh } from "ogl";
|
||||||
|
import "./LightRays.css";
|
||||||
|
|
||||||
|
const DEFAULT_COLOR = "#ffffff";
|
||||||
|
|
||||||
|
const hexToRgb = (hex: string): [number, number, number] => {
|
||||||
|
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return m
|
||||||
|
? [
|
||||||
|
parseInt(m[1], 16) / 255,
|
||||||
|
parseInt(m[2], 16) / 255,
|
||||||
|
parseInt(m[3], 16) / 255,
|
||||||
|
]
|
||||||
|
: [1, 1, 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
type RaysOrigin =
|
||||||
|
| "top-left"
|
||||||
|
| "top-right"
|
||||||
|
| "top-center"
|
||||||
|
| "left"
|
||||||
|
| "right"
|
||||||
|
| "bottom-left"
|
||||||
|
| "bottom-center"
|
||||||
|
| "bottom-right";
|
||||||
|
|
||||||
|
const getAnchorAndDir = (
|
||||||
|
origin: RaysOrigin,
|
||||||
|
w: number,
|
||||||
|
h: number
|
||||||
|
): { anchor: [number, number]; dir: [number, number] } => {
|
||||||
|
const outside = 0.2;
|
||||||
|
switch (origin) {
|
||||||
|
case "top-left":
|
||||||
|
return { anchor: [0, -outside * h], dir: [0, 1] };
|
||||||
|
case "top-right":
|
||||||
|
return { anchor: [w, -outside * h], dir: [0, 1] };
|
||||||
|
case "left":
|
||||||
|
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
|
||||||
|
case "right":
|
||||||
|
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
|
||||||
|
case "bottom-left":
|
||||||
|
return { anchor: [0, (1 + outside) * h], dir: [0, -1] };
|
||||||
|
case "bottom-center":
|
||||||
|
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
|
||||||
|
case "bottom-right":
|
||||||
|
return { anchor: [w, (1 + outside) * h], dir: [0, -1] };
|
||||||
|
default:
|
||||||
|
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LightRaysProps {
|
||||||
|
raysOrigin?: RaysOrigin;
|
||||||
|
raysColor?: string;
|
||||||
|
raysSpeed?: number;
|
||||||
|
lightSpread?: number;
|
||||||
|
rayLength?: number;
|
||||||
|
pulsating?: boolean;
|
||||||
|
fadeDistance?: number;
|
||||||
|
saturation?: number;
|
||||||
|
followMouse?: boolean;
|
||||||
|
mouseInfluence?: number;
|
||||||
|
noiseAmount?: number;
|
||||||
|
distortion?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LightRays({
|
||||||
|
raysOrigin = "top-center",
|
||||||
|
raysColor = DEFAULT_COLOR,
|
||||||
|
raysSpeed = 1,
|
||||||
|
lightSpread = 1,
|
||||||
|
rayLength = 2,
|
||||||
|
pulsating = false,
|
||||||
|
fadeDistance = 1.0,
|
||||||
|
saturation = 1.0,
|
||||||
|
followMouse = true,
|
||||||
|
mouseInfluence = 0.1,
|
||||||
|
noiseAmount = 0.0,
|
||||||
|
distortion = 0.0,
|
||||||
|
className = "",
|
||||||
|
}: LightRaysProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const uniformsRef = useRef<Record<string, { value: unknown }> | null>(null);
|
||||||
|
const rendererRef = useRef<InstanceType<typeof Renderer> | null>(null);
|
||||||
|
const mouseRef = useRef({ x: 0.5, y: 0.5 });
|
||||||
|
const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });
|
||||||
|
const animationIdRef = useRef<number | null>(null);
|
||||||
|
const meshRef = useRef<InstanceType<typeof Mesh> | null>(null);
|
||||||
|
const cleanupFunctionRef = useRef<(() => void) | null>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [useFallback, setUseFallback] = useState(false);
|
||||||
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
observerRef.current = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
setIsVisible(entry.isIntersecting);
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observerRef.current.observe(containerRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
observerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible || !containerRef.current) return;
|
||||||
|
|
||||||
|
setUseFallback(false);
|
||||||
|
if (cleanupFunctionRef.current) {
|
||||||
|
cleanupFunctionRef.current();
|
||||||
|
cleanupFunctionRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeWebGL = async () => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const isMobile =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
|
(window.innerWidth <= 768 || "ontouchstart" in window);
|
||||||
|
const dpr = isMobile ? 1 : Math.min(window.devicePixelRatio, 2);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const renderer = new Renderer({
|
||||||
|
dpr,
|
||||||
|
alpha: true,
|
||||||
|
});
|
||||||
|
rendererRef.current = renderer;
|
||||||
|
|
||||||
|
const gl = renderer.gl;
|
||||||
|
gl.canvas.style.width = "100%";
|
||||||
|
gl.canvas.style.height = "100%";
|
||||||
|
|
||||||
|
while (containerRef.current.firstChild) {
|
||||||
|
containerRef.current.removeChild(containerRef.current.firstChild);
|
||||||
|
}
|
||||||
|
containerRef.current.appendChild(gl.canvas);
|
||||||
|
|
||||||
|
const vert = `
|
||||||
|
attribute vec2 position;
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = position * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const frag = `precision mediump float;
|
||||||
|
|
||||||
|
uniform float iTime;
|
||||||
|
uniform vec2 iResolution;
|
||||||
|
|
||||||
|
uniform vec2 rayPos;
|
||||||
|
uniform vec2 rayDir;
|
||||||
|
uniform vec3 raysColor;
|
||||||
|
uniform float raysSpeed;
|
||||||
|
uniform float lightSpread;
|
||||||
|
uniform float rayLength;
|
||||||
|
uniform float pulsating;
|
||||||
|
uniform float fadeDistance;
|
||||||
|
uniform float saturation;
|
||||||
|
uniform vec2 mousePos;
|
||||||
|
uniform float mouseInfluence;
|
||||||
|
uniform float noiseAmount;
|
||||||
|
uniform float distortion;
|
||||||
|
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
float noise(vec2 st) {
|
||||||
|
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
||||||
|
}
|
||||||
|
|
||||||
|
float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,
|
||||||
|
float seedA, float seedB, float speed) {
|
||||||
|
vec2 sourceToCoord = coord - raySource;
|
||||||
|
vec2 dirNorm = normalize(sourceToCoord);
|
||||||
|
float cosAngle = dot(dirNorm, rayRefDirection);
|
||||||
|
|
||||||
|
float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
|
||||||
|
|
||||||
|
float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
|
||||||
|
|
||||||
|
float distance = length(sourceToCoord);
|
||||||
|
float maxDistance = iResolution.x * rayLength;
|
||||||
|
float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
|
||||||
|
|
||||||
|
float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);
|
||||||
|
float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;
|
||||||
|
|
||||||
|
float baseStrength = clamp(
|
||||||
|
(0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +
|
||||||
|
(0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),
|
||||||
|
0.0, 1.0
|
||||||
|
);
|
||||||
|
|
||||||
|
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||||
|
vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);
|
||||||
|
|
||||||
|
vec2 finalRayDir = rayDir;
|
||||||
|
if (mouseInfluence > 0.0) {
|
||||||
|
vec2 mouseScreenPos = mousePos * iResolution.xy;
|
||||||
|
vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
|
||||||
|
finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 rays1 = vec4(1.0) *
|
||||||
|
rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,
|
||||||
|
1.5 * raysSpeed);
|
||||||
|
vec4 rays2 = vec4(1.0) *
|
||||||
|
rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,
|
||||||
|
1.1 * raysSpeed);
|
||||||
|
|
||||||
|
fragColor = rays1 * 0.5 + rays2 * 0.4;
|
||||||
|
|
||||||
|
if (noiseAmount > 0.0) {
|
||||||
|
float n = noise(coord * 0.01 + iTime * 0.1);
|
||||||
|
fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);
|
||||||
|
}
|
||||||
|
|
||||||
|
float brightness = 1.0 - (coord.y / iResolution.y);
|
||||||
|
fragColor.x *= 0.1 + brightness * 0.8;
|
||||||
|
fragColor.y *= 0.3 + brightness * 0.6;
|
||||||
|
fragColor.z *= 0.5 + brightness * 0.5;
|
||||||
|
|
||||||
|
if (saturation != 1.0) {
|
||||||
|
float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));
|
||||||
|
fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor.rgb *= raysColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color;
|
||||||
|
mainImage(color, gl_FragCoord.xy);
|
||||||
|
gl_FragColor = color;
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const uniforms = {
|
||||||
|
iTime: { value: 0 },
|
||||||
|
iResolution: { value: [1, 1] as [number, number] },
|
||||||
|
|
||||||
|
rayPos: { value: [0, 0] as [number, number] },
|
||||||
|
rayDir: { value: [0, 1] as [number, number] },
|
||||||
|
|
||||||
|
raysColor: { value: hexToRgb(raysColor) },
|
||||||
|
raysSpeed: { value: raysSpeed },
|
||||||
|
lightSpread: { value: lightSpread },
|
||||||
|
rayLength: { value: rayLength },
|
||||||
|
pulsating: { value: pulsating ? 1.0 : 0.0 },
|
||||||
|
fadeDistance: { value: fadeDistance },
|
||||||
|
saturation: { value: saturation },
|
||||||
|
mousePos: { value: [0.5, 0.5] as [number, number] },
|
||||||
|
mouseInfluence: { value: mouseInfluence },
|
||||||
|
noiseAmount: { value: noiseAmount },
|
||||||
|
distortion: { value: distortion },
|
||||||
|
};
|
||||||
|
uniformsRef.current = uniforms as Record<string, { value: unknown }>;
|
||||||
|
|
||||||
|
const geometry = new Triangle(gl);
|
||||||
|
const program = new Program(gl, {
|
||||||
|
vertex: vert,
|
||||||
|
fragment: frag,
|
||||||
|
uniforms,
|
||||||
|
});
|
||||||
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
|
meshRef.current = mesh;
|
||||||
|
|
||||||
|
const updatePlacement = () => {
|
||||||
|
if (!containerRef.current || !renderer) return;
|
||||||
|
|
||||||
|
renderer.dpr = isMobile ? 1 : Math.min(window.devicePixelRatio, 2);
|
||||||
|
|
||||||
|
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
|
||||||
|
renderer.setSize(wCSS, hCSS);
|
||||||
|
|
||||||
|
const dpr = renderer.dpr;
|
||||||
|
const w = wCSS * dpr;
|
||||||
|
const h = hCSS * dpr;
|
||||||
|
|
||||||
|
uniforms.iResolution.value = [w, h];
|
||||||
|
|
||||||
|
const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);
|
||||||
|
uniforms.rayPos.value = anchor;
|
||||||
|
uniforms.rayDir.value = dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loop = (t: number) => {
|
||||||
|
if (!rendererRef.current || !uniformsRef.current || !meshRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniforms = uniformsRef.current as typeof uniforms;
|
||||||
|
uniforms.iTime.value = t * 0.001;
|
||||||
|
|
||||||
|
if (followMouse && mouseInfluence > 0.0) {
|
||||||
|
const smoothing = 0.92;
|
||||||
|
|
||||||
|
smoothMouseRef.current.x =
|
||||||
|
smoothMouseRef.current.x * smoothing +
|
||||||
|
mouseRef.current.x * (1 - smoothing);
|
||||||
|
smoothMouseRef.current.y =
|
||||||
|
smoothMouseRef.current.y * smoothing +
|
||||||
|
mouseRef.current.y * (1 - smoothing);
|
||||||
|
|
||||||
|
uniforms.mousePos.value = [
|
||||||
|
smoothMouseRef.current.x,
|
||||||
|
smoothMouseRef.current.y,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderer.render({ scene: mesh });
|
||||||
|
animationIdRef.current = requestAnimationFrame(loop);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("WebGL rendering error:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", updatePlacement);
|
||||||
|
const resizeObserver =
|
||||||
|
typeof ResizeObserver !== "undefined" &&
|
||||||
|
new ResizeObserver(() => updatePlacement());
|
||||||
|
if (resizeObserver && containerRef.current) {
|
||||||
|
resizeObserver.observe(containerRef.current);
|
||||||
|
}
|
||||||
|
updatePlacement();
|
||||||
|
animationIdRef.current = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
cleanupFunctionRef.current = () => {
|
||||||
|
if (resizeObserver && containerRef.current) {
|
||||||
|
resizeObserver.unobserve(containerRef.current);
|
||||||
|
}
|
||||||
|
if (animationIdRef.current) {
|
||||||
|
cancelAnimationFrame(animationIdRef.current);
|
||||||
|
animationIdRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.removeEventListener("resize", updatePlacement);
|
||||||
|
|
||||||
|
if (renderer) {
|
||||||
|
try {
|
||||||
|
const canvas = renderer.gl.canvas;
|
||||||
|
const loseContextExt =
|
||||||
|
renderer.gl.getExtension("WEBGL_lose_context");
|
||||||
|
if (loseContextExt) {
|
||||||
|
loseContextExt.loseContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas && canvas.parentNode) {
|
||||||
|
canvas.parentNode.removeChild(canvas);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Error during WebGL cleanup:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rendererRef.current = null;
|
||||||
|
uniformsRef.current = null;
|
||||||
|
meshRef.current = null;
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("LightRays WebGL init failed (e.g. on mobile):", error);
|
||||||
|
setUseFallback(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeWebGL();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (cleanupFunctionRef.current) {
|
||||||
|
cleanupFunctionRef.current();
|
||||||
|
cleanupFunctionRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
isVisible,
|
||||||
|
raysOrigin,
|
||||||
|
raysColor,
|
||||||
|
raysSpeed,
|
||||||
|
lightSpread,
|
||||||
|
rayLength,
|
||||||
|
pulsating,
|
||||||
|
fadeDistance,
|
||||||
|
saturation,
|
||||||
|
followMouse,
|
||||||
|
mouseInfluence,
|
||||||
|
noiseAmount,
|
||||||
|
distortion,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!uniformsRef.current || !containerRef.current || !rendererRef.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const u = uniformsRef.current as Record<string, { value: unknown }>;
|
||||||
|
const renderer = rendererRef.current;
|
||||||
|
|
||||||
|
u.raysColor.value = hexToRgb(raysColor);
|
||||||
|
u.raysSpeed.value = raysSpeed;
|
||||||
|
u.lightSpread.value = lightSpread;
|
||||||
|
u.rayLength.value = rayLength;
|
||||||
|
u.pulsating.value = pulsating ? 1.0 : 0.0;
|
||||||
|
u.fadeDistance.value = fadeDistance;
|
||||||
|
u.saturation.value = saturation;
|
||||||
|
u.mouseInfluence.value = mouseInfluence;
|
||||||
|
u.noiseAmount.value = noiseAmount;
|
||||||
|
u.distortion.value = distortion;
|
||||||
|
|
||||||
|
const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
|
||||||
|
const dpr = renderer.dpr;
|
||||||
|
const { anchor, dir } = getAnchorAndDir(
|
||||||
|
raysOrigin,
|
||||||
|
wCSS * dpr,
|
||||||
|
hCSS * dpr
|
||||||
|
);
|
||||||
|
u.rayPos.value = anchor;
|
||||||
|
u.rayDir.value = dir;
|
||||||
|
}, [
|
||||||
|
raysColor,
|
||||||
|
raysSpeed,
|
||||||
|
lightSpread,
|
||||||
|
raysOrigin,
|
||||||
|
rayLength,
|
||||||
|
pulsating,
|
||||||
|
fadeDistance,
|
||||||
|
saturation,
|
||||||
|
mouseInfluence,
|
||||||
|
noiseAmount,
|
||||||
|
distortion,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!containerRef.current || !rendererRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
|
mouseRef.current = { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (followMouse) {
|
||||||
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
|
return () => window.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
}
|
||||||
|
}, [followMouse]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`light-rays-container ${className}`.trim()}
|
||||||
|
>
|
||||||
|
{useFallback && (
|
||||||
|
<div
|
||||||
|
className="light-rays-fallback"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, ${raysColor}50 0%, ${raysColor}20 25%, ${raysColor}08 50%, transparent 85%)`,
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/components/Logo.tsx
Normal file
39
src/components/Logo.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
interface LogoProps {
|
||||||
|
className?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Logo = ({ className, width = 30, height = 30 }: LogoProps) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
className={className}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{/* rounded square background */}
|
||||||
|
<rect x="48" y="48" width="416" height="416" rx="92" fill="#FFFFFF" />
|
||||||
|
{/* tilted oval */}
|
||||||
|
<ellipse cx="256" cy="256" rx="210" ry="150" fill="#111111" transform="rotate(-28 256 256)" />
|
||||||
|
{/* small dot */}
|
||||||
|
<circle cx="356" cy="172" r="18" fill="#FFFFFF" />
|
||||||
|
{/* enlarged sparkle star */}
|
||||||
|
<g transform="translate(256 256) rotate(-28)">
|
||||||
|
<path
|
||||||
|
d="
|
||||||
|
M 0,-170
|
||||||
|
C 22,-105 60,-72 120,-56
|
||||||
|
C 60,-40 22,-8 0,64
|
||||||
|
C -22,-8 -60,-40 -120,-56
|
||||||
|
C -60,-72 -22,-105 0,-170
|
||||||
|
Z
|
||||||
|
"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Calendar, MessageSquareOff, TrendingDown, Folders } from "lucide-react";
|
import { Calendar, MessageSquareOff, TrendingDown, Folders } from "lucide-react";
|
||||||
|
import { LampTop } from "@/components/ui/lamp";
|
||||||
|
import LightRays from "@/components/LightRays";
|
||||||
|
|
||||||
const ProblemSection = () => {
|
const ProblemSection = () => {
|
||||||
const problems = [
|
const problems = [
|
||||||
@@ -21,8 +23,25 @@ const ProblemSection = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 md:py-32 bg-background relative">
|
<section className="section-problem-solution py-24 md:py-32 relative overflow-hidden">
|
||||||
<div className="container mx-auto px-6">
|
<div className="absolute inset-0 w-full overflow-hidden z-0">
|
||||||
|
<LightRays
|
||||||
|
raysOrigin="top-center"
|
||||||
|
raysColor="#ef4444"
|
||||||
|
raysSpeed={1}
|
||||||
|
lightSpread={0.5}
|
||||||
|
rayLength={3}
|
||||||
|
followMouse={false}
|
||||||
|
mouseInfluence={0}
|
||||||
|
noiseAmount={0}
|
||||||
|
distortion={0}
|
||||||
|
pulsating
|
||||||
|
fadeDistance={2}
|
||||||
|
saturation={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<LampTop />
|
||||||
|
<div className="container mx-auto px-6 relative z-10">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div className="mb-16 md:mb-20 max-w-4xl">
|
<div className="mb-16 md:mb-20 max-w-4xl">
|
||||||
<div className="label-tag mb-4">Das Problem</div>
|
<div className="label-tag mb-4">Das Problem</div>
|
||||||
@@ -39,7 +58,7 @@ const ProblemSection = () => {
|
|||||||
{problems.map((problem, index) => (
|
{problems.map((problem, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-start gap-4 p-6 border border-border rounded-lg bg-card/50 hover:border-foreground/20 transition-colors"
|
className="problem-section-tint flex items-start gap-4 p-6 border border-border rounded-lg bg-card/50 hover:border-foreground/20 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 rounded-full border border-destructive/30 bg-destructive/10 flex items-center justify-center flex-shrink-0">
|
<div className="w-10 h-10 rounded-full border border-destructive/30 bg-destructive/10 flex items-center justify-center flex-shrink-0">
|
||||||
<problem.icon className="w-5 h-5 text-destructive" />
|
<problem.icon className="w-5 h-5 text-destructive" />
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
|
import CountUp from "@/components/CountUp";
|
||||||
|
|
||||||
const Process = () => {
|
const Process = () => {
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
number: "01",
|
number: 1,
|
||||||
title: "Erstgespräch",
|
title: "Erstgespräch",
|
||||||
description: "Wir lernen Ihr Unternehmen und Ihre Ziele kennen. In einem unverbindlichen Gespräch besprechen wir Ihre Wünsche.",
|
description: "Wir lernen Ihr Unternehmen und Ihre Ziele kennen. In einem unverbindlichen Gespräch besprechen wir Ihre Wünsche.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "02",
|
number: 2,
|
||||||
title: "Konzept & Design",
|
title: "Konzept & Design",
|
||||||
description: "Basierend auf unserer Analyse erstellen wir ein individuelles Konzept und Design für Ihre Website.",
|
description: "Basierend auf unserer Analyse erstellen wir ein individuelles Konzept und Design für Ihre Website.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "03",
|
number: 3,
|
||||||
title: "Entwicklung",
|
title: "Entwicklung",
|
||||||
description: "Unsere Entwickler setzen Ihre Website mit modernsten Technologien um. Sie bleiben informiert.",
|
description: "Unsere Entwickler setzen Ihre Website mit modernsten Technologien um. Sie bleiben informiert.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "04",
|
number: 4,
|
||||||
title: "Launch & Support",
|
title: "Launch & Support",
|
||||||
description: "Nach gründlichen Tests geht Ihre Website live. Wir stehen Ihnen auch danach mit Support zur Seite.",
|
description: "Nach gründlichen Tests geht Ihre Website live. Wir stehen Ihnen auch danach mit Support zur Seite.",
|
||||||
},
|
},
|
||||||
@@ -45,13 +47,22 @@ const Process = () => {
|
|||||||
{/* Number */}
|
{/* Number */}
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<span className="stat-number text-6xl text-border group-hover:text-muted-foreground/30 transition-colors">
|
<span className="stat-number text-6xl text-border group-hover:text-muted-foreground/30 transition-colors">
|
||||||
{step.number}
|
<CountUp
|
||||||
|
from={0}
|
||||||
|
to={step.number}
|
||||||
|
direction="up"
|
||||||
|
duration={1}
|
||||||
|
padMinLength={2}
|
||||||
|
startWhen={true}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<span className="md:hidden label-tag mb-2 block">{step.number}</span>
|
<span className="md:hidden label-tag mb-2 block">
|
||||||
|
<CountUp from={0} to={step.number} duration={1} padMinLength={2} startWhen={true} />
|
||||||
|
</span>
|
||||||
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground mb-4 uppercase tracking-tight">
|
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground mb-4 uppercase tracking-tight">
|
||||||
{step.title}
|
{step.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -1,30 +1,42 @@
|
|||||||
import { ArrowUpRight } from "lucide-react";
|
import { ArrowUpRight } from "lucide-react";
|
||||||
|
|
||||||
const projects = [
|
type Project = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const projects: Project[] = [
|
||||||
{
|
{
|
||||||
title: "Triple AI",
|
title: "Email Sorter",
|
||||||
description: "Webentwicklung / UI Design / Custom Code",
|
description: "E-Mails automatisch sortieren",
|
||||||
image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop",
|
image: "/project%20pics/emailsorter.png",
|
||||||
|
url: "https://emailsorter.webklar.com/",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Neutral",
|
title: "Neutral",
|
||||||
description: "Webentwicklung / Custom Code",
|
description: "Webentwicklung / Custom Code",
|
||||||
image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop",
|
image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop",
|
||||||
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Verbatim Labs",
|
title: "Verbatim Labs",
|
||||||
description: "Webentwicklung / UI Design / Custom Code",
|
description: "Webentwicklung / UI Design / Custom Code",
|
||||||
image: "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=800&h=600&fit=crop",
|
image: "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=800&h=600&fit=crop",
|
||||||
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "JMK Engineers",
|
title: "JMK Engineers",
|
||||||
description: "Webentwicklung / UI Design / Custom Code",
|
description: "Webentwicklung / UI Design / Custom Code",
|
||||||
image: "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=800&h=600&fit=crop",
|
image: "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=800&h=600&fit=crop",
|
||||||
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "GOODZ Club",
|
title: "GOODZ Club",
|
||||||
description: "Webentwicklung / Custom Code / Lokalisierung",
|
description: "Webentwicklung / Custom Code / Lokalisierung",
|
||||||
image: "https://images.unsplash.com/photo-1542744094-3a31f272c490?w=800&h=600&fit=crop",
|
image: "https://images.unsplash.com/photo-1542744094-3a31f272c490?w=800&h=600&fit=crop",
|
||||||
|
url: "#",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -45,7 +57,9 @@ const ProjectShowcase = () => {
|
|||||||
{projects.map((project, index) => (
|
{projects.map((project, index) => (
|
||||||
<a
|
<a
|
||||||
key={project.title}
|
key={project.title}
|
||||||
href="#"
|
href={project.url}
|
||||||
|
target={project.url.startsWith("http") ? "_blank" : undefined}
|
||||||
|
rel={project.url.startsWith("http") ? "noopener noreferrer" : undefined}
|
||||||
className="group block project-card rounded-lg p-6 md:p-8"
|
className="group block project-card rounded-lg p-6 md:p-8"
|
||||||
style={{ animationDelay: `${index * 0.1}s` }}
|
style={{ animationDelay: `${index * 0.1}s` }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowRight, CheckCircle2 } from "lucide-react";
|
import { ArrowRight, CheckCircle2 } from "lucide-react";
|
||||||
|
import { LampTop } from "@/components/ui/lamp";
|
||||||
|
import LightRays from "@/components/LightRays";
|
||||||
|
|
||||||
const SolutionSection = () => {
|
const SolutionSection = () => {
|
||||||
const benefits = [
|
const benefits = [
|
||||||
@@ -10,8 +12,25 @@ const SolutionSection = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 md:py-32 bg-background relative">
|
<section className="section-problem-solution py-24 md:py-32 relative overflow-hidden">
|
||||||
<div className="container mx-auto px-6">
|
<div className="absolute inset-0 w-full overflow-hidden z-0">
|
||||||
|
<LightRays
|
||||||
|
raysOrigin="top-center"
|
||||||
|
raysColor="#22d3ee"
|
||||||
|
raysSpeed={1}
|
||||||
|
lightSpread={0.5}
|
||||||
|
rayLength={3}
|
||||||
|
followMouse={false}
|
||||||
|
mouseInfluence={0}
|
||||||
|
noiseAmount={0}
|
||||||
|
distortion={0}
|
||||||
|
pulsating
|
||||||
|
fadeDistance={2}
|
||||||
|
saturation={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<LampTop lineClassName="bg-cyan-400" />
|
||||||
|
<div className="container mx-auto px-6 relative z-10">
|
||||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||||
{/* Left Content */}
|
{/* Left Content */}
|
||||||
<div>
|
<div>
|
||||||
@@ -47,7 +66,7 @@ const SolutionSection = () => {
|
|||||||
|
|
||||||
{/* Right Content - Visual Element */}
|
{/* Right Content - Visual Element */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="aspect-square bg-secondary/50 rounded-2xl border border-border p-8 md:p-12 flex flex-col justify-center">
|
<div className="solution-section-tint aspect-square bg-secondary/50 rounded-2xl border border-border p-8 md:p-12 flex flex-col justify-center">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-sm uppercase tracking-wider text-muted-foreground">Das Ergebnis</div>
|
<div className="text-sm uppercase tracking-wider text-muted-foreground">Das Ergebnis</div>
|
||||||
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground uppercase tracking-tight">
|
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground uppercase tracking-tight">
|
||||||
|
|||||||
@@ -35,8 +35,17 @@ const Values = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="features" className="py-24 md:py-32 bg-background relative">
|
<section id="features" className="py-24 md:py-32 bg-background relative overflow-hidden">
|
||||||
<div className="container mx-auto px-6">
|
{/* Hintergrundbild: kleiner, leicht transparent */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-right bg-no-repeat opacity-[0.3]"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/backgroud_effect.png)",
|
||||||
|
backgroundSize: "45%",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="container mx-auto px-6 relative z-10">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div className="mb-16 md:mb-24 max-w-3xl">
|
<div className="mb-16 md:mb-24 max-w-3xl">
|
||||||
<div className="label-tag mb-4">Was Sie bekommen</div>
|
<div className="label-tag mb-4">Was Sie bekommen</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cn } from "@/lib/utils";
|
|||||||
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "btn-primary-style rounded-full bg-[hsl(198,93%,42%)] text-white hover:bg-[hsl(198,93%,48%)] border border-white/20",
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
|||||||
39
src/components/ui/lamp.tsx
Normal file
39
src/components/ui/lamp.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const S = 2.5;
|
||||||
|
|
||||||
|
export const LampTop = ({
|
||||||
|
className,
|
||||||
|
lineClassName = "bg-red-500",
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
lineClassName?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0 left-0 right-0 w-full min-h-0 pointer-events-none z-50 flex items-start justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: `${15 * S}rem` }}
|
||||||
|
whileInView={{ width: `${30 * S}rem` }}
|
||||||
|
transition={{
|
||||||
|
delay: 0.3,
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
className={cn("absolute top-0 left-1/2 -translate-x-1/2 h-0.5", lineClassName)}
|
||||||
|
style={{ width: `${30 * S}rem` }}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -290,13 +290,13 @@ export const NavbarButton = ({
|
|||||||
...props
|
...props
|
||||||
}: NavbarButtonProps) => {
|
}: NavbarButtonProps) => {
|
||||||
const baseStyles =
|
const baseStyles =
|
||||||
"px-4 py-2 rounded-md bg-white text-black text-sm font-bold relative cursor-pointer hover:-translate-y-0.5 transition duration-200 inline-block text-center";
|
"px-4 py-2 rounded-full text-sm font-bold relative cursor-pointer transition duration-200 inline-block text-center";
|
||||||
|
|
||||||
const variantStyles = {
|
const variantStyles = {
|
||||||
primary:
|
primary:
|
||||||
"shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
|
"bg-[hsl(198,93%,42%)] text-white border border-white/20 shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),inset_0_2px_2px_rgba(255,255,255,0.2),0_2px_4px_rgba(0,0,0,0.2),0_4px_8px_rgba(0,0,0,0.15)] hover:border-white/40 hover:shadow-[inset_0_1px_1px_rgba(255,255,255,0.3),inset_0_2px_2px_rgba(255,255,255,0.25),0_2px_4px_rgba(0,0,0,0.2),0_4px_8px_rgba(0,0,0,0.15)]",
|
||||||
secondary: "bg-transparent shadow-none dark:text-white",
|
secondary: "bg-transparent shadow-none dark:text-white",
|
||||||
dark: "bg-black text-white shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
|
dark: "btn !text-white",
|
||||||
gradient:
|
gradient:
|
||||||
"bg-gradient-to-b from-blue-500 to-blue-700 text-white shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]",
|
"bg-gradient-to-b from-blue-500 to-blue-700 text-white shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]",
|
||||||
};
|
};
|
||||||
|
|||||||
521
src/index.css
521
src/index.css
@@ -1,19 +1,17 @@
|
|||||||
|
/* Fonts first – @import must be at the very top */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600;700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&display=swap");
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* webklar Design System - Muradov Inspired Minimal Dark Theme */
|
/* webklar Design System - Muradov Inspired Minimal Dark Theme */
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap");
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600;700&display=swap");
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&display=swap");
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
/* Ultra Minimal Deep Black Theme - Muradov Inspired */
|
/* Ultra Minimal Deep Black Theme - Muradov Inspired */
|
||||||
@@ -156,6 +154,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
/* Gemeinsamer Hintergrund für Problem- und Lösungs-Sektion */
|
||||||
|
.section-problem-solution {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leichter roter Tint auf Inhaltsblöcken der Problem-Sektion */
|
||||||
|
.problem-section-tint {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.problem-section-tint::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgb(239 68 68 / 0.06);
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Leichter blauer Tint auf dem Ergebnis-Block der Lösungs-Sektion */
|
||||||
|
.solution-section-tint {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.solution-section-tint::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgb(34 211 238 / 0.06);
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
/* Minimal glass nav */
|
/* Minimal glass nav */
|
||||||
.glass-nav {
|
.glass-nav {
|
||||||
@apply backdrop-blur-xl border-b;
|
@apply backdrop-blur-xl border-b;
|
||||||
@@ -241,6 +270,461 @@
|
|||||||
background: hsl(0 0% 10%);
|
background: hsl(0 0% 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom CTA button (System-Demo) */
|
||||||
|
.btn-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: fit-content;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
--border-radius: 24px;
|
||||||
|
--padding: 4px;
|
||||||
|
--transition: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--button-color: #101010;
|
||||||
|
--highlight-color-hue: 210deg;
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5em 1.1em;
|
||||||
|
font-family: "Poppins", "Inter", "Segoe UI", sans-serif;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 400;
|
||||||
|
background-color: var(--button-color);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
inset 0px 1px 1px rgba(255, 255, 255, 0.2),
|
||||||
|
inset 0px 2px 2px rgba(255, 255, 255, 0.15),
|
||||||
|
inset 0px 4px 4px rgba(255, 255, 255, 0.1),
|
||||||
|
inset 0px 8px 8px rgba(255, 255, 255, 0.05),
|
||||||
|
inset 0px 16px 16px rgba(255, 255, 255, 0.05),
|
||||||
|
0px -1px 1px rgba(0, 0, 0, 0.02),
|
||||||
|
0px -2px 2px rgba(0, 0, 0, 0.03),
|
||||||
|
0px -4px 4px rgba(0, 0, 0, 0.05),
|
||||||
|
0px -8px 8px rgba(0, 0, 0, 0.06),
|
||||||
|
0px -16px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
border: solid 1px rgba(255, 255, 255, 0.13);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow var(--transition), border var(--transition), background-color var(--transition);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: calc(0px - var(--padding));
|
||||||
|
left: calc(0px - var(--padding));
|
||||||
|
width: calc(100% + var(--padding) * 2);
|
||||||
|
height: calc(100% + var(--padding) * 2);
|
||||||
|
border-radius: calc(var(--border-radius) + var(--padding));
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.27), rgba(0, 0, 0, 0.4));
|
||||||
|
z-index: -1;
|
||||||
|
transition: box-shadow var(--transition), filter var(--transition);
|
||||||
|
box-shadow:
|
||||||
|
0 -8px 8px -6px transparent inset,
|
||||||
|
0 -16px 16px -8px transparent inset,
|
||||||
|
1px 1px 1px rgba(255, 255, 255, 0.13),
|
||||||
|
2px 2px 2px rgba(255, 255, 255, 0.07),
|
||||||
|
-1px -1px 1px rgba(0, 0, 0, 0.13),
|
||||||
|
-2px -2px 2px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
#fff,
|
||||||
|
hsl(var(--highlight-color-hue), 100%, 70%),
|
||||||
|
hsla(var(--highlight-color-hue), 100%, 70%, 0.5),
|
||||||
|
8%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-position: 0 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition), filter var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-letter {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
color: rgba(255, 255, 255, 0.33);
|
||||||
|
animation: letter-anim 2.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
transition: color var(--transition), text-shadow var(--transition), opacity var(--transition), transform var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes letter-anim {
|
||||||
|
50% {
|
||||||
|
text-shadow: 0 0 3px rgba(255, 255, 255, 0.53);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
fill: #e8e8e8;
|
||||||
|
animation: flicker 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.6));
|
||||||
|
transition: fill var(--transition), filter var(--transition), opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-width-helper {
|
||||||
|
visibility: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: inherit;
|
||||||
|
word-spacing: -1em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-width-helper .btn-letter {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-letter-space {
|
||||||
|
min-width: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-1,
|
||||||
|
.txt-2 {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
word-spacing: -1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-1 {
|
||||||
|
animation: appear-anim 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-2 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appear-anim {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus .txt-1 {
|
||||||
|
animation: opacity-anim 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus .txt-2 {
|
||||||
|
animation: opacity-anim 0.4s cubic-bezier(0.4, 0, 0.2, 1) reverse forwards;
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes opacity-anim {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus .btn-letter {
|
||||||
|
animation: focused-letter-anim 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards, letter-anim 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
animation-delay: 0s, 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes focused-letter-anim {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.08);
|
||||||
|
filter: blur(2px) brightness(150%)
|
||||||
|
drop-shadow(0 0 8px hsl(var(--highlight-color-hue), 100%, 70%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus .btn-svg {
|
||||||
|
animation-duration: 1.2s;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus::before {
|
||||||
|
box-shadow:
|
||||||
|
0 -8px 12px -6px rgba(255, 255, 255, 0.2) inset,
|
||||||
|
0 -16px 16px -8px hsla(var(--highlight-color-hue), 100%, 70%, 0.2) inset,
|
||||||
|
1px 1px 1px rgba(255, 255, 255, 0.2),
|
||||||
|
2px 2px 2px rgba(255, 255, 255, 0.07),
|
||||||
|
-1px -1px 1px rgba(0, 0, 0, 0.13),
|
||||||
|
-2px -2px 2px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus::after {
|
||||||
|
opacity: 0.6;
|
||||||
|
mask-image: linear-gradient(0deg, #fff, transparent);
|
||||||
|
filter: brightness(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-letter:nth-child(1),
|
||||||
|
.btn:focus .btn-letter:nth-child(1) { animation-delay: 0s; }
|
||||||
|
.btn-letter:nth-child(2),
|
||||||
|
.btn:focus .btn-letter:nth-child(2) { animation-delay: 0.08s; }
|
||||||
|
.btn-letter:nth-child(3),
|
||||||
|
.btn:focus .btn-letter:nth-child(3) { animation-delay: 0.16s; }
|
||||||
|
.btn-letter:nth-child(4),
|
||||||
|
.btn:focus .btn-letter:nth-child(4) { animation-delay: 0.24s; }
|
||||||
|
.btn-letter:nth-child(5),
|
||||||
|
.btn:focus .btn-letter:nth-child(5) { animation-delay: 0.32s; }
|
||||||
|
.btn-letter:nth-child(6),
|
||||||
|
.btn:focus .btn-letter:nth-child(6) { animation-delay: 0.4s; }
|
||||||
|
.btn-letter:nth-child(7),
|
||||||
|
.btn:focus .btn-letter:nth-child(7) { animation-delay: 0.48s; }
|
||||||
|
.btn-letter:nth-child(8),
|
||||||
|
.btn:focus .btn-letter:nth-child(8) { animation-delay: 0.56s; }
|
||||||
|
.btn-letter:nth-child(9),
|
||||||
|
.btn:focus .btn-letter:nth-child(9) { animation-delay: 0.64s; }
|
||||||
|
.btn-letter:nth-child(10),
|
||||||
|
.btn:focus .btn-letter:nth-child(10) { animation-delay: 0.72s; }
|
||||||
|
.btn-letter:nth-child(11),
|
||||||
|
.btn:focus .btn-letter:nth-child(11) { animation-delay: 0.8s; }
|
||||||
|
.btn-letter:nth-child(12),
|
||||||
|
.btn:focus .btn-letter:nth-child(12) { animation-delay: 0.88s; }
|
||||||
|
.btn-letter:nth-child(13),
|
||||||
|
.btn:focus .btn-letter:nth-child(13) { animation-delay: 0.96s; }
|
||||||
|
.btn-letter:nth-child(14),
|
||||||
|
.btn:focus .btn-letter:nth-child(14) { animation-delay: 1.04s; }
|
||||||
|
.btn-letter:nth-child(15),
|
||||||
|
.btn:focus .btn-letter:nth-child(15) { animation-delay: 1.12s; }
|
||||||
|
.btn-letter:nth-child(16),
|
||||||
|
.btn:focus .btn-letter:nth-child(16) { animation-delay: 1.2s; }
|
||||||
|
.btn-letter:nth-child(17),
|
||||||
|
.btn:focus .btn-letter:nth-child(17) { animation-delay: 1.28s; }
|
||||||
|
.btn-letter:nth-child(18),
|
||||||
|
.btn:focus .btn-letter:nth-child(18) { animation-delay: 1.36s; }
|
||||||
|
.btn-letter:nth-child(19),
|
||||||
|
.btn:focus .btn-letter:nth-child(19) { animation-delay: 1.44s; }
|
||||||
|
.btn-letter:nth-child(20),
|
||||||
|
.btn:focus .btn-letter:nth-child(20) { animation-delay: 1.52s; }
|
||||||
|
.btn-letter:nth-child(21),
|
||||||
|
.btn:focus .btn-letter:nth-child(21) { animation-delay: 1.6s; }
|
||||||
|
.btn-letter:nth-child(22),
|
||||||
|
.btn:focus .btn-letter:nth-child(22) { animation-delay: 1.68s; }
|
||||||
|
.btn-letter:nth-child(23),
|
||||||
|
.btn:focus .btn-letter:nth-child(23) { animation-delay: 1.76s; }
|
||||||
|
.btn-letter:nth-child(24),
|
||||||
|
.btn:focus .btn-letter:nth-child(24) { animation-delay: 1.84s; }
|
||||||
|
.btn-letter:nth-child(25),
|
||||||
|
.btn:focus .btn-letter:nth-child(25) { animation-delay: 1.92s; }
|
||||||
|
.btn-letter:nth-child(26),
|
||||||
|
.btn:focus .btn-letter:nth-child(26) { animation-delay: 2s; }
|
||||||
|
.btn-letter:nth-child(27),
|
||||||
|
.btn:focus .btn-letter:nth-child(27) { animation-delay: 2.08s; }
|
||||||
|
.btn-letter:nth-child(28),
|
||||||
|
.btn:focus .btn-letter:nth-child(28) { animation-delay: 2.16s; }
|
||||||
|
.btn-letter:nth-child(29),
|
||||||
|
.btn:focus .btn-letter:nth-child(29) { animation-delay: 2.24s; }
|
||||||
|
.btn-letter:nth-child(30),
|
||||||
|
.btn:focus .btn-letter:nth-child(30) { animation-delay: 2.32s; }
|
||||||
|
.btn-letter:nth-child(31),
|
||||||
|
.btn:focus .btn-letter:nth-child(31) { animation-delay: 2.4s; }
|
||||||
|
.btn-letter:nth-child(32),
|
||||||
|
.btn:focus .btn-letter:nth-child(32) { animation-delay: 2.48s; }
|
||||||
|
.btn-letter:nth-child(33),
|
||||||
|
.btn:focus .btn-letter:nth-child(33) { animation-delay: 2.56s; }
|
||||||
|
.btn-letter:nth-child(34),
|
||||||
|
.btn:focus .btn-letter:nth-child(34) { animation-delay: 2.64s; }
|
||||||
|
.btn-letter:nth-child(35),
|
||||||
|
.btn:focus .btn-letter:nth-child(35) { animation-delay: 2.72s; }
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
border: solid 1px hsla(var(--highlight-color-hue), 100%, 80%, 0.7);
|
||||||
|
background-color: hsla(var(--highlight-color-hue), 50%, 20%, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active::before {
|
||||||
|
box-shadow:
|
||||||
|
0 -8px 12px -6px rgba(255, 255, 255, 0.67) inset,
|
||||||
|
0 -16px 16px -8px hsla(var(--highlight-color-hue), 100%, 70%, 0.8) inset,
|
||||||
|
1px 1px 1px rgba(255, 255, 255, 0.27),
|
||||||
|
2px 2px 2px rgba(255, 255, 255, 0.13),
|
||||||
|
-1px -1px 1px rgba(0, 0, 0, 0.13),
|
||||||
|
-2px -2px 2px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active::after {
|
||||||
|
opacity: 1;
|
||||||
|
mask-image: linear-gradient(0deg, #fff, transparent);
|
||||||
|
filter: brightness(200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active .btn-letter {
|
||||||
|
text-shadow: 0 0 1px hsla(var(--highlight-color-hue), 100%, 90%, 0.9);
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
border: solid 1px hsla(var(--highlight-color-hue), 100%, 80%, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::before {
|
||||||
|
box-shadow:
|
||||||
|
0 -8px 8px -6px rgba(255, 255, 255, 0.67) inset,
|
||||||
|
0 -16px 16px -8px hsla(var(--highlight-color-hue), 100%, 70%, 0.3) inset,
|
||||||
|
1px 1px 1px rgba(255, 255, 255, 0.13),
|
||||||
|
2px 2px 2px rgba(255, 255, 255, 0.07),
|
||||||
|
-1px -1px 1px rgba(0, 0, 0, 0.13),
|
||||||
|
-2px -2px 2px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
mask-image: linear-gradient(0deg, #fff, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover .btn-svg {
|
||||||
|
fill: #fff;
|
||||||
|
filter: drop-shadow(0 0 3px hsl(var(--highlight-color-hue), 100%, 70%))
|
||||||
|
drop-shadow(0 -4px 6px rgba(0, 0, 0, 0.6));
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.btn {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5em 0.9em;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.btn-svg,
|
||||||
|
.btn-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared primary button look – for all blue/primary buttons */
|
||||||
|
.btn-primary-style {
|
||||||
|
padding: 0.5em 1.1em;
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0px 1px 1px rgba(255, 255, 255, 0.25),
|
||||||
|
inset 0px 2px 2px rgba(255, 255, 255, 0.2),
|
||||||
|
0px 2px 4px rgba(0, 0, 0, 0.2),
|
||||||
|
0px 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: box-shadow 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
border 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
background-color 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary-style:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
box-shadow:
|
||||||
|
inset 0px 1px 1px rgba(255, 255, 255, 0.3),
|
||||||
|
inset 0px 2px 2px rgba(255, 255, 255, 0.25),
|
||||||
|
0px 2px 4px rgba(0, 0, 0, 0.2),
|
||||||
|
0px 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary (blue) variant – same structure, blue color */
|
||||||
|
.btn-primary {
|
||||||
|
--button-color: hsl(198, 93%, 42%);
|
||||||
|
--highlight-color-hue: 198deg;
|
||||||
|
box-shadow:
|
||||||
|
inset 0px 1px 1px rgba(255, 255, 255, 0.25),
|
||||||
|
inset 0px 2px 2px rgba(255, 255, 255, 0.2),
|
||||||
|
0px 2px 4px rgba(0, 0, 0, 0.2),
|
||||||
|
0px 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
border: solid 1px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary::before {
|
||||||
|
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary .btn-letter {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes letter-anim-primary {
|
||||||
|
0%, 100% {
|
||||||
|
text-shadow: none;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
text-shadow: 0 0 6px rgba(255, 255, 255, 0.95), 0 0 12px rgba(255, 255, 255, 0.4);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary .btn-letter {
|
||||||
|
animation: letter-anim-primary 2.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arrow icon (stroke-based) – same size and animation as .btn-svg */
|
||||||
|
.btn-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: #e8e8e8;
|
||||||
|
animation: flicker 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.6));
|
||||||
|
transition: color var(--transition), filter var(--transition), opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover .btn-icon {
|
||||||
|
color: #fff;
|
||||||
|
filter: drop-shadow(0 0 3px hsl(var(--highlight-color-hue), 100%, 70%))
|
||||||
|
drop-shadow(0 -4px 6px rgba(0, 0, 0, 0.6));
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus .btn-icon {
|
||||||
|
animation-duration: 1.2s;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
/* Stats number */
|
/* Stats number */
|
||||||
.stat-number {
|
.stat-number {
|
||||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||||
@@ -249,6 +733,23 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Count-up with gradient */
|
||||||
|
.count-up-text {
|
||||||
|
background: linear-gradient(135deg, hsl(0 0% 92%) 0%, hsl(0 0% 70%) 50%, hsl(0 0% 92%) 100%);
|
||||||
|
background-size: 200% auto;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .count-up-text {
|
||||||
|
background: linear-gradient(135deg, hsl(0 0% 98%) 0%, hsl(0 0% 75%) 50%, hsl(0 0% 98%) 100%);
|
||||||
|
background-size: 200% auto;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
/* Grid line decoration */
|
/* Grid line decoration */
|
||||||
.grid-lines {
|
.grid-lines {
|
||||||
background-image:
|
background-image:
|
||||||
|
|||||||
14
src/lib/appwrite.ts
Normal file
14
src/lib/appwrite.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Client, Databases } from "appwrite";
|
||||||
|
|
||||||
|
const APPWRITE_ENDPOINT = "https://appwrite.webklar.com/v1";
|
||||||
|
const APPWRITE_PROJECT = "696b82270034001dab69";
|
||||||
|
const DATABASE_ID = "698124a20035e8f6dc42";
|
||||||
|
export const CONTACTS_COLLECTION_ID = "contact_submissions";
|
||||||
|
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(APPWRITE_ENDPOINT)
|
||||||
|
.setProject(APPWRITE_PROJECT);
|
||||||
|
|
||||||
|
export const databases = new Databases(client);
|
||||||
|
|
||||||
|
export { DATABASE_ID };
|
||||||
292
src/pages/AGB.tsx
Normal file
292
src/pages/AGB.tsx
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft, FileText } from "lucide-react";
|
||||||
|
|
||||||
|
const contractDivider = (
|
||||||
|
<div className="my-8 border-t border-border" aria-hidden />
|
||||||
|
);
|
||||||
|
|
||||||
|
const AGB = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="fixed top-0 left-0 right-0 z-50 glass-nav py-4">
|
||||||
|
<div className="container mx-auto px-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link to="/" className="flex items-center gap-2 group">
|
||||||
|
<span className="text-xl font-display font-medium text-foreground tracking-tight">
|
||||||
|
Webklar
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<Link to="/">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Zurück
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="pt-32 pb-24">
|
||||||
|
<div className="container mx-auto px-6">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="mb-12">
|
||||||
|
<div className="label-tag mb-4 flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4" />
|
||||||
|
Vertrag
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-display font-medium text-foreground tracking-tight uppercase mb-2">
|
||||||
|
Kaufvertrag – WEBklar
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-lg mb-8">
|
||||||
|
(Modularer Projektvertrag)
|
||||||
|
</p>
|
||||||
|
<div className="text-muted-foreground space-y-1">
|
||||||
|
<p><strong className="text-foreground">zwischen</strong></p>
|
||||||
|
<p>WEBklar<br /><span className="text-sm">(im Folgenden „Anbieter“)</span></p>
|
||||||
|
<p className="pt-2"><strong className="text-foreground">und</strong></p>
|
||||||
|
<p>Kunde laut Angebot<br /><span className="text-sm">(im Folgenden „Kunde“)</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contract Content */}
|
||||||
|
<article className="space-y-8 text-foreground">
|
||||||
|
{/* 1. Vertragsgegenstand */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
1. Vertragsgegenstand
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen.</li>
|
||||||
|
<li>Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen.</li>
|
||||||
|
<li>Maßgeblich ist das jeweils angenommene Angebot von WEBklar.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 2. Leistungsart */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
2. Leistungsart
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar.</li>
|
||||||
|
<li>Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet.</li>
|
||||||
|
<li>WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 3. Leistungsmodul A – Webseite */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-2">
|
||||||
|
3. Leistungsmodul A – Webseite (einmalig)
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">(aktiv, wenn im Angebot enthalten)</p>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>WEBklar erstellt eine individuelle Webseite gemäß Angebot.</li>
|
||||||
|
<li>Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben.</li>
|
||||||
|
<li>Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden.</li>
|
||||||
|
<li>Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen.</li>
|
||||||
|
<li>Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 4. Leistungsmodul B – Automatisierung / Virtualisierung */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-2">
|
||||||
|
4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig)
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">(aktiv, wenn im Angebot enthalten)</p>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme.</li>
|
||||||
|
<li>Die Leistung stellt eine reine Entwicklungsleistung dar.</li>
|
||||||
|
<li>Optional kann eine Beratungsleistung Bestandteil des Projektes sein.</li>
|
||||||
|
<li>Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart.</li>
|
||||||
|
<li>Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 5. Leistungsmodul C – Hosting */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-2">
|
||||||
|
5. Leistungsmodul C – Hosting (jährlich)
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">(aktiv, wenn im Angebot enthalten)</p>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>WEBklar stellt optional Hosting-Leistungen zur Verfügung.</li>
|
||||||
|
<li>Hostingverträge haben eine jährliche Laufzeit und verlängern sich automatisch, sofern nicht fristgerecht gekündigt wird.</li>
|
||||||
|
<li>WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen.</li>
|
||||||
|
<li>WEBklar übernimmt keine Haftung für Ausfälle externer Anbieter.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 6. Quellcode und Eigentum */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
6. Quellcode und Eigentum
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4 text-muted-foreground">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-foreground font-medium mb-2">6.1 Webseite</h3>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite.</li>
|
||||||
|
<li>Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben.</li>
|
||||||
|
<li>Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-foreground font-medium mb-2">6.2 Apps, Automatisierungen und Backend</h3>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar.</li>
|
||||||
|
<li>Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung.</li>
|
||||||
|
<li>Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 7. Mitwirkungspflichten */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
7. Mitwirkungspflichten des Kunden
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.</li>
|
||||||
|
<li>Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar.</li>
|
||||||
|
<li>WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 8. Abnahme */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
8. Abnahme
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt.</li>
|
||||||
|
<li>Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen.</li>
|
||||||
|
<li>Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 9. Vergütung und Zahlung */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
9. Vergütung und Zahlung
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Die Vergütung ergibt sich aus dem Angebot.</li>
|
||||||
|
<li>Projektleistungen sind nach Vereinbarung fällig.</li>
|
||||||
|
<li>Hosting-Leistungen sind jährlich im Voraus zu zahlen.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 10. Zahlungsverzug */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
10. Zahlungsverzug
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen.</li>
|
||||||
|
<li>Danach ist WEBklar berechtigt:
|
||||||
|
<ul className="list-disc list-inside mt-2 space-y-1 pl-2">
|
||||||
|
<li>Leistungen zu sperren</li>
|
||||||
|
<li>Verzugszinsen zu berechnen</li>
|
||||||
|
<li>den Vertrag außerordentlich zu kündigen</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 11. Haftung */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
11. Haftung
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>WEBklar haftet nur bei Vorsatz und grober Fahrlässigkeit.</li>
|
||||||
|
<li>Keine Haftung für:
|
||||||
|
<ul className="list-disc list-inside mt-2 space-y-1 pl-2">
|
||||||
|
<li>Umsatzausfälle</li>
|
||||||
|
<li>Datenverlust</li>
|
||||||
|
<li>Systemausfälle</li>
|
||||||
|
<li>externe Dienste</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Die Haftung ist der Höhe nach auf den Auftragswert begrenzt.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 12. Kündigung */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
12. Kündigung
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Laufzeitverträge (z. B. Hosting, Wartung) können zum Ende der jeweiligen Laufzeit gekündigt werden.</li>
|
||||||
|
<li>Das Recht zur außerordentlichen Kündigung bleibt unberührt.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 13. Referenzen */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
13. Referenzen
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{contractDivider}
|
||||||
|
|
||||||
|
{/* 14. Schlussbestimmungen */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-4">
|
||||||
|
14. Schlussbestimmungen
|
||||||
|
</h2>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-muted-foreground leading-relaxed [&>li]:pl-2">
|
||||||
|
<li>Es gilt deutsches Recht.</li>
|
||||||
|
<li>Gerichtsstand ist der Sitz von WEBklar, soweit zulässig.</li>
|
||||||
|
<li>Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Back / Contact */}
|
||||||
|
<div className="mt-16 pt-12 border-t border-border flex flex-wrap gap-4">
|
||||||
|
<Link to="/">
|
||||||
|
<Button variant="outline" className="rounded-full">
|
||||||
|
Zur Startseite
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/kontakt">
|
||||||
|
<Button className="btn-minimal rounded-full">
|
||||||
|
Kontakt
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AGB;
|
||||||
@@ -6,6 +6,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ArrowLeft, Send } from "lucide-react";
|
import { ArrowLeft, Send } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { ID, databases, DATABASE_ID, CONTACTS_COLLECTION_ID } from "@/lib/appwrite";
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -30,16 +31,39 @@ const Contact = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
// Simulate form submission
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
const doc: Record<string, string> = {
|
||||||
|
name: formData.name.trim(),
|
||||||
|
email: formData.email.trim(),
|
||||||
|
message: formData.message.trim(),
|
||||||
|
};
|
||||||
|
if (formData.company.trim()) {
|
||||||
|
doc.company = formData.company.trim();
|
||||||
|
}
|
||||||
|
|
||||||
toast({
|
await databases.createDocument(
|
||||||
title: "Nachricht gesendet!",
|
DATABASE_ID,
|
||||||
description: "Wir melden uns innerhalb von 24 Stunden bei Ihnen.",
|
CONTACTS_COLLECTION_ID,
|
||||||
});
|
ID.unique(),
|
||||||
|
doc
|
||||||
|
);
|
||||||
|
|
||||||
setFormData({ name: "", email: "", company: "", message: "" });
|
toast({
|
||||||
setIsSubmitting(false);
|
title: "Nachricht gesendet!",
|
||||||
|
description: "Wir melden uns innerhalb von 24 Stunden bei Ihnen.",
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormData({ name: "", email: "", company: "", message: "" });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fehler beim Senden:", err);
|
||||||
|
toast({
|
||||||
|
title: "Fehler",
|
||||||
|
description: "Die Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut oder kontaktieren Sie uns direkt per E-Mail.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,19 +195,19 @@ const Contact = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="label-tag mb-2">E-Mail</div>
|
<div className="label-tag mb-2">E-Mail</div>
|
||||||
<a
|
<a
|
||||||
href="mailto:hello@webklar.de"
|
href="mailto:support@webklar.com"
|
||||||
className="text-foreground hover:text-muted-foreground transition-colors"
|
className="text-foreground hover:text-muted-foreground transition-colors"
|
||||||
>
|
>
|
||||||
hello@webklar.de
|
support@webklar.com
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="label-tag mb-2">Telefon</div>
|
<div className="label-tag mb-2">Telefon</div>
|
||||||
<a
|
<a
|
||||||
href="tel:+4912345678"
|
href="tel:+491704969375"
|
||||||
className="text-foreground hover:text-muted-foreground transition-colors"
|
className="text-foreground hover:text-muted-foreground transition-colors"
|
||||||
>
|
>
|
||||||
+49 123 456 78
|
0170 4969375
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { componentTagger } from "lovable-tagger";
|
import { componentTagger } from "lovable-tagger";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user