Compare commits
25 Commits
ed2ec6a6a8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edb2ea6f8a | ||
|
|
8fa7c2c078 | ||
|
|
e3269518bd | ||
|
|
95c4cefef1 | ||
|
|
37327cc2c8 | ||
|
|
80c03411ae | ||
| ac40edc468 | |||
| 0986bc8173 | |||
|
|
305de38ff6 | ||
|
|
82668796d6 | ||
| 6ad076473b | |||
|
|
591c0eb4c7 | ||
|
|
2b39e0855f | ||
|
|
8be94e4518 | ||
|
|
f2a3f47e07 | ||
|
|
6af24d28e7 | ||
|
|
217bbdc6a7 | ||
|
|
7e8d40878b | ||
|
|
2dc5401179 | ||
|
|
3b9e35a447 | ||
|
|
a95932cd79 | ||
|
|
01102ef3f7 | ||
| 22d641e4e5 | |||
| 1d4584e5d9 | |||
| 6228945065 |
11
.env.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Appwrite – Kopie auf dem Server als .env anlegen (wird nicht mit Git gepusht!)
|
||||||
|
# Nach dem Klonen/Pullen: npm run setup:env oder cp .env.example .env
|
||||||
|
|
||||||
|
VITE_APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=696b82270034001dab69
|
||||||
|
VITE_APPWRITE_DATABASE_ID=698124a20035e8f6dc42
|
||||||
|
VITE_APPWRITE_CONTACT_COLLECTION_ID=contact_submissions
|
||||||
|
|
||||||
|
# Optional: Supabase (falls später genutzt)
|
||||||
|
# VITE_SUPABASE_URL=
|
||||||
|
# VITE_SUPABASE_ANON_KEY=
|
||||||
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
32
docs/APPWRITE-CORS.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Appwrite CORS / Plattformen einrichten
|
||||||
|
|
||||||
|
Wenn das Kontaktformular mit **403 (Forbidden)** oder **CORS** fehlschlägt, fehlt deine Origin in Appwrite.
|
||||||
|
|
||||||
|
## In der Appwrite Console
|
||||||
|
|
||||||
|
1. Öffne **https://appwrite.webklar.com** (oder deine Appwrite-Installation).
|
||||||
|
2. Wähle dein **Projekt** (Project ID: `696b82270034001dab69`).
|
||||||
|
3. Gehe zu **Settings** (Projekt-Einstellungen).
|
||||||
|
4. Unter **Platforms** (Plattformen) siehst du z. B. eine Web-App mit `http://localhost:8080`.
|
||||||
|
|
||||||
|
## Erlaubte Origins hinzufügen
|
||||||
|
|
||||||
|
Du musst **jede URL**, von der aus die Webseite läuft, als **Plattform** anlegen (oder die bestehende Web-Plattform um weitere Hostnamen erweitern, falls deine Appwrite-Version das unterstützt).
|
||||||
|
|
||||||
|
Typischerweise:
|
||||||
|
|
||||||
|
| Umgebung | URL (Origin) |
|
||||||
|
|----------|----------------|
|
||||||
|
| Lokal / Dev | `http://localhost:8080` |
|
||||||
|
| Server (IP) | `http://100.84.12.11:8080` |
|
||||||
|
| Produktion | `https://webklar.com` (bzw. deine echte Domain) |
|
||||||
|
|
||||||
|
### Vorgehen
|
||||||
|
|
||||||
|
- **Neue Plattform** → **Web App** anlegen:
|
||||||
|
- **Name:** z. B. „Webklar Produktion“
|
||||||
|
- **Hostname:** `webklar.com` (ohne https://)
|
||||||
|
Oder für die IP: `100.84.12.11` (und ggf. Port in den Einstellungen, falls nötig).
|
||||||
|
- Oder in der **bestehenden** Web-App alle benötigten Hostnames eintragen (je nach Appwrite-Version: mehrere Hostnames oder mehrere Web-Apps).
|
||||||
|
|
||||||
|
Nach dem Speichern sind die neuen Origins erlaubt und der CORS-Fehler sollte verschwinden. Seite und Kontaktformular neu laden und testen.
|
||||||
34
docs/DEPLOY-SERVER.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Deployment auf dem Server
|
||||||
|
|
||||||
|
## .env wird nicht mit Git übertragen
|
||||||
|
|
||||||
|
Die Datei **`.env`** steht in `.gitignore` und wird beim `git push` **nicht** mit ins Repository übernommen. Auf dem Server fehlen dadurch die Umgebungsvariablen für Appwrite – die Meldung *"Appwrite ist nicht konfiguriert"* entsteht genau deshalb.
|
||||||
|
|
||||||
|
## Lösung: .env auf dem Server anlegen
|
||||||
|
|
||||||
|
**Im Repo mitgepusht:** `.env.example`, `scripts/setup-env.cjs`, und die Script-Einträge in `package.json` (`setup:env`, `deploy:server`). Die `.env` wird weiterhin **nicht** ins Git aufgenommen.
|
||||||
|
|
||||||
|
**Nach dem ersten Klonen oder nach jedem Pullen auf dem Server:**
|
||||||
|
|
||||||
|
**Option A – ein Befehl (empfohlen):**
|
||||||
|
Legt `.env` aus `.env.example` an (falls noch nicht vorhanden) und baut die App:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run deploy:server
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B – einzeln:**
|
||||||
|
Nur `.env` anlegen (überschreibt keine bestehende `.env`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run setup:env
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C – mit deploy.sh / Container:**
|
||||||
|
Wenn auf dem Server ein `deploy.sh` und Docker/Compose genutzt werden: Nach `git pull` kann `deploy.sh` die `.env` aus `.env.example` erzeugen (falls fehlend) und vor jedem Build eine `.env.local` aus den Container-Umgebungsvariablen schreiben (z. B. alle `VITE_APPWRITE_*`). So kommt die Appwrite-Konfiguration zuverlässig ins Frontend-Build. Ein Neustart von Nginx ist in der Regel nicht nötig; der neue Build liegt in `/out` (oder dem konfigurierten Ausgabeordner).
|
||||||
|
|
||||||
|
**Option D – manuell:**
|
||||||
|
`.env` selbst anlegen mit dem Inhalt von `.env.example` oder den Werten aus dieser Doku, danach `npm run build`.
|
||||||
|
|
||||||
|
**.env nicht committen** – sie bleibt nur lokal bzw. auf dem Server und enthält projektspezifische Werte.
|
||||||
36
docs/PROMPT-SERVER-SETUP.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Prompt für KI mit Server-Zugriff (automatisches .env + Build)
|
||||||
|
|
||||||
|
Kopiere den folgenden Block und gib ihn einer KI, die Zugriff auf deinen Server hat (z. B. Cursor Agent, SSH-Terminal, Deployment-KI).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt (zum Kopieren)
|
||||||
|
|
||||||
|
```
|
||||||
|
Kontext: Das Projekt ist eine Vite/React-Webapp (Webklar). Die Datei .env wird aus Sicherheitsgründen nicht mit Git mitgepusht. Auf dem Server fehlt sie deshalb nach git pull – dadurch erscheint die Meldung "Appwrite ist nicht konfiguriert".
|
||||||
|
|
||||||
|
Aufgabe:
|
||||||
|
1. In das Projektverzeichnis der Webklar-App wechseln (dort wo package.json und .env.example liegen).
|
||||||
|
2. .env aus der Vorlage anlegen, falls sie noch nicht existiert:
|
||||||
|
- Befehl ausführen: npm run setup:env
|
||||||
|
- (Das Skript kopiert .env.example nach .env und überschreibt keine bestehende .env.)
|
||||||
|
3. Die App neu bauen, damit die Umgebungsvariablen ins Frontend kommen:
|
||||||
|
- Befehl ausführen: npm run build
|
||||||
|
4. Optional: Den Webserver/Prozess neu starten (z. B. PM2, systemd, nginx), der die gebauten Dateien aus dist/ ausliefert oder die App betreibt.
|
||||||
|
|
||||||
|
Alternativ kannst du in einem Schritt beides machen: npm run deploy:server (legt .env an, falls fehlend, und führt danach vite build aus).
|
||||||
|
|
||||||
|
Wichtig: Im Projektroot ausführen, also dort wo package.json und scripts/setup-env.js liegen.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kurzversion (nur Befehle)
|
||||||
|
|
||||||
|
Wenn die KI schon im richtigen Projektordner ist:
|
||||||
|
|
||||||
|
```
|
||||||
|
Führe nacheinander aus:
|
||||||
|
1. npm run deploy:server
|
||||||
|
2. [Webserver/App neu starten, falls nötig – z. B. pm2 restart webklar oder systemctl restart dein-service]
|
||||||
|
```
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
2911
package-lock.json
generated
14
package.json
@@ -6,6 +6,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"postinstall": "patch-package",
|
||||||
|
"setup:env": "node scripts/setup-env.cjs",
|
||||||
|
"deploy:server": "npm run setup:env && vite build",
|
||||||
"build:dev": "vite build --mode development",
|
"build:dev": "vite build --mode development",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
@@ -41,7 +44,10 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
|
"@react-three/fiber": "^8.18.0",
|
||||||
|
"@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",
|
||||||
@@ -49,7 +55,9 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
|
"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",
|
||||||
@@ -60,18 +68,19 @@
|
|||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"three": "^0.182.0",
|
||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.32.0",
|
"@eslint/js": "^9.32.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@testing-library/jest-dom": "^6.6.0",
|
"@testing-library/jest-dom": "^6.6.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"@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",
|
||||||
@@ -79,6 +88,7 @@
|
|||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^20.0.3",
|
"jsdom": "^20.0.3",
|
||||||
"lovable-tagger": "^1.1.13",
|
"lovable-tagger": "^1.1.13",
|
||||||
|
"patch-package": "^8.0.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
|
|||||||
13
patches/appwrite+22.0.0.patch
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/node_modules/appwrite/dist/esm/sdk.js b/node_modules/appwrite/dist/esm/sdk.js
|
||||||
|
index b1fcf3b..4ba8d80 100644
|
||||||
|
--- a/node_modules/appwrite/dist/esm/sdk.js
|
||||||
|
+++ b/node_modules/appwrite/dist/esm/sdk.js
|
||||||
|
@@ -466,7 +466,7 @@ const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true });
|
||||||
|
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
||||||
|
const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
||||||
|
function reviver(_key, value) {
|
||||||
|
- if (BigNumber.isBigNumber(value)) {
|
||||||
|
+ if (value && value._isBigNumber === true) {
|
||||||
|
if (value.isInteger()) {
|
||||||
|
const str = value.toFixed();
|
||||||
|
const bi = BigInt(str);
|
||||||
31
patches/json-bigint+1.0.0.patch
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
diff --git a/node_modules/json-bigint/lib/parse.js b/node_modules/json-bigint/lib/parse.js
|
||||||
|
index bb4e5eb..0ea96e8 100644
|
||||||
|
--- a/node_modules/json-bigint/lib/parse.js
|
||||||
|
+++ b/node_modules/json-bigint/lib/parse.js
|
||||||
|
@@ -206,6 +206,7 @@ var json_parse = function (options) {
|
||||||
|
error('Bad number');
|
||||||
|
} else {
|
||||||
|
if (BigNumber == null) BigNumber = require('bignumber.js');
|
||||||
|
+ if (typeof BigNumber === 'object' && BigNumber && typeof BigNumber.default === 'function') BigNumber = BigNumber.default;
|
||||||
|
//if (number > 9007199254740992 || number < -9007199254740992)
|
||||||
|
// Bignumber has stricter check: everything with length > 15 digits disallowed
|
||||||
|
if (string.length > 15)
|
||||||
|
diff --git a/node_modules/json-bigint/lib/stringify.js b/node_modules/json-bigint/lib/stringify.js
|
||||||
|
index 3bd5269..ab80d3e 100644
|
||||||
|
--- a/node_modules/json-bigint/lib/stringify.js
|
||||||
|
+++ b/node_modules/json-bigint/lib/stringify.js
|
||||||
|
@@ -1,4 +1,5 @@
|
||||||
|
var BigNumber = require('bignumber.js');
|
||||||
|
+if (typeof BigNumber === 'object' && BigNumber && typeof BigNumber.default === 'function') BigNumber = BigNumber.default;
|
||||||
|
|
||||||
|
/*
|
||||||
|
json2.js
|
||||||
|
@@ -215,7 +216,7 @@ var JSON = module.exports;
|
||||||
|
mind = gap,
|
||||||
|
partial,
|
||||||
|
value = holder[key],
|
||||||
|
- isBigNumber = value != null && (value instanceof BigNumber || BigNumber.isBigNumber(value));
|
||||||
|
+ isBigNumber = value != null && (value instanceof BigNumber || (value && value._isBigNumber === true));
|
||||||
|
|
||||||
|
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||||
|
|
||||||
BIN
public/backgroud_effect.png
Normal file
|
After Width: | Height: | Size: 974 KiB |
BIN
public/backgroud_effect1.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
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/loesung.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
public/problem.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/problem_blitz.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
public/project pics/emailsorter.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
25
scripts/setup-env.cjs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Legt .env aus .env.example an, falls .env noch nicht existiert.
|
||||||
|
* .cjs damit es unter "type": "module" (package.json) mit require() läuft.
|
||||||
|
* Für Deployment: deploy.sh kann vor dem Build .env.local aus Container-Env schreiben.
|
||||||
|
*/
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const envPath = path.join(process.cwd(), ".env");
|
||||||
|
const examplePath = path.join(process.cwd(), ".env.example");
|
||||||
|
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
console.log(".env existiert bereits – nichts geändert.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(examplePath)) {
|
||||||
|
console.error(".env.example nicht gefunden. Bitte manuell .env anlegen.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(examplePath, envPath);
|
||||||
|
console.log(".env wurde aus .env.example erstellt. Bitte danach 'npm run build' ausführen.");
|
||||||
|
process.exit(0);
|
||||||
43
scripts/vite-json-bigint-bignumber.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Behebt "Right-hand side of 'instanceof' is not callable" bei json-bigint.
|
||||||
|
* Beim Vite-Build liefert require('bignumber.js') bei ESM-Alias das Modul-Objekt
|
||||||
|
* { default: BigNumber }; json-bigint nutzt aber "value instanceof BigNumber".
|
||||||
|
* Wir setzen BigNumber auf den echten Konstruktor (default-Export), falls vorhanden.
|
||||||
|
*/
|
||||||
|
const DEFAULT_EXTRACT =
|
||||||
|
"\nif (typeof BigNumber === 'object' && BigNumber && typeof BigNumber.default === 'function') BigNumber = BigNumber.default;";
|
||||||
|
|
||||||
|
export function jsonBigintBigNumberFix() {
|
||||||
|
return {
|
||||||
|
name: "json-bigint-bignumber-fix",
|
||||||
|
enforce: "pre",
|
||||||
|
transform(src, id) {
|
||||||
|
const idNorm = id.replace(/\\/g, "/");
|
||||||
|
if (!idNorm.includes("json-bigint")) return null;
|
||||||
|
const isStringify = idNorm.includes("stringify.js");
|
||||||
|
const isParse = idNorm.includes("parse.js");
|
||||||
|
if (!isStringify && !isParse) return null;
|
||||||
|
|
||||||
|
if (isStringify) {
|
||||||
|
const firstLine = "var BigNumber = require('bignumber.js');";
|
||||||
|
if (!src.includes(firstLine)) return null;
|
||||||
|
return {
|
||||||
|
code: src.replace(firstLine, firstLine + DEFAULT_EXTRACT),
|
||||||
|
map: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isParse) {
|
||||||
|
const requireLine =
|
||||||
|
"if (BigNumber == null) BigNumber = require('bignumber.js');";
|
||||||
|
if (!src.includes(requireLine)) return null;
|
||||||
|
return {
|
||||||
|
code: src.replace(requireLine, requireLine + DEFAULT_EXTRACT),
|
||||||
|
map: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ 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 ImpressumPage from "./pages/Impressum";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -14,10 +16,12 @@ const App = () => (
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Sonner />
|
<Sonner />
|
||||||
<BrowserRouter>
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<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 />} />
|
||||||
|
<Route path="/impressum" element={<ImpressumPage />} />
|
||||||
{/* 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
@@ -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,105 +1,98 @@
|
|||||||
const Footer = () => {
|
import React from 'react';
|
||||||
const currentYear = new Date().getFullYear();
|
import Logo from '@/components/Logo';
|
||||||
|
|
||||||
const links = {
|
|
||||||
services: [
|
|
||||||
{ label: "Strategieberatung", href: "#services" },
|
|
||||||
{ label: "UX/UI Design", href: "#services" },
|
|
||||||
{ label: "Entwicklung", href: "#services" },
|
|
||||||
{ label: "SEO & Support", href: "#services" },
|
|
||||||
],
|
|
||||||
company: [
|
|
||||||
{ label: "Über uns", href: "#about" },
|
|
||||||
{ label: "Projekte", href: "#projects" },
|
|
||||||
{ label: "Ablauf", href: "#process" },
|
|
||||||
{ label: "Kontakt", href: "#contact" },
|
|
||||||
],
|
|
||||||
legal: [
|
|
||||||
{ label: "Impressum", href: "/impressum" },
|
|
||||||
{ label: "Datenschutz", href: "/datenschutz" },
|
|
||||||
{ label: "AGB", href: "/agb" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const DevStudio: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-secondary/20 border-t border-border relative">
|
<p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
|
||||||
<div className="container mx-auto px-6 py-16">
|
WEBklar
|
||||||
<div className="grid md:grid-cols-4 gap-12 mb-16">
|
|
||||||
{/* Logo & Description */}
|
|
||||||
<div className="md:col-span-1">
|
|
||||||
<span className="text-xl font-display font-medium text-foreground tracking-tight mb-6 block">
|
|
||||||
webklar
|
|
||||||
</span>
|
|
||||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
|
||||||
Maßgeschneiderte Weblösungen für Ihr Unternehmen. Sicher, zuverlässig und modern.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Services */}
|
|
||||||
<div>
|
|
||||||
<h4 className="label-tag mb-6">Leistungen</h4>
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{links.services.map((link) => (
|
|
||||||
<li key={link.label}>
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Company */}
|
|
||||||
<div>
|
|
||||||
<h4 className="label-tag mb-6">Unternehmen</h4>
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{links.company.map((link) => (
|
|
||||||
<li key={link.label}>
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Legal */}
|
|
||||||
<div>
|
|
||||||
<h4 className="label-tag mb-6">Rechtliches</h4>
|
|
||||||
<ul className="space-y-3">
|
|
||||||
{links.legal.map((link) => (
|
|
||||||
<li key={link.label}>
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Bar */}
|
|
||||||
<div className="divider mb-8" />
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
© {currentYear} webklar. Alle Rechte vorbehalten.
|
|
||||||
</p>
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
Made in Germany
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Footer: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden border-t border-white/[0.1] bg-black px-8 py-20 dark:border-white/[0.1] dark:bg-black">
|
||||||
|
<div className="mx-auto flex max-w-7xl flex-col items-start justify-between text-sm text-neutral-500 sm:flex-row md:px-8">
|
||||||
|
<div>
|
||||||
|
<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="/">
|
||||||
|
<Logo width={30} height={30} />
|
||||||
|
<span className="font-medium text-white">WEBklar</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 ml-2">© copyright WEBklar 2024. All rights reserved.</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 grid grid-cols-2 items-start gap-10 sm:mt-0 md:mt-0 lg:grid-cols-4">
|
||||||
|
<div className="flex w-full flex-col justify-center space-y-4">
|
||||||
|
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Pages</p>
|
||||||
|
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">All Products</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Studio</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Clients</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Pricing</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Blog</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center space-y-4">
|
||||||
|
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Socials</p>
|
||||||
|
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Facebook</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Instagram</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Twitter</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">LinkedIn</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center space-y-4">
|
||||||
|
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Legal</p>
|
||||||
|
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/agb">AGBs</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/impressum">Impressum</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center space-y-4">
|
||||||
|
<p className="hover:text-neutral-300 font-bold text-neutral-600 transition-colors dark:text-neutral-300">Register</p>
|
||||||
|
<ul className="hover:text-neutral-300 list-none space-y-4 text-neutral-600 transition-colors dark:text-neutral-300">
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Sign Up</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Login</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-none">
|
||||||
|
<a className="hover:text-neutral-300 transition-colors" href="/products">Forgot Password</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="inset-x-0 mt-20 bg-gradient-to-b from-black via-neutral-950 to-neutral-900 bg-clip-text text-center text-5xl font-bold text-transparent md:text-9xl lg:text-[12rem] xl:text-[13rem]">
|
||||||
|
WEBklar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DevStudio, Footer };
|
||||||
export default Footer;
|
export default Footer;
|
||||||
|
|||||||
@@ -1,104 +1,100 @@
|
|||||||
import { useState, useEffect } from "react";
|
"use client";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Menu, X } from "lucide-react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import {
|
||||||
|
Navbar,
|
||||||
|
NavBody,
|
||||||
|
NavItems,
|
||||||
|
MobileNav,
|
||||||
|
NavbarLogo,
|
||||||
|
NavbarButton,
|
||||||
|
MobileNavHeader,
|
||||||
|
MobileNavToggle,
|
||||||
|
MobileNavMenu,
|
||||||
|
} from "@/components/ui/resizable-navbar";
|
||||||
|
import Logo from "@/components/Logo";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const navItems = [
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
{ name: "Über uns", link: "#about" },
|
||||||
|
{ name: "Leistungen", link: "#services" },
|
||||||
useEffect(() => {
|
{ name: "Projekte", link: "#projects" },
|
||||||
const handleScroll = () => {
|
{ name: "Ablauf", link: "#process" },
|
||||||
setIsScrolled(window.scrollY > 20);
|
|
||||||
};
|
|
||||||
window.addEventListener("scroll", handleScroll);
|
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const navLinks = [
|
|
||||||
{ href: "#about", label: "Über uns" },
|
|
||||||
{ href: "#services", label: "Leistungen" },
|
|
||||||
{ href: "#projects", label: "Projekte" },
|
|
||||||
{ href: "#process", label: "Ablauf" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<div className="relative w-full">
|
||||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
|
<Navbar>
|
||||||
isScrolled
|
|
||||||
? "glass-nav py-4"
|
|
||||||
: "bg-transparent py-6"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="container mx-auto px-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{/* Logo */}
|
|
||||||
<a href="#" className="flex items-center gap-2 group">
|
|
||||||
<span className="text-xl font-display font-medium text-foreground tracking-tight">Webklar</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden md:flex items-center gap-10">
|
<NavBody>
|
||||||
{navLinks.map((link) => (
|
<NavbarLogo href="#">
|
||||||
<a
|
<Logo width={30} height={30} />
|
||||||
key={link.href}
|
<span className="font-display text-lg font-medium tracking-tight">
|
||||||
href={link.href}
|
Webklar
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors duration-300 text-sm font-medium uppercase tracking-wider"
|
</span>
|
||||||
>
|
</NavbarLogo>
|
||||||
{link.label}
|
<NavItems items={navItems} />
|
||||||
</a>
|
<div className="navbar-actions flex items-center gap-4">
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<div className="hidden md:block">
|
|
||||||
<Link to="/kontakt">
|
<Link to="/kontakt">
|
||||||
<Button
|
<NavbarButton
|
||||||
className="btn-minimal rounded-full px-6 py-5 text-sm font-medium"
|
as="span"
|
||||||
|
variant="dark"
|
||||||
>
|
>
|
||||||
Kontakt
|
Kontakt
|
||||||
</Button>
|
</NavbarButton>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</NavBody>
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Navigation */}
|
||||||
<button
|
<MobileNav>
|
||||||
className="md:hidden p-2 text-foreground hover:text-muted-foreground transition-colors"
|
<MobileNavHeader>
|
||||||
|
<NavbarLogo href="#">
|
||||||
|
<Logo width={30} height={30} />
|
||||||
|
<span className="font-display text-lg font-medium tracking-tight">
|
||||||
|
Webklar
|
||||||
|
</span>
|
||||||
|
</NavbarLogo>
|
||||||
|
<MobileNavToggle
|
||||||
|
isOpen={isMobileMenuOpen}
|
||||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
aria-label="Toggle menu"
|
/>
|
||||||
>
|
</MobileNavHeader>
|
||||||
{isMobileMenuOpen ? (
|
|
||||||
<X className="w-6 h-6" />
|
|
||||||
) : (
|
|
||||||
<Menu className="w-6 h-6" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
<MobileNavMenu
|
||||||
{isMobileMenuOpen && (
|
isOpen={isMobileMenuOpen}
|
||||||
<div className="md:hidden absolute top-full left-0 right-0 bg-background/98 backdrop-blur-xl border-b border-border p-6 animate-fade-in">
|
onClose={() => setIsMobileMenuOpen(false)}
|
||||||
<nav className="flex flex-col gap-6">
|
|
||||||
{navLinks.map((link) => (
|
|
||||||
<a
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
className="text-foreground hover:text-muted-foreground transition-colors text-lg font-medium uppercase tracking-wider"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
>
|
||||||
{link.label}
|
{navItems.map((item, idx) => (
|
||||||
|
<a
|
||||||
|
key={`mobile-link-${idx}`}
|
||||||
|
href={item.link}
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="relative text-neutral-600 dark:text-neutral-300"
|
||||||
|
>
|
||||||
|
<span className="block font-medium uppercase tracking-wider">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
<div className="flex w-full flex-col gap-4">
|
||||||
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
|
<Link to="/kontakt" onClick={() => setIsMobileMenuOpen(false)}>
|
||||||
<Button className="btn-minimal rounded-full mt-4 py-5 text-sm font-medium w-full">
|
<NavbarButton
|
||||||
|
as="span"
|
||||||
|
variant="dark"
|
||||||
|
className="block w-full text-center"
|
||||||
|
>
|
||||||
Kontakt
|
Kontakt
|
||||||
</Button>
|
</NavbarButton>
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</MobileNavMenu>
|
||||||
|
</MobileNav>
|
||||||
|
</Navbar>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +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 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 = () => {
|
||||||
@@ -16,28 +83,35 @@ const Hero = () => {
|
|||||||
const totalSeconds = Math.floor(diff / 1000);
|
const totalSeconds = Math.floor(diff / 1000);
|
||||||
const days = Math.floor(totalSeconds / (60 * 60 * 24));
|
const days = Math.floor(totalSeconds / (60 * 60 * 24));
|
||||||
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
|
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
|
||||||
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
|
|
||||||
const seconds = totalSeconds % 60;
|
|
||||||
|
|
||||||
const years = Math.floor(days / 365);
|
const years = Math.floor(days / 365);
|
||||||
const remainingDays = days % 365;
|
const remainingDays = days % 365;
|
||||||
|
|
||||||
if (years > 0) {
|
if (years > 0) {
|
||||||
setCompanyAge(`${years}J ${remainingDays}T ${hours}h ${minutes}m ${seconds}s`);
|
setCompanyAge(`${years}J ${remainingDays}T ${hours}h`);
|
||||||
} else {
|
} else {
|
||||||
setCompanyAge(`${days}T ${hours}h ${minutes}m ${seconds}s`);
|
setCompanyAge(`${days}T ${hours}h`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
calculateAge();
|
calculateAge();
|
||||||
const interval = setInterval(calculateAge, 1000); // Update every second
|
const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown)
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-screen flex flex-col justify-center bg-background overflow-hidden pt-20">
|
<section className="relative min-h-screen flex flex-col justify-center overflow-hidden pt-20">
|
||||||
{/* Subtle grid lines */}
|
{/* Silk animated background */}
|
||||||
<div className="absolute inset-0 grid-lines opacity-30" />
|
<div className="absolute inset-0 z-0 w-full h-full">
|
||||||
|
<Silk
|
||||||
|
speed={3}
|
||||||
|
scale={0.5}
|
||||||
|
color="#6a6a6a"
|
||||||
|
noiseIntensity={4
|
||||||
|
}
|
||||||
|
rotation={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="container mx-auto px-6 relative z-10">
|
<div className="container mx-auto px-6 relative z-10">
|
||||||
<div className="max-w-6xl">
|
<div className="max-w-6xl">
|
||||||
@@ -57,29 +131,55 @@ const Hero = () => {
|
|||||||
Wir digitalisieren, automatisieren und vernetzen Ihre gesamte Firma in einem einzigen System – damit Ihr Unternehmen wachsen kann, ohne dass Sie mehr arbeiten müssen.
|
Wir digitalisieren, automatisieren und vernetzen Ihre gesamte Firma in einem einzigen System – damit Ihr Unternehmen wachsen kann, ohne dass Sie mehr arbeiten müssen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Kurztext */}
|
|
||||||
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mb-8 animate-fade-in" style={{ animationDelay: '0.4s' }}>
|
|
||||||
Die meisten Unternehmen arbeiten mit zu vielen Tools, manuellen Prozessen und ineffizienten Abläufen. Wir ersetzen Chaos durch Struktur und bauen Ihnen eine digitale Infrastruktur, die Zeit spart, Fehler reduziert und Wachstum planbar macht.
|
|
||||||
</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">
|
||||||
|
<DemoButtonLetters text="Wird weitergeleitet..." />
|
||||||
|
</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"
|
||||||
>
|
>
|
||||||
System-Demo anfordern
|
{SPARKLE_SVG}
|
||||||
</Button>
|
<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 */}
|
||||||
@@ -94,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' }}>
|
||||||
@@ -102,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
@@ -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
@@ -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
@@ -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,33 @@ 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">
|
{/* Hintergrundbild: nur Blitze rechts, auf Handy maximale Breite */}
|
||||||
|
<div
|
||||||
|
className="problem-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] z-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/problem_blitz.jpg)",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<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 +66,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` }}
|
||||||
>
|
>
|
||||||
|
|||||||
158
src/components/Silk.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/* eslint-disable react/no-unknown-property */
|
||||||
|
import { Canvas, useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { forwardRef, useRef, useMemo, useLayoutEffect } from "react";
|
||||||
|
import { Color, type Mesh, type ShaderMaterial } from "three";
|
||||||
|
|
||||||
|
const hexToNormalizedRGB = (hex: string): [number, number, number] => {
|
||||||
|
hex = hex.replace("#", "");
|
||||||
|
return [
|
||||||
|
parseInt(hex.slice(0, 2), 16) / 255,
|
||||||
|
parseInt(hex.slice(2, 4), 16) / 255,
|
||||||
|
parseInt(hex.slice(4, 6), 16) / 255,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const vertexShader = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying vec3 vPosition;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vPosition = position;
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fragmentShader = `
|
||||||
|
varying vec2 vUv;
|
||||||
|
varying vec3 vPosition;
|
||||||
|
|
||||||
|
uniform float uTime;
|
||||||
|
uniform vec3 uColor;
|
||||||
|
uniform float uSpeed;
|
||||||
|
uniform float uScale;
|
||||||
|
uniform float uRotation;
|
||||||
|
uniform float uNoiseIntensity;
|
||||||
|
|
||||||
|
const float e = 2.71828182845904523536;
|
||||||
|
|
||||||
|
float noise(vec2 texCoord) {
|
||||||
|
float G = e;
|
||||||
|
vec2 r = (G * sin(G * texCoord));
|
||||||
|
return fract(r.x * r.y * (1.0 + texCoord.x));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 rotateUvs(vec2 uv, float angle) {
|
||||||
|
float c = cos(angle);
|
||||||
|
float s = sin(angle);
|
||||||
|
mat2 rot = mat2(c, -s, s, c);
|
||||||
|
return rot * uv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float rnd = noise(gl_FragCoord.xy);
|
||||||
|
vec2 uv = rotateUvs(vUv * uScale, uRotation);
|
||||||
|
vec2 tex = uv * uScale;
|
||||||
|
float tOffset = uSpeed * uTime;
|
||||||
|
|
||||||
|
tex.y += 0.03 * sin(8.0 * tex.x - tOffset);
|
||||||
|
|
||||||
|
float pattern = 0.6 +
|
||||||
|
0.4 * sin(5.0 * (tex.x + tex.y +
|
||||||
|
cos(3.0 * tex.x + 5.0 * tex.y) +
|
||||||
|
0.02 * tOffset) +
|
||||||
|
sin(20.0 * (tex.x + tex.y - 0.1 * tOffset)));
|
||||||
|
|
||||||
|
vec4 col = vec4(uColor, 1.0) * vec4(pattern) - rnd / 15.0 * uNoiseIntensity;
|
||||||
|
col.a = 1.0;
|
||||||
|
gl_FragColor = col;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type SilkPlaneProps = {
|
||||||
|
uniforms: {
|
||||||
|
uSpeed: { value: number };
|
||||||
|
uScale: { value: number };
|
||||||
|
uNoiseIntensity: { value: number };
|
||||||
|
uColor: { value: Color };
|
||||||
|
uRotation: { value: number };
|
||||||
|
uTime: { value: number };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const SilkPlane = forwardRef<Mesh, SilkPlaneProps>(function SilkPlane(
|
||||||
|
{ uniforms },
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
const { viewport } = useThree();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (ref && typeof ref !== "function" && ref.current) {
|
||||||
|
ref.current.scale.set(viewport.width, viewport.height, 1);
|
||||||
|
}
|
||||||
|
}, [ref, viewport]);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (ref && typeof ref !== "function" && ref.current) {
|
||||||
|
const mat = ref.current.material as ShaderMaterial & {
|
||||||
|
uniforms: { uTime: { value: number } };
|
||||||
|
};
|
||||||
|
if (mat.uniforms?.uTime) mat.uniforms.uTime.value += 0.1 * delta;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh ref={ref}>
|
||||||
|
<planeGeometry args={[1, 1, 1, 1]} />
|
||||||
|
<shaderMaterial
|
||||||
|
uniforms={uniforms}
|
||||||
|
vertexShader={vertexShader}
|
||||||
|
fragmentShader={fragmentShader}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
SilkPlane.displayName = "SilkPlane";
|
||||||
|
|
||||||
|
type SilkProps = {
|
||||||
|
speed?: number;
|
||||||
|
scale?: number;
|
||||||
|
color?: string;
|
||||||
|
noiseIntensity?: number;
|
||||||
|
rotation?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Silk = ({
|
||||||
|
speed = 5,
|
||||||
|
scale = 1,
|
||||||
|
color = "#7B7481",
|
||||||
|
noiseIntensity = 1.5,
|
||||||
|
rotation = 0,
|
||||||
|
}: SilkProps) => {
|
||||||
|
const meshRef = useRef<Mesh>(null);
|
||||||
|
|
||||||
|
const uniforms = useMemo(
|
||||||
|
() => ({
|
||||||
|
uSpeed: { value: speed },
|
||||||
|
uScale: { value: scale },
|
||||||
|
uNoiseIntensity: { value: noiseIntensity },
|
||||||
|
uColor: { value: new Color(...hexToNormalizedRGB(color)) },
|
||||||
|
uRotation: { value: rotation },
|
||||||
|
uTime: { value: 0 },
|
||||||
|
}),
|
||||||
|
[speed, scale, noiseIntensity, color, rotation]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
dpr={[1, 2]}
|
||||||
|
frameloop="always"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
camera={{ position: [0, 0, 5], fov: 75 }}
|
||||||
|
>
|
||||||
|
<SilkPlane ref={meshRef} uniforms={uniforms} />
|
||||||
|
</Canvas>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Silk;
|
||||||
@@ -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,33 @@ 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">
|
{/* Hintergrundbild mittig, auf Handy maximale Breite */}
|
||||||
|
<div
|
||||||
|
className="solution-section-bg absolute inset-0 bg-center bg-no-repeat opacity-[0.3] z-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/loesung.jpg)",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<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 +74,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,16 @@ 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: auf Handy maximale Breite */}
|
||||||
|
<div
|
||||||
|
className="values-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3]"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/backgroud_effect.png)",
|
||||||
|
}}
|
||||||
|
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
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
317
src/components/ui/resizable-navbar.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { IconMenu2, IconX } from "@tabler/icons-react";
|
||||||
|
import {
|
||||||
|
motion,
|
||||||
|
AnimatePresence,
|
||||||
|
useScroll,
|
||||||
|
useMotionValueEvent,
|
||||||
|
} from "motion/react";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Navbar = ({ children, className }: NavbarProps) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const { scrollY } = useScroll({
|
||||||
|
target: ref,
|
||||||
|
offset: ["start start", "end start"],
|
||||||
|
});
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useMotionValueEvent(scrollY, "change", (latest) => {
|
||||||
|
if (latest > 100) {
|
||||||
|
setVisible(true);
|
||||||
|
} else {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("fixed inset-x-0 top-0 z-40 w-full", className)}
|
||||||
|
>
|
||||||
|
{React.Children.map(children, (child) =>
|
||||||
|
React.isValidElement(child)
|
||||||
|
? React.cloneElement(child, { visible } as { visible: boolean })
|
||||||
|
: child
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavBodyProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavBody = ({ children, className, visible }: NavBodyProps) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
backdropFilter: visible ? "blur(10px)" : "none",
|
||||||
|
boxShadow: visible
|
||||||
|
? "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"
|
||||||
|
: "none",
|
||||||
|
width: visible ? "40%" : "100%",
|
||||||
|
y: visible ? 20 : 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 50,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
minWidth: "800px",
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full bg-transparent px-4 py-2 lg:flex",
|
||||||
|
"text-white [&_a]:text-white [&_a:hover]:text-white/90 [&_.navbar-actions_a]:!text-black",
|
||||||
|
visible && "bg-black/90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NavItemsProps {
|
||||||
|
items: NavItem[];
|
||||||
|
className?: string;
|
||||||
|
onItemClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavItems = ({
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
onItemClick,
|
||||||
|
}: NavItemsProps) => {
|
||||||
|
const [hovered, setHovered] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
onMouseLeave={() => setHovered(null)}
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 hidden flex-1 flex-row items-center justify-center space-x-2 text-sm font-medium text-zinc-600 transition duration-200 hover:text-zinc-800 lg:flex lg:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<a
|
||||||
|
onMouseEnter={() => setHovered(idx)}
|
||||||
|
onClick={onItemClick}
|
||||||
|
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
|
||||||
|
key={`link-${idx}`}
|
||||||
|
href={item.link}
|
||||||
|
>
|
||||||
|
{hovered === idx && (
|
||||||
|
<motion.div
|
||||||
|
layoutId="hovered"
|
||||||
|
className="absolute inset-0 h-full w-full rounded-full bg-white/10"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="relative z-20">{item.name}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobileNavProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileNav = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
visible,
|
||||||
|
}: MobileNavProps) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
backdropFilter: visible ? "blur(10px)" : "none",
|
||||||
|
boxShadow: visible
|
||||||
|
? "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"
|
||||||
|
: "none",
|
||||||
|
width: visible ? "90%" : "100%",
|
||||||
|
paddingRight: visible ? "12px" : "0px",
|
||||||
|
paddingLeft: visible ? "12px" : "0px",
|
||||||
|
borderRadius: visible ? "4px" : "2rem",
|
||||||
|
y: visible ? 20 : 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 50,
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
|
||||||
|
"[&>div:first-child]:text-white [&>div:first-child_a]:text-white [&>div:first-child_svg]:text-white",
|
||||||
|
visible && "bg-black/90",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobileNavHeaderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileNavHeader = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: MobileNavHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-full flex-row items-center justify-between",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobileNavMenuProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileNavMenu = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
isOpen,
|
||||||
|
}: MobileNavMenuProps) => {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-x-0 top-16 z-50 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white px-4 py-8 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:bg-neutral-950",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobileNavToggleProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileNavToggle = ({ isOpen, onClick }: MobileNavToggleProps) => {
|
||||||
|
return isOpen ? (
|
||||||
|
<IconX
|
||||||
|
className="h-6 w-6 cursor-pointer text-black dark:text-white"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconMenu2
|
||||||
|
className="h-6 w-6 cursor-pointer text-black dark:text-white"
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavbarLogoProps {
|
||||||
|
href?: string;
|
||||||
|
logoSrc?: string;
|
||||||
|
logoAlt?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavbarLogo = ({
|
||||||
|
href = "#",
|
||||||
|
logoSrc,
|
||||||
|
logoAlt = "Logo",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: NavbarLogoProps) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={cn(
|
||||||
|
"relative z-20 mr-4 flex items-center space-x-2 px-2 py-1 text-sm font-normal text-black dark:text-white",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{logoSrc ? (
|
||||||
|
<img src={logoSrc} alt={logoAlt} width={30} height={30} />
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavbarButtonProps
|
||||||
|
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||||
|
href?: string;
|
||||||
|
as?: "a" | "button";
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
variant?: "primary" | "secondary" | "dark" | "gradient";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavbarButton = ({
|
||||||
|
href,
|
||||||
|
as: Tag = "a",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
variant = "primary",
|
||||||
|
...props
|
||||||
|
}: NavbarButtonProps) => {
|
||||||
|
const baseStyles =
|
||||||
|
"px-4 py-2 rounded-full text-sm font-bold relative cursor-pointer transition duration-200 inline-block text-center";
|
||||||
|
|
||||||
|
const variantStyles = {
|
||||||
|
primary:
|
||||||
|
"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",
|
||||||
|
dark: "btn !text-white",
|
||||||
|
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]",
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentProps =
|
||||||
|
Tag === "a"
|
||||||
|
? { href: href ?? undefined, ...props }
|
||||||
|
: { ...props };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
className={cn(baseStyles, variantStyles[variant], className)}
|
||||||
|
{...(componentProps as React.ComponentProps<typeof Tag>)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
554
src/index.css
@@ -1,23 +1,21 @@
|
|||||||
|
/* 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 */
|
||||||
--background: 0 0% 3%;
|
--background: 0 0% 0%;
|
||||||
--foreground: 0 0% 92%;
|
--foreground: 0 0% 92%;
|
||||||
|
|
||||||
--card: 0 0% 6%;
|
--card: 0 0% 6%;
|
||||||
@@ -141,6 +139,7 @@
|
|||||||
@apply bg-background text-foreground antialiased;
|
@apply bg-background text-foreground antialiased;
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
|
position: relative; /* für Motion useScroll – Scroll-Container braucht nicht-static position */
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
@@ -156,6 +155,67 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
/* Gemeinsamer Hintergrund für Problem- und Lösungs-Sektion */
|
||||||
|
.section-problem-solution {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem-Section Hintergrundbild: auf Handy maximale Breite, sonst 45% */
|
||||||
|
.problem-section-bg {
|
||||||
|
background-size: 45%;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.problem-section-bg {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lösung-Section Hintergrundbild: auf Handy maximale Breite, sonst 45% */
|
||||||
|
.solution-section-bg {
|
||||||
|
background-size: 45%;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.solution-section-bg {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Was Sie bekommen (Values) Hintergrundbild: auf Handy maximale Breite, sonst 45% */
|
||||||
|
.values-section-bg {
|
||||||
|
background-size: 45%;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.values-section-bg {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 +301,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 +764,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:
|
||||||
|
|||||||
52
src/lib/appwrite.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Appwrite-Anbindung für das Kontaktformular.
|
||||||
|
*
|
||||||
|
* In der Appwrite Console anlegen:
|
||||||
|
* 1. Database (z. B. ID: "contacts")
|
||||||
|
* 2. Collection (z. B. ID: "messages") mit String-Attributen: name, email, company, message
|
||||||
|
* 3. Unter "Settings" der Collection: Create-Berechtigung für "Any" aktivieren (öffentliches Formular)
|
||||||
|
* 4. IDs in .env setzen: VITE_APPWRITE_DATABASE_ID, VITE_APPWRITE_CONTACT_COLLECTION_ID
|
||||||
|
*/
|
||||||
|
import { Client, Databases, ID } from "appwrite";
|
||||||
|
|
||||||
|
const CONTACT_DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID ?? "698124a20035e8f6dc42";
|
||||||
|
const CONTACT_COLLECTION_ID = import.meta.env.VITE_APPWRITE_CONTACT_COLLECTION_ID ?? "contact_submissions";
|
||||||
|
|
||||||
|
function getDatabases(): Databases {
|
||||||
|
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT;
|
||||||
|
const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID;
|
||||||
|
if (!endpoint || !projectId) {
|
||||||
|
throw new Error(
|
||||||
|
"Appwrite ist nicht konfiguriert. Bitte VITE_APPWRITE_ENDPOINT und VITE_APPWRITE_PROJECT_ID setzen. " +
|
||||||
|
"Lokal: .env anlegen (z. B. mit „npm run setup:env“) und Build/Dev-Server neu starten. " +
|
||||||
|
"Server: gleiche Variablen in der Build-Umgebung setzen (z. B. in .env vor „npm run build“)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const client = new Client().setEndpoint(endpoint).setProject(projectId);
|
||||||
|
return new Databases(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _databases: Databases | null = null;
|
||||||
|
|
||||||
|
export type ContactFormData = {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
company: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createContactDocument(data: ContactFormData) {
|
||||||
|
if (!_databases) _databases = getDatabases();
|
||||||
|
return _databases.createDocument<ContactFormData>(
|
||||||
|
CONTACT_DATABASE_ID,
|
||||||
|
CONTACT_COLLECTION_ID,
|
||||||
|
ID.unique(),
|
||||||
|
{
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
company: data.company,
|
||||||
|
message: data.message,
|
||||||
|
},
|
||||||
|
[] // Keine Dokument-Permissions; nur Collection-Berechtigung „Create für Any“ wird genutzt
|
||||||
|
);
|
||||||
|
}
|
||||||
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 { createContactDocument } from "@/lib/appwrite";
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -29,17 +30,23 @@ const Contact = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
// Simulate form submission
|
await createContactDocument(formData);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Nachricht gesendet!",
|
title: "Nachricht gesendet!",
|
||||||
description: "Wir melden uns innerhalb von 24 Stunden bei Ihnen.",
|
description: "Wir melden uns innerhalb von 24 Stunden bei Ihnen.",
|
||||||
});
|
});
|
||||||
|
|
||||||
setFormData({ name: "", email: "", company: "", message: "" });
|
setFormData({ name: "", email: "", company: "", message: "" });
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Speichern fehlgeschlagen.";
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Fehler beim Senden",
|
||||||
|
description: message,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,19 +178,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>
|
||||||
|
|||||||
196
src/pages/Impressum.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowLeft, Scale } from "lucide-react";
|
||||||
|
|
||||||
|
const impressumDivider = (
|
||||||
|
<div className="my-8 border-t border-border" aria-hidden />
|
||||||
|
);
|
||||||
|
|
||||||
|
const Impressum = () => {
|
||||||
|
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">
|
||||||
|
<Scale className="w-4 h-4" />
|
||||||
|
Rechtliches
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-display font-medium text-foreground tracking-tight uppercase mb-2">
|
||||||
|
Impressum
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-lg">
|
||||||
|
Angaben gemäß § 5 TMG
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Impressum Content */}
|
||||||
|
<article className="space-y-8 text-foreground">
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
WEBklar GbR
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed whitespace-pre-line">
|
||||||
|
Am Schwimmbad 10<br />
|
||||||
|
67722 Winnweiler<br />
|
||||||
|
Deutschland
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Vertreten durch
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Geschäftsführer:<br />
|
||||||
|
Kenso Gri
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Kontakt
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Telefon: 0176 23726355<br />
|
||||||
|
E-Mail: kenso.gri@gmail.com
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Rechtsform
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Gesellschaft bürgerlichen Rechts (GbR)
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Umsatzsteuer
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">
|
||||||
|
Gemäß § 19 UStG wird keine Umsatzsteuer erhoben (Kleinunternehmerregelung).<br />
|
||||||
|
(Falls das nicht stimmt oder sich ändert, unbedingt sagen.)
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Verantwortlich für den Inhalt nach § 55 Absatz 2 RStV
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed whitespace-pre-line">
|
||||||
|
Kenso Gri<br />
|
||||||
|
Schliertal 21<br />
|
||||||
|
67468 Frankenstein<br />
|
||||||
|
Deutschland
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Haftung für Inhalte
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed space-y-2">
|
||||||
|
Als Diensteanbieter sind wir gemäß § 7 Absatz 1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich.<br />
|
||||||
|
Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Haftung für Links
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed space-y-2">
|
||||||
|
Unsere Webseite enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben.<br />
|
||||||
|
Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen.<br />
|
||||||
|
Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Urheberrecht
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed space-y-2">
|
||||||
|
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht.<br />
|
||||||
|
Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
{impressumDivider}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-display font-medium uppercase tracking-tight text-foreground mb-3">
|
||||||
|
Online-Streitbeilegung
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground leading-relaxed space-y-2">
|
||||||
|
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:<br />
|
||||||
|
<a
|
||||||
|
href="https://ec.europa.eu/consumers/odr"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-foreground underline hover:no-underline"
|
||||||
|
>
|
||||||
|
https://ec.europa.eu/consumers/odr
|
||||||
|
</a>
|
||||||
|
<br /><br />
|
||||||
|
Wir sind nicht verpflichtet und nicht bereit, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
|
||||||
|
</p>
|
||||||
|
</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 Impressum;
|
||||||
11
src/vite-env.d.ts
vendored
@@ -1 +1,12 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APPWRITE_PROJECT_ID: string;
|
||||||
|
readonly VITE_APPWRITE_ENDPOINT: string;
|
||||||
|
readonly VITE_APPWRITE_DATABASE_ID?: string;
|
||||||
|
readonly VITE_APPWRITE_CONTACT_COLLECTION_ID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|||||||
2
starter-for-react/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_APPWRITE_PROJECT_ID="696b82270034001dab69"
|
||||||
|
VITE_APPWRITE_ENDPOINT="https://appwrite.webklar.com/v1"
|
||||||
26
starter-for-react/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
21
starter-for-react/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Appwrite
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
26
starter-for-react/README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# React starter kit with Appwrite
|
||||||
|
|
||||||
|
Kickstart your React development with this ready-to-use starter project integrated with [Appwrite](https://www.appwrite.io)
|
||||||
|
|
||||||
|
## 🚀Getting started
|
||||||
|
|
||||||
|
###
|
||||||
|
Clone the Project
|
||||||
|
Clone this repository to your local machine using Git:
|
||||||
|
|
||||||
|
`git clone https://github.com/appwrite/starter-for-react`
|
||||||
|
|
||||||
|
## 🛠️ Development guid
|
||||||
|
1. **Configure Appwrite**<br/>
|
||||||
|
Navigate to `.env` and update the values to match your Appwrite project credentials.
|
||||||
|
2. **Customize as needed**<br/>
|
||||||
|
Modify the starter kit to suit your app's requirements. Adjust UI, features, or backend
|
||||||
|
integrations as per your needs.
|
||||||
|
3. **Install dependencies**<br/>
|
||||||
|
Run `npm install` to install all dependencies.
|
||||||
|
4. **Run the app**<br/>
|
||||||
|
Start the project by running `npm run dev`.
|
||||||
|
|
||||||
|
## 💡 Additional notes
|
||||||
|
- This starter project is designed to streamline your React development with Appwrite.
|
||||||
|
- Refer to the [Appwrite documentation](https://appwrite.io/docs) for detailed integration guidance.
|
||||||
38
starter-for-react/eslint.config.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...js.configs.recommended.rules,
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react/jsx-no-target-blank': 'off',
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
19
starter-for-react/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Inter:opsz,wght@14..32,100..900&family=Poppins:wght@300;400&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/appwrite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Appwrite + React</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#FAFAFB] font-[Inter] text-sm text-[#56565C]">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5080
starter-for-react/package-lock.json
generated
Normal file
33
starter-for-react/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "react-starter-kit-for-appwrite",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@appwrite.io/pink-icons": "^1.0.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.14",
|
||||||
|
"appwrite": "^21.2.1",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.0.14"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.19.0",
|
||||||
|
"@types/react": "^19.0.8",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
|
"eslint-plugin-react": "^7.37.4",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"vite": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
starter-for-react/public/appwrite.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M24.4429 16.4322V21.9096H10.7519C6.76318 21.9096 3.28044 19.7067 1.4171 16.4322C1.14622 15.9561 0.909137 15.4567 0.710264 14.9383C0.319864 13.9225 0.0744552 12.8325 0 11.6952V10.2143C0.0161646 9.96089 0.0416361 9.70942 0.0749451 9.46095C0.143032 8.95105 0.245898 8.45211 0.381093 7.96711C1.66006 3.36909 5.81877 0 10.7519 0C15.6851 0 19.8433 3.36909 21.1223 7.96711H15.2682C14.3072 6.4683 12.6437 5.4774 10.7519 5.4774C8.86017 5.4774 7.19668 6.4683 6.23562 7.96711C5.9427 8.42274 5.71542 8.92516 5.56651 9.46095C5.43425 9.93599 5.36371 10.4369 5.36371 10.9548C5.36371 12.5248 6.01324 13.94 7.05463 14.9383C8.01961 15.865 9.32061 16.4322 10.7519 16.4322H24.4429Z"
|
||||||
|
fill="#FD366E" />
|
||||||
|
<path
|
||||||
|
d="M24.4429 9.46094V14.9383H14.4492C15.4906 13.94 16.1401 12.5248 16.1401 10.9548C16.1401 10.4369 16.0696 9.93598 15.9373 9.46094H24.4429Z"
|
||||||
|
fill="#FD366E" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1012 B |
6
starter-for-react/public/react.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true" role="img" className="iconify iconify--logos" width="35.93" height="32"
|
||||||
|
preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228">
|
||||||
|
<path fill="#00D8FF"
|
||||||
|
d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
20
starter-for-react/src/App.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
.checker-background::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-image: linear-gradient(#e6e6e690 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, #e6e6e690 1px, transparent 1px);
|
||||||
|
background-size: 3.7em 3.7em;
|
||||||
|
mask-image: radial-gradient(ellipse at 50% 40%, black 0%, transparent 55%);
|
||||||
|
z-index: -1;
|
||||||
|
background-position-x: center;
|
||||||
|
}
|
||||||
311
starter-for-react/src/App.jsx
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
|
import "./App.css";
|
||||||
|
import { client } from "./lib/appwrite";
|
||||||
|
import { AppwriteException } from "appwrite";
|
||||||
|
import AppwriteSvg from "../public/appwrite.svg";
|
||||||
|
import ReactSvg from "../public/react.svg";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [detailHeight, setDetailHeight] = useState(55);
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [status, setStatus] = useState("idle");
|
||||||
|
const [showLogs, setShowLogs] = useState(false);
|
||||||
|
|
||||||
|
const detailsRef = useRef(null);
|
||||||
|
|
||||||
|
const updateHeight = useCallback(() => {
|
||||||
|
if (detailsRef.current) {
|
||||||
|
setDetailHeight(detailsRef.current.clientHeight);
|
||||||
|
}
|
||||||
|
}, [logs, showLogs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateHeight();
|
||||||
|
window.addEventListener("resize", updateHeight);
|
||||||
|
return () => window.removeEventListener("resize", updateHeight);
|
||||||
|
}, [updateHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!detailsRef.current) return;
|
||||||
|
detailsRef.current.addEventListener("toggle", updateHeight);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!detailsRef.current) return;
|
||||||
|
detailsRef.current.removeEventListener("toggle", updateHeight);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function sendPing() {
|
||||||
|
if (status === "loading") return;
|
||||||
|
setStatus("loading");
|
||||||
|
try {
|
||||||
|
const result = await client.ping();
|
||||||
|
const log = {
|
||||||
|
date: new Date(),
|
||||||
|
method: "GET",
|
||||||
|
path: "/v1/ping",
|
||||||
|
status: 200,
|
||||||
|
response: JSON.stringify(result),
|
||||||
|
};
|
||||||
|
setLogs((prevLogs) => [log, ...prevLogs]);
|
||||||
|
setStatus("success");
|
||||||
|
} catch (err) {
|
||||||
|
const log = {
|
||||||
|
date: new Date(),
|
||||||
|
method: "GET",
|
||||||
|
path: "/v1/ping",
|
||||||
|
status: err instanceof AppwriteException ? err.code : 500,
|
||||||
|
response:
|
||||||
|
err instanceof AppwriteException
|
||||||
|
? err.message
|
||||||
|
: "Something went wrong",
|
||||||
|
};
|
||||||
|
setLogs((prevLogs) => [log, ...prevLogs]);
|
||||||
|
setStatus("error");
|
||||||
|
}
|
||||||
|
setShowLogs(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
className="checker-background flex flex-col items-center p-5"
|
||||||
|
style={{ marginBottom: `${detailHeight}px` }}
|
||||||
|
>
|
||||||
|
<div className="mt-25 flex w-full max-w-[40em] items-center justify-center lg:mt-34">
|
||||||
|
<div className="rounded-[25%] border border-[#19191C0A] bg-[#F9F9FA] p-3 shadow-[0px_9.36px_9.36px_0px_hsla(0,0%,0%,0.04)]">
|
||||||
|
<div className="rounded-[25%] border border-[#FAFAFB] bg-white p-5 shadow-[0px_2px_12px_0px_hsla(0,0%,0%,0.03)] lg:p-9">
|
||||||
|
<img
|
||||||
|
alt={"React logo"}
|
||||||
|
src={ReactSvg}
|
||||||
|
className="h-14 w-14"
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`flex w-38 items-center transition-opacity duration-2500 ${status === "success" ? "opacity-100" : "opacity-0"}`}
|
||||||
|
>
|
||||||
|
<div className="to-[rgba(253, 54, 110, 0.15)] h-[1px] flex-1 bg-gradient-to-l from-[#f02e65]"></div>
|
||||||
|
<div className="icon-check flex h-5 w-5 items-center justify-center rounded-full border border-[#FD366E52] bg-[#FD366E14] text-[#FD366E]"></div>
|
||||||
|
<div className="to-[rgba(253, 54, 110, 0.15)] h-[1px] flex-1 bg-gradient-to-r from-[#f02e65]"></div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-[25%] border border-[#19191C0A] bg-[#F9F9FA] p-3 shadow-[0px_9.36px_9.36px_0px_hsla(0,0%,0%,0.04)]">
|
||||||
|
<div className="rounded-[25%] border border-[#FAFAFB] bg-white p-5 shadow-[0px_2px_12px_0px_hsla(0,0%,0%,0.03)] lg:p-9">
|
||||||
|
<img
|
||||||
|
alt={"Appwrite logo"}
|
||||||
|
src={AppwriteSvg}
|
||||||
|
className="h-14 w-14"
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="mt-12 flex h-52 flex-col items-center">
|
||||||
|
{status === "loading" ? (
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<div role="status">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-5 w-5 animate-spin fill-[#FD366E] text-gray-200 dark:text-gray-600"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<span>Waiting for connection...</span>
|
||||||
|
</div>
|
||||||
|
) : status === "success" ? (
|
||||||
|
<h1 className="font-[Poppins] text-2xl font-light text-[#2D2D31]">
|
||||||
|
Congratulations!
|
||||||
|
</h1>
|
||||||
|
) : (
|
||||||
|
<h1 className="font-[Poppins] text-2xl font-light text-[#2D2D31]">
|
||||||
|
Check connection
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="mt-2 mb-8">
|
||||||
|
{status === "success" ? (
|
||||||
|
<span>You connected your app successfully.</span>
|
||||||
|
) : status === "error" || status === "idle" ? (
|
||||||
|
<span>Send a ping to verify the connection</span>
|
||||||
|
) : null}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={sendPing}
|
||||||
|
className={`cursor-pointer rounded-md bg-[#FD366E] px-2.5 py-1.5 ${status === "loading" ? "hidden" : "visible"}`}
|
||||||
|
>
|
||||||
|
<span className="text-white">Send a ping</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="grid grid-rows-3 gap-7 lg:grid-cols-3 lg:grid-rows-none">
|
||||||
|
<div className="flex h-full w-72 flex-col gap-2 rounded-md border border-[#EDEDF0] bg-white p-4">
|
||||||
|
<h2 className="text-xl font-light text-[#2D2D31]">Edit your app</h2>
|
||||||
|
<p>
|
||||||
|
Edit{" "}
|
||||||
|
<code className="rounded-sm bg-[#EDEDF0] p-1">app/page.js</code> to
|
||||||
|
get started with building your app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://cloud.appwrite.io"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="flex h-full w-72 flex-col gap-2 rounded-md border border-[#EDEDF0] bg-white p-4">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<h2 className="text-xl font-light text-[#2D2D31]">
|
||||||
|
Go to console
|
||||||
|
</h2>
|
||||||
|
<span className="icon-arrow-right text-[#D8D8DB]"></span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Navigate to the console to control and oversee the Appwrite
|
||||||
|
services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://appwrite.io/docs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div className="flex h-full w-72 flex-col gap-2 rounded-md border border-[#EDEDF0] bg-white p-4">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<h2 className="text-xl font-light text-[#2D2D31]">
|
||||||
|
Explore docs
|
||||||
|
</h2>
|
||||||
|
<span className="icon-arrow-right text-[#D8D8DB]"></span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Discover the full power of Appwrite by diving into our
|
||||||
|
documentation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className="fixed bottom-0 flex w-full cursor-pointer border-t border-[#EDEDF0] bg-white">
|
||||||
|
<details open={showLogs} ref={detailsRef} className={"w-full"}>
|
||||||
|
<summary className="flex w-full flex-row justify-between p-4 marker:content-none">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span className="font-semibold">Logs</span>
|
||||||
|
{logs.length > 0 && (
|
||||||
|
<div className="flex items-center rounded-md bg-[#E6E6E6] px-2">
|
||||||
|
<span className="font-semibold">{logs.length}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="icon">
|
||||||
|
<span className="icon-cheveron-down" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
</summary>
|
||||||
|
<div className="flex w-full flex-col lg:flex-row">
|
||||||
|
<div className="flex flex-col border-r border-[#EDEDF0]">
|
||||||
|
<div className="border-y border-[#EDEDF0] bg-[#FAFAFB] px-4 py-2 text-[#97979B]">
|
||||||
|
Project
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 p-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-[#97979B]">Endpoint</span>
|
||||||
|
<span className="truncate">
|
||||||
|
{import.meta.env.VITE_APPWRITE_ENDPOINT}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-[#97979B]">Project-ID</span>
|
||||||
|
<span className="truncate">
|
||||||
|
{import.meta.env.VITE_APPWRITE_PROJECT_ID}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-[#97979B]">Project name</span>
|
||||||
|
<span className="truncate">
|
||||||
|
{import.meta.env.VITE_APPWRITE_PROJECT_NAME}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-y border-[#EDEDF0] bg-[#FAFAFB] text-[#97979B]">
|
||||||
|
{logs.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<td className="w-52 py-2 pl-4">Date</td>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>Method</td>
|
||||||
|
<td className="hidden lg:table-cell">Path</td>
|
||||||
|
<td className="hidden lg:table-cell">Response</td>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<td className="py-2 pl-4">Logs</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{logs.length > 0 ? (
|
||||||
|
logs.map((log) => (
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 pl-4 font-[Fira_Code]">
|
||||||
|
{log.date.toLocaleString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{log.status > 400 ? (
|
||||||
|
<div className="w-fit rounded-sm bg-[#FF453A3D] px-1 text-[#B31212]">
|
||||||
|
{log.status}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-fit rounded-sm bg-[#10B9813D] px-1 text-[#0A714F]">
|
||||||
|
{log.status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>{log.method}</td>
|
||||||
|
<td className="hidden lg:table-cell">{log.path}</td>
|
||||||
|
<td className="hidden font-[Fira_Code] lg:table-cell">
|
||||||
|
{log.response}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td className="py-2 pl-4 font-[Fira_Code]">
|
||||||
|
There are no logs to show
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
10
starter-for-react/src/lib/appwrite.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Client, Account, Databases } from "appwrite";
|
||||||
|
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(import.meta.env.VITE_APPWRITE_ENDPOINT)
|
||||||
|
.setProject(import.meta.env.VITE_APPWRITE_PROJECT_ID);
|
||||||
|
|
||||||
|
const account = new Account(client);
|
||||||
|
const databases = new Databases(client);
|
||||||
|
|
||||||
|
export { client, account, databases };
|
||||||
10
starter-for-react/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import "@appwrite.io/pink-icons";
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
8
starter-for-react/vite.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
})
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
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";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(({ mode }) => ({
|
||||||
|
base: process.env.VITE_BASE_PATH || "/",
|
||||||
server: {
|
server: {
|
||||||
host: "::",
|
host: "::",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
@@ -12,10 +13,18 @@ export default defineConfig(({ mode }) => ({
|
|||||||
overlay: false,
|
overlay: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
|
plugins: [
|
||||||
|
react(),
|
||||||
|
mode === "development" && componentTagger(),
|
||||||
|
].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
// ESM-Variante erzwingen, damit BigNumber.isBigNumber im Build verfügbar ist (Appwrite-SDK)
|
||||||
|
"bignumber.js": path.resolve(__dirname, "node_modules/bignumber.js/bignumber.mjs"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ["bignumber.js"],
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||