Compare commits
55 Commits
538344ca0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 82d2a62b2d | |||
|
|
e3a28b0a1c | ||
| 5da8027b68 | |||
| 2708c04b59 | |||
| f525e79d22 | |||
| 6071561ba4 | |||
| cf05e60251 | |||
| d7432fa65c | |||
| c79e7b59ff | |||
| c9e776ae98 | |||
| 018b751723 | |||
| 45a7704ea6 | |||
| 50c0f33dd7 | |||
| c7c1864fc6 | |||
| 52c9dfb472 | |||
| 5c09c50af9 | |||
| 9ebe223873 | |||
|
|
7c210524a9 | ||
| eb6f5f2200 | |||
| 824ad69bf2 | |||
| b9fd72d150 | |||
| 3dfed67539 | |||
| 438b3dff7b | |||
| 4fc7d332eb | |||
| 53bac4e2b4 | |||
| 5c48def4e1 | |||
| 1a9548cf25 | |||
| d81938060b | |||
| 1a7ebdc3b6 | |||
| 9be9b2987b | |||
| 0b2305a3ad | |||
| cc28a157d4 | |||
| 6b57a2acfc | |||
| 99cd885833 | |||
|
|
673ead2e3a | ||
|
|
a1a3367ce4 | ||
|
|
58b4ad9314 | ||
|
|
3bad23c258 | ||
|
|
3675f573e1 | ||
|
|
c24544a1b7 | ||
|
|
84624e50cf | ||
|
|
91dddc5137 | ||
|
|
30a2537eaa | ||
|
|
b2dbd3e652 | ||
|
|
88b12aeb00 | ||
|
|
4ddf16ed20 | ||
|
|
396477660b | ||
|
|
6e6041bb5b | ||
|
|
eda0ac98d9 | ||
|
|
3fea520317 | ||
|
|
e36628d49d | ||
|
|
3ebcc94349 | ||
|
|
4b79bbcbc9 | ||
|
|
222d885107 | ||
|
|
c99a08d6a3 |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10-1080p/slide-1.jpg
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10-1080p/slide-2.jpg
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10-1080p/slide-3.jpg
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10-1080p/slide-4.jpg
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10-1080p/slide-5.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10-1080p/slide-6.jpg
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10/slide-1.jpg
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10/slide-2.jpg
Normal file
|
After Width: | Height: | Size: 310 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10/slide-3.jpg
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10/slide-4.jpg
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10/slide-5.jpg
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
.codex-tmp/greenlens-tiktok-2026-07-10/slide-6.jpg
Normal file
|
After Width: | Height: | Size: 343 KiB |
@@ -28,3 +28,4 @@ DISCORD_WEBHOOK_DOWNLOADS_URL=
|
||||
TIKTOK_CLIENT_KEY=
|
||||
TIKTOK_CLIENT_SECRET=
|
||||
TIKTOK_REDIRECT_URI=https://greenlenspro.com/api/tiktok/callback
|
||||
TIKTOK_EXPECTED_OPEN_ID=
|
||||
|
||||
27
.gitignore
vendored
@@ -29,6 +29,33 @@ server/data/*.sqlite-*
|
||||
# Expo
|
||||
.expo/
|
||||
|
||||
# Generated social media assets
|
||||
social_out/
|
||||
|
||||
# Local marketing exports and working files
|
||||
marketing/app-store-banner-5screens-2026-07-6.5in/
|
||||
marketing/app-store-banner-5screens-2026-07/
|
||||
marketing/app-store-banner-5screens-continuous/
|
||||
marketing/app-store-banner-5screens-triage/
|
||||
marketing/popup-hhu-flyer-prompts.md
|
||||
output/
|
||||
tmp/
|
||||
ads/
|
||||
.codex-temp/
|
||||
|
||||
# Local/generated exports
|
||||
greenlens-chrome-extension*.zip
|
||||
create_slot02.py
|
||||
assets/blogs/
|
||||
|
||||
# Standalone projects, versioned and published independently
|
||||
greenlens-cli/
|
||||
greenlens-python/
|
||||
greenlens-vscode/
|
||||
greenlens-chrome-extension/
|
||||
homebrew-greenlens/
|
||||
|
||||
# Claude / Agents (symlinks incompatible with EAS Build on Windows)
|
||||
.agents/
|
||||
.claude/
|
||||
.env
|
||||
|
||||
388
.playwright-mcp/page-2026-07-10T08-43-38-727Z.yml
Normal file
@@ -0,0 +1,388 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- navigation [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- link "GreenLens Home" [ref=e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- text: GREENLENS
|
||||
- generic [ref=e6]:
|
||||
- link "Funktionen" [ref=e7] [cursor=pointer]:
|
||||
- /url: "#advantage"
|
||||
- link "Technologie" [ref=e8] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- link "FAQ" [ref=e9] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- link "So funktioniert es" [ref=e10] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- link "Support" [ref=e11] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- group "Language selector" [ref=e13]:
|
||||
- link "DE" [ref=e14] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "EN" [ref=e15] [cursor=pointer]:
|
||||
- /url: /en
|
||||
- link "ES" [ref=e16] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "App laden" [ref=e18] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- main [ref=e19]:
|
||||
- generic [ref=e20]:
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e22]: KI-Pflanzenerkennung
|
||||
- heading "Gieß-Erinnerung App" [level=1] [ref=e23]
|
||||
- paragraph [ref=e24]: Nie wieder Gießen vergessen — Erinnerungen, die zu jeder Pflanze passen.
|
||||
- paragraph [ref=e25]: GreenLens ist eine Gieß-Erinnerung App für iOS und Android. Pflanze scannen, und die App legt automatisch einen Gießplan mit Push-Erinnerung an — abgestimmt auf Art, Standort und Jahreszeit statt auf einen starren Timer.
|
||||
- generic [ref=e26]:
|
||||
- link "App laden" [ref=e27] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- link "So funktioniert es" [ref=e28] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- generic [ref=e29]:
|
||||
- generic [ref=e30]:
|
||||
- generic [ref=e31]: KI-gestützt
|
||||
- generic [ref=e32]: Sofortige Analyse
|
||||
- generic [ref=e33]:
|
||||
- generic [ref=e34]: Ganzheitlich
|
||||
- generic [ref=e35]: Erkennung & Pflegeplan
|
||||
- generic [ref=e37]:
|
||||
- img "Smartphone scannt eine Lotusblüte" [ref=e38]
|
||||
- generic [ref=e41]:
|
||||
- generic [ref=e42]: Indische Lotosblume
|
||||
- generic [ref=e43]: Erkannt in 0,8s
|
||||
- generic [ref=e44]:
|
||||
- generic [ref=e45]:
|
||||
- generic [ref=e46]: — Der Vorteil
|
||||
- heading "Mehr als nur Erkennung. Ein ganzheitlicher Ansatz für Pflanzenpflege." [level=2] [ref=e47]
|
||||
- paragraph [ref=e48]: "Eine Gieß-Erinnerung App erinnert dich daran, deine Pflanzen zu gießen, bevor sie vertrocknen oder überwässert werden. GreenLens erstellt die Erinnerung nicht aus einem festen Intervall, sondern aus dem Scan-Ergebnis: Die erkannte Art bestimmt den Wasserbedarf, Standort und Jahreszeit passen den Rhythmus an, und der Gesundheitscheck greift ein, wenn die Pflanze Stresssignale zeigt."
|
||||
- generic [ref=e49]: "Algorithmen aktualisiert: Juli 2026"
|
||||
- generic [ref=e50]:
|
||||
- heading "Wusstest du schon?" [level=3] [ref=e55]
|
||||
- list [ref=e56]:
|
||||
- listitem [ref=e57]:
|
||||
- strong [ref=e58]: Überwässerung ist Todesursache Nr. 1
|
||||
- text: Die meisten Zimmerpflanzen sterben an Wurzelfäule durch zu häufiges Gießen. GreenLens berechnet präzise Trocknungszeiten.
|
||||
- listitem [ref=e59]:
|
||||
- strong [ref=e60]: Lichtintensität fällt rasant ab
|
||||
- text: Schon 1 Meter vom Fenster entfernt sinkt die Lichtintensität um 50 %. Pflegehinweise berücksichtigen den Standort.
|
||||
- listitem [ref=e61]:
|
||||
- strong [ref=e62]: Pflanzen reinigen die Raumluft
|
||||
- text: Bestimmte Arten können einen Großteil der Schadstoffe aus der Luft filtern. GreenLens hebt luftreinigende Pflanzen hervor.
|
||||
- generic [ref=e64]:
|
||||
- generic [ref=e65]:
|
||||
- generic [ref=e66]: — Die Technologie
|
||||
- heading "Unsere Methodik" [level=2] [ref=e67]
|
||||
- paragraph [ref=e68]: Wie GreenLens aus einem einfachen Handyfoto eine vollständige botanische Analyse macht.
|
||||
- generic [ref=e69]:
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]: "1"
|
||||
- heading "Bildaufnahme" [level=3] [ref=e72]
|
||||
- paragraph [ref=e73]: Die hochauflösende Aufnahme analysiert Blattmorphologie, Blattadern und Stängelstruktur. Unscharfe Bilder werden aussortiert.
|
||||
- generic [ref=e74]:
|
||||
- generic [ref=e75]: "2"
|
||||
- heading "Merkmals-Extraktion" [level=3] [ref=e76]
|
||||
- paragraph [ref=e77]: Das neuronale Netz isoliert über 120 spezifische Merkmale und vergleicht sie mit einer umfangreichen botanischen Datenbank.
|
||||
- generic [ref=e78]:
|
||||
- generic [ref=e79]: "3"
|
||||
- heading "Diagnose" [level=3] [ref=e80]
|
||||
- paragraph [ref=e81]: Ein zweiter Durchlauf sucht gezielt nach Verfärbungen, Schädlingsspuren und Welke — basierend auf typischen Krankheiten der jeweiligen Art.
|
||||
- generic [ref=e82]:
|
||||
- generic [ref=e83]: "4"
|
||||
- heading "Plan-Erstellung" [level=3] [ref=e84]
|
||||
- paragraph [ref=e85]: Aus Erkennung und Gesundheitszustand entsteht ein individueller Pflegeplan, angepasst an Standort und Lichtverhältnisse.
|
||||
- generic [ref=e87]:
|
||||
- generic [ref=e89]:
|
||||
- generic [ref=e90]: — Im Vergleich
|
||||
- heading "GreenLens vs. Timer- und Kalender-Erinnerungen" [level=2] [ref=e91]
|
||||
- paragraph [ref=e92]: "Warum ernsthafte Pflanzenfans auf unsere Plattform statt auf Standard-Erkennungstools setzen: verwertbare Daten statt bloßer Namen."
|
||||
- generic [ref=e93]:
|
||||
- heading "Botanik-Tipp" [level=4] [ref=e94]
|
||||
- paragraph [ref=e95]: Regelmäßige Pflege ist besser als seltene Intensiv-Pflege. Nutze Erinnerungen für eine gute Routine.
|
||||
- link "Pflanzen Pflege App →" [ref=e96] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- generic [ref=e97]:
|
||||
- generic [ref=e98]:
|
||||
- generic [ref=e99]:
|
||||
- heading "Gießerinnerung einrichten" [level=3] [ref=e104]
|
||||
- generic [ref=e105]: GreenLens
|
||||
- paragraph [ref=e106]: Pflanze scannen — der Gießplan mit Erinnerung wird automatisch aus der erkannten Art erstellt.
|
||||
- generic [ref=e107]:
|
||||
- generic [ref=e108]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e109]: Intervall selbst recherchieren und für jede Pflanze manuell eintragen.
|
||||
- generic [ref=e110]:
|
||||
- generic [ref=e111]:
|
||||
- heading "Gießintervall" [level=3] [ref=e116]
|
||||
- generic [ref=e117]: GreenLens
|
||||
- paragraph [ref=e118]: Passt sich an Pflanzenart, Standort, Licht und Jahreszeit an — Sukkulente und Monstera bekommen unterschiedliche Rhythmen.
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e120]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e121]: Festes Intervall (z.B. alle 7 Tage), unabhängig davon, was die Pflanze braucht.
|
||||
- generic [ref=e122]:
|
||||
- generic [ref=e123]:
|
||||
- heading "Mehrere Pflanzen" [level=3] [ref=e128]
|
||||
- generic [ref=e129]: GreenLens
|
||||
- paragraph [ref=e130]: Jede Pflanze hat ein eigenes Profil mit Foto, Notizen und eigener Erinnerung.
|
||||
- generic [ref=e131]:
|
||||
- generic [ref=e132]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e133]: Eine lange Erinnerungsliste ohne Pflanzenkontext — leicht zu verwechseln.
|
||||
- generic [ref=e134]:
|
||||
- generic [ref=e135]:
|
||||
- heading "Überwässerung" [level=3] [ref=e140]
|
||||
- generic [ref=e141]: GreenLens
|
||||
- paragraph [ref=e142]: Gesundheitscheck erkennt Überwässerungssymptome und passt die Empfehlung an, statt stur weiter zu erinnern.
|
||||
- generic [ref=e143]:
|
||||
- generic [ref=e144]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e145]: Der Timer läuft weiter, auch wenn die Pflanze bereits zu nass steht.
|
||||
- generic [ref=e146]:
|
||||
- generic [ref=e147]:
|
||||
- heading "Mehr als Gießen" [level=3] [ref=e152]
|
||||
- generic [ref=e153]: GreenLens
|
||||
- paragraph [ref=e154]: Erinnert auch an Düngen und Umtopfen — abgestimmt auf die erkannte Art.
|
||||
- generic [ref=e155]:
|
||||
- generic [ref=e156]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e157]: Jede weitere Erinnerung muss separat angelegt und gepflegt werden.
|
||||
- generic [ref=e159]:
|
||||
- article [ref=e160]:
|
||||
- generic [ref=e161]: So funktioniert es
|
||||
- heading "Vom Scan zur Gießerinnerung in unter einer Minute." [level=2] [ref=e162]
|
||||
- paragraph [ref=e163]: Statt Gießintervalle zu googeln und manuell einzutragen, scannst du die Pflanze einmal. GreenLens erkennt die Art, legt ein Profil an und erstellt daraus den Gießplan mit Push-Erinnerung — für jede Pflanze einzeln.
|
||||
- list [ref=e164]:
|
||||
- listitem [ref=e165]:
|
||||
- generic [ref=e169]: Pflanze fotografieren — Art und Wasserbedarf werden automatisch erkannt.
|
||||
- listitem [ref=e170]:
|
||||
- generic [ref=e174]: Gießplan pro Pflanze, angepasst an Standort, Licht und Jahreszeit.
|
||||
- listitem [ref=e175]:
|
||||
- generic [ref=e179]: Push-Erinnerung, wenn eine Pflanze dran ist — mit Foto, damit nichts verwechselt wird.
|
||||
- article [ref=e180]:
|
||||
- generic [ref=e181]: Zu viel oder zu wenig
|
||||
- heading "Die Erinnerung, die auch vor Überwässerung schützt." [level=2] [ref=e182]
|
||||
- paragraph [ref=e183]: Die meisten Zimmerpflanzen sterben nicht am Vergessen, sondern am zu häufigen Gießen. Zeigt eine Pflanze gelbe Blätter oder weiche Stiele, prüfst du sie mit dem Gesundheitscheck — GreenLens erkennt Überwässerung und passt die Empfehlung an, statt die nächste Erinnerung zu erzwingen.
|
||||
- article [ref=e184]:
|
||||
- generic [ref=e185]: Urlaub und Alltag
|
||||
- heading "Gießerinnerungen, die deinen Rhythmus mitmachen." [level=2] [ref=e186]
|
||||
- paragraph [ref=e187]: "Ob Urlaubsvorbereitung, Umzug ans andere Fenster oder Winterruhe: Gießintervalle lassen sich pro Pflanze jederzeit anpassen. Die Pflegehistorie zeigt dir, wann zuletzt gegossen wurde — auch wenn mehrere Personen im Haushalt gießen."
|
||||
- generic [ref=e188]:
|
||||
- article [ref=e189]:
|
||||
- generic [ref=e190]: Passt gut
|
||||
- heading "Wähle GreenLens, wenn:" [level=2] [ref=e191]
|
||||
- list [ref=e192]:
|
||||
- listitem [ref=e193]:
|
||||
- generic [ref=e197]: Du vergisst regelmäßig das Gießen und willst Push-Erinnerungen pro Pflanze statt eines Standard-Timers.
|
||||
- listitem [ref=e198]:
|
||||
- generic [ref=e202]: Du hast Pflanzen mit unterschiedlichem Wasserbedarf — Sukkulenten, Zimmerpflanzen, Balkonkästen — und willst sie getrennt verwalten.
|
||||
- listitem [ref=e203]:
|
||||
- generic [ref=e207]: Du willst, dass die Erinnerung sich anpasst, wenn eine Pflanze gestresst aussieht, statt blind weiterzulaufen.
|
||||
- article [ref=e208]:
|
||||
- generic [ref=e209]: Nicht ideal
|
||||
- heading "GreenLens ist nicht die richtige Wahl, wenn:" [level=2] [ref=e210]
|
||||
- list [ref=e211]:
|
||||
- listitem [ref=e212]:
|
||||
- generic [ref=e213]: –
|
||||
- generic [ref=e214]: Du hast nur eine einzige robuste Pflanze und ein Wecker reicht dir völlig.
|
||||
- listitem [ref=e215]:
|
||||
- generic [ref=e216]: –
|
||||
- generic [ref=e217]: Du suchst Bewässerungssteuerung für Sensoren oder automatische Bewässerungssysteme — GreenLens arbeitet mit Erinnerungen, nicht mit Hardware.
|
||||
- generic [ref=e219]:
|
||||
- heading "Häufige Fragen" [level=2] [ref=e221]
|
||||
- generic [ref=e222]:
|
||||
- generic [ref=e223]:
|
||||
- heading "Welche App erinnert mich ans Pflanzen gießen?" [level=3] [ref=e224]
|
||||
- paragraph [ref=e225]: GreenLens erinnert dich per Push-Nachricht ans Gießen — für jede Pflanze einzeln. Nach dem Scan wird der Gießplan automatisch aus der erkannten Art erstellt und an Standort und Jahreszeit angepasst.
|
||||
- generic [ref=e226]:
|
||||
- heading "Ist die Gieß-Erinnerung in GreenLens kostenlos?" [level=3] [ref=e227]
|
||||
- paragraph [ref=e228]: Ja. Pflanzensammlung, Gießpläne und Gieß-Erinnerungen sind in der kostenlosen Version enthalten. Kostenpflichtig sind nur erweiterte KI-Gesundheitschecks und unbegrenzte Scans.
|
||||
- generic [ref=e229]:
|
||||
- heading "Woher weiß die App, wie oft meine Pflanze Wasser braucht?" [level=3] [ref=e230]
|
||||
- paragraph [ref=e231]: Beim Scan erkennt GreenLens die Pflanzenart und leitet daraus den Wasserbedarf ab. Der Gießplan berücksichtigt zusätzlich Standort, Licht und Jahreszeit — eine Sukkulente am Südfenster bekommt einen anderen Rhythmus als ein Farn im Bad.
|
||||
- generic [ref=e232]:
|
||||
- heading "Kann ich für jede Pflanze eine eigene Gießerinnerung einstellen?" [level=3] [ref=e233]
|
||||
- paragraph [ref=e234]: Ja. Jede gespeicherte Pflanze hat ein eigenes Profil mit Foto, Notizen und eigenem Gießintervall. Du kannst jedes Intervall manuell anpassen, wenn du es anders willst als vorgeschlagen.
|
||||
- generic [ref=e235]:
|
||||
- heading "Erinnert die App auch an Düngen und Umtopfen?" [level=3] [ref=e236]
|
||||
- paragraph [ref=e237]: Ja. GreenLens-Pflegepläne umfassen Gießen, Düngen und Umtopfen. Jeder Erinnerungstyp lässt sich pro Pflanze einzeln aktivieren und anpassen.
|
||||
- generic [ref=e238]:
|
||||
- heading "Was passiert, wenn ich eine Gießerinnerung verpasse?" [level=3] [ref=e239]
|
||||
- paragraph [ref=e240]: Die Erinnerung bleibt offen, bis du das Gießen bestätigst — nichts verschiebt sich stillschweigend. Die Pflegehistorie zeigt dir jederzeit, wann jede Pflanze zuletzt gegossen wurde.
|
||||
- generic [ref=e241]:
|
||||
- heading "Hilft die App auch gegen Überwässerung?" [level=3] [ref=e242]
|
||||
- paragraph [ref=e243]: Ja. Überwässerung ist die häufigste Todesursache bei Zimmerpflanzen. Zeigt eine Pflanze weiche Stiele oder gelbe Blätter, erkennt der GreenLens-Gesundheitscheck Überwässerung und empfiehlt eine Gießpause, statt weiter zu erinnern.
|
||||
- generic [ref=e244]:
|
||||
- heading "Funktioniert die Gieß-Erinnerung auch für Balkon- und Gartenpflanzen?" [level=3] [ref=e245]
|
||||
- paragraph [ref=e246]: Ja. GreenLens unterstützt Innen- und Außenpflanzen. Balkonkästen und Gartenpflanzen bekommen eigene Profile mit Intervallen, die zur Saison passen.
|
||||
- generic [ref=e247]:
|
||||
- heading "Brauche ich für die Erinnerungen eine Internetverbindung?" [level=3] [ref=e248]
|
||||
- paragraph [ref=e249]: Nein. Scannen erfordert eine Verbindung, aber deine Pflanzensammlung, Gießpläne und Erinnerungen funktionieren auch offline.
|
||||
- generic [ref=e250]:
|
||||
- generic [ref=e251]:
|
||||
- generic [ref=e252]: Verwandt
|
||||
- heading "Weitere Guides" [level=2] [ref=e253]
|
||||
- generic [ref=e254]:
|
||||
- link [ref=e255] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- heading "Pflanzen Pflege App" [level=3] [ref=e256]
|
||||
- paragraph [ref=e257]: "Der komplette Pflegeplan: Gießen, Düngen, Standort und Gesundheitscheck in einer App."
|
||||
- link [ref=e258] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- heading "Pflanzen bestimmen" [level=3] [ref=e259]
|
||||
- paragraph [ref=e260]: Pflanze per Foto bestimmen — die Grundlage für den automatischen Gießplan.
|
||||
- link [ref=e261] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- heading "Zimmerpflanzen bestimmen" [level=3] [ref=e262]
|
||||
- paragraph [ref=e263]: Zimmerpflanzen erkennen und direkt passende Gießerinnerungen einrichten.
|
||||
- link [ref=e264] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- heading "Pflanzenkrankheiten erkennen" [level=3] [ref=e265]
|
||||
- paragraph [ref=e266]: Gelbe Blätter trotz Gießplan? Symptome per Foto analysieren.
|
||||
- contentinfo [ref=e267]:
|
||||
- generic [ref=e268]:
|
||||
- generic [ref=e269]:
|
||||
- generic [ref=e270]:
|
||||
- generic [ref=e271]: Download
|
||||
- heading "Bereit für bessere Pflanzenpflege?" [level=2] [ref=e272]: Bereit fürbesserePflanzenpflege?
|
||||
- paragraph [ref=e273]: GreenLens hilft dir beim Erkennen, Verstehen und Pflegen deiner Pflanzen. Bei Fragen erreichst du uns über die Support-Seite.
|
||||
- link [ref=e275] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=e276]
|
||||
- paragraph [ref=e277]:
|
||||
- text: Die App ist in den Stores verfügbar.
|
||||
- link "Support" [ref=e278] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- img "GreenLens" [ref=e280]
|
||||
- generic [ref=e282]:
|
||||
- generic [ref=e283]:
|
||||
- heading "GREENLENS" [level=3] [ref=e284]
|
||||
- paragraph [ref=e285]: Die App für Pflanzenfans, die Analyse, Erkennung und Pflege an einem Ort vereint.
|
||||
- generic [ref=e286]:
|
||||
- heading "Produkt" [level=4] [ref=e287]
|
||||
- list [ref=e288]:
|
||||
- listitem [ref=e289]:
|
||||
- link "Funktionen" [ref=e290] [cursor=pointer]:
|
||||
- /url: "#advantage"
|
||||
- listitem [ref=e291]:
|
||||
- link "Technologie" [ref=e292] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- listitem [ref=e293]:
|
||||
- link "App laden" [ref=e294] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- listitem [ref=e295]:
|
||||
- link "Support" [ref=e296] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e297]:
|
||||
- heading "Unternehmen" [level=4] [ref=e298]
|
||||
- list [ref=e299]:
|
||||
- listitem [ref=e300]:
|
||||
- link "So funktioniert es" [ref=e301] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- listitem [ref=e302]:
|
||||
- link "FAQ" [ref=e303] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- listitem [ref=e304]:
|
||||
- link "Support" [ref=e305] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e306]:
|
||||
- heading "Rechtliches" [level=4] [ref=e307]
|
||||
- list [ref=e308]:
|
||||
- listitem [ref=e309]:
|
||||
- link "Impressum" [ref=e310] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- listitem [ref=e311]:
|
||||
- link "Datenschutz" [ref=e312] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- listitem [ref=e313]:
|
||||
- link "Nutzungsbedingungen" [ref=e314] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=e315]:
|
||||
- generic [ref=e316]:
|
||||
- heading "Use Cases" [level=4] [ref=e317]
|
||||
- list [ref=e318]:
|
||||
- listitem [ref=e319]:
|
||||
- link "Best Plant Identification App" [ref=e320] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- listitem [ref=e321]:
|
||||
- link "Plant Identifier App" [ref=e322] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- listitem [ref=e323]:
|
||||
- link "Plant Scanner" [ref=e324] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- listitem [ref=e325]:
|
||||
- link "Flower Scanner" [ref=e326] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- listitem [ref=e327]:
|
||||
- link "Houseplant Identifier" [ref=e328] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- listitem [ref=e329]:
|
||||
- link "Succulent Identifier" [ref=e330] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- listitem [ref=e331]:
|
||||
- link "Identify Plant by Photo" [ref=e332] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- listitem [ref=e333]:
|
||||
- link "Plant Disease Identifier" [ref=e334] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- listitem [ref=e335]:
|
||||
- link "Plant Health App" [ref=e336] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- listitem [ref=e337]:
|
||||
- link "Plant Care App" [ref=e338] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- listitem [ref=e339]:
|
||||
- link "vs PictureThis" [ref=e340] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- listitem [ref=e341]:
|
||||
- link "vs Plantum" [ref=e342] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- listitem [ref=e343]:
|
||||
- link "vs iNaturalist" [ref=e344] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- listitem [ref=e345]:
|
||||
- link "vs Google Lens" [ref=e346] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=e347]:
|
||||
- heading "Pflanzen erkennen" [level=4] [ref=e348]
|
||||
- list [ref=e349]:
|
||||
- listitem [ref=e350]:
|
||||
- link "Pflanzen erkennen kostenlos" [ref=e351] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- listitem [ref=e352]:
|
||||
- link "Pflanzen erkennen App" [ref=e353] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- listitem [ref=e354]:
|
||||
- link "Pflanzen bestimmen per Foto" [ref=e355] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- listitem [ref=e356]:
|
||||
- link "Blumen Scanner kostenlos" [ref=e357] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- listitem [ref=e358]:
|
||||
- link "Zimmerpflanzen bestimmen" [ref=e359] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- listitem [ref=e360]:
|
||||
- link "Pflanzen Pflege App" [ref=e361] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- listitem [ref=e362]:
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=e363] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- listitem [ref=e364]:
|
||||
- link "Gieß-Erinnerung App" [ref=e365] [cursor=pointer]:
|
||||
- /url: /giess-erinnerung-app
|
||||
- generic [ref=e366]:
|
||||
- heading "En español" [level=4] [ref=e367]
|
||||
- list [ref=e368]:
|
||||
- listitem [ref=e369]:
|
||||
- link "Identificador de plantas" [ref=e370] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- listitem [ref=e371]:
|
||||
- link "App para cuidar plantas" [ref=e372] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- listitem [ref=e373]:
|
||||
- link "Diagnosticar enfermedades de plantas" [ref=e374] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- listitem [ref=e375]:
|
||||
- link "Escaner de plantas" [ref=e376] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- listitem [ref=e377]:
|
||||
- link "GreenLens vs Google Lens para plantas" [ref=e378] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=e379]: GREENLENS
|
||||
- generic [ref=e381]:
|
||||
- paragraph [ref=e382]: © 2026 GreenLens. Alle Rechte vorbehalten.
|
||||
- generic [ref=e383]: GREENLENS
|
||||
- alert [ref=e384]
|
||||
372
.playwright-mcp/page-2026-07-10T08-46-57-250Z.yml
Normal file
@@ -0,0 +1,372 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- navigation [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- link "GreenLens Home" [ref=e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- text: GREENLENS
|
||||
- generic [ref=e6]:
|
||||
- link "App laden" [ref=e7] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- button "Open menu" [ref=e8] [cursor=pointer]
|
||||
- main [ref=e11]:
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]:
|
||||
- generic [ref=e14]: KI-Pflanzenerkennung
|
||||
- heading "Gieß-Erinnerung App" [level=1] [ref=e15]
|
||||
- paragraph [ref=e16]: Nie wieder Gießen vergessen — Erinnerungen, die zu jeder Pflanze passen.
|
||||
- paragraph [ref=e17]: GreenLens ist eine Gieß-Erinnerung App für iOS und Android. Pflanze scannen, und die App legt automatisch einen Gießplan mit Push-Erinnerung an — abgestimmt auf Art, Standort und Jahreszeit statt auf einen starren Timer.
|
||||
- generic [ref=e18]:
|
||||
- link "App laden" [ref=e19] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- link "So funktioniert es" [ref=e20] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e22]:
|
||||
- generic [ref=e23]: KI-gestützt
|
||||
- generic [ref=e24]: Sofortige Analyse
|
||||
- generic [ref=e25]:
|
||||
- generic [ref=e26]: Ganzheitlich
|
||||
- generic [ref=e27]: Erkennung & Pflegeplan
|
||||
- generic [ref=e29]:
|
||||
- img "Smartphone scannt eine Lotusblüte" [ref=e30]
|
||||
- generic [ref=e33]:
|
||||
- generic [ref=e34]: Indische Lotosblume
|
||||
- generic [ref=e35]: Erkannt in 0,8s
|
||||
- generic [ref=e36]:
|
||||
- generic [ref=e37]:
|
||||
- generic [ref=e38]: — Der Vorteil
|
||||
- heading "Mehr als nur Erkennung. Ein ganzheitlicher Ansatz für Pflanzenpflege." [level=2] [ref=e39]
|
||||
- paragraph [ref=e40]: "Eine Gieß-Erinnerung App erinnert dich daran, deine Pflanzen zu gießen, bevor sie vertrocknen oder überwässert werden. GreenLens erstellt die Erinnerung nicht aus einem festen Intervall, sondern aus dem Scan-Ergebnis: Die erkannte Art bestimmt den Wasserbedarf, Standort und Jahreszeit passen den Rhythmus an, und der Gesundheitscheck greift ein, wenn die Pflanze Stresssignale zeigt."
|
||||
- generic [ref=e41]: "Algorithmen aktualisiert: Juli 2026"
|
||||
- generic [ref=e42]:
|
||||
- heading "Wusstest du schon?" [level=3] [ref=e47]
|
||||
- list [ref=e48]:
|
||||
- listitem [ref=e49]:
|
||||
- strong [ref=e50]: Überwässerung ist Todesursache Nr. 1
|
||||
- text: Die meisten Zimmerpflanzen sterben an Wurzelfäule durch zu häufiges Gießen. GreenLens berechnet präzise Trocknungszeiten.
|
||||
- listitem [ref=e51]:
|
||||
- strong [ref=e52]: Lichtintensität fällt rasant ab
|
||||
- text: Schon 1 Meter vom Fenster entfernt sinkt die Lichtintensität um 50 %. Pflegehinweise berücksichtigen den Standort.
|
||||
- listitem [ref=e53]:
|
||||
- strong [ref=e54]: Pflanzen reinigen die Raumluft
|
||||
- text: Bestimmte Arten können einen Großteil der Schadstoffe aus der Luft filtern. GreenLens hebt luftreinigende Pflanzen hervor.
|
||||
- generic [ref=e56]:
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: — Die Technologie
|
||||
- heading "Unsere Methodik" [level=2] [ref=e59]
|
||||
- paragraph [ref=e60]: Wie GreenLens aus einem einfachen Handyfoto eine vollständige botanische Analyse macht.
|
||||
- generic [ref=e61]:
|
||||
- generic [ref=e62]:
|
||||
- generic [ref=e63]: "1"
|
||||
- heading "Bildaufnahme" [level=3] [ref=e64]
|
||||
- paragraph [ref=e65]: Die hochauflösende Aufnahme analysiert Blattmorphologie, Blattadern und Stängelstruktur. Unscharfe Bilder werden aussortiert.
|
||||
- generic [ref=e66]:
|
||||
- generic [ref=e67]: "2"
|
||||
- heading "Merkmals-Extraktion" [level=3] [ref=e68]
|
||||
- paragraph [ref=e69]: Das neuronale Netz isoliert über 120 spezifische Merkmale und vergleicht sie mit einer umfangreichen botanischen Datenbank.
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]: "3"
|
||||
- heading "Diagnose" [level=3] [ref=e72]
|
||||
- paragraph [ref=e73]: Ein zweiter Durchlauf sucht gezielt nach Verfärbungen, Schädlingsspuren und Welke — basierend auf typischen Krankheiten der jeweiligen Art.
|
||||
- generic [ref=e74]:
|
||||
- generic [ref=e75]: "4"
|
||||
- heading "Plan-Erstellung" [level=3] [ref=e76]
|
||||
- paragraph [ref=e77]: Aus Erkennung und Gesundheitszustand entsteht ein individueller Pflegeplan, angepasst an Standort und Lichtverhältnisse.
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e81]:
|
||||
- generic [ref=e82]: — Im Vergleich
|
||||
- heading "GreenLens vs. Timer- und Kalender-Erinnerungen" [level=2] [ref=e83]
|
||||
- paragraph [ref=e84]: "Warum ernsthafte Pflanzenfans auf unsere Plattform statt auf Standard-Erkennungstools setzen: verwertbare Daten statt bloßer Namen."
|
||||
- generic [ref=e85]:
|
||||
- heading "Botanik-Tipp" [level=4] [ref=e86]
|
||||
- paragraph [ref=e87]: Regelmäßige Pflege ist besser als seltene Intensiv-Pflege. Nutze Erinnerungen für eine gute Routine.
|
||||
- link "Pflanzen Pflege App →" [ref=e88] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- generic [ref=e89]:
|
||||
- generic [ref=e90]:
|
||||
- generic [ref=e91]:
|
||||
- heading "Gießerinnerung einrichten" [level=3] [ref=e96]
|
||||
- generic [ref=e97]: GreenLens
|
||||
- paragraph [ref=e98]: Pflanze scannen — der Gießplan mit Erinnerung wird automatisch aus der erkannten Art erstellt.
|
||||
- generic [ref=e99]:
|
||||
- generic [ref=e100]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e101]: Intervall selbst recherchieren und für jede Pflanze manuell eintragen.
|
||||
- generic [ref=e102]:
|
||||
- generic [ref=e103]:
|
||||
- heading "Gießintervall" [level=3] [ref=e108]
|
||||
- generic [ref=e109]: GreenLens
|
||||
- paragraph [ref=e110]: Passt sich an Pflanzenart, Standort, Licht und Jahreszeit an — Sukkulente und Monstera bekommen unterschiedliche Rhythmen.
|
||||
- generic [ref=e111]:
|
||||
- generic [ref=e112]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e113]: Festes Intervall (z.B. alle 7 Tage), unabhängig davon, was die Pflanze braucht.
|
||||
- generic [ref=e114]:
|
||||
- generic [ref=e115]:
|
||||
- heading "Mehrere Pflanzen" [level=3] [ref=e120]
|
||||
- generic [ref=e121]: GreenLens
|
||||
- paragraph [ref=e122]: Jede Pflanze hat ein eigenes Profil mit Foto, Notizen und eigener Erinnerung.
|
||||
- generic [ref=e123]:
|
||||
- generic [ref=e124]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e125]: Eine lange Erinnerungsliste ohne Pflanzenkontext — leicht zu verwechseln.
|
||||
- generic [ref=e126]:
|
||||
- generic [ref=e127]:
|
||||
- heading "Überwässerung" [level=3] [ref=e132]
|
||||
- generic [ref=e133]: GreenLens
|
||||
- paragraph [ref=e134]: Gesundheitscheck erkennt Überwässerungssymptome und passt die Empfehlung an, statt stur weiter zu erinnern.
|
||||
- generic [ref=e135]:
|
||||
- generic [ref=e136]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e137]: Der Timer läuft weiter, auch wenn die Pflanze bereits zu nass steht.
|
||||
- generic [ref=e138]:
|
||||
- generic [ref=e139]:
|
||||
- heading "Mehr als Gießen" [level=3] [ref=e144]
|
||||
- generic [ref=e145]: GreenLens
|
||||
- paragraph [ref=e146]: Erinnert auch an Düngen und Umtopfen — abgestimmt auf die erkannte Art.
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]: Timer- und Kalender-Apps
|
||||
- paragraph [ref=e149]: Jede weitere Erinnerung muss separat angelegt und gepflegt werden.
|
||||
- generic [ref=e151]:
|
||||
- article [ref=e152]:
|
||||
- generic [ref=e153]: So funktioniert es
|
||||
- heading "Vom Scan zur Gießerinnerung in unter einer Minute." [level=2] [ref=e154]
|
||||
- paragraph [ref=e155]: Statt Gießintervalle zu googeln und manuell einzutragen, scannst du die Pflanze einmal. GreenLens erkennt die Art, legt ein Profil an und erstellt daraus den Gießplan mit Push-Erinnerung — für jede Pflanze einzeln.
|
||||
- list [ref=e156]:
|
||||
- listitem [ref=e157]:
|
||||
- generic [ref=e161]: Pflanze fotografieren — Art und Wasserbedarf werden automatisch erkannt.
|
||||
- listitem [ref=e162]:
|
||||
- generic [ref=e166]: Gießplan pro Pflanze, angepasst an Standort, Licht und Jahreszeit.
|
||||
- listitem [ref=e167]:
|
||||
- generic [ref=e171]: Push-Erinnerung, wenn eine Pflanze dran ist — mit Foto, damit nichts verwechselt wird.
|
||||
- article [ref=e172]:
|
||||
- generic [ref=e173]: Zu viel oder zu wenig
|
||||
- heading "Die Erinnerung, die auch vor Überwässerung schützt." [level=2] [ref=e174]
|
||||
- paragraph [ref=e175]: Die meisten Zimmerpflanzen sterben nicht am Vergessen, sondern am zu häufigen Gießen. Zeigt eine Pflanze gelbe Blätter oder weiche Stiele, prüfst du sie mit dem Gesundheitscheck — GreenLens erkennt Überwässerung und passt die Empfehlung an, statt die nächste Erinnerung zu erzwingen.
|
||||
- article [ref=e176]:
|
||||
- generic [ref=e177]: Urlaub und Alltag
|
||||
- heading "Gießerinnerungen, die deinen Rhythmus mitmachen." [level=2] [ref=e178]
|
||||
- paragraph [ref=e179]: "Ob Urlaubsvorbereitung, Umzug ans andere Fenster oder Winterruhe: Gießintervalle lassen sich pro Pflanze jederzeit anpassen. Die Pflegehistorie zeigt dir, wann zuletzt gegossen wurde — auch wenn mehrere Personen im Haushalt gießen."
|
||||
- generic [ref=e180]:
|
||||
- article [ref=e181]:
|
||||
- generic [ref=e182]: Passt gut
|
||||
- heading "Wähle GreenLens, wenn:" [level=2] [ref=e183]
|
||||
- list [ref=e184]:
|
||||
- listitem [ref=e185]:
|
||||
- generic [ref=e189]: Du vergisst regelmäßig das Gießen und willst Push-Erinnerungen pro Pflanze statt eines Standard-Timers.
|
||||
- listitem [ref=e190]:
|
||||
- generic [ref=e194]: Du hast Pflanzen mit unterschiedlichem Wasserbedarf — Sukkulenten, Zimmerpflanzen, Balkonkästen — und willst sie getrennt verwalten.
|
||||
- listitem [ref=e195]:
|
||||
- generic [ref=e199]: Du willst, dass die Erinnerung sich anpasst, wenn eine Pflanze gestresst aussieht, statt blind weiterzulaufen.
|
||||
- article [ref=e200]:
|
||||
- generic [ref=e201]: Nicht ideal
|
||||
- heading "GreenLens ist nicht die richtige Wahl, wenn:" [level=2] [ref=e202]
|
||||
- list [ref=e203]:
|
||||
- listitem [ref=e204]:
|
||||
- generic [ref=e205]: –
|
||||
- generic [ref=e206]: Du hast nur eine einzige robuste Pflanze und ein Wecker reicht dir völlig.
|
||||
- listitem [ref=e207]:
|
||||
- generic [ref=e208]: –
|
||||
- generic [ref=e209]: Du suchst Bewässerungssteuerung für Sensoren oder automatische Bewässerungssysteme — GreenLens arbeitet mit Erinnerungen, nicht mit Hardware.
|
||||
- generic [ref=e211]:
|
||||
- heading "Häufige Fragen" [level=2] [ref=e213]
|
||||
- generic [ref=e214]:
|
||||
- generic [ref=e215]:
|
||||
- heading "Welche App erinnert mich ans Pflanzen gießen?" [level=3] [ref=e216]
|
||||
- paragraph [ref=e217]: GreenLens erinnert dich per Push-Nachricht ans Gießen — für jede Pflanze einzeln. Nach dem Scan wird der Gießplan automatisch aus der erkannten Art erstellt und an Standort und Jahreszeit angepasst.
|
||||
- generic [ref=e218]:
|
||||
- heading "Ist die Gieß-Erinnerung in GreenLens kostenlos?" [level=3] [ref=e219]
|
||||
- paragraph [ref=e220]: Ja. Pflanzensammlung, Gießpläne und Gieß-Erinnerungen sind in der kostenlosen Version enthalten. Kostenpflichtig sind nur erweiterte KI-Gesundheitschecks und unbegrenzte Scans.
|
||||
- generic [ref=e221]:
|
||||
- heading "Woher weiß die App, wie oft meine Pflanze Wasser braucht?" [level=3] [ref=e222]
|
||||
- paragraph [ref=e223]: Beim Scan erkennt GreenLens die Pflanzenart und leitet daraus den Wasserbedarf ab. Der Gießplan berücksichtigt zusätzlich Standort, Licht und Jahreszeit — eine Sukkulente am Südfenster bekommt einen anderen Rhythmus als ein Farn im Bad.
|
||||
- generic [ref=e224]:
|
||||
- heading "Kann ich für jede Pflanze eine eigene Gießerinnerung einstellen?" [level=3] [ref=e225]
|
||||
- paragraph [ref=e226]: Ja. Jede gespeicherte Pflanze hat ein eigenes Profil mit Foto, Notizen und eigenem Gießintervall. Du kannst jedes Intervall manuell anpassen, wenn du es anders willst als vorgeschlagen.
|
||||
- generic [ref=e227]:
|
||||
- heading "Erinnert die App auch an Düngen und Umtopfen?" [level=3] [ref=e228]
|
||||
- paragraph [ref=e229]: Ja. GreenLens-Pflegepläne umfassen Gießen, Düngen und Umtopfen. Jeder Erinnerungstyp lässt sich pro Pflanze einzeln aktivieren und anpassen.
|
||||
- generic [ref=e230]:
|
||||
- heading "Was passiert, wenn ich eine Gießerinnerung verpasse?" [level=3] [ref=e231]
|
||||
- paragraph [ref=e232]: Die Erinnerung bleibt offen, bis du das Gießen bestätigst — nichts verschiebt sich stillschweigend. Die Pflegehistorie zeigt dir jederzeit, wann jede Pflanze zuletzt gegossen wurde.
|
||||
- generic [ref=e233]:
|
||||
- heading "Hilft die App auch gegen Überwässerung?" [level=3] [ref=e234]
|
||||
- paragraph [ref=e235]: Ja. Überwässerung ist die häufigste Todesursache bei Zimmerpflanzen. Zeigt eine Pflanze weiche Stiele oder gelbe Blätter, erkennt der GreenLens-Gesundheitscheck Überwässerung und empfiehlt eine Gießpause, statt weiter zu erinnern.
|
||||
- generic [ref=e236]:
|
||||
- heading "Funktioniert die Gieß-Erinnerung auch für Balkon- und Gartenpflanzen?" [level=3] [ref=e237]
|
||||
- paragraph [ref=e238]: Ja. GreenLens unterstützt Innen- und Außenpflanzen. Balkonkästen und Gartenpflanzen bekommen eigene Profile mit Intervallen, die zur Saison passen.
|
||||
- generic [ref=e239]:
|
||||
- heading "Brauche ich für die Erinnerungen eine Internetverbindung?" [level=3] [ref=e240]
|
||||
- paragraph [ref=e241]: Nein. Scannen erfordert eine Verbindung, aber deine Pflanzensammlung, Gießpläne und Erinnerungen funktionieren auch offline.
|
||||
- generic [ref=e242]:
|
||||
- generic [ref=e243]:
|
||||
- generic [ref=e244]: Verwandt
|
||||
- heading "Weitere Guides" [level=2] [ref=e245]
|
||||
- generic [ref=e246]:
|
||||
- link [ref=e247] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- heading "Pflanzen Pflege App" [level=3] [ref=e248]
|
||||
- paragraph [ref=e249]: "Der komplette Pflegeplan: Gießen, Düngen, Standort und Gesundheitscheck in einer App."
|
||||
- link [ref=e250] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- heading "Pflanzen bestimmen" [level=3] [ref=e251]
|
||||
- paragraph [ref=e252]: Pflanze per Foto bestimmen — die Grundlage für den automatischen Gießplan.
|
||||
- link [ref=e253] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- heading "Zimmerpflanzen bestimmen" [level=3] [ref=e254]
|
||||
- paragraph [ref=e255]: Zimmerpflanzen erkennen und direkt passende Gießerinnerungen einrichten.
|
||||
- link [ref=e256] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- heading "Pflanzenkrankheiten erkennen" [level=3] [ref=e257]
|
||||
- paragraph [ref=e258]: Gelbe Blätter trotz Gießplan? Symptome per Foto analysieren.
|
||||
- contentinfo [ref=e259]:
|
||||
- generic [ref=e260]:
|
||||
- generic [ref=e261]:
|
||||
- generic [ref=e262]:
|
||||
- generic [ref=e263]: Download
|
||||
- heading "Bereit für bessere Pflanzenpflege?" [level=2] [ref=e264]: Bereit fürbesserePflanzenpflege?
|
||||
- paragraph [ref=e265]: GreenLens hilft dir beim Erkennen, Verstehen und Pflegen deiner Pflanzen. Bei Fragen erreichst du uns über die Support-Seite.
|
||||
- link [ref=e267] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=e268]
|
||||
- paragraph [ref=e269]:
|
||||
- text: Die App ist in den Stores verfügbar.
|
||||
- link "Support" [ref=e270] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- img "GreenLens" [ref=e272]
|
||||
- generic [ref=e274]:
|
||||
- generic [ref=e275]:
|
||||
- heading "GREENLENS" [level=3] [ref=e276]
|
||||
- paragraph [ref=e277]: Die App für Pflanzenfans, die Analyse, Erkennung und Pflege an einem Ort vereint.
|
||||
- generic [ref=e278]:
|
||||
- heading "Produkt" [level=4] [ref=e279]
|
||||
- list [ref=e280]:
|
||||
- listitem [ref=e281]:
|
||||
- link "Funktionen" [ref=e282] [cursor=pointer]:
|
||||
- /url: "#advantage"
|
||||
- listitem [ref=e283]:
|
||||
- link "Technologie" [ref=e284] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- listitem [ref=e285]:
|
||||
- link "App laden" [ref=e286] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- listitem [ref=e287]:
|
||||
- link "Support" [ref=e288] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e289]:
|
||||
- heading "Unternehmen" [level=4] [ref=e290]
|
||||
- list [ref=e291]:
|
||||
- listitem [ref=e292]:
|
||||
- link "So funktioniert es" [ref=e293] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- listitem [ref=e294]:
|
||||
- link "FAQ" [ref=e295] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- listitem [ref=e296]:
|
||||
- link "Support" [ref=e297] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e298]:
|
||||
- heading "Rechtliches" [level=4] [ref=e299]
|
||||
- list [ref=e300]:
|
||||
- listitem [ref=e301]:
|
||||
- link "Impressum" [ref=e302] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- listitem [ref=e303]:
|
||||
- link "Datenschutz" [ref=e304] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- listitem [ref=e305]:
|
||||
- link "Nutzungsbedingungen" [ref=e306] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=e307]:
|
||||
- generic [ref=e308]:
|
||||
- heading "Use Cases" [level=4] [ref=e309]
|
||||
- list [ref=e310]:
|
||||
- listitem [ref=e311]:
|
||||
- link "Best Plant Identification App" [ref=e312] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- listitem [ref=e313]:
|
||||
- link "Plant Identifier App" [ref=e314] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- listitem [ref=e315]:
|
||||
- link "Plant Scanner" [ref=e316] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- listitem [ref=e317]:
|
||||
- link "Flower Scanner" [ref=e318] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- listitem [ref=e319]:
|
||||
- link "Houseplant Identifier" [ref=e320] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- listitem [ref=e321]:
|
||||
- link "Succulent Identifier" [ref=e322] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- listitem [ref=e323]:
|
||||
- link "Identify Plant by Photo" [ref=e324] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- listitem [ref=e325]:
|
||||
- link "Plant Disease Identifier" [ref=e326] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- listitem [ref=e327]:
|
||||
- link "Plant Health App" [ref=e328] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- listitem [ref=e329]:
|
||||
- link "Plant Care App" [ref=e330] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- listitem [ref=e331]:
|
||||
- link "vs PictureThis" [ref=e332] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- listitem [ref=e333]:
|
||||
- link "vs Plantum" [ref=e334] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- listitem [ref=e335]:
|
||||
- link "vs iNaturalist" [ref=e336] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- listitem [ref=e337]:
|
||||
- link "vs Google Lens" [ref=e338] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=e339]:
|
||||
- heading "Pflanzen erkennen" [level=4] [ref=e340]
|
||||
- list [ref=e341]:
|
||||
- listitem [ref=e342]:
|
||||
- link "Pflanzen erkennen kostenlos" [ref=e343] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- listitem [ref=e344]:
|
||||
- link "Pflanzen erkennen App" [ref=e345] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- listitem [ref=e346]:
|
||||
- link "Pflanzen bestimmen per Foto" [ref=e347] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- listitem [ref=e348]:
|
||||
- link "Blumen Scanner kostenlos" [ref=e349] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- listitem [ref=e350]:
|
||||
- link "Zimmerpflanzen bestimmen" [ref=e351] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- listitem [ref=e352]:
|
||||
- link "Pflanzen Pflege App" [ref=e353] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- listitem [ref=e354]:
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=e355] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- listitem [ref=e356]:
|
||||
- link "Gieß-Erinnerung App" [ref=e357] [cursor=pointer]:
|
||||
- /url: /giess-erinnerung-app
|
||||
- generic [ref=e358]:
|
||||
- heading "En español" [level=4] [ref=e359]
|
||||
- list [ref=e360]:
|
||||
- listitem [ref=e361]:
|
||||
- link "Identificador de plantas" [ref=e362] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- listitem [ref=e363]:
|
||||
- link "App para cuidar plantas" [ref=e364] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- listitem [ref=e365]:
|
||||
- link "Diagnosticar enfermedades de plantas" [ref=e366] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- listitem [ref=e367]:
|
||||
- link "Escaner de plantas" [ref=e368] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- listitem [ref=e369]:
|
||||
- link "GreenLens vs Google Lens para plantas" [ref=e370] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=e371]: GREENLENS
|
||||
- generic [ref=e373]:
|
||||
- paragraph [ref=e374]: © 2026 GreenLens. Alle Rechte vorbehalten.
|
||||
- generic [ref=e375]: GREENLENS
|
||||
- alert [ref=e376]
|
||||
308
.playwright-mcp/page-2026-07-10T08-49-38-325Z.yml
Normal file
@@ -0,0 +1,308 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- navigation [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- link "GREENLENS" [ref=e5] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e6]:
|
||||
- link "Get the App" [ref=e7] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- button "menu" [ref=e8] [cursor=pointer]
|
||||
- main [ref=e10]:
|
||||
- generic [ref=e11]:
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]: AI-POWERED PLANT IDENTIFICATION
|
||||
- heading "Identify plants. Fast & Precise." [level=1] [ref=e14]: Identify plants.Fast & Precise.
|
||||
- paragraph [ref=e15]: GreenLens is your smart pocket companion. Scan any plant for instant identification, get customized care plans and professional diagnostics for visible symptoms. We bring botanical expertise directly to your smartphone.
|
||||
- generic [ref=e16]:
|
||||
- link "Download App" [ref=e17] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- link "So funktioniert es" [ref=e18] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- generic [ref=e19]:
|
||||
- generic [ref=e20]:
|
||||
- generic [ref=e21]: AI-powered
|
||||
- generic [ref=e22]: INSTANT ANALYSIS
|
||||
- generic [ref=e23]:
|
||||
- generic [ref=e24]: Holistic
|
||||
- generic [ref=e25]: IDENTIFICATION & CARE PLANS
|
||||
- generic [ref=e27]:
|
||||
- img "Phone scanning a lotus flower" [ref=e28]
|
||||
- generic [ref=e31]:
|
||||
- generic [ref=e32]: Sacred lotus
|
||||
- generic [ref=e33]: IDENTIFIED IN 0.8s
|
||||
- generic [ref=e34]:
|
||||
- generic [ref=e35]:
|
||||
- generic [ref=e36]: — The Advantage
|
||||
- heading "More than just identification. A holistic approach to plant care." [level=2] [ref=e37]
|
||||
- paragraph [ref=e38]: A basic plant identification app identifies plants in photos and provides species names along with general care tips. GreenLens goes much further, leveraging state-of-the-art image recognition and massive botanical databases.
|
||||
- paragraph [ref=e39]: Each scan automatically generates a customized care plan tailored to your specific climate zone and indoor environment. The app creates precise watering reminders based on species transpiration rates and catalogs your collection. A separate health diagnostic tool analyzes visible symptoms like chlorosis (yellowing), necrosis (browning), or structural decay to suggest immediate, actionable measures.
|
||||
- generic [ref=e40]: "UPDATED ALGORITHMS: APRIL 2024"
|
||||
- generic [ref=e41]:
|
||||
- generic [ref=e42]:
|
||||
- generic [ref=e43]: psychology
|
||||
- heading "Did you know?" [level=3] [ref=e44]
|
||||
- list [ref=e45]:
|
||||
- listitem [ref=e46]:
|
||||
- strong [ref=e47]: "Overwatering is the #1 cause of death"
|
||||
- text: 90% of all houseplants die from root rot due to overwatering. GreenLens calculates precise drying times.
|
||||
- listitem [ref=e48]:
|
||||
- strong [ref=e49]: Light intensity drops quickly
|
||||
- text: Light intensity drops by 50% just 1 meter away from the window. Our light meter ensures the optimal placement.
|
||||
- listitem [ref=e50]:
|
||||
- strong [ref=e51]: Plants purify indoor air
|
||||
- text: Certain species can filter up to 87% of airborne toxins. GreenLens highlights air-purifying plants.
|
||||
- generic [ref=e53]:
|
||||
- generic [ref=e54]:
|
||||
- generic [ref=e55]: — The Technology
|
||||
- heading "Our Methodology" [level=2] [ref=e56]
|
||||
- paragraph [ref=e57]: How GreenLens turns a simple smartphone photo into a comprehensive botanical analysis.
|
||||
- generic [ref=e58]:
|
||||
- generic [ref=e59]:
|
||||
- generic [ref=e60]: "1"
|
||||
- heading "Image Capture" [level=3] [ref=e61]
|
||||
- paragraph [ref=e62]: High-resolution capture analyzes leaf morphology, venation, and stem structure. Blurry images are discarded to ensure accuracy.
|
||||
- generic [ref=e63]:
|
||||
- generic [ref=e64]: "2"
|
||||
- heading "Feature Extraction" [level=3] [ref=e65]
|
||||
- paragraph [ref=e66]: Our neural network isolates over 120 specific features and matches them against a massive database.
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68]: "3"
|
||||
- heading "Diagnostics" [level=3] [ref=e69]
|
||||
- paragraph [ref=e70]: A second pass specifically looks for discoloration, pest traces, and wilting, based on typical diseases of the respective species.
|
||||
- generic [ref=e71]:
|
||||
- generic [ref=e72]: "4"
|
||||
- heading "Plan Generation" [level=3] [ref=e73]
|
||||
- paragraph [ref=e74]: From the identification and health status, an individual care plan is created that adapts to local weather and lighting conditions.
|
||||
- generic [ref=e76]:
|
||||
- generic [ref=e78]:
|
||||
- generic [ref=e79]: — Perspective
|
||||
- heading "GreenLens vs. Basic Apps" [level=2] [ref=e80]
|
||||
- paragraph [ref=e81]: Why serious plant enthusiasts and professionals trust our platform more than standard identification tools. We deliver actionable data, not just trivia.
|
||||
- generic [ref=e82]:
|
||||
- heading "Botanical Tip" [level=4] [ref=e83]
|
||||
- paragraph [ref=e84]: Regular care is better than intensive, infrequent care. Use our scheduling tools for a good routine.
|
||||
- link "Read Care Guides →" [ref=e85] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e86]:
|
||||
- generic [ref=e87]:
|
||||
- generic [ref=e88]:
|
||||
- generic [ref=e89]:
|
||||
- generic [ref=e90]: check_circle
|
||||
- heading "Plant Identification" [level=3] [ref=e91]
|
||||
- generic [ref=e92]: GREENLENS
|
||||
- paragraph [ref=e93]: Der AI-powerede Scan liefert Artenname, Trivialname, botanischen Hintergrund, Herkunftsdaten und Giftigkeitswarnungen in unter einer Sekunde.
|
||||
- generic [ref=e94]:
|
||||
- generic [ref=e95]: BASIC APPS
|
||||
- paragraph [ref=e96]: The species name is often vaguely provided – without further context or next steps.
|
||||
- generic [ref=e97]:
|
||||
- generic [ref=e98]:
|
||||
- generic [ref=e99]:
|
||||
- generic [ref=e100]: calendar_month
|
||||
- heading "Care Planning" [level=3] [ref=e101]
|
||||
- generic [ref=e102]: GREENLENS
|
||||
- paragraph [ref=e103]: Watering, fertilizing, and repotting are dynamically scheduled based on species needs, local weather, and your environmental data.
|
||||
- generic [ref=e104]:
|
||||
- generic [ref=e105]: BASIC APPS
|
||||
- paragraph [ref=e106]: Generic, static texts like "Water when the soil is dry". Care must be manually researched and tracked.
|
||||
- generic [ref=e107]:
|
||||
- generic [ref=e108]:
|
||||
- generic [ref=e109]:
|
||||
- generic [ref=e110]: healing
|
||||
- heading "Gesundheits-Diagnostics" [level=3] [ref=e111]
|
||||
- generic [ref=e112]: GREENLENS
|
||||
- paragraph [ref=e113]: Specialized scan for visible symptoms. Identifies pests (thrips, mites), fungal infections, and nutrient deficiencies with concrete treatment protocols.
|
||||
- generic [ref=e114]:
|
||||
- generic [ref=e115]: BASIC APPS
|
||||
- paragraph [ref=e116]: Barely existent. Most identification apps offer no diagnosis or confuse damage with a different plant.
|
||||
- generic [ref=e117]:
|
||||
- generic [ref=e118]:
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e120]: wb_sunny
|
||||
- heading "Environment Tools" [level=3] [ref=e121]
|
||||
- generic [ref=e122]: GREENLENS
|
||||
- paragraph [ref=e123]: A built-in light meter uses your camera to measure exact lighting conditions and matches them with the identified plants requirements.
|
||||
- generic [ref=e124]:
|
||||
- generic [ref=e125]: BASIC APPS
|
||||
- paragraph [ref=e126]: No tools included for measuring the environment.
|
||||
- generic [ref=e128]:
|
||||
- heading "Frequently Asked Questions" [level=2] [ref=e130]
|
||||
- generic [ref=e131]:
|
||||
- generic [ref=e132]:
|
||||
- heading "Wie genau ist die Plant Identification?" [level=4] [ref=e133]
|
||||
- paragraph [ref=e134]: Our neural network offers extremely high accuracy for common houseplants and rare cultivars. It is constantly improved through verified community scans.
|
||||
- generic [ref=e135]:
|
||||
- heading "Are pests and diseases also recognized?" [level=4] [ref=e136]
|
||||
- paragraph [ref=e137]: Yes. The specialized health check looks for visible signs of common pests (spider mites, mealybugs) and diseases (powdery mildew, root rot) and provides immediate countermeasures.
|
||||
- generic [ref=e138]:
|
||||
- heading "Does the light meter really work?" [level=4] [ref=e139]
|
||||
- paragraph [ref=e140]: The integrated light meter uses the ambient light sensor and your smartphones camera to calculate a reliable estimate in lux, which is perfectly sufficient for indoor placement.
|
||||
- generic [ref=e141]:
|
||||
- heading "Is there a limit to how many plants I can save?" [level=4] [ref=e142]
|
||||
- paragraph [ref=e143]: In the free version, you can save up to 5 plants. GreenLens Premium offers unlimited tracking, custom folders, and advanced history charts.
|
||||
- generic [ref=e144]:
|
||||
- heading "How does the watering plan adapt?" [level=4] [ref=e145]
|
||||
- paragraph [ref=e146]: The plans are not static. They adapt to seasons, local weather (temperature and humidity), and your feedback (if the soil is still wet, the plan shifts).
|
||||
- generic [ref=e147]:
|
||||
- heading "What if the app is wrong?" [level=4] [ref=e148]
|
||||
- paragraph [ref=e149]: In cases of low confidence, the app shows the 3 most likely matches. Premium users can request a review by real botanists for tricky scans.
|
||||
- contentinfo [ref=e150]:
|
||||
- generic [ref=e151]:
|
||||
- generic [ref=e152]:
|
||||
- generic [ref=e153]:
|
||||
- generic [ref=e154]: DOWNLOAD
|
||||
- heading "Ready for better plant care?" [level=2] [ref=e155]: Ready forbetterplant care?
|
||||
- paragraph [ref=e156]: GreenLens helps you identify, understand, and care for your plants. Use the one-click feature, if you are not there yet, you can reach us via the support page.
|
||||
- link [ref=e158] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/plant-doctor-greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=e159]
|
||||
- paragraph [ref=e160]:
|
||||
- text: The app is already available in the stores.
|
||||
- link "Support" [ref=e161] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- img "Pflanzenregal" [ref=e163]
|
||||
- generic [ref=e165]:
|
||||
- generic [ref=e166]:
|
||||
- heading "GREENLENS" [level=3] [ref=e167]
|
||||
- paragraph [ref=e168]: The app for plant lovers that combines analysis, identification, and care in one place.
|
||||
- generic [ref=e169]:
|
||||
- heading "PRODUCT" [level=4] [ref=e170]
|
||||
- list [ref=e171]:
|
||||
- listitem [ref=e172]:
|
||||
- link "Features" [ref=e173] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e174]:
|
||||
- link "Technology" [ref=e175] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e176]:
|
||||
- link "Get the App" [ref=e177] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e178]:
|
||||
- link "Support" [ref=e179] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e180]:
|
||||
- heading "COMPANY" [level=4] [ref=e181]
|
||||
- list [ref=e182]:
|
||||
- listitem [ref=e183]:
|
||||
- link "How it works" [ref=e184] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e185]:
|
||||
- link "FAQ" [ref=e186] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e187]:
|
||||
- link "Support" [ref=e188] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e189]:
|
||||
- heading "LEGAL" [level=4] [ref=e190]
|
||||
- list [ref=e191]:
|
||||
- listitem [ref=e192]:
|
||||
- link "Imprint" [ref=e193] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e194]:
|
||||
- link "Privacy" [ref=e195] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e196]:
|
||||
- link "Terms of Use" [ref=e197] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e198]:
|
||||
- generic [ref=e199]:
|
||||
- heading "USE CASES & MEHR" [level=4] [ref=e200]
|
||||
- list [ref=e201]:
|
||||
- listitem [ref=e202]:
|
||||
- link "Best Plant ID App" [ref=e203] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e204]:
|
||||
- link "Plant Identifier app" [ref=e205] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e206]:
|
||||
- link "Plant Scanner" [ref=e207] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e208]:
|
||||
- link "Picture This App" [ref=e209] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e210]:
|
||||
- link "Houseplant Identifier" [ref=e211] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e212]:
|
||||
- link "Succulent Identifier" [ref=e213] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e214]:
|
||||
- link "Identify Plant by Photo" [ref=e215] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e216]:
|
||||
- link "Plant Disease Identifier" [ref=e217] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e218]:
|
||||
- link "Plant Health App" [ref=e219] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e220]:
|
||||
- link "Plant Care App" [ref=e221] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e222]:
|
||||
- link "vs PictureThis" [ref=e223] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e224]:
|
||||
- link "vs Planta" [ref=e225] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e226]:
|
||||
- link "vs Blossom" [ref=e227] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e228]:
|
||||
- link "vs Google Lens" [ref=e229] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e230]:
|
||||
- heading "PFLANZEN ERKENNEN" [level=4] [ref=e231]
|
||||
- list [ref=e232]:
|
||||
- listitem [ref=e233]:
|
||||
- link "Healthy or Sick" [ref=e234] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e235]:
|
||||
- link "Identify plants for free" [ref=e236] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e237]:
|
||||
- link "Pflanzen erkennen app" [ref=e238] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e239]:
|
||||
- link "Determine plants" [ref=e240] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e241]:
|
||||
- link "Blumen Scanner" [ref=e242] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e243]:
|
||||
- link "Recognize flowers by photo" [ref=e244] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e245]:
|
||||
- link "Identify houseplants" [ref=e246] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e247]:
|
||||
- link "Plant Care App" [ref=e248] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e249]:
|
||||
- link "Identify plant diseases" [ref=e250] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- generic [ref=e251]:
|
||||
- heading "LANGUAGES" [level=4] [ref=e252]
|
||||
- list [ref=e253]:
|
||||
- listitem [ref=e254]:
|
||||
- link "GreenLens en español" [ref=e255] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e256]:
|
||||
- link "Identificador de plantas" [ref=e257] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e258]:
|
||||
- link "Escaner de plantas" [ref=e259] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e260]:
|
||||
- link "App para cuidar plantas" [ref=e261] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e262]:
|
||||
- link "Diagnosticar enfermedades" [ref=e263] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- listitem [ref=e264]:
|
||||
- link "GreenLens vs Google Lens" [ref=e265] [cursor=pointer]:
|
||||
- /url: "#"
|
||||
- heading "GREENLENS" [level=1] [ref=e267]
|
||||
- generic [ref=e268]:
|
||||
- paragraph [ref=e269]: © 2024 GreenLens. All rights reserved.
|
||||
- generic [ref=e270]: GREENLENS
|
||||
- alert [ref=e271]
|
||||
363
.playwright-mcp/page-2026-07-10T08-53-10-721Z.yml
Normal file
@@ -0,0 +1,363 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- navigation "Main navigation" [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- link "GreenLens Home" [ref=e4] [cursor=pointer]:
|
||||
- /url: /
|
||||
- text: GREENLENS
|
||||
- generic [ref=e5]:
|
||||
- link "Features" [ref=e6] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "Technologie" [ref=e7] [cursor=pointer]:
|
||||
- /url: "#intelligence"
|
||||
- link "FAQ" [ref=e8] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- link "So funktioniert es" [ref=e9] [cursor=pointer]:
|
||||
- /url: "#how"
|
||||
- link "Support" [ref=e10] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- link "App laden" [ref=e11] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- group "Language selector" [ref=e12]:
|
||||
- link "DE" [ref=e13] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "EN" [ref=e14] [cursor=pointer]:
|
||||
- /url: /en
|
||||
- link "ES" [ref=e15] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Jetzt starten" [ref=e16] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- main [ref=e17]:
|
||||
- region "Hero" [ref=e18]:
|
||||
- generic [ref=e22]:
|
||||
- generic [ref=e23]: GreenLens App - Botanische Intelligenz
|
||||
- heading [level=1] [ref=e26]:
|
||||
- text: Dein UrbanJungle,
|
||||
- emphasis [ref=e27]: besser gepflegt.
|
||||
- paragraph [ref=e28]: Scanne Pflanzen, verstehe ihre Bedürfnisse und organisiere Pflege, Erinnerungen und Sammlung in einer App.
|
||||
- generic [ref=e29]:
|
||||
- link "App entdecken" [ref=e30] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- link "Mehr erfahren" [ref=e34] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "GreenLens on Product Hunt" [ref=e35] [cursor=pointer]:
|
||||
- /url: https://www.producthunt.com/products/greenlens?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-greenlens
|
||||
- img "GreenLens - Scan plants, understand care, and grow smarter | Product Hunt" [ref=e36]
|
||||
- group "Wobei hilft dir GreenLens?" [ref=e37]:
|
||||
- paragraph [ref=e38]: Wobei hilft dir GreenLens?
|
||||
- generic [ref=e39]:
|
||||
- button "Ich will die Pflanze erkennen." [ref=e40] [cursor=pointer]
|
||||
- button "Ich will Pflegeprobleme verstehen." [ref=e42] [cursor=pointer]
|
||||
- generic [ref=e45]:
|
||||
- generic [ref=e46]: Scan it
|
||||
- generic [ref=e48]: Track it
|
||||
- generic [ref=e50]: Live Design
|
||||
- generic [ref=e52]: Urban Jungle
|
||||
- generic [ref=e54]: Botanical KI
|
||||
- generic [ref=e56]: Pflege-Tipps
|
||||
- generic [ref=e58]: Scan it
|
||||
- generic [ref=e60]: Track it
|
||||
- generic [ref=e62]: Live Design
|
||||
- generic [ref=e64]: Urban Jungle
|
||||
- generic [ref=e66]: Botanical KI
|
||||
- generic [ref=e68]: Pflege-Tipps
|
||||
- generic [ref=e70]: Scan it
|
||||
- generic [ref=e72]: Track it
|
||||
- generic [ref=e74]: Live Design
|
||||
- generic [ref=e76]: Urban Jungle
|
||||
- generic [ref=e78]: Botanical KI
|
||||
- generic [ref=e80]: Pflege-Tipps
|
||||
- generic [ref=e82]: Scan it
|
||||
- generic [ref=e84]: Track it
|
||||
- generic [ref=e86]: Live Design
|
||||
- generic [ref=e88]: Urban Jungle
|
||||
- generic [ref=e90]: Botanical KI
|
||||
- generic [ref=e92]: Pflege-Tipps
|
||||
- region [ref=e94]:
|
||||
- generic [ref=e95]:
|
||||
- generic [ref=e96]:
|
||||
- paragraph [ref=e97]: Features
|
||||
- heading "Alles, was dein Urban Jungle braucht." [level=2] [ref=e98]: Alles, was deinUrban Jungle braucht.
|
||||
- paragraph [ref=e99]: Von der ersten Identifikation bis zur laufenden Pflege hilft GreenLens dir, Pflanzen besser zu verstehen und besser zu organisieren. Das Lexikon umfasst über 450 Pflanzenarten.
|
||||
- generic [ref=e100]:
|
||||
- generic [ref=e101]:
|
||||
- img "Person scannt eine Pflanze mit der GreenLens App" [ref=e102]
|
||||
- generic [ref=e104]:
|
||||
- generic [ref=e105]: KI Scan
|
||||
- heading "Scan it." [level=3] [ref=e106]
|
||||
- paragraph [ref=e107]: Richte die Kamera auf jede Pflanze – GreenLens erkennt sie in Sekundenbruchteilen und liefert alle Infos.
|
||||
- generic [ref=e108]:
|
||||
- generic [ref=e109]:
|
||||
- img "Pflanzen auf einem Regal mit Pflegeplänen" [ref=e110]
|
||||
- generic [ref=e112]:
|
||||
- generic [ref=e113]: Tracking
|
||||
- heading "Track it." [level=3] [ref=e114]
|
||||
- paragraph [ref=e115]: Gießplan, Lichtbedarf und Wachstum – alles in einer Timeline.
|
||||
- generic [ref=e116]:
|
||||
- img "Pflanzensammlung im Urban Jungle Stil" [ref=e117]
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e120]: Sammlung
|
||||
- heading "Grow it." [level=3] [ref=e121]
|
||||
- paragraph [ref=e122]: Baue deine digitale Pflanzenbibliothek auf – mit Fotos und Notizen.
|
||||
- generic [ref=e123]:
|
||||
- generic [ref=e128]:
|
||||
- heading "Smarte Erinnerungen" [level=4] [ref=e129]
|
||||
- paragraph [ref=e130]: Vergiss nie mehr das Gießen – personalisiert für jede Pflanze.
|
||||
- generic [ref=e135]:
|
||||
- heading "Diagnose & Hilfe" [level=4] [ref=e136]
|
||||
- paragraph [ref=e137]: KI erkennt Krankheiten und Schädlinge sofort.
|
||||
- generic [ref=e143]:
|
||||
- heading "Standort-Tipps" [level=4] [ref=e144]
|
||||
- paragraph [ref=e145]: Pflegehinweise basierend auf deinem Klima und Licht.
|
||||
- region [ref=e146]:
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- paragraph [ref=e149]: Pflanzenhilfe
|
||||
- heading [level=2] [ref=e150]:
|
||||
- text: Weniger raten.
|
||||
- emphasis [ref=e151]: Schneller verstehen, was deiner Pflanze fehlt.
|
||||
- paragraph [ref=e152]: GreenLens hilft dir dabei, Symptome einzuordnen, Pflanzen zu identifizieren und nächste Pflegeschritte klarer zu machen.
|
||||
- generic [ref=e153]:
|
||||
- generic "Vergleichs-Slider" [ref=e154]:
|
||||
- generic [ref=e155]: Vorher
|
||||
- generic [ref=e159]: Nachher
|
||||
- slider "Vergleichs-Slider" [ref=e163]: "50"
|
||||
- generic: Botanical Intelligence
|
||||
- generic [ref=e164]:
|
||||
- generic [ref=e169]:
|
||||
- heading "Braune Spitzen einordnen" [level=4] [ref=e170]
|
||||
- paragraph [ref=e171]: Schneller von Unsicherheit zu einer klareren Ursache
|
||||
- generic [ref=e177]:
|
||||
- heading "Pflegeplan strukturieren" [level=4] [ref=e178]
|
||||
- paragraph [ref=e179]: Weniger Chaos bei Licht, Wasser und Intervallen
|
||||
- generic [ref=e185]:
|
||||
- heading "Hinweise an einem Ort" [level=4] [ref=e186]
|
||||
- paragraph [ref=e187]: Scan, Sammlung und Care Notes zusammengeführt
|
||||
- region [ref=e188]:
|
||||
- generic [ref=e190]:
|
||||
- paragraph [ref=e191]: Technologie
|
||||
- heading [level=2] [ref=e192]:
|
||||
- text: Botanical
|
||||
- emphasis [ref=e193]: Intelligence.
|
||||
- paragraph [ref=e194]: GreenLens verbindet Scan-Ergebnisse, Pflegekontext und Sammlungsverwaltung in einer App. So kommst du schneller von einem Pflanzenfoto zu einer verständlichen Entscheidung.
|
||||
- generic [ref=e195]:
|
||||
- generic [ref=e201]:
|
||||
- heading "Scan-basierte Erkennung" [level=4] [ref=e202]
|
||||
- paragraph [ref=e203]: Vom Foto zur besseren Einordnung in wenigen Schritten.
|
||||
- generic [ref=e208]:
|
||||
- heading "Pflegeorientierte Hinweise" [level=4] [ref=e209]
|
||||
- paragraph [ref=e210]: Hilft dir, nächste Pflegeentscheidungen schneller zu treffen.
|
||||
- generic [ref=e216]:
|
||||
- heading "Sammlung und Verlauf" [level=4] [ref=e217]
|
||||
- paragraph [ref=e218]: Behalte Scans, Pflanzen und Notizen an einem Ort.
|
||||
- generic [ref=e223]:
|
||||
- heading "Lexikon und Suche" [level=4] [ref=e224]
|
||||
- paragraph [ref=e225]: Suche Pflanzen und vergleiche Informationen in einer App.
|
||||
- region [ref=e226]:
|
||||
- generic [ref=e228]:
|
||||
- paragraph [ref=e229]: So funktioniert es
|
||||
- heading [level=2] [ref=e230]:
|
||||
- text: Einfacherals du
|
||||
- emphasis [ref=e231]: denkst.
|
||||
- generic [ref=e232]:
|
||||
- generic [ref=e233]:
|
||||
- generic [ref=e234]: "01"
|
||||
- generic [ref=e235]:
|
||||
- heading "Pflanze fotografieren" [level=4] [ref=e236]
|
||||
- paragraph [ref=e237]: Öffne die App, richte die Kamera auf deine Pflanze und tippe auf Scan. Das war's schon.
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e239]: "02"
|
||||
- generic [ref=e240]:
|
||||
- heading "KI identifiziert sofort" [level=4] [ref=e241]
|
||||
- paragraph [ref=e242]: In unter einer Sekunde erhältst du den genauen Namen, die Art und alle wichtigen Eckdaten.
|
||||
- generic [ref=e243]:
|
||||
- generic [ref=e244]: "03"
|
||||
- generic [ref=e245]:
|
||||
- heading "Pflegeplan erhalten" [level=4] [ref=e246]
|
||||
- paragraph [ref=e247]: GreenLens erstellt automatisch einen personalisierten Pflegeplan passend zu deiner Pflanze und deinem Standort.
|
||||
- generic [ref=e248]:
|
||||
- generic [ref=e249]: "04"
|
||||
- generic [ref=e250]:
|
||||
- heading "Wachstum verfolgen" [level=4] [ref=e251]
|
||||
- paragraph [ref=e252]: Dokumentiere Fotos, tracke das Gießen und lass dich an wichtige Pflegetermine erinnern.
|
||||
- generic [ref=e254]:
|
||||
- generic [ref=e255]:
|
||||
- generic [ref=e256]: Fragen
|
||||
- heading [level=2] [ref=e257]:
|
||||
- text: Häufig gestellte
|
||||
- emphasis [ref=e258]: Fragen
|
||||
- paragraph [ref=e259]: Alles, was du über GreenLens und den Einstieg wissen musst.
|
||||
- generic [ref=e260]:
|
||||
- heading "Wie erkennt GreenLens eine Pflanze?" [level=3] [ref=e263] [cursor=pointer]
|
||||
- heading "Ist GreenLens kostenlos?" [level=3] [ref=e269] [cursor=pointer]
|
||||
- heading "Kann ich GreenLens offline nutzen?" [level=3] [ref=e275] [cursor=pointer]
|
||||
- heading "Für welche Pflanzen kann ich GreenLens nutzen?" [level=3] [ref=e281] [cursor=pointer]
|
||||
- heading "Wie starte ich meine Pflanzensammlung?" [level=3] [ref=e287] [cursor=pointer]
|
||||
- region [ref=e291]:
|
||||
- generic [ref=e292]:
|
||||
- generic [ref=e293]:
|
||||
- paragraph [ref=e294]: Guides
|
||||
- heading "Pflanzen schneller erkennen und richtig handeln." [level=2] [ref=e295]
|
||||
- generic [ref=e296]:
|
||||
- link [ref=e297] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- paragraph [ref=e298]: Blumen
|
||||
- heading "Blumen Scanner" [level=3] [ref=e299]
|
||||
- paragraph [ref=e300]: Blumen per Foto erkennen, Namen erhalten und direkt Pflegehinweise nutzen.
|
||||
- link [ref=e301] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- paragraph [ref=e302]: Foto-Erkennung
|
||||
- heading "Blumen per Foto erkennen" [level=3] [ref=e303]
|
||||
- paragraph [ref=e304]: Ideal, wenn du eine Blume fotografiert hast und sofort wissen willst, was sie braucht.
|
||||
- link [ref=e305] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- paragraph [ref=e306]: Pflanzen
|
||||
- heading "Pflanzen bestimmen" [level=3] [ref=e307]
|
||||
- paragraph [ref=e308]: Pflanze scannen, Artname sehen und den passenden Pflegeplan erhalten.
|
||||
- link [ref=e309] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- paragraph [ref=e310]: Gießerinnerung
|
||||
- heading "Pflanzen gießen Erinnerung" [level=3] [ref=e311]
|
||||
- paragraph [ref=e312]: Pflegeplan, Push-Erinnerung und Gießrhythmus pro Pflanze verwalten.
|
||||
- link [ref=e313] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- paragraph [ref=e314]: Google Lens
|
||||
- heading "Google Pflanzen erkennen" [level=3] [ref=e315]
|
||||
- paragraph [ref=e316]: Vergleiche Google Lens mit GreenLens für Pflanzenerkennung, Pflege und Diagnose.
|
||||
- link [ref=e317] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- paragraph [ref=e318]: English
|
||||
- heading "Identify plant by photo" [level=3] [ref=e319]
|
||||
- paragraph [ref=e320]: Use a plant photo or picture to get the name, care plan, and reminders.
|
||||
- link [ref=e321] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- paragraph [ref=e322]: Diagnose
|
||||
- heading "Pflanzenkrankheiten erkennen" [level=3] [ref=e323]
|
||||
- paragraph [ref=e324]: Gelbe Blätter, braune Spitzen oder weiche Stiele einordnen und den nächsten Schritt finden.
|
||||
- region [ref=e325]:
|
||||
- generic [ref=e328]:
|
||||
- paragraph [ref=e329]: Download
|
||||
- heading [level=2] [ref=e330]:
|
||||
- text: Bereit für bessere
|
||||
- emphasis [ref=e331]: Pflanzenpflege?
|
||||
- paragraph [ref=e332]: GreenLens hilft dir beim Erkennen, Verstehen und Pflegen deiner Pflanzen. Wenn der Store-Link noch nicht live ist, erreichst du uns über die Support-Seite.
|
||||
- generic [ref=e333]:
|
||||
- link "App Store or support" [ref=e334] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- generic [ref=e337]:
|
||||
- generic [ref=e338]: Laden im
|
||||
- text: App Store
|
||||
- link "Google Play or contact" [ref=e339] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e342]:
|
||||
- generic [ref=e343]: Direkt
|
||||
- text: E-Mail senden
|
||||
- paragraph [ref=e344]:
|
||||
- text: Die App ist bereits in den Stores verfügbar.
|
||||
- link "Support" [ref=e345] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- contentinfo [ref=e346]:
|
||||
- generic [ref=e347]:
|
||||
- generic [ref=e348]:
|
||||
- generic [ref=e349]:
|
||||
- link "GREENLENS" [ref=e350] [cursor=pointer]:
|
||||
- /url: /
|
||||
- paragraph [ref=e351]: Die App für Pflanzenfans, die Sammlung, Identifikation und Pflege an einem Ort wollen.
|
||||
- link [ref=e353] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/plant-doctor-greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=e354]
|
||||
- generic [ref=e355]:
|
||||
- generic [ref=e356]: Produkt
|
||||
- link "Features" [ref=e357] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "Technologie" [ref=e358] [cursor=pointer]:
|
||||
- /url: "#intelligence"
|
||||
- link "App laden" [ref=e359] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- link "Support" [ref=e360] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e361]:
|
||||
- generic [ref=e362]: Ressourcen
|
||||
- link "So funktioniert es" [ref=e363] [cursor=pointer]:
|
||||
- /url: "#how"
|
||||
- link "FAQ" [ref=e364] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- link "Support" [ref=e365] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e366]:
|
||||
- generic [ref=e367]: Rechtliches
|
||||
- link "Impressum" [ref=e368] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- link "Datenschutz" [ref=e369] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- link "Nutzungsbedingungen" [ref=e370] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=e371]:
|
||||
- generic [ref=e372]: Identify & Care
|
||||
- link "Best Plant ID App" [ref=e373] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- link "Plant Identifier App" [ref=e374] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- link "Plant Scanner" [ref=e375] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- link "Flower Scanner" [ref=e376] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- link "Houseplant Identifier" [ref=e377] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- link "Succulent Identifier" [ref=e378] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- link "Identify Plant by Photo" [ref=e379] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- link "Plant Disease Identifier" [ref=e380] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- link "Plant Health App" [ref=e381] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- link "Plant Care App" [ref=e382] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- link "vs PictureThis" [ref=e383] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- link "vs Plantum" [ref=e384] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- link "vs iNaturalist" [ref=e385] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- link "vs Google Lens" [ref=e386] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=e387]:
|
||||
- generic [ref=e388]: Pflanzen & Erkennen
|
||||
- link "GreenLens auf Deutsch" [ref=e389] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "Pflanzen erkennen kostenlos" [ref=e390] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- link "Pflanzen erkennen App" [ref=e391] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- link "Pflanzen bestimmen" [ref=e392] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- link "Blumen Scanner" [ref=e393] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Blumen per Foto erkennen" [ref=e394] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Zimmerpflanzen bestimmen" [ref=e395] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- link "Pflanzen Pflege App" [ref=e396] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=e397] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- generic [ref=e398]:
|
||||
- generic [ref=e399]: Español
|
||||
- link "GreenLens en español" [ref=e400] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Identificador de plantas" [ref=e401] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- link "Escaner de plantas" [ref=e402] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- link "App para cuidar plantas" [ref=e403] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- link "Diagnosticar enfermedades" [ref=e404] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- link "GreenLens vs Google Lens" [ref=e405] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=e406]: GREENLENS
|
||||
- generic [ref=e407]:
|
||||
- paragraph [ref=e408]: © 2026 GreenLens. Alle Rechte vorbehalten.
|
||||
- link "Support" [ref=e409] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- alert [ref=e410]
|
||||
375
.playwright-mcp/page-2026-07-10T08-54-40-749Z.yml
Normal file
@@ -0,0 +1,375 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- navigation "Main navigation" [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- link "GreenLens Home" [ref=e4] [cursor=pointer]:
|
||||
- /url: /
|
||||
- text: GREENLENS
|
||||
- generic [ref=e5]:
|
||||
- link "Features" [ref=e6] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "Technologie" [ref=e7] [cursor=pointer]:
|
||||
- /url: "#intelligence"
|
||||
- link "FAQ" [ref=e8] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- link "So funktioniert es" [ref=e9] [cursor=pointer]:
|
||||
- /url: "#how"
|
||||
- link "Support" [ref=e10] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- link "App laden" [ref=e11] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- group "Language selector" [ref=e12]:
|
||||
- link "DE" [ref=e13] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "EN" [ref=e14] [cursor=pointer]:
|
||||
- /url: /en
|
||||
- link "ES" [ref=e15] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Jetzt starten" [ref=e16] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- main [ref=e17]:
|
||||
- region "Hero" [ref=e18]:
|
||||
- generic [ref=e21]:
|
||||
- generic [ref=e22]:
|
||||
- generic [ref=e23]: GreenLens App - Botanische Intelligenz
|
||||
- heading [level=1] [ref=e26]:
|
||||
- text: Dein UrbanJungle,
|
||||
- emphasis [ref=e27]: besser gepflegt.
|
||||
- paragraph [ref=e28]: Scanne Pflanzen, verstehe ihre Bedürfnisse und organisiere Pflege, Erinnerungen und Sammlung in einer App.
|
||||
- generic [ref=e29]:
|
||||
- link "App entdecken" [ref=e30] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- link "Mehr erfahren" [ref=e34] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "GreenLens on Product Hunt" [ref=e35] [cursor=pointer]:
|
||||
- /url: https://www.producthunt.com/products/greenlens?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-greenlens
|
||||
- img "GreenLens - Scan plants, understand care, and grow smarter | Product Hunt" [ref=e36]
|
||||
- group "Wobei hilft dir GreenLens?" [ref=e37]:
|
||||
- paragraph [ref=e38]: Wobei hilft dir GreenLens?
|
||||
- generic [ref=e39]:
|
||||
- button "Ich will die Pflanze erkennen." [ref=e40] [cursor=pointer]
|
||||
- button "Ich will Pflegeprobleme verstehen." [ref=e42] [cursor=pointer]
|
||||
- generic [ref=e45]:
|
||||
- generic "GreenLens App Demo" [ref=e46]
|
||||
- generic [ref=e48]: Plant care with AI
|
||||
- generic [ref=e51]:
|
||||
- generic [ref=e52]: Scan it
|
||||
- generic [ref=e54]: Track it
|
||||
- generic [ref=e56]: Live Design
|
||||
- generic [ref=e58]: Urban Jungle
|
||||
- generic [ref=e60]: Botanical KI
|
||||
- generic [ref=e62]: Pflege-Tipps
|
||||
- generic [ref=e64]: Scan it
|
||||
- generic [ref=e66]: Track it
|
||||
- generic [ref=e68]: Live Design
|
||||
- generic [ref=e70]: Urban Jungle
|
||||
- generic [ref=e72]: Botanical KI
|
||||
- generic [ref=e74]: Pflege-Tipps
|
||||
- generic [ref=e76]: Scan it
|
||||
- generic [ref=e78]: Track it
|
||||
- generic [ref=e80]: Live Design
|
||||
- generic [ref=e82]: Urban Jungle
|
||||
- generic [ref=e84]: Botanical KI
|
||||
- generic [ref=e86]: Pflege-Tipps
|
||||
- generic [ref=e88]: Scan it
|
||||
- generic [ref=e90]: Track it
|
||||
- generic [ref=e92]: Live Design
|
||||
- generic [ref=e94]: Urban Jungle
|
||||
- generic [ref=e96]: Botanical KI
|
||||
- generic [ref=e98]: Pflege-Tipps
|
||||
- region [ref=e100]:
|
||||
- generic [ref=e101]:
|
||||
- generic [ref=e102]:
|
||||
- paragraph [ref=e103]: Features
|
||||
- heading "Alles, was dein Urban Jungle braucht." [level=2] [ref=e104]: Alles, was deinUrban Jungle braucht.
|
||||
- paragraph [ref=e105]: Von der ersten Identifikation bis zur laufenden Pflege hilft GreenLens dir, Pflanzen besser zu verstehen und besser zu organisieren. Das Lexikon umfasst über 450 Pflanzenarten.
|
||||
- generic [ref=e106]:
|
||||
- generic [ref=e107]:
|
||||
- img "Person scannt eine Pflanze mit der GreenLens App" [ref=e108]
|
||||
- generic [ref=e110]:
|
||||
- generic [ref=e111]: KI Scan
|
||||
- heading "Scan it." [level=3] [ref=e112]
|
||||
- paragraph [ref=e113]: Richte die Kamera auf jede Pflanze – GreenLens erkennt sie in Sekundenbruchteilen und liefert alle Infos.
|
||||
- generic [ref=e114]:
|
||||
- generic [ref=e115]:
|
||||
- img "Pflanzen auf einem Regal mit Pflegeplänen" [ref=e116]
|
||||
- generic [ref=e118]:
|
||||
- generic [ref=e119]: Tracking
|
||||
- heading "Track it." [level=3] [ref=e120]
|
||||
- paragraph [ref=e121]: Gießplan, Lichtbedarf und Wachstum – alles in einer Timeline.
|
||||
- generic [ref=e122]:
|
||||
- img "Pflanzensammlung im Urban Jungle Stil" [ref=e123]
|
||||
- generic [ref=e125]:
|
||||
- generic [ref=e126]: Sammlung
|
||||
- heading "Grow it." [level=3] [ref=e127]
|
||||
- paragraph [ref=e128]: Baue deine digitale Pflanzenbibliothek auf – mit Fotos und Notizen.
|
||||
- generic [ref=e129]:
|
||||
- generic [ref=e134]:
|
||||
- heading "Smarte Erinnerungen" [level=4] [ref=e135]
|
||||
- paragraph [ref=e136]: Vergiss nie mehr das Gießen – personalisiert für jede Pflanze.
|
||||
- generic [ref=e141]:
|
||||
- heading "Diagnose & Hilfe" [level=4] [ref=e142]
|
||||
- paragraph [ref=e143]: KI erkennt Krankheiten und Schädlinge sofort.
|
||||
- generic [ref=e149]:
|
||||
- heading "Standort-Tipps" [level=4] [ref=e150]
|
||||
- paragraph [ref=e151]: Pflegehinweise basierend auf deinem Klima und Licht.
|
||||
- region [ref=e152]:
|
||||
- generic [ref=e153]:
|
||||
- generic [ref=e154]:
|
||||
- paragraph [ref=e155]: Pflanzenhilfe
|
||||
- heading [level=2] [ref=e156]:
|
||||
- text: Weniger raten.
|
||||
- emphasis [ref=e157]: Schneller verstehen, was deiner Pflanze fehlt.
|
||||
- paragraph [ref=e158]: GreenLens hilft dir dabei, Symptome einzuordnen, Pflanzen zu identifizieren und nächste Pflegeschritte klarer zu machen.
|
||||
- generic [ref=e159]:
|
||||
- generic "Vergleichs-Slider" [ref=e160]:
|
||||
- generic [ref=e161]: Vorher
|
||||
- generic [ref=e165]: Nachher
|
||||
- slider "Vergleichs-Slider" [ref=e169]: "50"
|
||||
- generic: Botanical Intelligence
|
||||
- generic [ref=e170]:
|
||||
- generic [ref=e175]:
|
||||
- heading "Braune Spitzen einordnen" [level=4] [ref=e176]
|
||||
- paragraph [ref=e177]: Schneller von Unsicherheit zu einer klareren Ursache
|
||||
- generic [ref=e183]:
|
||||
- heading "Pflegeplan strukturieren" [level=4] [ref=e184]
|
||||
- paragraph [ref=e185]: Weniger Chaos bei Licht, Wasser und Intervallen
|
||||
- generic [ref=e191]:
|
||||
- heading "Hinweise an einem Ort" [level=4] [ref=e192]
|
||||
- paragraph [ref=e193]: Scan, Sammlung und Care Notes zusammengeführt
|
||||
- region [ref=e194]:
|
||||
- generic [ref=e195]:
|
||||
- generic [ref=e196]:
|
||||
- paragraph [ref=e197]: Technologie
|
||||
- heading [level=2] [ref=e198]:
|
||||
- text: Botanical
|
||||
- emphasis [ref=e199]: Intelligence.
|
||||
- paragraph [ref=e200]: GreenLens verbindet Scan-Ergebnisse, Pflegekontext und Sammlungsverwaltung in einer App. So kommst du schneller von einem Pflanzenfoto zu einer verständlichen Entscheidung.
|
||||
- generic [ref=e201]:
|
||||
- generic [ref=e207]:
|
||||
- heading "Scan-basierte Erkennung" [level=4] [ref=e208]
|
||||
- paragraph [ref=e209]: Vom Foto zur besseren Einordnung in wenigen Schritten.
|
||||
- generic [ref=e214]:
|
||||
- heading "Pflegeorientierte Hinweise" [level=4] [ref=e215]
|
||||
- paragraph [ref=e216]: Hilft dir, nächste Pflegeentscheidungen schneller zu treffen.
|
||||
- generic [ref=e222]:
|
||||
- heading "Sammlung und Verlauf" [level=4] [ref=e223]
|
||||
- paragraph [ref=e224]: Behalte Scans, Pflanzen und Notizen an einem Ort.
|
||||
- generic [ref=e229]:
|
||||
- heading "Lexikon und Suche" [level=4] [ref=e230]
|
||||
- paragraph [ref=e231]: Suche Pflanzen und vergleiche Informationen in einer App.
|
||||
- generic [ref=e233]:
|
||||
- img "Botanical AI plant analysis visualization" [ref=e234]
|
||||
- heading [level=3] [ref=e237]:
|
||||
- text: Botanical
|
||||
- emphasis [ref=e238]: Intelligence.
|
||||
- region [ref=e239]:
|
||||
- generic [ref=e240]:
|
||||
- generic [ref=e241]:
|
||||
- paragraph [ref=e242]: So funktioniert es
|
||||
- heading [level=2] [ref=e243]:
|
||||
- text: Einfacherals du
|
||||
- emphasis [ref=e244]: denkst.
|
||||
- generic [ref=e245]:
|
||||
- generic [ref=e246]:
|
||||
- generic [ref=e247]: "01"
|
||||
- generic [ref=e248]:
|
||||
- heading "Pflanze fotografieren" [level=4] [ref=e249]
|
||||
- paragraph [ref=e250]: Öffne die App, richte die Kamera auf deine Pflanze und tippe auf Scan. Das war's schon.
|
||||
- generic [ref=e251]:
|
||||
- generic [ref=e252]: "02"
|
||||
- generic [ref=e253]:
|
||||
- heading "KI identifiziert sofort" [level=4] [ref=e254]
|
||||
- paragraph [ref=e255]: In unter einer Sekunde erhältst du den genauen Namen, die Art und alle wichtigen Eckdaten.
|
||||
- generic [ref=e256]:
|
||||
- generic [ref=e257]: "03"
|
||||
- generic [ref=e258]:
|
||||
- heading "Pflegeplan erhalten" [level=4] [ref=e259]
|
||||
- paragraph [ref=e260]: GreenLens erstellt automatisch einen personalisierten Pflegeplan passend zu deiner Pflanze und deinem Standort.
|
||||
- generic [ref=e261]:
|
||||
- generic [ref=e262]: "04"
|
||||
- generic [ref=e263]:
|
||||
- heading "Wachstum verfolgen" [level=4] [ref=e264]
|
||||
- paragraph [ref=e265]: Dokumentiere Fotos, tracke das Gießen und lass dich an wichtige Pflegetermine erinnern.
|
||||
- img "GreenLens App in Verwendung – Pflanze wird gescannt" [ref=e268]
|
||||
- generic [ref=e270]:
|
||||
- generic [ref=e271]:
|
||||
- generic [ref=e272]: Fragen
|
||||
- heading [level=2] [ref=e273]:
|
||||
- text: Häufig gestellte
|
||||
- emphasis [ref=e274]: Fragen
|
||||
- paragraph [ref=e275]: Alles, was du über GreenLens und den Einstieg wissen musst.
|
||||
- generic [ref=e276]:
|
||||
- heading "Wie erkennt GreenLens eine Pflanze?" [level=3] [ref=e279] [cursor=pointer]
|
||||
- heading "Ist GreenLens kostenlos?" [level=3] [ref=e285] [cursor=pointer]
|
||||
- heading "Kann ich GreenLens offline nutzen?" [level=3] [ref=e291] [cursor=pointer]
|
||||
- heading "Für welche Pflanzen kann ich GreenLens nutzen?" [level=3] [ref=e297] [cursor=pointer]
|
||||
- heading "Wie starte ich meine Pflanzensammlung?" [level=3] [ref=e303] [cursor=pointer]
|
||||
- region [ref=e307]:
|
||||
- generic [ref=e308]:
|
||||
- generic [ref=e309]:
|
||||
- paragraph [ref=e310]: Guides
|
||||
- heading "Pflanzen schneller erkennen und richtig handeln." [level=2] [ref=e311]
|
||||
- generic [ref=e312]:
|
||||
- link [ref=e313] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- paragraph [ref=e314]: Blumen
|
||||
- heading "Blumen Scanner" [level=3] [ref=e315]
|
||||
- paragraph [ref=e316]: Blumen per Foto erkennen, Namen erhalten und direkt Pflegehinweise nutzen.
|
||||
- link [ref=e317] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- paragraph [ref=e318]: Foto-Erkennung
|
||||
- heading "Blumen per Foto erkennen" [level=3] [ref=e319]
|
||||
- paragraph [ref=e320]: Ideal, wenn du eine Blume fotografiert hast und sofort wissen willst, was sie braucht.
|
||||
- link [ref=e321] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- paragraph [ref=e322]: Pflanzen
|
||||
- heading "Pflanzen bestimmen" [level=3] [ref=e323]
|
||||
- paragraph [ref=e324]: Pflanze scannen, Artname sehen und den passenden Pflegeplan erhalten.
|
||||
- link [ref=e325] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- paragraph [ref=e326]: Gießerinnerung
|
||||
- heading "Pflanzen gießen Erinnerung" [level=3] [ref=e327]
|
||||
- paragraph [ref=e328]: Pflegeplan, Push-Erinnerung und Gießrhythmus pro Pflanze verwalten.
|
||||
- link [ref=e329] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- paragraph [ref=e330]: Google Lens
|
||||
- heading "Google Pflanzen erkennen" [level=3] [ref=e331]
|
||||
- paragraph [ref=e332]: Vergleiche Google Lens mit GreenLens für Pflanzenerkennung, Pflege und Diagnose.
|
||||
- link [ref=e333] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- paragraph [ref=e334]: English
|
||||
- heading "Identify plant by photo" [level=3] [ref=e335]
|
||||
- paragraph [ref=e336]: Use a plant photo or picture to get the name, care plan, and reminders.
|
||||
- link [ref=e337] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- paragraph [ref=e338]: Diagnose
|
||||
- heading "Pflanzenkrankheiten erkennen" [level=3] [ref=e339]
|
||||
- paragraph [ref=e340]: Gelbe Blätter, braune Spitzen oder weiche Stiele einordnen und den nächsten Schritt finden.
|
||||
- region [ref=e341]:
|
||||
- generic [ref=e344]:
|
||||
- paragraph [ref=e345]: Download
|
||||
- heading [level=2] [ref=e346]:
|
||||
- text: Bereit für bessere
|
||||
- emphasis [ref=e347]: Pflanzenpflege?
|
||||
- paragraph [ref=e348]: GreenLens hilft dir beim Erkennen, Verstehen und Pflegen deiner Pflanzen. Wenn der Store-Link noch nicht live ist, erreichst du uns über die Support-Seite.
|
||||
- generic [ref=e349]:
|
||||
- link "App Store or support" [ref=e350] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- generic [ref=e353]:
|
||||
- generic [ref=e354]: Laden im
|
||||
- text: App Store
|
||||
- link "Google Play or contact" [ref=e355] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e358]:
|
||||
- generic [ref=e359]: Direkt
|
||||
- text: E-Mail senden
|
||||
- paragraph [ref=e360]:
|
||||
- text: Die App ist bereits in den Stores verfügbar.
|
||||
- link "Support" [ref=e361] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- contentinfo [ref=e364]:
|
||||
- generic [ref=e365]:
|
||||
- generic [ref=e366]:
|
||||
- generic [ref=e367]:
|
||||
- link "GREENLENS" [ref=e368] [cursor=pointer]:
|
||||
- /url: /
|
||||
- paragraph [ref=e369]: Die App für Pflanzenfans, die Sammlung, Identifikation und Pflege an einem Ort wollen.
|
||||
- link [ref=e371] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/plant-doctor-greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=e372]
|
||||
- generic [ref=e373]:
|
||||
- generic [ref=e374]: Produkt
|
||||
- link "Features" [ref=e375] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "Technologie" [ref=e376] [cursor=pointer]:
|
||||
- /url: "#intelligence"
|
||||
- link "App laden" [ref=e377] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- link "Support" [ref=e378] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e379]:
|
||||
- generic [ref=e380]: Ressourcen
|
||||
- link "So funktioniert es" [ref=e381] [cursor=pointer]:
|
||||
- /url: "#how"
|
||||
- link "FAQ" [ref=e382] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- link "Support" [ref=e383] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e384]:
|
||||
- generic [ref=e385]: Rechtliches
|
||||
- link "Impressum" [ref=e386] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- link "Datenschutz" [ref=e387] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- link "Nutzungsbedingungen" [ref=e388] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=e389]:
|
||||
- generic [ref=e390]: Identify & Care
|
||||
- link "Best Plant ID App" [ref=e391] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- link "Plant Identifier App" [ref=e392] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- link "Plant Scanner" [ref=e393] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- link "Flower Scanner" [ref=e394] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- link "Houseplant Identifier" [ref=e395] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- link "Succulent Identifier" [ref=e396] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- link "Identify Plant by Photo" [ref=e397] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- link "Plant Disease Identifier" [ref=e398] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- link "Plant Health App" [ref=e399] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- link "Plant Care App" [ref=e400] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- link "vs PictureThis" [ref=e401] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- link "vs Plantum" [ref=e402] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- link "vs iNaturalist" [ref=e403] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- link "vs Google Lens" [ref=e404] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=e405]:
|
||||
- generic [ref=e406]: Pflanzen & Erkennen
|
||||
- link "GreenLens auf Deutsch" [ref=e407] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "Pflanzen erkennen kostenlos" [ref=e408] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- link "Pflanzen erkennen App" [ref=e409] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- link "Pflanzen bestimmen" [ref=e410] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- link "Blumen Scanner" [ref=e411] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Blumen per Foto erkennen" [ref=e412] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Zimmerpflanzen bestimmen" [ref=e413] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- link "Pflanzen Pflege App" [ref=e414] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=e415] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- generic [ref=e416]:
|
||||
- generic [ref=e417]: Español
|
||||
- link "GreenLens en español" [ref=e418] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Identificador de plantas" [ref=e419] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- link "Escaner de plantas" [ref=e420] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- link "App para cuidar plantas" [ref=e421] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- link "Diagnosticar enfermedades" [ref=e422] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- link "GreenLens vs Google Lens" [ref=e423] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=e424]: GREENLENS
|
||||
- generic [ref=e425]:
|
||||
- paragraph [ref=e426]: © 2026 GreenLens. Alle Rechte vorbehalten.
|
||||
- link "Support" [ref=e427] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- alert [ref=e428]
|
||||
375
.playwright-mcp/page-2026-07-10T08-55-03-578Z.yml
Normal file
@@ -0,0 +1,375 @@
|
||||
- generic [active] [ref=f1e1]:
|
||||
- navigation "Main navigation" [ref=f1e2]:
|
||||
- generic [ref=f1e3]:
|
||||
- link "GreenLens Home" [ref=f1e4] [cursor=pointer]:
|
||||
- /url: /
|
||||
- text: GREENLENS
|
||||
- generic [ref=f1e5]:
|
||||
- link "Features" [ref=f1e6] [cursor=pointer]:
|
||||
- /url: /#features
|
||||
- link "Technology" [ref=f1e7] [cursor=pointer]:
|
||||
- /url: /#intelligence
|
||||
- link "FAQ" [ref=f1e8] [cursor=pointer]:
|
||||
- /url: /#faq
|
||||
- link "How it works" [ref=f1e9] [cursor=pointer]:
|
||||
- /url: /#how
|
||||
- link "Support" [ref=f1e10] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- link "Get the App" [ref=f1e11] [cursor=pointer]:
|
||||
- /url: /#cta
|
||||
- group "Language selector" [ref=f1e12]:
|
||||
- link "DE" [ref=f1e13] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "EN" [ref=f1e14] [cursor=pointer]:
|
||||
- /url: /en
|
||||
- link "ES" [ref=f1e15] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Get Started" [ref=f1e16] [cursor=pointer]:
|
||||
- /url: /#cta
|
||||
- main [ref=f1e17]:
|
||||
- region "Hero" [ref=f1e18]:
|
||||
- generic [ref=f1e21]:
|
||||
- generic [ref=f1e22]:
|
||||
- generic [ref=f1e23]: GreenLens App - Botanical intelligence
|
||||
- heading [level=1] [ref=f1e26]:
|
||||
- text: Your UrbanJungle,
|
||||
- emphasis [ref=f1e27]: better cared for.
|
||||
- paragraph [ref=f1e28]: Scan plants, understand what they need, and keep care, reminders, and your collection in one app.
|
||||
- generic [ref=f1e29]:
|
||||
- link "Explore the App" [ref=f1e30] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- link "Learn more" [ref=f1e34] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "GreenLens on Product Hunt" [ref=f1e35] [cursor=pointer]:
|
||||
- /url: https://www.producthunt.com/products/greenlens?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-greenlens
|
||||
- img "GreenLens - Scan plants, understand care, and grow smarter | Product Hunt" [ref=f1e36]
|
||||
- group "What do you need help with?" [ref=f1e37]:
|
||||
- paragraph [ref=f1e38]: What do you need help with?
|
||||
- generic [ref=f1e39]:
|
||||
- button "I want to identify the plant." [ref=f1e40] [cursor=pointer]
|
||||
- button "I want to understand care problems." [ref=f1e42] [cursor=pointer]
|
||||
- generic [ref=f1e45]:
|
||||
- generic "GreenLens App Demo" [ref=f1e46]
|
||||
- generic [ref=f1e48]: Plant care with AI
|
||||
- generic [ref=f1e51]:
|
||||
- generic [ref=f1e52]: Scan it
|
||||
- generic [ref=f1e54]: Track it
|
||||
- generic [ref=f1e56]: Live Design
|
||||
- generic [ref=f1e58]: Urban Jungle
|
||||
- generic [ref=f1e60]: Botanical KI
|
||||
- generic [ref=f1e62]: Pflege-Tipps
|
||||
- generic [ref=f1e64]: Scan it
|
||||
- generic [ref=f1e66]: Track it
|
||||
- generic [ref=f1e68]: Live Design
|
||||
- generic [ref=f1e70]: Urban Jungle
|
||||
- generic [ref=f1e72]: Botanical KI
|
||||
- generic [ref=f1e74]: Pflege-Tipps
|
||||
- generic [ref=f1e76]: Scan it
|
||||
- generic [ref=f1e78]: Track it
|
||||
- generic [ref=f1e80]: Live Design
|
||||
- generic [ref=f1e82]: Urban Jungle
|
||||
- generic [ref=f1e84]: Botanical KI
|
||||
- generic [ref=f1e86]: Pflege-Tipps
|
||||
- generic [ref=f1e88]: Scan it
|
||||
- generic [ref=f1e90]: Track it
|
||||
- generic [ref=f1e92]: Live Design
|
||||
- generic [ref=f1e94]: Urban Jungle
|
||||
- generic [ref=f1e96]: Botanical KI
|
||||
- generic [ref=f1e98]: Pflege-Tipps
|
||||
- region [ref=f1e100]:
|
||||
- generic [ref=f1e101]:
|
||||
- generic [ref=f1e102]:
|
||||
- paragraph [ref=f1e103]: Features
|
||||
- heading "Everything your Urban Jungle needs." [level=2] [ref=f1e104]: Everything yourUrban Jungle needs.
|
||||
- paragraph [ref=f1e105]: From first identification to ongoing care, GreenLens helps you understand plants better and stay organized. The lexicon covers 450+ plant species.
|
||||
- generic [ref=f1e106]:
|
||||
- generic [ref=f1e107]:
|
||||
- img "Person scanning a plant with the GreenLens app" [ref=f1e108]
|
||||
- generic [ref=f1e110]:
|
||||
- generic [ref=f1e111]: AI Scan
|
||||
- heading "Scan it." [level=3] [ref=f1e112]
|
||||
- paragraph [ref=f1e113]: Point your camera at any plant – GreenLens identifies it in milliseconds and delivers all the info.
|
||||
- generic [ref=f1e114]:
|
||||
- generic [ref=f1e115]:
|
||||
- img "Plants on a shelf with care plans" [ref=f1e116]
|
||||
- generic [ref=f1e118]:
|
||||
- generic [ref=f1e119]: Tracking
|
||||
- heading "Track it." [level=3] [ref=f1e120]
|
||||
- paragraph [ref=f1e121]: Watering schedule, light needs and growth – all in one timeline.
|
||||
- generic [ref=f1e122]:
|
||||
- img "Plant collection in urban jungle style" [ref=f1e123]
|
||||
- generic [ref=f1e125]:
|
||||
- generic [ref=f1e126]: Collection
|
||||
- heading "Grow it." [level=3] [ref=f1e127]
|
||||
- paragraph [ref=f1e128]: Build your digital plant library – with photos and notes.
|
||||
- generic [ref=f1e129]:
|
||||
- generic [ref=f1e134]:
|
||||
- heading "Smart Reminders" [level=4] [ref=f1e135]
|
||||
- paragraph [ref=f1e136]: Never forget watering again – personalized for every plant.
|
||||
- generic [ref=f1e141]:
|
||||
- heading "Diagnosis & Help" [level=4] [ref=f1e142]
|
||||
- paragraph [ref=f1e143]: AI detects diseases and pests instantly.
|
||||
- generic [ref=f1e149]:
|
||||
- heading "Location Tips" [level=4] [ref=f1e150]
|
||||
- paragraph [ref=f1e151]: Care advice based on your climate and light conditions.
|
||||
- region [ref=f1e152]:
|
||||
- generic [ref=f1e153]:
|
||||
- generic [ref=f1e154]:
|
||||
- paragraph [ref=f1e155]: Plant help
|
||||
- heading [level=2] [ref=f1e156]:
|
||||
- text: Less guessing.
|
||||
- emphasis [ref=f1e157]: Understand faster what your plant may need.
|
||||
- paragraph [ref=f1e158]: GreenLens helps you interpret symptoms, identify plants, and get clearer next-step care guidance.
|
||||
- generic [ref=f1e159]:
|
||||
- generic "Before and after slider" [ref=f1e160]:
|
||||
- generic [ref=f1e161]: Before
|
||||
- generic [ref=f1e165]: After
|
||||
- slider "Before and after slider" [ref=f1e169]: "50"
|
||||
- generic: Botanical Intelligence
|
||||
- generic [ref=f1e170]:
|
||||
- generic [ref=f1e175]:
|
||||
- heading "Assess brown leaf tips" [level=4] [ref=f1e176]
|
||||
- paragraph [ref=f1e177]: Move from uncertainty to a clearer diagnosis path
|
||||
- generic [ref=f1e183]:
|
||||
- heading "Structure the care routine" [level=4] [ref=f1e184]
|
||||
- paragraph [ref=f1e185]: Reduce the chaos around light, water, and intervals
|
||||
- generic [ref=f1e191]:
|
||||
- heading "Keep guidance in one place" [level=4] [ref=f1e192]
|
||||
- paragraph [ref=f1e193]: Scan, collection, and care notes together
|
||||
- region [ref=f1e194]:
|
||||
- generic [ref=f1e195]:
|
||||
- generic [ref=f1e196]:
|
||||
- paragraph [ref=f1e197]: Technology
|
||||
- heading [level=2] [ref=f1e198]:
|
||||
- text: Botanical
|
||||
- emphasis [ref=f1e199]: Intelligence.
|
||||
- paragraph [ref=f1e200]: GreenLens combines scan results, care context, and collection management in one app, helping you move from plant photo to a clearer decision faster.
|
||||
- generic [ref=f1e201]:
|
||||
- generic [ref=f1e207]:
|
||||
- heading "Scan-based identification" [level=4] [ref=f1e208]
|
||||
- paragraph [ref=f1e209]: Move from photo to clearer plant context in a few steps.
|
||||
- generic [ref=f1e214]:
|
||||
- heading "Care-focused guidance" [level=4] [ref=f1e215]
|
||||
- paragraph [ref=f1e216]: Helps you make faster next-step care decisions.
|
||||
- generic [ref=f1e222]:
|
||||
- heading "Collection and history" [level=4] [ref=f1e223]
|
||||
- paragraph [ref=f1e224]: Keep scans, plants, and notes in one place.
|
||||
- generic [ref=f1e229]:
|
||||
- heading "Lexicon and search" [level=4] [ref=f1e230]
|
||||
- paragraph [ref=f1e231]: Look up plants and compare information in one app.
|
||||
- generic [ref=f1e233]:
|
||||
- img "Botanical AI plant analysis visualization" [ref=f1e234]
|
||||
- heading [level=3] [ref=f1e237]:
|
||||
- text: Botanical
|
||||
- emphasis [ref=f1e238]: Intelligence.
|
||||
- region [ref=f1e239]:
|
||||
- generic [ref=f1e240]:
|
||||
- generic [ref=f1e241]:
|
||||
- paragraph [ref=f1e242]: How it works
|
||||
- heading [level=2] [ref=f1e243]:
|
||||
- text: Simplerthan you
|
||||
- emphasis [ref=f1e244]: think.
|
||||
- generic [ref=f1e245]:
|
||||
- generic [ref=f1e246]:
|
||||
- generic [ref=f1e247]: "01"
|
||||
- generic [ref=f1e248]:
|
||||
- heading "Photograph your plant" [level=4] [ref=f1e249]
|
||||
- paragraph [ref=f1e250]: Open the app, point the camera at your plant and tap Scan. That's it.
|
||||
- generic [ref=f1e251]:
|
||||
- generic [ref=f1e252]: "02"
|
||||
- generic [ref=f1e253]:
|
||||
- heading "AI identifies instantly" [level=4] [ref=f1e254]
|
||||
- paragraph [ref=f1e255]: In under a second you get the exact name, species and all key details.
|
||||
- generic [ref=f1e256]:
|
||||
- generic [ref=f1e257]: "03"
|
||||
- generic [ref=f1e258]:
|
||||
- heading "Receive care plan" [level=4] [ref=f1e259]
|
||||
- paragraph [ref=f1e260]: GreenLens automatically creates a personalized care plan for your plant and location.
|
||||
- generic [ref=f1e261]:
|
||||
- generic [ref=f1e262]: "04"
|
||||
- generic [ref=f1e263]:
|
||||
- heading "Track growth" [level=4] [ref=f1e264]
|
||||
- paragraph [ref=f1e265]: Document photos, track watering and get reminded of important care dates.
|
||||
- img "GreenLens app in use – scanning a plant" [ref=f1e268]
|
||||
- generic [ref=f1e270]:
|
||||
- generic [ref=f1e271]:
|
||||
- generic [ref=f1e272]: Questions
|
||||
- heading [level=2] [ref=f1e273]:
|
||||
- text: Frequently Asked
|
||||
- emphasis [ref=f1e274]: Questions
|
||||
- paragraph [ref=f1e275]: Everything you need to know about GreenLens and getting started.
|
||||
- generic [ref=f1e276]:
|
||||
- heading "How does GreenLens identify a plant?" [level=3] [ref=f1e279] [cursor=pointer]
|
||||
- heading "Is GreenLens free to use?" [level=3] [ref=f1e285] [cursor=pointer]
|
||||
- heading "Can I use GreenLens offline?" [level=3] [ref=f1e291] [cursor=pointer]
|
||||
- heading "What kind of plants can I use GreenLens for?" [level=3] [ref=f1e297] [cursor=pointer]
|
||||
- heading "How do I start my plant collection?" [level=3] [ref=f1e303] [cursor=pointer]
|
||||
- region [ref=f1e307]:
|
||||
- generic [ref=f1e308]:
|
||||
- generic [ref=f1e309]:
|
||||
- paragraph [ref=f1e310]: Guides
|
||||
- heading "Pflanzen schneller erkennen und richtig handeln." [level=2] [ref=f1e311]
|
||||
- generic [ref=f1e312]:
|
||||
- link [ref=f1e313] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- paragraph [ref=f1e314]: Blumen
|
||||
- heading "Blumen Scanner" [level=3] [ref=f1e315]
|
||||
- paragraph [ref=f1e316]: Blumen per Foto erkennen, Namen erhalten und direkt Pflegehinweise nutzen.
|
||||
- link [ref=f1e317] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- paragraph [ref=f1e318]: Foto-Erkennung
|
||||
- heading "Blumen per Foto erkennen" [level=3] [ref=f1e319]
|
||||
- paragraph [ref=f1e320]: Ideal, wenn du eine Blume fotografiert hast und sofort wissen willst, was sie braucht.
|
||||
- link [ref=f1e321] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- paragraph [ref=f1e322]: Pflanzen
|
||||
- heading "Pflanzen bestimmen" [level=3] [ref=f1e323]
|
||||
- paragraph [ref=f1e324]: Pflanze scannen, Artname sehen und den passenden Pflegeplan erhalten.
|
||||
- link [ref=f1e325] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- paragraph [ref=f1e326]: Gießerinnerung
|
||||
- heading "Pflanzen gießen Erinnerung" [level=3] [ref=f1e327]
|
||||
- paragraph [ref=f1e328]: Pflegeplan, Push-Erinnerung und Gießrhythmus pro Pflanze verwalten.
|
||||
- link [ref=f1e329] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- paragraph [ref=f1e330]: Google Lens
|
||||
- heading "Google Pflanzen erkennen" [level=3] [ref=f1e331]
|
||||
- paragraph [ref=f1e332]: Vergleiche Google Lens mit GreenLens für Pflanzenerkennung, Pflege und Diagnose.
|
||||
- link [ref=f1e333] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- paragraph [ref=f1e334]: English
|
||||
- heading "Identify plant by photo" [level=3] [ref=f1e335]
|
||||
- paragraph [ref=f1e336]: Use a plant photo or picture to get the name, care plan, and reminders.
|
||||
- link [ref=f1e337] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- paragraph [ref=f1e338]: Diagnose
|
||||
- heading "Pflanzenkrankheiten erkennen" [level=3] [ref=f1e339]
|
||||
- paragraph [ref=f1e340]: Gelbe Blätter, braune Spitzen oder weiche Stiele einordnen und den nächsten Schritt finden.
|
||||
- region [ref=f1e341]:
|
||||
- generic [ref=f1e344]:
|
||||
- paragraph [ref=f1e345]: Download
|
||||
- heading [level=2] [ref=f1e346]:
|
||||
- text: Ready for better
|
||||
- emphasis [ref=f1e347]: plant care?
|
||||
- paragraph [ref=f1e348]: GreenLens helps you identify, understand, and care for your plants. If the store listing is not live yet, use the support page to reach us.
|
||||
- generic [ref=f1e349]:
|
||||
- link "App Store or support" [ref=f1e350] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- generic [ref=f1e353]:
|
||||
- generic [ref=f1e354]: Download on the
|
||||
- text: App Store
|
||||
- link "Google Play or contact" [ref=f1e355] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=f1e358]:
|
||||
- generic [ref=f1e359]: Direct
|
||||
- text: Send Email
|
||||
- paragraph [ref=f1e360]:
|
||||
- text: The app is already available in app stores.
|
||||
- link "Support" [ref=f1e361] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- contentinfo [ref=f1e364]:
|
||||
- generic [ref=f1e365]:
|
||||
- generic [ref=f1e366]:
|
||||
- generic [ref=f1e367]:
|
||||
- link "GREENLENS" [ref=f1e368] [cursor=pointer]:
|
||||
- /url: /
|
||||
- paragraph [ref=f1e369]: The app for plant lovers who want identification, care, and collection management in one place.
|
||||
- link [ref=f1e371] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/plant-doctor-greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=f1e372]
|
||||
- generic [ref=f1e373]:
|
||||
- generic [ref=f1e374]: Product
|
||||
- link "Features" [ref=f1e375] [cursor=pointer]:
|
||||
- /url: /#features
|
||||
- link "Technology" [ref=f1e376] [cursor=pointer]:
|
||||
- /url: /#intelligence
|
||||
- link "Get the App" [ref=f1e377] [cursor=pointer]:
|
||||
- /url: /#cta
|
||||
- link "Support" [ref=f1e378] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=f1e379]:
|
||||
- generic [ref=f1e380]: Resources
|
||||
- link "How it works" [ref=f1e381] [cursor=pointer]:
|
||||
- /url: /#how
|
||||
- link "FAQ" [ref=f1e382] [cursor=pointer]:
|
||||
- /url: /#faq
|
||||
- link "Support" [ref=f1e383] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=f1e384]:
|
||||
- generic [ref=f1e385]: Legal
|
||||
- link "Imprint" [ref=f1e386] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- link "Privacy Policy" [ref=f1e387] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- link "Terms of Service" [ref=f1e388] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=f1e389]:
|
||||
- generic [ref=f1e390]: Identify & Care
|
||||
- link "Best Plant ID App" [ref=f1e391] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- link "Plant Identifier App" [ref=f1e392] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- link "Plant Scanner" [ref=f1e393] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- link "Flower Scanner" [ref=f1e394] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- link "Houseplant Identifier" [ref=f1e395] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- link "Succulent Identifier" [ref=f1e396] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- link "Identify Plant by Photo" [ref=f1e397] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- link "Plant Disease Identifier" [ref=f1e398] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- link "Plant Health App" [ref=f1e399] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- link "Plant Care App" [ref=f1e400] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- link "vs PictureThis" [ref=f1e401] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- link "vs Plantum" [ref=f1e402] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- link "vs iNaturalist" [ref=f1e403] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- link "vs Google Lens" [ref=f1e404] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=f1e405]:
|
||||
- generic [ref=f1e406]: Pflanzen & Erkennen
|
||||
- link "GreenLens auf Deutsch" [ref=f1e407] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "Pflanzen erkennen kostenlos" [ref=f1e408] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- link "Pflanzen erkennen App" [ref=f1e409] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- link "Pflanzen bestimmen" [ref=f1e410] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- link "Blumen Scanner" [ref=f1e411] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Blumen per Foto erkennen" [ref=f1e412] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Zimmerpflanzen bestimmen" [ref=f1e413] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- link "Pflanzen Pflege App" [ref=f1e414] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=f1e415] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- generic [ref=f1e416]:
|
||||
- generic [ref=f1e417]: Español
|
||||
- link "GreenLens en español" [ref=f1e418] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Identificador de plantas" [ref=f1e419] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- link "Escaner de plantas" [ref=f1e420] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- link "App para cuidar plantas" [ref=f1e421] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- link "Diagnosticar enfermedades" [ref=f1e422] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- link "GreenLens vs Google Lens" [ref=f1e423] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=f1e424]: GREENLENS
|
||||
- generic [ref=f1e425]:
|
||||
- paragraph [ref=f1e426]: © 2026 GreenLens. All rights reserved.
|
||||
- link "Support" [ref=f1e427] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- alert [ref=f1e428]
|
||||
311
.playwright-mcp/page-2026-07-10T08-55-29-850Z.yml
Normal file
@@ -0,0 +1,311 @@
|
||||
- generic [active] [ref=f2e1]:
|
||||
- generic [ref=f2e2]:
|
||||
- navigation [ref=f2e3]:
|
||||
- generic [ref=f2e4]:
|
||||
- link "GreenLens Home" [ref=f2e5] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- text: GREENLENS
|
||||
- button "Open menu" [ref=f2e7] [cursor=pointer]
|
||||
- main [ref=f2e10]:
|
||||
- generic [ref=f2e11]:
|
||||
- generic [ref=f2e12]:
|
||||
- generic [ref=f2e13]: Identificación de plantas por IA
|
||||
- heading "Identificador de plantas" [level=1] [ref=f2e14]
|
||||
- paragraph [ref=f2e15]: Escanea una planta y entiende que necesita en segundos.
|
||||
- paragraph [ref=f2e16]: GreenLens es un identificador de plantas para iOS y Android. Toma una foto, recibe el nombre de la especie y pasa directamente a cuidados, riego y diagnostico si la planta muestra sintomas.
|
||||
- generic [ref=f2e17]:
|
||||
- link "Descargar App" [ref=f2e18] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- link "Cómo funciona" [ref=f2e19] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- generic [ref=f2e20]:
|
||||
- generic [ref=f2e21]:
|
||||
- generic [ref=f2e22]: Impulsado por IA
|
||||
- generic [ref=f2e23]: Análisis instantáneo
|
||||
- generic [ref=f2e24]:
|
||||
- generic [ref=f2e25]: Holístico
|
||||
- generic [ref=f2e26]: Identificación y planes
|
||||
- generic [ref=f2e28]:
|
||||
- img "Teléfono escaneando una flor de loto" [ref=f2e29]
|
||||
- generic [ref=f2e32]:
|
||||
- generic [ref=f2e33]: Loto sagrado
|
||||
- generic [ref=f2e34]: Identificada en 0,8s
|
||||
- generic [ref=f2e35]:
|
||||
- generic [ref=f2e36]:
|
||||
- generic [ref=f2e37]: — La Ventaja
|
||||
- heading "Más que solo identificación. Un enfoque holístico para el cuidado de las plantas." [level=2] [ref=f2e38]
|
||||
- paragraph [ref=f2e39]: Un identificador de plantas usa IA para comparar una foto con una base de datos vegetal. GreenLens conecta esa identificacion con un plan de cuidado, recordatorios y revision de salud.
|
||||
- generic [ref=f2e40]: "Algoritmos actualizados: Mayo 2026"
|
||||
- generic [ref=f2e41]:
|
||||
- heading "¿Sabías que?" [level=3] [ref=f2e46]
|
||||
- list [ref=f2e47]:
|
||||
- listitem [ref=f2e48]:
|
||||
- strong [ref=f2e49]: El exceso de riego es la causa n.º 1 de muerte
|
||||
- text: La mayoría de las plantas de interior mueren por pudrición de la raíz debido al exceso de riego. GreenLens calcula tiempos de secado precisos.
|
||||
- listitem [ref=f2e50]:
|
||||
- strong [ref=f2e51]: La intensidad de la luz disminuye rápidamente
|
||||
- text: La intensidad de la luz disminuye un 50 % a solo 1 metro de la ventana. Los consejos de cuidado tienen en cuenta la ubicación.
|
||||
- listitem [ref=f2e52]:
|
||||
- strong [ref=f2e53]: Las plantas purifican el aire interior
|
||||
- text: Ciertas especies pueden filtrar gran parte de las toxinas del aire. GreenLens destaca las plantas purificadoras.
|
||||
- generic [ref=f2e55]:
|
||||
- generic [ref=f2e56]:
|
||||
- generic [ref=f2e57]: — La Tecnología
|
||||
- heading "Nuestra Metodología" [level=2] [ref=f2e58]
|
||||
- paragraph [ref=f2e59]: Cómo GreenLens convierte una simple foto de teléfono en un análisis botánico completo.
|
||||
- generic [ref=f2e60]:
|
||||
- generic [ref=f2e61]:
|
||||
- generic [ref=f2e62]: "1"
|
||||
- heading "Captura de Imagen" [level=3] [ref=f2e63]
|
||||
- paragraph [ref=f2e64]: La captura de alta resolución analiza la morfología de las hojas, las venas y la estructura del tallo. Se descartan las imágenes borrosas.
|
||||
- generic [ref=f2e65]:
|
||||
- generic [ref=f2e66]: "2"
|
||||
- heading "Extracción de Características" [level=3] [ref=f2e67]
|
||||
- paragraph [ref=f2e68]: Nuestra red neuronal aísla más de 120 características específicas y las compara con una enorme base de datos botánica.
|
||||
- generic [ref=f2e69]:
|
||||
- generic [ref=f2e70]: "3"
|
||||
- heading "Diagnóstico" [level=3] [ref=f2e71]
|
||||
- paragraph [ref=f2e72]: Una segunda pasada busca decoloración, rastros de plagas y marchitamiento, según las enfermedades típicas de cada especie.
|
||||
- generic [ref=f2e73]:
|
||||
- generic [ref=f2e74]: "4"
|
||||
- heading "Generación de Plan" [level=3] [ref=f2e75]
|
||||
- paragraph [ref=f2e76]: La identificación y el estado de salud se combinan en un plan de cuidado individual adaptado al clima y la luz locales.
|
||||
- generic [ref=f2e78]:
|
||||
- generic [ref=f2e80]:
|
||||
- generic [ref=f2e81]: — Perspectiva
|
||||
- heading "GreenLens frente a un identificador basico" [level=2] [ref=f2e82]
|
||||
- paragraph [ref=f2e83]: "Por qué los entusiastas serios de las plantas confían en nuestra plataforma más que en herramientas estándar: datos procesables, no solo curiosidades."
|
||||
- generic [ref=f2e84]:
|
||||
- heading "Consejo Botánico" [level=4] [ref=f2e85]
|
||||
- paragraph [ref=f2e86]: El cuidado regular es mejor que el cuidado intensivo y poco frecuente. Usa recordatorios para una buena rutina.
|
||||
- link "Escaner de plantas →" [ref=f2e87] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- generic [ref=f2e88]:
|
||||
- generic [ref=f2e89]:
|
||||
- generic [ref=f2e90]:
|
||||
- heading "Identificacion por foto" [level=3] [ref=f2e95]
|
||||
- generic [ref=f2e96]: GreenLens
|
||||
- paragraph [ref=f2e97]: Nombre, especie y perfil de cuidado en el mismo flujo.
|
||||
- generic [ref=f2e98]:
|
||||
- generic [ref=f2e99]: Apps basicas
|
||||
- paragraph [ref=f2e100]: Normalmente solo muestran el nombre o enlaces externos.
|
||||
- generic [ref=f2e101]:
|
||||
- generic [ref=f2e102]:
|
||||
- heading "Plan de cuidado" [level=3] [ref=f2e107]
|
||||
- generic [ref=f2e108]: GreenLens
|
||||
- paragraph [ref=f2e109]: Riego, luz y fertilizacion despues del escaneo.
|
||||
- generic [ref=f2e110]:
|
||||
- generic [ref=f2e111]: Apps basicas
|
||||
- paragraph [ref=f2e112]: El usuario debe buscar y configurar todo manualmente.
|
||||
- generic [ref=f2e113]:
|
||||
- generic [ref=f2e114]:
|
||||
- heading "Diagnostico de salud" [level=3] [ref=f2e119]
|
||||
- generic [ref=f2e120]: GreenLens
|
||||
- paragraph [ref=f2e121]: Analiza sintomas como hojas amarillas, manchas o tallos blandos.
|
||||
- generic [ref=f2e122]:
|
||||
- generic [ref=f2e123]: Apps basicas
|
||||
- paragraph [ref=f2e124]: Suele estar ausente o separado de la identificacion.
|
||||
- generic [ref=f2e125]:
|
||||
- article [ref=f2e126]:
|
||||
- generic [ref=f2e127]: Mejor opción
|
||||
- heading "Elige GreenLens si:" [level=2] [ref=f2e128]
|
||||
- list [ref=f2e129]:
|
||||
- listitem [ref=f2e130]:
|
||||
- generic [ref=f2e134]: Quieres identificar una planta y saber que hacer despues.
|
||||
- listitem [ref=f2e135]:
|
||||
- generic [ref=f2e139]: Tienes plantas de interior, jardin o suculentas y necesitas una guia sencilla.
|
||||
- listitem [ref=f2e140]:
|
||||
- generic [ref=f2e144]: Quieres guardar tus plantas con notas y recordatorios.
|
||||
- article [ref=f2e145]:
|
||||
- generic [ref=f2e146]: No es ideal
|
||||
- heading "GreenLens no es la herramienta adecuada si:" [level=2] [ref=f2e147]
|
||||
- list [ref=f2e148]:
|
||||
- listitem [ref=f2e149]:
|
||||
- generic [ref=f2e150]: –
|
||||
- generic [ref=f2e151]: Necesitas identificacion botanica profesional para especies raras.
|
||||
- listitem [ref=f2e152]:
|
||||
- generic [ref=f2e153]: –
|
||||
- generic [ref=f2e154]: Solo quieres una busqueda ocasional sin seguimiento de cuidado.
|
||||
- generic [ref=f2e156]:
|
||||
- heading "Preguntas Frecuentes" [level=2] [ref=f2e158]
|
||||
- generic [ref=f2e159]:
|
||||
- generic [ref=f2e160]:
|
||||
- heading "¿GreenLens identifica plantas desde fotos antiguas?" [level=3] [ref=f2e161]
|
||||
- paragraph [ref=f2e162]: Si. Puedes usar una foto de la galeria o tomar una nueva imagen desde la app.
|
||||
- generic [ref=f2e163]:
|
||||
- heading "¿GreenLens tambien ayuda con el cuidado?" [level=3] [ref=f2e164]
|
||||
- paragraph [ref=f2e165]: Si. Cada identificacion se conecta con recomendaciones de riego, luz, fertilizacion y seguimiento.
|
||||
- generic [ref=f2e166]:
|
||||
- heading "¿Funciona para plantas de interior?" [level=3] [ref=f2e167]
|
||||
- paragraph [ref=f2e168]: Si. GreenLens cubre plantas comunes de interior, plantas de jardin, flores y suculentas.
|
||||
- generic [ref=f2e169]:
|
||||
- generic [ref=f2e170]:
|
||||
- generic [ref=f2e171]: Relacionado
|
||||
- heading "Más guías" [level=2] [ref=f2e172]
|
||||
- generic [ref=f2e173]:
|
||||
- link [ref=f2e174] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- heading "Escaner de plantas" [level=3] [ref=f2e175]
|
||||
- paragraph [ref=f2e176]: Escanea hojas, flores o plantas completas desde el movil.
|
||||
- link [ref=f2e177] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- heading "App para cuidar plantas" [level=3] [ref=f2e178]
|
||||
- paragraph [ref=f2e179]: Organiza riego, luz y recordatorios para cada planta.
|
||||
- link [ref=f2e180] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- heading "Diagnosticar enfermedades de plantas" [level=3] [ref=f2e181]
|
||||
- paragraph [ref=f2e182]: Analiza sintomas y recibe el siguiente paso mas seguro.
|
||||
- contentinfo [ref=f2e183]:
|
||||
- generic [ref=f2e184]:
|
||||
- generic [ref=f2e185]:
|
||||
- generic [ref=f2e186]:
|
||||
- generic [ref=f2e187]: Download
|
||||
- heading "¿Listo para un mejor cuidado de plantas?" [level=2] [ref=f2e188]: ¿Listo paraun mejorcuidado de plantas?
|
||||
- paragraph [ref=f2e189]: GreenLens te ayuda a identificar, comprender y cuidar tus plantas. ¿Preguntas? Contáctanos a través de la página de soporte.
|
||||
- link [ref=f2e191] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=f2e192]
|
||||
- paragraph [ref=f2e193]:
|
||||
- text: La aplicación ya está disponible en las tiendas.
|
||||
- link "Soporte" [ref=f2e194] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- img "GreenLens" [ref=f2e196]
|
||||
- generic [ref=f2e198]:
|
||||
- generic [ref=f2e199]:
|
||||
- heading "GREENLENS" [level=3] [ref=f2e200]
|
||||
- paragraph [ref=f2e201]: La app para los amantes de las plantas que combina análisis, identificación y cuidado en un solo lugar.
|
||||
- generic [ref=f2e202]:
|
||||
- heading "Producto" [level=4] [ref=f2e203]
|
||||
- list [ref=f2e204]:
|
||||
- listitem [ref=f2e205]:
|
||||
- link "Funciones" [ref=f2e206] [cursor=pointer]:
|
||||
- /url: "#advantage"
|
||||
- listitem [ref=f2e207]:
|
||||
- link "Tecnología" [ref=f2e208] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- listitem [ref=f2e209]:
|
||||
- link "Descargar la App" [ref=f2e210] [cursor=pointer]:
|
||||
- /url: "#download"
|
||||
- listitem [ref=f2e211]:
|
||||
- link "Soporte" [ref=f2e212] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=f2e213]:
|
||||
- heading "Empresa" [level=4] [ref=f2e214]
|
||||
- list [ref=f2e215]:
|
||||
- listitem [ref=f2e216]:
|
||||
- link "Cómo funciona" [ref=f2e217] [cursor=pointer]:
|
||||
- /url: "#methodology"
|
||||
- listitem [ref=f2e218]:
|
||||
- link "FAQ" [ref=f2e219] [cursor=pointer]:
|
||||
- /url: "#faq"
|
||||
- listitem [ref=f2e220]:
|
||||
- link "Soporte" [ref=f2e221] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=f2e222]:
|
||||
- heading "Legal" [level=4] [ref=f2e223]
|
||||
- list [ref=f2e224]:
|
||||
- listitem [ref=f2e225]:
|
||||
- link "Aviso Legal" [ref=f2e226] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- listitem [ref=f2e227]:
|
||||
- link "Privacidad" [ref=f2e228] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- listitem [ref=f2e229]:
|
||||
- link "Términos de uso" [ref=f2e230] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=f2e231]:
|
||||
- generic [ref=f2e232]:
|
||||
- heading "Use Cases" [level=4] [ref=f2e233]
|
||||
- list [ref=f2e234]:
|
||||
- listitem [ref=f2e235]:
|
||||
- link "Best Plant Identification App" [ref=f2e236] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- listitem [ref=f2e237]:
|
||||
- link "Plant Identifier App" [ref=f2e238] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- listitem [ref=f2e239]:
|
||||
- link "Plant Scanner" [ref=f2e240] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- listitem [ref=f2e241]:
|
||||
- link "Flower Scanner" [ref=f2e242] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- listitem [ref=f2e243]:
|
||||
- link "Houseplant Identifier" [ref=f2e244] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- listitem [ref=f2e245]:
|
||||
- link "Succulent Identifier" [ref=f2e246] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- listitem [ref=f2e247]:
|
||||
- link "Identify Plant by Photo" [ref=f2e248] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- listitem [ref=f2e249]:
|
||||
- link "Plant Disease Identifier" [ref=f2e250] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- listitem [ref=f2e251]:
|
||||
- link "Plant Health App" [ref=f2e252] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- listitem [ref=f2e253]:
|
||||
- link "Plant Care App" [ref=f2e254] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- listitem [ref=f2e255]:
|
||||
- link "vs PictureThis" [ref=f2e256] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- listitem [ref=f2e257]:
|
||||
- link "vs Plantum" [ref=f2e258] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- listitem [ref=f2e259]:
|
||||
- link "vs iNaturalist" [ref=f2e260] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- listitem [ref=f2e261]:
|
||||
- link "vs Google Lens" [ref=f2e262] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=f2e263]:
|
||||
- heading "Auf Deutsch" [level=4] [ref=f2e264]
|
||||
- list [ref=f2e265]:
|
||||
- listitem [ref=f2e266]:
|
||||
- link "Pflanzen erkennen kostenlos" [ref=f2e267] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- listitem [ref=f2e268]:
|
||||
- link "Pflanzen erkennen App" [ref=f2e269] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- listitem [ref=f2e270]:
|
||||
- link "Pflanzen bestimmen per Foto" [ref=f2e271] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- listitem [ref=f2e272]:
|
||||
- link "Blumen Scanner kostenlos" [ref=f2e273] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- listitem [ref=f2e274]:
|
||||
- link "Zimmerpflanzen bestimmen" [ref=f2e275] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- listitem [ref=f2e276]:
|
||||
- link "Pflanzen Pflege App" [ref=f2e277] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- listitem [ref=f2e278]:
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=f2e279] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- listitem [ref=f2e280]:
|
||||
- link "Gieß-Erinnerung App" [ref=f2e281] [cursor=pointer]:
|
||||
- /url: /giess-erinnerung-app
|
||||
- generic [ref=f2e282]:
|
||||
- heading "En español" [level=4] [ref=f2e283]
|
||||
- list [ref=f2e284]:
|
||||
- listitem [ref=f2e285]:
|
||||
- link "Identificador de plantas" [ref=f2e286] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- listitem [ref=f2e287]:
|
||||
- link "App para cuidar plantas" [ref=f2e288] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- listitem [ref=f2e289]:
|
||||
- link "Diagnosticar enfermedades de plantas" [ref=f2e290] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- listitem [ref=f2e291]:
|
||||
- link "Escaner de plantas" [ref=f2e292] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- listitem [ref=f2e293]:
|
||||
- link "GreenLens vs Google Lens para plantas" [ref=f2e294] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=f2e295]: GREENLENS
|
||||
- generic [ref=f2e297]:
|
||||
- paragraph [ref=f2e298]: © 2026 GreenLens. Todos los derechos reservados.
|
||||
- generic [ref=f2e299]: GREENLENS
|
||||
- alert [ref=f2e300]
|
||||
353
.playwright-mcp/page-2026-07-10T09-14-34-355Z.yml
Normal file
@@ -0,0 +1,353 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- navigation "Main navigation" [ref=e2]:
|
||||
- generic [ref=e3]:
|
||||
- link "GreenLens Home" [ref=e4] [cursor=pointer]:
|
||||
- /url: /
|
||||
- text: GREENLENS
|
||||
- generic [ref=e5]:
|
||||
- link "Funciones" [ref=e6] [cursor=pointer]:
|
||||
- /url: /#features
|
||||
- link "Tecnologia" [ref=e7] [cursor=pointer]:
|
||||
- /url: /#intelligence
|
||||
- link "FAQ" [ref=e8] [cursor=pointer]:
|
||||
- /url: /#faq
|
||||
- link "Como funciona" [ref=e9] [cursor=pointer]:
|
||||
- /url: /#how
|
||||
- link "Support" [ref=e10] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- link "Descargar" [ref=e11] [cursor=pointer]:
|
||||
- /url: /#cta
|
||||
- group "Language selector" [ref=e12]:
|
||||
- link "DE" [ref=e13] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "EN" [ref=e14] [cursor=pointer]:
|
||||
- /url: /en
|
||||
- link "ES" [ref=e15] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Empezar" [ref=e16] [cursor=pointer]:
|
||||
- /url: /#cta
|
||||
- main [ref=e17]:
|
||||
- region "Hero" [ref=e18]:
|
||||
- generic [ref=e22]:
|
||||
- generic [ref=e23]: GreenLens App - Inteligencia botanica
|
||||
- heading [level=1] [ref=e26]:
|
||||
- text: Tu jardinurbano,
|
||||
- emphasis [ref=e27]: mejor cuidado.
|
||||
- paragraph [ref=e28]: Escanea plantas, entiende lo que necesitan y organiza cuidado, recordatorios y coleccion en una sola app.
|
||||
- generic [ref=e29]:
|
||||
- link "Explorar la app" [ref=e30] [cursor=pointer]:
|
||||
- /url: "#cta"
|
||||
- link "Saber mas" [ref=e34] [cursor=pointer]:
|
||||
- /url: "#features"
|
||||
- link "GreenLens on Product Hunt" [ref=e35] [cursor=pointer]:
|
||||
- /url: https://www.producthunt.com/products/greenlens?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-greenlens
|
||||
- img "GreenLens - Scan plants, understand care, and grow smarter | Product Hunt" [ref=e36]
|
||||
- group "En que te ayuda GreenLens?" [ref=e37]:
|
||||
- paragraph [ref=e38]: En que te ayuda GreenLens?
|
||||
- generic [ref=e39]:
|
||||
- button "Quiero identificar la planta." [ref=e40] [cursor=pointer]
|
||||
- button "Quiero entender problemas de cuidado." [ref=e42] [cursor=pointer]
|
||||
- generic [ref=e45]:
|
||||
- generic [ref=e46]: Scan it
|
||||
- generic [ref=e48]: Track it
|
||||
- generic [ref=e50]: Live Design
|
||||
- generic [ref=e52]: Urban Jungle
|
||||
- generic [ref=e54]: Botanical KI
|
||||
- generic [ref=e56]: Pflege-Tipps
|
||||
- generic [ref=e58]: Scan it
|
||||
- generic [ref=e60]: Track it
|
||||
- generic [ref=e62]: Live Design
|
||||
- generic [ref=e64]: Urban Jungle
|
||||
- generic [ref=e66]: Botanical KI
|
||||
- generic [ref=e68]: Pflege-Tipps
|
||||
- generic [ref=e70]: Scan it
|
||||
- generic [ref=e72]: Track it
|
||||
- generic [ref=e74]: Live Design
|
||||
- generic [ref=e76]: Urban Jungle
|
||||
- generic [ref=e78]: Botanical KI
|
||||
- generic [ref=e80]: Pflege-Tipps
|
||||
- generic [ref=e82]: Scan it
|
||||
- generic [ref=e84]: Track it
|
||||
- generic [ref=e86]: Live Design
|
||||
- generic [ref=e88]: Urban Jungle
|
||||
- generic [ref=e90]: Botanical KI
|
||||
- generic [ref=e92]: Pflege-Tipps
|
||||
- region [ref=e94]:
|
||||
- generic [ref=e95]:
|
||||
- generic [ref=e96]:
|
||||
- paragraph [ref=e97]: Funciones
|
||||
- heading "Todo lo que tu jardin urbano necesita." [level=2] [ref=e98]: Todo lo que tujardin urbano necesita.
|
||||
- paragraph [ref=e99]: Desde la primera identificacion hasta el cuidado continuo, GreenLens te ayuda a entender mejor tus plantas y a organizarte. El lexico cubre mas de 450 especies de plantas.
|
||||
- generic [ref=e100]:
|
||||
- generic [ref=e101]:
|
||||
- img "Persona escaneando una planta con la app GreenLens" [ref=e102]
|
||||
- generic [ref=e104]:
|
||||
- generic [ref=e105]: Escaneo IA
|
||||
- heading "Escanéala." [level=3] [ref=e106]
|
||||
- paragraph [ref=e107]: Apunta la cámara a cualquier planta – GreenLens la identifica en milisegundos y entrega toda la información.
|
||||
- generic [ref=e108]:
|
||||
- generic [ref=e109]:
|
||||
- img "Plantas en un estante con planes de cuidado" [ref=e110]
|
||||
- generic [ref=e112]:
|
||||
- generic [ref=e113]: Seguimiento
|
||||
- heading "Monitoréala." [level=3] [ref=e114]
|
||||
- paragraph [ref=e115]: Plan de riego, necesidades de luz y crecimiento – todo en una línea de tiempo.
|
||||
- generic [ref=e116]:
|
||||
- img "Colección de plantas estilo jungla urbana" [ref=e117]
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e120]: Colección
|
||||
- heading "Hazla crecer." [level=3] [ref=e121]
|
||||
- paragraph [ref=e122]: Construye tu biblioteca digital de plantas – con fotos y notas.
|
||||
- generic [ref=e123]:
|
||||
- generic [ref=e128]:
|
||||
- heading "Recordatorios inteligentes" [level=4] [ref=e129]
|
||||
- paragraph [ref=e130]: Nunca olvides regar – personalizado para cada planta.
|
||||
- generic [ref=e135]:
|
||||
- heading "Diagnóstico y ayuda" [level=4] [ref=e136]
|
||||
- paragraph [ref=e137]: La IA detecta enfermedades y plagas al instante.
|
||||
- generic [ref=e143]:
|
||||
- heading "Consejos por ubicación" [level=4] [ref=e144]
|
||||
- paragraph [ref=e145]: Consejos basados en tu clima y condiciones de luz.
|
||||
- region [ref=e146]:
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- paragraph [ref=e149]: Ayuda para plantas
|
||||
- heading [level=2] [ref=e150]:
|
||||
- text: Menos adivinanzas.
|
||||
- emphasis [ref=e151]: Entiende mas rapido que puede necesitar tu planta.
|
||||
- paragraph [ref=e152]: GreenLens te ayuda a interpretar sintomas, identificar plantas y obtener pasos de cuidado mas claros.
|
||||
- generic [ref=e153]:
|
||||
- generic "Comparador antes y despues" [ref=e154]:
|
||||
- generic [ref=e155]: Antes
|
||||
- generic [ref=e159]: Despues
|
||||
- slider "Comparador antes y despues" [ref=e163]: "50"
|
||||
- generic: Botanical Intelligence
|
||||
- generic [ref=e164]:
|
||||
- generic [ref=e169]:
|
||||
- heading "Evaluar puntas marrones" [level=4] [ref=e170]
|
||||
- paragraph [ref=e171]: Pasa de la duda a una causa mas clara
|
||||
- generic [ref=e177]:
|
||||
- heading "Ordenar la rutina" [level=4] [ref=e178]
|
||||
- paragraph [ref=e179]: Menos caos con luz, riego e intervalos
|
||||
- generic [ref=e185]:
|
||||
- heading "Guardar ayuda en un lugar" [level=4] [ref=e186]
|
||||
- paragraph [ref=e187]: Escaneo, coleccion y notas juntos
|
||||
- region [ref=e188]:
|
||||
- generic [ref=e190]:
|
||||
- paragraph [ref=e191]: Tecnologia
|
||||
- heading [level=2] [ref=e192]:
|
||||
- text: Botanical
|
||||
- emphasis [ref=e193]: Intelligence.
|
||||
- paragraph [ref=e194]: GreenLens combina resultados de escaneo, contexto de cuidado y gestion de coleccion en una sola app para ayudarte a pasar de una foto a una decision mas clara.
|
||||
- generic [ref=e195]:
|
||||
- generic [ref=e201]:
|
||||
- heading "Identificacion basada en escaneo" [level=4] [ref=e202]
|
||||
- paragraph [ref=e203]: Pasa de una foto a un contexto mas claro en pocos pasos.
|
||||
- generic [ref=e208]:
|
||||
- heading "Guias centradas en cuidado" [level=4] [ref=e209]
|
||||
- paragraph [ref=e210]: Te ayuda a decidir los siguientes pasos mas rapido.
|
||||
- generic [ref=e216]:
|
||||
- heading "Coleccion e historial" [level=4] [ref=e217]
|
||||
- paragraph [ref=e218]: Guarda escaneos, plantas y notas en un solo lugar.
|
||||
- generic [ref=e223]:
|
||||
- heading "Lexico y busqueda" [level=4] [ref=e224]
|
||||
- paragraph [ref=e225]: Busca plantas y compara informacion dentro de una app.
|
||||
- region [ref=e226]:
|
||||
- generic [ref=e228]:
|
||||
- paragraph [ref=e229]: Cómo funciona
|
||||
- heading [level=2] [ref=e230]:
|
||||
- text: Más fácilde lo que
|
||||
- emphasis [ref=e231]: crees.
|
||||
- generic [ref=e232]:
|
||||
- generic [ref=e233]:
|
||||
- generic [ref=e234]: "01"
|
||||
- generic [ref=e235]:
|
||||
- heading "Fotografía tu planta" [level=4] [ref=e236]
|
||||
- paragraph [ref=e237]: Abre la app, apunta la cámara a tu planta y toca Escanear. Eso es todo.
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e239]: "02"
|
||||
- generic [ref=e240]:
|
||||
- heading "La IA identifica al instante" [level=4] [ref=e241]
|
||||
- paragraph [ref=e242]: En menos de un segundo obtienes el nombre exacto, la especie y todos los datos clave.
|
||||
- generic [ref=e243]:
|
||||
- generic [ref=e244]: "03"
|
||||
- generic [ref=e245]:
|
||||
- heading "Recibe el plan de cuidado" [level=4] [ref=e246]
|
||||
- paragraph [ref=e247]: GreenLens crea automáticamente un plan de cuidado personalizado para tu planta y ubicación.
|
||||
- generic [ref=e248]:
|
||||
- generic [ref=e249]: "04"
|
||||
- generic [ref=e250]:
|
||||
- heading "Seguimiento del crecimiento" [level=4] [ref=e251]
|
||||
- paragraph [ref=e252]: Documenta fotos, registra el riego y recibe recordatorios de citas de cuidado importantes.
|
||||
- generic [ref=e254]:
|
||||
- generic [ref=e255]:
|
||||
- generic [ref=e256]: Preguntas
|
||||
- heading [level=2] [ref=e257]:
|
||||
- text: Preguntas
|
||||
- emphasis [ref=e258]: Frecuentes
|
||||
- paragraph [ref=e259]: Todo lo que necesitas saber sobre GreenLens y el inicio.
|
||||
- generic [ref=e260]:
|
||||
- heading "Como identifica GreenLens una planta?" [level=3] [ref=e263] [cursor=pointer]
|
||||
- heading "Es GreenLens gratuito?" [level=3] [ref=e269] [cursor=pointer]
|
||||
- heading "Puedo usar GreenLens sin conexion?" [level=3] [ref=e275] [cursor=pointer]
|
||||
- heading "Para que tipo de plantas puedo usar GreenLens?" [level=3] [ref=e281] [cursor=pointer]
|
||||
- heading "Como empiezo mi coleccion de plantas?" [level=3] [ref=e287] [cursor=pointer]
|
||||
- region [ref=e291]:
|
||||
- generic [ref=e292]:
|
||||
- generic [ref=e293]:
|
||||
- paragraph [ref=e294]: Guías
|
||||
- heading "Identifica plantas más rápido y actúa con claridad." [level=2] [ref=e295]
|
||||
- generic [ref=e296]:
|
||||
- link [ref=e297] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- paragraph [ref=e298]: Identificar
|
||||
- heading "Identificador de plantas" [level=3] [ref=e299]
|
||||
- paragraph [ref=e300]: Escanea una planta y entiende qué necesita en segundos.
|
||||
- link [ref=e301] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- paragraph [ref=e302]: Escáner
|
||||
- heading "Escáner de plantas" [level=3] [ref=e303]
|
||||
- paragraph [ref=e304]: Apunta la cámara a cualquier planta y recibe nombre, cuidados y diagnóstico.
|
||||
- link [ref=e305] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- paragraph [ref=e306]: Cuidado
|
||||
- heading "App para cuidar plantas" [level=3] [ref=e307]
|
||||
- paragraph [ref=e308]: Planes de cuidado, recordatorios de riego y tu colección en una sola app.
|
||||
- link [ref=e309] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- paragraph [ref=e310]: Diagnóstico
|
||||
- heading "Diagnosticar enfermedades" [level=3] [ref=e311]
|
||||
- paragraph [ref=e312]: ¿Hojas amarillas o puntas marrones? Analiza los síntomas con una foto.
|
||||
- link [ref=e313] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- paragraph [ref=e314]: Google Lens
|
||||
- heading "GreenLens vs Google Lens" [level=3] [ref=e315]
|
||||
- paragraph [ref=e316]: Compara Google Lens con GreenLens para identificación, cuidado y diagnóstico.
|
||||
- region [ref=e317]:
|
||||
- generic [ref=e320]:
|
||||
- paragraph [ref=e321]: Descarga
|
||||
- heading [level=2] [ref=e322]:
|
||||
- text: Listo para un mejor
|
||||
- emphasis [ref=e323]: cuidado de plantas?
|
||||
- paragraph [ref=e324]: GreenLens te ayuda a identificar, entender y cuidar tus plantas. Si la ficha de la tienda aun no esta activa, usa la pagina de soporte.
|
||||
- generic [ref=e325]:
|
||||
- link "App Store or support" [ref=e326] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/greenlens-pro/id6759843546?l=en-GB
|
||||
- generic [ref=e329]:
|
||||
- generic [ref=e330]: Descargar en
|
||||
- text: App Store
|
||||
- link "Google Play or contact" [ref=e331] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e334]:
|
||||
- generic [ref=e335]: Contacto
|
||||
- text: Enviar correo
|
||||
- paragraph [ref=e336]:
|
||||
- text: La app ya esta disponible en las tiendas.
|
||||
- link "Support" [ref=e337] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- contentinfo [ref=e338]:
|
||||
- generic [ref=e339]:
|
||||
- generic [ref=e340]:
|
||||
- generic [ref=e341]:
|
||||
- link "GREENLENS" [ref=e342] [cursor=pointer]:
|
||||
- /url: /
|
||||
- paragraph [ref=e343]: La app para quienes quieren identificacion, cuidado y coleccion de plantas en un solo lugar.
|
||||
- link [ref=e345] [cursor=pointer]:
|
||||
- /url: https://apps.apple.com/de/app/plant-doctor-greenlens-pro/id6759843546?l=en-GB
|
||||
- img "Download on the App Store" [ref=e346]
|
||||
- generic [ref=e347]:
|
||||
- generic [ref=e348]: Producto
|
||||
- link "Funciones" [ref=e349] [cursor=pointer]:
|
||||
- /url: /#features
|
||||
- link "Tecnologia" [ref=e350] [cursor=pointer]:
|
||||
- /url: /#intelligence
|
||||
- link "Descargar" [ref=e351] [cursor=pointer]:
|
||||
- /url: /#cta
|
||||
- link "Support" [ref=e352] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e353]:
|
||||
- generic [ref=e354]: Recursos
|
||||
- link "Como funciona" [ref=e355] [cursor=pointer]:
|
||||
- /url: /#how
|
||||
- link "FAQ" [ref=e356] [cursor=pointer]:
|
||||
- /url: /#faq
|
||||
- link "Support" [ref=e357] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- generic [ref=e358]:
|
||||
- generic [ref=e359]: Legal
|
||||
- link "Aviso legal" [ref=e360] [cursor=pointer]:
|
||||
- /url: /imprint
|
||||
- link "Privacidad" [ref=e361] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- link "Terminos del servicio" [ref=e362] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- generic [ref=e363]:
|
||||
- generic [ref=e364]: Identify & Care
|
||||
- link "Best Plant ID App" [ref=e365] [cursor=pointer]:
|
||||
- /url: /best-plant-identification-app
|
||||
- link "Plant Identifier App" [ref=e366] [cursor=pointer]:
|
||||
- /url: /plant-identifier-app
|
||||
- link "Plant Scanner" [ref=e367] [cursor=pointer]:
|
||||
- /url: /plant-scanner
|
||||
- link "Flower Scanner" [ref=e368] [cursor=pointer]:
|
||||
- /url: /flower-scanner
|
||||
- link "Houseplant Identifier" [ref=e369] [cursor=pointer]:
|
||||
- /url: /houseplant-identifier
|
||||
- link "Succulent Identifier" [ref=e370] [cursor=pointer]:
|
||||
- /url: /succulent-identifier
|
||||
- link "Identify Plant by Photo" [ref=e371] [cursor=pointer]:
|
||||
- /url: /identify-plant-photo
|
||||
- link "Plant Disease Identifier" [ref=e372] [cursor=pointer]:
|
||||
- /url: /plant-disease-identifier
|
||||
- link "Plant Health App" [ref=e373] [cursor=pointer]:
|
||||
- /url: /plant-health-app
|
||||
- link "Plant Care App" [ref=e374] [cursor=pointer]:
|
||||
- /url: /plant-care-app
|
||||
- link "vs PictureThis" [ref=e375] [cursor=pointer]:
|
||||
- /url: /vs/picturethis
|
||||
- link "vs Plantum" [ref=e376] [cursor=pointer]:
|
||||
- /url: /vs/plantum
|
||||
- link "vs iNaturalist" [ref=e377] [cursor=pointer]:
|
||||
- /url: /vs/inaturalist
|
||||
- link "vs Google Lens" [ref=e378] [cursor=pointer]:
|
||||
- /url: /vs/google-lens
|
||||
- generic [ref=e379]:
|
||||
- generic [ref=e380]: Pflanzen & Erkennen
|
||||
- link "GreenLens auf Deutsch" [ref=e381] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "Pflanzen erkennen kostenlos" [ref=e382] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-kostenlos
|
||||
- link "Pflanzen erkennen App" [ref=e383] [cursor=pointer]:
|
||||
- /url: /pflanzen-erkennen-app
|
||||
- link "Pflanzen bestimmen" [ref=e384] [cursor=pointer]:
|
||||
- /url: /pflanzen-bestimmen
|
||||
- link "Blumen Scanner" [ref=e385] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Blumen per Foto erkennen" [ref=e386] [cursor=pointer]:
|
||||
- /url: /blumen-scanner
|
||||
- link "Zimmerpflanzen bestimmen" [ref=e387] [cursor=pointer]:
|
||||
- /url: /zimmerpflanzen-bestimmen
|
||||
- link "Pflanzen Pflege App" [ref=e388] [cursor=pointer]:
|
||||
- /url: /pflanzen-pflege-app
|
||||
- link "Pflanzenkrankheiten erkennen" [ref=e389] [cursor=pointer]:
|
||||
- /url: /pflanzen-krankheiten-erkennen
|
||||
- generic [ref=e390]:
|
||||
- generic [ref=e391]: Español
|
||||
- link "GreenLens en español" [ref=e392] [cursor=pointer]:
|
||||
- /url: /es
|
||||
- link "Identificador de plantas" [ref=e393] [cursor=pointer]:
|
||||
- /url: /es/identificador-de-plantas
|
||||
- link "Escaner de plantas" [ref=e394] [cursor=pointer]:
|
||||
- /url: /es/escaner-de-plantas
|
||||
- link "App para cuidar plantas" [ref=e395] [cursor=pointer]:
|
||||
- /url: /es/app-para-cuidar-plantas
|
||||
- link "Diagnosticar enfermedades" [ref=e396] [cursor=pointer]:
|
||||
- /url: /es/diagnosticar-enfermedades-plantas
|
||||
- link "GreenLens vs Google Lens" [ref=e397] [cursor=pointer]:
|
||||
- /url: /es/comparar/google-lens
|
||||
- generic [ref=e398]: GREENLENS
|
||||
- generic [ref=e399]:
|
||||
- paragraph [ref=e400]: © 2026 GreenLens. Todos los derechos reservados.
|
||||
- link "Support" [ref=e401] [cursor=pointer]:
|
||||
- /url: /support
|
||||
- alert [ref=e402]
|
||||
37
CHECKPOINT-seo-2026-08-04.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Checkpoint SEO-Umsetzung — 04.08.2026
|
||||
|
||||
## Erledigt
|
||||
|
||||
**Phase 0 — Titles/Metas** (`greenlns-landing/lib/seoPages.ts`)
|
||||
|
||||
- `blumen-scanner`: Title/Meta/H1 auf exaktes Keyword „Blumen Scanner" + kostenlos + 2026
|
||||
- `flower-scanner`: dito auf Englisch
|
||||
- `pflanzen-erkennen-app`: Title/Meta, „Pflanzenerkenner" ergänzt (Pos. 10,6 / 49 Imp.)
|
||||
- `giess-erinnerung-app`: Title/Meta/H1, „Gießerinnerung" zusammengeschrieben (Query-Form)
|
||||
|
||||
**Phase 1 — Scan-Logging**
|
||||
|
||||
- Neu: `server/lib/scanEvents.js` — Tabelle `scan_events`, bewusst ohne `user_id`/Bildreferenz. `recordScanEvent` schluckt Fehler, damit ein Log-Fehler nie einen Scan kippt. Zusätzlich `getScanEventStats` für spätere Auswertung.
|
||||
- `server/index.js`: `ensureScanEventSchema` im Start-Hook, `recordScanEvent` nach Erfolg in `/v1/scan` (species, confidence, locale) und `/v1/health-check` (diagnosis, health_status).
|
||||
- `app/scanner.tsx`: `species` und `health_status` als Properties auf `paid_scan_completed`.
|
||||
- Syntaxcheck Backend lief durch (`node --check index.js`).
|
||||
|
||||
**Phase 3 — drei neue Seiten**
|
||||
|
||||
Profile in `contentClusterSeoPages` (`lib/seoPages.ts`), Slugs in `germanSeoPageSlugs`, Routen unter `app/<slug>/page.tsx`:
|
||||
|
||||
- `/giessplan-zimmerpflanzen` — 4 Sections, Intervalle für 8 Pflanzengruppen, Pflegekarte, 5 HowTo-Schritte, 6 FAQs
|
||||
- `/pflanzen-diagnose` — 5 Sections inkl. Symptomzuordnung, Schädlinge, Pilze, „nicht mehr zu retten", 5 HowTo-Schritte, 6 FAQs
|
||||
- `/welche-pflanze-ist-das` — 4 Sections, 6 Bestimmungswege, Merkmale ohne App, Giftigkeitshinweis, 5 HowTo-Schritte, 6 FAQs
|
||||
|
||||
## Offen
|
||||
|
||||
1. **Build/Typecheck nicht gelaufen.** `cd greenlns-landing && npm run build`. Die drei Profile sind neu getippt — wenn etwas bricht, dann dort.
|
||||
2. **Interne Rückverlinkung fehlt.** Die neuen Seiten verlinken nach außen, aber noch zeigt kein bestehendes Profil auf sie. `relatedLinks` ergänzen bei:
|
||||
- → `/giessplan-zimmerpflanzen`: in `giess-erinnerung-app`, `pflanzen-pflege-app`, `ueberwaessert-oder-zu-trocken`
|
||||
- → `/pflanzen-diagnose`: in `pflanzen-krankheiten-erkennen`, `gelbe-blaetter-zimmerpflanze`, `braune-blattspitzen`, `trauermuecken-blumenerde`, `wurzelfaeule-erkennen`
|
||||
- → `/welche-pflanze-ist-das`: in `pflanzen-bestimmen`, `zimmerpflanzen-bestimmen`, `blumen-scanner`, `vs/google-lens`
|
||||
3. **Hero-Bilder.** Alle drei Seiten laufen auf `/hero.jpg`. Eigene `heroImage` + `heroImageAlt` setzen — bei `/pflanzen-diagnose` sind Symptomfotos ohnehin der eigentliche Ranking-Faktor (`pflanzenkrankheiten bilder` ist ein eigenes Keyword).
|
||||
4. **Privacy Policy.** Ein Satz zu anonymer, aggregierter Scan-Statistik ergänzen.
|
||||
5. **Phase 2 Benchmark** — liegt bei dir: 50 Pflanzen fotografieren, durch die vier Apps schicken. Speist danach `/welche-pflanze-ist-das`.
|
||||
6. **CTR-Gegenprüfung** der Phase-0-Änderungen in GSC in 4 Wochen, also ~KW 36. Änderungsdatum: 04.08.2026.
|
||||
BIN
GreenLens_CTR_Optimierung.docx
Normal file
BIN
GreenLens_OnPage_Rewrites.docx
Normal file
@@ -30,7 +30,7 @@ describe('server auth account deletion', () => {
|
||||
));
|
||||
expect(billingAccountDeletes).toHaveLength(1);
|
||||
|
||||
const signupChecks = get.mock.calls.filter(([, sql], params) => (
|
||||
const signupChecks = get.mock.calls.filter(([, sql, params]) => (
|
||||
typeof sql === 'string'
|
||||
&& sql.includes('SELECT id FROM auth_users WHERE LOWER(email)')
|
||||
&& params?.[0] === email
|
||||
|
||||
@@ -17,8 +17,8 @@ describe('server billing timestamp normalization', () => {
|
||||
userId: 'usr_mnjcdwpo_ax9lf68b',
|
||||
plan: 'free',
|
||||
provider: 'revenuecat',
|
||||
cycleStartedAt: new Date('2026-04-01T00:00:00.000Z'),
|
||||
cycleEndsAt: new Date('2026-05-01T00:00:00.000Z'),
|
||||
cycleStartedAt: new Date('2027-04-01T00:00:00.000Z'),
|
||||
cycleEndsAt: new Date('2027-05-01T00:00:00.000Z'),
|
||||
monthlyAllowance: 15,
|
||||
usedThisCycle: 0,
|
||||
topupBalance: 0,
|
||||
@@ -37,8 +37,8 @@ describe('server billing timestamp normalization', () => {
|
||||
expect(upsertCall).toBeTruthy();
|
||||
|
||||
const params = upsertCall[2];
|
||||
expect(params[3]).toBe('2026-04-01T00:00:00.000Z');
|
||||
expect(params[4]).toBe('2026-05-01T00:00:00.000Z');
|
||||
expect(params[3]).toBe('2027-04-01T00:00:00.000Z');
|
||||
expect(params[4]).toBe('2027-05-01T00:00:00.000Z');
|
||||
expect(params[3]).not.toContain('Coordinated Universal Time');
|
||||
expect(params[4]).not.toContain('Coordinated Universal Time');
|
||||
});
|
||||
|
||||
@@ -241,6 +241,76 @@ describe('mockBackendService billing simulation', () => {
|
||||
expect(response.billing.entitlement.renewsAt).toBe('2026-04-30T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('grants pro from a weekly_pro purchase and renews after 7 days', async () => {
|
||||
const response = await mockBackendService.simulatePurchase({
|
||||
userId: 'test-user-weekly-purchase',
|
||||
idempotencyKey: 'weekly-1',
|
||||
productId: 'weekly_pro',
|
||||
});
|
||||
|
||||
expect(response.appliedProduct).toBe('weekly_pro');
|
||||
expect(response.billing.entitlement.plan).toBe('pro');
|
||||
expect(response.billing.credits.monthlyAllowance).toBe(100);
|
||||
|
||||
// Das Wochenabo verlaengert sich nach 7 Tagen, nicht nach 30 wie die anderen Plaene.
|
||||
const renewsInDays = (new Date(response.billing.entitlement.renewsAt as string).getTime() - Date.now())
|
||||
/ (24 * 60 * 60 * 1000);
|
||||
expect(renewsInDays).toBeCloseTo(7, 3);
|
||||
});
|
||||
|
||||
it('offers weekly_pro as a purchasable product', async () => {
|
||||
const response = await mockBackendService.simulatePurchase({
|
||||
userId: 'test-user-weekly-products',
|
||||
idempotencyKey: 'weekly-products-1',
|
||||
productId: 'topup_small',
|
||||
});
|
||||
|
||||
expect(response.billing.availableProducts).toContain('weekly_pro');
|
||||
});
|
||||
|
||||
it('syncs pro entitlement from a weekly_pro RevenueCat subscription', async () => {
|
||||
// Regressionsschutz: fehlt weekly_pro im unterstuetzten Produkt-Set, verwirft der
|
||||
// lokale Sync das aktive Entitlement und der Kaeufer bleibt auf 'free'.
|
||||
const response = await mockBackendService.syncRevenueCatState({
|
||||
userId: 'test-user-rc-weekly',
|
||||
customerInfo: {
|
||||
entitlements: {
|
||||
active: {
|
||||
pro: {
|
||||
productIdentifier: 'weekly_pro',
|
||||
expirationDate: '2026-04-30T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
nonSubscriptions: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.billing.entitlement.plan).toBe('pro');
|
||||
expect(response.billing.entitlement.status).toBe('active');
|
||||
});
|
||||
|
||||
it('limits a weekly_pro trial to trial credits', async () => {
|
||||
const response = await mockBackendService.syncRevenueCatState({
|
||||
userId: 'test-user-rc-weekly-trial',
|
||||
customerInfo: {
|
||||
entitlements: {
|
||||
active: {
|
||||
pro: {
|
||||
productIdentifier: 'weekly_pro',
|
||||
expirationDate: '2026-04-30T00:00:00.000Z',
|
||||
periodType: 'TRIAL',
|
||||
},
|
||||
},
|
||||
},
|
||||
nonSubscriptions: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.billing.entitlement.plan).toBe('pro');
|
||||
expect(response.billing.credits.monthlyAllowance).toBe(30);
|
||||
});
|
||||
|
||||
it('limits RevenueCat trial entitlement to trial credits', async () => {
|
||||
const response = await mockBackendService.syncRevenueCatState({
|
||||
userId: 'test-user-rc-trial',
|
||||
|
||||
@@ -103,16 +103,16 @@ describe('StorageService', () => {
|
||||
expect(result).toBe('en');
|
||||
});
|
||||
|
||||
it('defaults to de when no language stored', async () => {
|
||||
it('defaults to en when no language stored', async () => {
|
||||
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
|
||||
const result = await StorageService.getLanguage();
|
||||
expect(result).toBe('de');
|
||||
expect(result).toBe('en');
|
||||
});
|
||||
|
||||
it('defaults to de on error', async () => {
|
||||
it('defaults to en on error', async () => {
|
||||
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('fail'));
|
||||
const result = await StorageService.getLanguage();
|
||||
expect(result).toBe('de');
|
||||
expect(result).toBe('en');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ describe('StorageService', () => {
|
||||
it('falls back to default profile name when empty', async () => {
|
||||
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(' ');
|
||||
const result = await StorageService.getProfileName();
|
||||
expect(result).toBe('Alex Rivera');
|
||||
expect(result).toBe('GreenLens User');
|
||||
});
|
||||
|
||||
it('stores normalized profile name', async () => {
|
||||
|
||||
82
__tests__/utils/shareIntent.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { detectLoginWall, extractHtmlImageCandidates, fetchOgImageFromUrl } from '../../utils/shareIntent';
|
||||
|
||||
describe('shareIntent login wall handling', () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const mockFetchHtml = (html: string, ok = true) => {
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
ok,
|
||||
text: async () => html,
|
||||
}) as unknown as typeof fetch;
|
||||
};
|
||||
|
||||
describe('detectLoginWall', () => {
|
||||
it('detects Instagram login wall markers', () => {
|
||||
expect(detectLoginWall('<a href="/accounts/login/?next=%2Fp%2Fabc">Log in</a>')).toBe(true);
|
||||
expect(detectLoginWall('{"page":"LoginAndSignupPage"}')).toBe(true);
|
||||
expect(detectLoginWall('<form id="loginForm">')).toBe(true);
|
||||
expect(detectLoginWall('<body class="not-logged-in">')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag regular post pages', () => {
|
||||
expect(detectLoginWall('<meta property="og:image" content="https://cdn.example.com/post.jpg" />')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractHtmlImageCandidates', () => {
|
||||
const baseUrl = 'https://www.instagram.com/p/abc/';
|
||||
|
||||
it('decodes HTML entities in og:image URLs so signed CDN params survive', () => {
|
||||
const html = '<meta property="og:image" content="https://scontent.cdninstagram.com/v/t51/img.jpg?stp=dst-jpg_s640x640&_nc_ohc=token&oh=hash&oe=expiry" />';
|
||||
const [candidate] = extractHtmlImageCandidates(html, baseUrl);
|
||||
expect(candidate).toBe('https://scontent.cdninstagram.com/v/t51/img.jpg?stp=dst-jpg_s640x640&_nc_ohc=token&oh=hash&oe=expiry');
|
||||
expect(candidate).not.toContain('&');
|
||||
});
|
||||
|
||||
it('skips inline base64 placeholder images from HTML', () => {
|
||||
const html = '<img width="80px" src="data:image/png;base64,iVBORw0KGgo=" /><meta property="og:image" content="https://cdn.example.com/post.jpg" />';
|
||||
const candidates = extractHtmlImageCandidates(html, baseUrl);
|
||||
expect(candidates).toEqual(['https://cdn.example.com/post.jpg']);
|
||||
});
|
||||
|
||||
it('prefers the og:image over small profile pictures', () => {
|
||||
const html = [
|
||||
'<meta property="og:image" content="https://cdn.example.com/v/post.jpg?stp=dst-jpg_s640x640" />',
|
||||
'<img src="https://cdn.example.com/v/profile_pic_s150x150.jpg" />',
|
||||
].join('');
|
||||
const candidates = extractHtmlImageCandidates(html, baseUrl);
|
||||
expect(candidates[0]).toContain('s640x640');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchOgImageFromUrl', () => {
|
||||
it('returns login_wall when the page is a login wall without usable images', async () => {
|
||||
mockFetchHtml('<html><body><form id="loginForm">accounts/login</form></body></html>');
|
||||
const result = await fetchOgImageFromUrl('https://www.instagram.com/p/abc/');
|
||||
expect(result).toEqual({ failureReason: 'login_wall' });
|
||||
});
|
||||
|
||||
it('returns no_image when the page has no images and no login markers', async () => {
|
||||
mockFetchHtml('<html><body><p>Nothing here</p></body></html>');
|
||||
const result = await fetchOgImageFromUrl('https://example.com/post');
|
||||
expect(result).toEqual({ failureReason: 'no_image' });
|
||||
});
|
||||
|
||||
it('returns no_image when the response is not ok', async () => {
|
||||
mockFetchHtml('', false);
|
||||
const result = await fetchOgImageFromUrl('https://example.com/missing');
|
||||
expect(result).toEqual({ failureReason: 'no_image' });
|
||||
});
|
||||
|
||||
it('returns no_image when the fetch throws', async () => {
|
||||
global.fetch = jest.fn().mockRejectedValue(new Error('network down')) as unknown as typeof fetch;
|
||||
const result = await fetchOgImageFromUrl('https://example.com/offline');
|
||||
expect(result).toEqual({ failureReason: 'no_image' });
|
||||
});
|
||||
});
|
||||
});
|
||||
8
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "GreenLens",
|
||||
"slug": "greenlens",
|
||||
"version": "2.2.9",
|
||||
"version": "2.4.7",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
@@ -19,6 +19,7 @@
|
||||
"supportsTablet": true,
|
||||
"usesAppleSignIn": true,
|
||||
"bundleIdentifier": "com.greenlens.app",
|
||||
"buildNumber": "122",
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "GreenLens needs camera access to identify plants.",
|
||||
"NSPhotoLibraryUsageDescription": "GreenLens needs photo library access to identify plants from your gallery.",
|
||||
@@ -55,7 +56,10 @@
|
||||
"NSExtensionActivationSupportsWebPageWithMaxCount": 1,
|
||||
"NSExtensionActivationSupportsImageWithMaxCount": 1
|
||||
},
|
||||
"androidIntentFilters": ["text/*", "image/*"],
|
||||
"androidIntentFilters": [
|
||||
"text/*",
|
||||
"image/*"
|
||||
],
|
||||
"iosShareExtensionName": "GreenLens Share",
|
||||
"iosAppGroupIdentifier": "group.com.greenlens.app",
|
||||
"preprocessorInjectJS": "try{function glAddCandidate(list,value){if(value&&typeof value==='string'&&list.indexOf(value)===-1){list.push(value)}} function glSrcsetCandidate(value){if(!value||typeof value!=='string')return null;var parts=value.split(',').map(function(item){return item.trim().split(/\\s+/)[0]}).filter(Boolean);return parts.length?parts[parts.length-1]:null} function glEach(selector,callback){var nodes=document.querySelectorAll(selector);for(var i=0;i<nodes.length;i++){callback(nodes[i])}} var glCandidates=[];['og:image','og:image:url','og:image:secure_url','twitter:image','twitter:image:src','image'].forEach(function(key){glAddCandidate(glCandidates,metas[key])}); glEach('meta[itemprop=\"image\"]',function(meta){glAddCandidate(glCandidates,meta.getAttribute('content'))}); glEach('img',function(img){glAddCandidate(glCandidates,img.currentSrc);glAddCandidate(glCandidates,img.src);glAddCandidate(glCandidates,img.getAttribute('data-src'));glAddCandidate(glCandidates,img.getAttribute('data-original'));glAddCandidate(glCandidates,img.getAttribute('data-lazy-src'));glAddCandidate(glCandidates,glSrcsetCandidate(img.getAttribute('srcset')))}); glEach('picture source,source',function(source){glAddCandidate(glCandidates,source.src);glAddCandidate(glCandidates,source.getAttribute('src'));glAddCandidate(glCandidates,glSrcsetCandidate(source.getAttribute('srcset')))}); glEach('video',function(video){glAddCandidate(glCandidates,video.getAttribute('poster'))}); glEach('[style*=\"background-image\"]',function(el){var bg=(el.style&&el.style.backgroundImage)||'';var match=bg.match(/url\\([\"']?([^\"')]+)[\"']?\\)/);if(match){glAddCandidate(glCandidates,match[1])}});metas['greenlens:imageCandidates']=JSON.stringify(glCandidates.slice(0,12));metas['greenlens:imageCandidateCount']=String(glCandidates.length);metas['og:image']=metas['og:image']||glCandidates[0]}catch(e){metas['greenlens:preprocessorError']=String(e)}"
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { SafeImage } from '../../components/SafeImage';
|
||||
import { Plant } from '../../types';
|
||||
import { useCoachMarks } from '../../context/CoachMarksContext';
|
||||
import { OnboardingProgressService } from '../../services/onboardingProgressService';
|
||||
import { OnboardingProgressService, PersistentOnboardingStep } from '../../services/onboardingProgressService';
|
||||
|
||||
const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
|
||||
|
||||
@@ -40,6 +40,10 @@ function OnboardingChecklist({
|
||||
colorPalette,
|
||||
lexiconExplored,
|
||||
customizationDone,
|
||||
scanDone,
|
||||
reminderDone,
|
||||
collectionDone,
|
||||
userId,
|
||||
colors,
|
||||
router,
|
||||
t,
|
||||
@@ -52,6 +56,10 @@ function OnboardingChecklist({
|
||||
colorPalette: string;
|
||||
lexiconExplored: boolean;
|
||||
customizationDone: boolean;
|
||||
scanDone: boolean;
|
||||
reminderDone: boolean;
|
||||
collectionDone: boolean;
|
||||
userId: number | null;
|
||||
colors: any;
|
||||
router: any;
|
||||
t: any;
|
||||
@@ -61,17 +69,52 @@ function OnboardingChecklist({
|
||||
}) {
|
||||
const cardRef = useRef<View>(null);
|
||||
const previousProgressRef = useRef<number | null>(null);
|
||||
const persistedStepsRef = useRef<Set<string>>(new Set());
|
||||
const lexiconHistoryCount = getLexiconSearchHistory().length;
|
||||
const plantsCount = plants.length;
|
||||
const reminderReady = plants.some((plant) => Boolean(plant.notificationsEnabled));
|
||||
const themeCustomized = customizationDone || appearanceMode !== 'system' || colorPalette !== 'forest';
|
||||
const lexiconCompleted = lexiconExplored || lexiconHistoryCount > 0;
|
||||
const reminderLive = plants.some((plant) => Boolean(plant.notificationsEnabled));
|
||||
const themeLive = appearanceMode !== 'system' || colorPalette !== 'forest';
|
||||
const lexiconLive = lexiconHistoryCount > 0;
|
||||
const scanLive = plantsCount > 0;
|
||||
const collectionLive = plantsCount >= 3;
|
||||
|
||||
// Once a step is reached live, persist it so it never reverts (e.g. deleting a plant
|
||||
// or disabling notifications previously flipped the checklist backward).
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
const toPersist: Array<[PersistentOnboardingStep, boolean]> = [
|
||||
['scan', scanLive && !scanDone],
|
||||
['reminder', reminderLive && !reminderDone],
|
||||
['customize', themeLive && !customizationDone],
|
||||
['lexicon', lexiconLive && !lexiconExplored],
|
||||
['collection', collectionLive && !collectionDone],
|
||||
];
|
||||
for (const [step, shouldPersist] of toPersist) {
|
||||
if (shouldPersist && !persistedStepsRef.current.has(step)) {
|
||||
persistedStepsRef.current.add(step);
|
||||
OnboardingProgressService.completeStep(userId, step);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
userId,
|
||||
scanLive,
|
||||
scanDone,
|
||||
reminderLive,
|
||||
reminderDone,
|
||||
themeLive,
|
||||
customizationDone,
|
||||
lexiconLive,
|
||||
lexiconExplored,
|
||||
collectionLive,
|
||||
collectionDone,
|
||||
]);
|
||||
|
||||
const checklist = [
|
||||
{ id: 'scan' as const, label: t.stepScan, completed: plantsCount > 0, icon: 'camera-outline' },
|
||||
{ id: 'reminder' as const, label: t.stepReminder, completed: reminderReady, icon: 'notifications-outline' },
|
||||
{ id: 'lexicon' as const, label: t.stepLexicon, completed: lexiconCompleted, icon: 'search-outline' },
|
||||
{ id: 'theme' as const, label: t.stepTheme, completed: themeCustomized, icon: 'color-palette-outline' },
|
||||
{ id: 'collection' as const, label: t.stepCollection, completed: plantsCount >= 3, icon: 'albums-outline' },
|
||||
{ id: 'scan' as const, label: t.stepScan, completed: scanDone || scanLive, icon: 'camera-outline' },
|
||||
{ id: 'reminder' as const, label: t.stepReminder, completed: reminderDone || reminderLive, icon: 'notifications-outline' },
|
||||
{ id: 'lexicon' as const, label: t.stepLexicon, completed: lexiconExplored || lexiconLive, icon: 'search-outline' },
|
||||
{ id: 'theme' as const, label: t.stepTheme, completed: customizationDone || themeLive, icon: 'color-palette-outline' },
|
||||
{ id: 'collection' as const, label: t.stepCollection, completed: collectionDone || collectionLive, icon: 'albums-outline' },
|
||||
];
|
||||
const completedCount = checklist.filter((item) => item.completed).length;
|
||||
const progressRatio = completedCount / checklist.length;
|
||||
@@ -229,6 +272,9 @@ export default function HomeScreen() {
|
||||
const [onboardingSignals, setOnboardingSignals] = useState({
|
||||
lexiconExplored: false,
|
||||
customizationDone: false,
|
||||
scanDone: false,
|
||||
reminderDone: false,
|
||||
collectionDone: false,
|
||||
});
|
||||
const { layouts, registerLayout, startTour } = useCoachMarks();
|
||||
const fabRef = useRef<View>(null);
|
||||
@@ -241,6 +287,9 @@ export default function HomeScreen() {
|
||||
setOnboardingSignals({
|
||||
lexiconExplored: false,
|
||||
customizationDone: false,
|
||||
scanDone: false,
|
||||
reminderDone: false,
|
||||
collectionDone: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -500,6 +549,10 @@ export default function HomeScreen() {
|
||||
colorPalette={colorPalette}
|
||||
lexiconExplored={onboardingSignals.lexiconExplored}
|
||||
customizationDone={onboardingSignals.customizationDone}
|
||||
scanDone={onboardingSignals.scanDone}
|
||||
reminderDone={onboardingSignals.reminderDone}
|
||||
collectionDone={onboardingSignals.collectionDone}
|
||||
userId={session?.userId ?? null}
|
||||
colors={colors}
|
||||
router={router}
|
||||
t={t}
|
||||
|
||||
@@ -54,6 +54,10 @@ const getProfileCopy = (language: Language) => {
|
||||
logoutConfirmTitle: 'Abmelden?',
|
||||
logoutConfirmMessage: 'Möchtest du dich wirklich abmelden?',
|
||||
logoutConfirmBtn: 'Abmelden',
|
||||
guestTitle: 'Du bist noch nicht angemeldet',
|
||||
guestBody: 'Melde dich an, um Pflanzen zu scannen und deine Sammlung zu sichern.',
|
||||
guestSignIn: 'Anmelden',
|
||||
guestSignUp: 'Kostenlos registrieren',
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
@@ -79,6 +83,10 @@ const getProfileCopy = (language: Language) => {
|
||||
logoutConfirmTitle: '¿Cerrar sesión?',
|
||||
logoutConfirmMessage: '¿Realmente quieres cerrar sesión?',
|
||||
logoutConfirmBtn: 'Cerrar sesión',
|
||||
guestTitle: 'Aún no has iniciado sesión',
|
||||
guestBody: 'Inicia sesión para escanear plantas y guardar tu colección.',
|
||||
guestSignIn: 'Iniciar sesión',
|
||||
guestSignUp: 'Registrarse gratis',
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -103,6 +111,10 @@ const getProfileCopy = (language: Language) => {
|
||||
logoutConfirmTitle: 'Sign out?',
|
||||
logoutConfirmMessage: 'Do you really want to sign out?',
|
||||
logoutConfirmBtn: 'Sign Out',
|
||||
guestTitle: "You're not signed in yet",
|
||||
guestBody: 'Sign in to scan plants and keep your collection safe.',
|
||||
guestSignIn: 'Log in',
|
||||
guestSignUp: 'Sign up for free',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -118,6 +130,7 @@ export default function ProfileScreen() {
|
||||
profileName,
|
||||
setProfileName,
|
||||
signOut,
|
||||
session,
|
||||
} = useApp();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -199,7 +212,7 @@ export default function ProfileScreen() {
|
||||
const menuItems = [
|
||||
{ label: copy.menuSettings, icon: 'settings-outline', route: '/profile/preferences' as any },
|
||||
{ label: copy.menuBilling, icon: 'card-outline', route: '/profile/billing' as any },
|
||||
{ label: copy.menuData, icon: 'shield-checkmark-outline', route: '/profile/data' as any },
|
||||
...(session ? [{ label: copy.menuData, icon: 'shield-checkmark-outline', route: '/profile/data' as any }] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -228,6 +241,26 @@ export default function ProfileScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!session ? (
|
||||
<View style={[styles.card, styles.accountCard, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
|
||||
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.guestTitle}</Text>
|
||||
<Text style={[styles.guestBody, { color: colors.textSecondary }]}>{copy.guestBody}</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.guestPrimaryBtn, { backgroundColor: colors.primary }]}
|
||||
onPress={() => router.push('/auth/signup')}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.guestPrimaryText, { color: colors.onPrimary }]}>{copy.guestSignUp}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.guestSecondaryBtn}
|
||||
onPress={() => router.push('/auth/login')}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={[styles.guestSecondaryText, { color: colors.primary }]}>{copy.guestSignIn}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[styles.card, styles.accountCard, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder }]}>
|
||||
<Text style={[styles.cardTitle, { color: colors.text }]}>{copy.account}</Text>
|
||||
|
||||
@@ -315,6 +348,7 @@ export default function ProfileScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.cardBg, borderColor: colors.cardBorder, padding: 0, overflow: 'hidden' }]}>
|
||||
{menuItems.map((item, idx) => (
|
||||
@@ -336,6 +370,7 @@ export default function ProfileScreen() {
|
||||
))}
|
||||
</View>
|
||||
{/* Logout */}
|
||||
{session && (
|
||||
<TouchableOpacity
|
||||
style={[styles.logoutBtn, { borderColor: colors.dangerSoft, backgroundColor: colors.dangerSoft }]}
|
||||
activeOpacity={0.78}
|
||||
@@ -356,6 +391,7 @@ export default function ProfileScreen() {
|
||||
<Ionicons name="log-out-outline" size={18} color={colors.danger} />
|
||||
<Text style={[styles.logoutText, { color: colors.danger }]}>{copy.logout}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={{ height: 40 }} />
|
||||
</ScrollView>
|
||||
@@ -387,6 +423,30 @@ const styles = StyleSheet.create({
|
||||
accountCard: {
|
||||
marginBottom: 14,
|
||||
},
|
||||
guestBody: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
guestPrimaryBtn: {
|
||||
height: 50,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
guestPrimaryText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
},
|
||||
guestSecondaryBtn: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
guestSecondaryText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
},
|
||||
logoutBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -11,7 +11,9 @@ import { initDatabase, AppMetaDb } from '../services/database';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import { AuthService } from '../services/authService';
|
||||
import { Analytics } from '../services/analytics';
|
||||
import { AnimatedSplashScreen } from '../components/AnimatedSplashScreen';
|
||||
import { reportInstallOnce } from '../services/installPing';
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync().catch(() => { });
|
||||
@@ -67,20 +69,31 @@ const ensureInstallConsistency = async (): Promise<boolean> => {
|
||||
const sqliteMarker = AppMetaDb.get('install_marker_v2');
|
||||
const secureMarker = await SecureStore.getItemAsync(SECURE_INSTALL_MARKER).catch(() => null);
|
||||
|
||||
if (sqliteMarker === '1' && secureMarker === '1') {
|
||||
return false; // Alles gut, keine Neuinstallation
|
||||
}
|
||||
|
||||
if (sqliteMarker === '1' || secureMarker === '1') {
|
||||
// Teilweise vorhanden -> heilen, nicht löschen
|
||||
AppMetaDb.set('install_marker_v2', '1');
|
||||
// WICHTIG: Nur der SQLite-Marker taugt als Neuinstallations-Erkennung.
|
||||
// SecureStore liegt auf iOS im Keychain, und Keychain-Eintraege ueberleben
|
||||
// das Loeschen der App. Nach einer Neuinstallation gilt also immer
|
||||
// sqliteMarker === null && secureMarker === '1' - das ist der Normalfall,
|
||||
// nicht ein halb geschriebener Zustand.
|
||||
//
|
||||
// Vorher lief genau dieser Fall in den "heilen"-Zweig: die Session blieb
|
||||
// bestehen, der Nutzer war nach der Neuinstallation weiter eingeloggt und
|
||||
// hat das Onboarding nie wieder gesehen (Gate: !session && !hasCompletedOnboarding).
|
||||
if (sqliteMarker === '1') {
|
||||
// App-Daten vorhanden -> laufende Installation. Keychain-Marker nachziehen,
|
||||
// falls er fehlt (z. B. nach einem Keychain-Reset).
|
||||
if (secureMarker !== '1') {
|
||||
await SecureStore.setItemAsync(SECURE_INSTALL_MARKER, '1');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fresh Install: Alles zurücksetzen
|
||||
// Fresh Install: Alles zurücksetzen. Der Nutzer landet danach auf dem
|
||||
// Welcome-Screen und durchläuft das Onboarding erneut - inklusive
|
||||
// A/B-Zuweisung, die ebenfalls im geloeschten App-Storage lag.
|
||||
await AuthService.logout();
|
||||
await AsyncStorage.removeItem('greenlens_show_tour');
|
||||
// Experiment-Zuweisungen gehoeren zur Installation, nicht zum Keychain.
|
||||
await AsyncStorage.removeItem('greenlens_experiment_welcome_headline_v1');
|
||||
AppMetaDb.set('install_marker_v2', '1');
|
||||
await SecureStore.setItemAsync(SECURE_INSTALL_MARKER, '1');
|
||||
return true;
|
||||
@@ -96,11 +109,9 @@ function RootLayoutInner() {
|
||||
colorPalette,
|
||||
signOut,
|
||||
session,
|
||||
billingSummary,
|
||||
isActivatingEntitlement,
|
||||
hasCompletedOnboarding,
|
||||
isInitializing,
|
||||
isLoadingPlants,
|
||||
isLoadingBilling,
|
||||
} = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const pathname = usePathname();
|
||||
@@ -114,28 +125,30 @@ function RootLayoutInner() {
|
||||
await signOut();
|
||||
}
|
||||
setInstallCheckDone(true);
|
||||
// Fire-and-forget: pings the downloads Discord channel once per install.
|
||||
reportInstallOnce();
|
||||
})();
|
||||
}, [signOut]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathname) {
|
||||
Analytics.screen(pathname);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
const isAppReady = installCheckDone && !isInitializing && !isLoadingPlants;
|
||||
const hasActiveEntitlement = isActivatingEntitlement
|
||||
|| (billingSummary?.entitlement?.plan === 'pro'
|
||||
&& billingSummary?.entitlement?.status === 'active');
|
||||
const isAllowedWithoutSession = pathname.includes('onboarding')
|
||||
|| pathname.includes('auth/')
|
||||
|| pathname.includes('scanner')
|
||||
|| isShareIntentCallbackPath(pathname)
|
||||
|| pathname.includes('profile/billing');
|
||||
const isAllowedWithoutEntitlement = pathname.includes('auth/')
|
||||
|| pathname.includes('onboarding')
|
||||
|| pathname.includes('scanner')
|
||||
|| isShareIntentCallbackPath(pathname)
|
||||
|| pathname.includes('profile/billing');
|
||||
|
||||
let content = null;
|
||||
|
||||
if (isAppReady) {
|
||||
if (!session) {
|
||||
// Guests who finished onboarding (e.g. dismissed the soft paywall) browse the
|
||||
// full app; paid/credit actions are gated at the point of use instead.
|
||||
if (!session && !hasCompletedOnboarding) {
|
||||
// Only redirect if we are not already on an auth-related page or the scanner
|
||||
if (!isAllowedWithoutSession) {
|
||||
content = <Redirect href="/onboarding" />;
|
||||
@@ -148,10 +161,13 @@ function RootLayoutInner() {
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="onboarding" options={{ animation: 'none' }} />
|
||||
<Stack.Screen name="onboarding/slides" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/source" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/health-check" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/goal" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/experience" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/customize" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/personalizing" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="auth/login" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="auth/signup" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen
|
||||
@@ -166,12 +182,6 @@ function RootLayoutInner() {
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
} else if (!hasActiveEntitlement && !isLoadingBilling && billingSummary && !isAllowedWithoutEntitlement) {
|
||||
// Signed-in but confirmed non-pro: send to the paywall, not back to
|
||||
// onboarding — bouncing a logged-in user to onboarding looks like an
|
||||
// app restart. billingSummary === null means "unknown" (fetch failed),
|
||||
// never redirect on unknown.
|
||||
content = <Redirect href="/profile/billing" />;
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
@@ -182,10 +192,13 @@ function RootLayoutInner() {
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="onboarding" options={{ animation: 'none' }} />
|
||||
<Stack.Screen name="onboarding/slides" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/source" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/health-check" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/goal" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/experience" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/customize" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="onboarding/personalizing" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="auth/login" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="auth/signup" options={{ animation: 'slide_from_right' }} />
|
||||
<Stack.Screen name="(tabs)" options={{ animation: 'none' }} />
|
||||
|
||||
@@ -1,38 +1,78 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
Image,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import Constants from 'expo-constants';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { AuthService } from '../../services/authService';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import Constants from 'expo-constants';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
|
||||
import { Language } from '../../types';
|
||||
|
||||
const ONBOARDING_AUTH_BACKGROUND = {
|
||||
light: '#fbfaf3',
|
||||
dark: '#0a110b',
|
||||
const HERO_IMAGE = require('../../assets/welcome_hero_vertical.png');
|
||||
|
||||
const getCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
headline: 'Welcome back!',
|
||||
subline: 'Melde dich an und mache mit deiner Pflanzenpflege weiter.',
|
||||
loginCta: 'Anmelden',
|
||||
forgot: 'Passwort vergessen?',
|
||||
forgotTitle: 'Passwort zuruecksetzen',
|
||||
forgotBody: 'Ein Reset-Link ist noch nicht in der App verfuegbar. Bitte nutze aktuell deine gespeicherten Login-Daten.',
|
||||
newHere: 'Neu hier?',
|
||||
create: 'Account erstellen',
|
||||
emailLabel: 'E-Mail',
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
headline: 'Welcome back!',
|
||||
subline: 'Inicia sesion para continuar con el cuidado de tus plantas.',
|
||||
loginCta: 'Iniciar sesion',
|
||||
forgot: 'Olvidaste tu contrasena?',
|
||||
forgotTitle: 'Restablecer contrasena',
|
||||
forgotBody: 'El enlace de restablecimiento aun no esta disponible en la app. Usa tus datos guardados por ahora.',
|
||||
newHere: 'Nuevo aqui?',
|
||||
create: 'Crear cuenta',
|
||||
emailLabel: 'Email',
|
||||
};
|
||||
}
|
||||
return {
|
||||
headline: 'Welcome back!',
|
||||
subline: 'Log in to keep scanning, saving and caring for your plants.',
|
||||
loginCta: 'Log in',
|
||||
forgot: 'Forgot password?',
|
||||
forgotTitle: 'Reset password',
|
||||
forgotBody: 'Password reset is not available in the app yet. Please use your saved login details for now.',
|
||||
newHere: 'New here?',
|
||||
create: 'Create account',
|
||||
emailLabel: 'Email',
|
||||
};
|
||||
};
|
||||
|
||||
export default function LoginScreen() {
|
||||
const { isDarkMode, colorPalette, hydrateSession, t } = useApp();
|
||||
const { isDarkMode, colorPalette, hydrateSession, language, t } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const copy = getCopy(language);
|
||||
const posthog = useSafeAnalytics();
|
||||
const screenBackground = isDarkMode
|
||||
? ONBOARDING_AUTH_BACKGROUND.dark
|
||||
: ONBOARDING_AUTH_BACKGROUND.light;
|
||||
const params = useLocalSearchParams<{ returnTo?: string; topup?: string }>();
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -40,7 +80,6 @@ export default function LoginScreen() {
|
||||
const [appleAvailable, setAppleAvailable] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpoGo) {
|
||||
@@ -60,6 +99,27 @@ export default function LoginScreen() {
|
||||
};
|
||||
}, [isExpoGo]);
|
||||
|
||||
const finishAuth = async (session: Awaited<ReturnType<typeof AuthService.login>>) => {
|
||||
await hydrateSession(session);
|
||||
if (session?.userId) {
|
||||
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
|
||||
}
|
||||
if (session.isNewUser) {
|
||||
await AsyncStorage.setItem('greenlens_show_tour', 'true');
|
||||
}
|
||||
const returnTo = Array.isArray(params.returnTo) ? params.returnTo[0] : params.returnTo;
|
||||
const topup = Array.isArray(params.topup) ? params.topup[0] : params.topup;
|
||||
if (returnTo === 'billing') {
|
||||
router.replace({ pathname: '/profile/billing', params: topup ? { topup } : undefined });
|
||||
return;
|
||||
}
|
||||
if (returnTo === 'paywall') {
|
||||
router.replace('/profile/billing?view=paywall&context=out_of_credits');
|
||||
return;
|
||||
}
|
||||
router.replace('/(tabs)');
|
||||
};
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email.trim() || !password) {
|
||||
setError(t.errFillAllFields);
|
||||
@@ -69,19 +129,13 @@ export default function LoginScreen() {
|
||||
setError(null);
|
||||
try {
|
||||
const session = await AuthService.login(email, password);
|
||||
const billing = await hydrateSession(session);
|
||||
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
|
||||
// Non-pro accounts land on the paywall with context instead of being
|
||||
// bounced through the root redirect (which looks like an app restart).
|
||||
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
|
||||
await finishAuth(session);
|
||||
} catch (e: any) {
|
||||
if (e.message === 'USER_NOT_FOUND') {
|
||||
setError(t.errUserNotFound);
|
||||
} else if (e.message === 'WRONG_PASSWORD') {
|
||||
setError(t.errWrongPassword);
|
||||
} else if (e.message === 'BACKEND_URL_MISSING') {
|
||||
setError(t.errNetworkError);
|
||||
} else if (e.message === 'NETWORK_ERROR') {
|
||||
} else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') {
|
||||
setError(t.errNetworkError);
|
||||
} else {
|
||||
setError(t.errLoginFailed);
|
||||
@@ -117,21 +171,10 @@ export default function LoginScreen() {
|
||||
email: credential.email,
|
||||
name: fullName || undefined,
|
||||
});
|
||||
const billing = await hydrateSession(session);
|
||||
if (session.isNewUser) {
|
||||
await AsyncStorage.setItem('greenlens_show_tour', 'true');
|
||||
}
|
||||
posthog.capture('apple_login_succeeded', { surface: 'login' });
|
||||
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
|
||||
if (session.isNewUser) {
|
||||
router.replace('/onboarding/source');
|
||||
} else {
|
||||
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
|
||||
}
|
||||
await finishAuth(session);
|
||||
} catch (e: any) {
|
||||
if (e?.code === 'ERR_REQUEST_CANCELED') {
|
||||
return;
|
||||
}
|
||||
if (e?.code === 'ERR_REQUEST_CANCELED') return;
|
||||
posthog.capture('apple_login_failed', {
|
||||
surface: 'login',
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
@@ -146,59 +189,47 @@ export default function LoginScreen() {
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.flex, { backgroundColor: screenBackground }]}
|
||||
style={[styles.flex, { backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Logo / Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.backBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={20} color={colors.text} />
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => router.back()} activeOpacity={0.8}>
|
||||
<Ionicons name="arrow-back" size={20} color="#ffffff" />
|
||||
</TouchableOpacity>
|
||||
<Image
|
||||
source={require('../../assets/icon.png')}
|
||||
style={styles.logoIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={[styles.appName, { color: colors.text }]}>GreenLens</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
|
||||
{t.welcomeBack}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Card */}
|
||||
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.cardBorder, shadowColor: colors.cardShadow }]}>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
||||
<ImageBackground source={HERO_IMAGE} style={styles.hero} imageStyle={styles.heroImage}>
|
||||
<View style={styles.heroOverlay} />
|
||||
<View style={styles.heroCopy}>
|
||||
<Text style={styles.heroTitle}>{copy.headline}</Text>
|
||||
<Text style={styles.heroSubline}>{copy.subline}</Text>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
|
||||
<View style={[styles.sheet, { backgroundColor: isDarkMode ? '#101a12' : '#fbfaf3' }]}>
|
||||
{appleAvailable ? (
|
||||
<AppleAuthentication.AppleAuthenticationButton
|
||||
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
|
||||
buttonStyle={isDarkMode
|
||||
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
|
||||
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
|
||||
cornerRadius={12}
|
||||
cornerRadius={14}
|
||||
style={styles.appleButton}
|
||||
onPress={handleAppleSignIn}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{appleAvailable ? (
|
||||
<View style={styles.dividerRowCompact}>
|
||||
<View style={styles.dividerRow}>
|
||||
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
||||
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
|
||||
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Email */}
|
||||
<View style={styles.form}>
|
||||
<View style={styles.fieldGroup}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>E-Mail</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.emailLabel}</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
@@ -214,14 +245,13 @@ export default function LoginScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Password */}
|
||||
<View style={styles.fieldGroup}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
placeholder="••••••••"
|
||||
placeholder="Password"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
@@ -231,56 +261,47 @@ export default function LoginScreen() {
|
||||
onSubmitEditing={handleLogin}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={18}
|
||||
color={colors.textMuted}
|
||||
/>
|
||||
<Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<TouchableOpacity
|
||||
style={styles.forgotBtn}
|
||||
onPress={() => Alert.alert(copy.forgotTitle, copy.forgotBody)}
|
||||
activeOpacity={0.78}
|
||||
>
|
||||
<Text style={[styles.forgotText, { color: colors.primary }]}>{copy.forgot}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{error ? (
|
||||
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
|
||||
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
|
||||
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
|
||||
<Text style={[styles.errorText, { color: colors.danger }]} selectable>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Login Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.7 : 1 }]}
|
||||
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.72 : 1 }]}
|
||||
onPress={handleLogin}
|
||||
activeOpacity={0.82}
|
||||
activeOpacity={0.84}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={colors.onPrimary} size="small" />
|
||||
) : (
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{t.onboardingLogin}</Text>
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{copy.loginCta}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Divider */}
|
||||
<View style={styles.dividerRow}>
|
||||
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
||||
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
|
||||
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
||||
</View>
|
||||
|
||||
{/* Sign Up Link */}
|
||||
<TouchableOpacity
|
||||
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
|
||||
onPress={() => router.replace('/auth/signup')}
|
||||
activeOpacity={0.82}
|
||||
>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>
|
||||
{t.noAccountYet}{' '}
|
||||
<Text style={{ color: colors.primary, fontWeight: '600' }}>{t.onboardingRegister}</Text>
|
||||
<TouchableOpacity style={styles.signupLink} onPress={() => router.replace({ pathname: '/auth/signup', params: { returnTo: params.returnTo, topup: params.topup } })} activeOpacity={0.78}>
|
||||
<Text style={[styles.signupLinkText, { color: colors.textSecondary }]}>
|
||||
{copy.newHere}{' '}
|
||||
<Text style={{ color: colors.primary, fontWeight: '800' }}>{copy.create}</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
@@ -288,136 +309,92 @@ export default function LoginScreen() {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
scroll: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
scroll: { flexGrow: 1 },
|
||||
hero: {
|
||||
minHeight: 290,
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 48,
|
||||
paddingTop: 56,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 32,
|
||||
heroImage: { resizeMode: 'cover' },
|
||||
heroOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(5, 12, 7, 0.48)',
|
||||
},
|
||||
backBtn: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
top: 54,
|
||||
left: 22,
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.28)',
|
||||
zIndex: 10,
|
||||
elevation: 10,
|
||||
},
|
||||
logoIcon: {
|
||||
width: 84,
|
||||
height: 84,
|
||||
borderRadius: 20,
|
||||
marginBottom: 16,
|
||||
heroCopy: { gap: 10 },
|
||||
heroTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 36,
|
||||
lineHeight: 41,
|
||||
fontWeight: '900',
|
||||
},
|
||||
appName: {
|
||||
fontSize: 30,
|
||||
heroSubline: {
|
||||
color: 'rgba(255, 255, 255, 0.88)',
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.5,
|
||||
marginBottom: 6,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '400',
|
||||
},
|
||||
card: {
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
padding: 24,
|
||||
gap: 16,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
},
|
||||
appleButton: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
marginBottom: 2,
|
||||
},
|
||||
dividerRowCompact: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginVertical: 2,
|
||||
},
|
||||
fieldGroup: {
|
||||
gap: 6,
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
marginLeft: 2,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
height: 50,
|
||||
},
|
||||
inputIcon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
sheet: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
height: 50,
|
||||
marginTop: -24,
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
paddingHorizontal: 22,
|
||||
paddingTop: 24,
|
||||
paddingBottom: 34,
|
||||
gap: 14,
|
||||
},
|
||||
eyeBtn: {
|
||||
padding: 4,
|
||||
marginLeft: 6,
|
||||
appleButton: { width: '100%', height: 56 },
|
||||
dividerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||
dividerLine: { flex: 1, height: 1 },
|
||||
dividerText: { fontSize: 12, fontWeight: '800' },
|
||||
form: { gap: 12 },
|
||||
fieldGroup: { gap: 6 },
|
||||
label: { fontSize: 13, fontWeight: '800', marginLeft: 2 },
|
||||
inputRow: {
|
||||
minHeight: 54,
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
inputIcon: { marginRight: 10 },
|
||||
input: { flex: 1, minHeight: 54, fontSize: 15 },
|
||||
eyeBtn: { padding: 5, marginLeft: 6 },
|
||||
forgotBtn: { alignItems: 'flex-end', marginTop: -4 },
|
||||
forgotText: { fontSize: 14, fontWeight: '800' },
|
||||
errorBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
borderRadius: 10,
|
||||
gap: 7,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 13,
|
||||
flex: 1,
|
||||
},
|
||||
errorText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' },
|
||||
primaryBtn: {
|
||||
height: 52,
|
||||
minHeight: 56,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
primaryBtnText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
dividerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginVertical: 20,
|
||||
gap: 12,
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
},
|
||||
dividerText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
secondaryBtn: {
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
secondaryBtnText: {
|
||||
fontSize: 15,
|
||||
},
|
||||
primaryBtnText: { fontSize: 17, fontWeight: '900' },
|
||||
signupLink: { alignItems: 'center', paddingTop: 8 },
|
||||
signupLinkText: { fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
|
||||
@@ -1,39 +1,84 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
ActivityIndicator,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
Image,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import Constants from 'expo-constants';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { AuthService } from '../../services/authService';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import Constants from 'expo-constants';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
|
||||
import { Language } from '../../types';
|
||||
|
||||
const ONBOARDING_AUTH_BACKGROUND = {
|
||||
light: '#fbfaf3',
|
||||
dark: '#0a110b',
|
||||
const HERO_IMAGE = require('../../assets/welcome_hero_vertical.png');
|
||||
|
||||
const getCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
headline: "Let's finish your setup!",
|
||||
subline: 'Erstelle einen Account, speichere deine Pflanzen und sichere dir 3 Gratis-Scans pro Monat.',
|
||||
emailCta: 'Mit E-Mail fortfahren',
|
||||
createCta: 'Account erstellen',
|
||||
already: 'Schon einen Account?',
|
||||
login: 'Anmelden',
|
||||
legal: 'Mit dem Fortfahren akzeptierst du Datenschutz und Nutzungsbedingungen.',
|
||||
nameLabel: 'Name',
|
||||
emailLabel: 'E-Mail',
|
||||
savePlantPrefix: 'Dein Scan wird nach der Registrierung gespeichert:',
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
headline: "Let's finish your setup!",
|
||||
subline: 'Crea una cuenta para guardar tus plantas y recibir 3 escaneos gratis al mes.',
|
||||
emailCta: 'Continuar con email',
|
||||
createCta: 'Crear cuenta',
|
||||
already: 'Ya tienes cuenta?',
|
||||
login: 'Iniciar sesion',
|
||||
legal: 'Al continuar aceptas la Politica de privacidad y los Terminos.',
|
||||
nameLabel: 'Nombre',
|
||||
emailLabel: 'Email',
|
||||
savePlantPrefix: 'Guardaremos tu escaneo despues del registro:',
|
||||
};
|
||||
}
|
||||
return {
|
||||
headline: "Let's finish your setup!",
|
||||
subline: 'Create an account to save your plants and 3 free scans per month.',
|
||||
emailCta: 'Continue with Email',
|
||||
createCta: 'Create Account',
|
||||
already: 'Already have an account?',
|
||||
login: 'Log in',
|
||||
legal: 'By continuing you agree to our Privacy Policy and Terms.',
|
||||
nameLabel: 'Name',
|
||||
emailLabel: 'Email',
|
||||
savePlantPrefix: 'Your scan will be saved after signup:',
|
||||
};
|
||||
};
|
||||
|
||||
export default function SignupScreen() {
|
||||
const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, t } = useApp();
|
||||
const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, language, t } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const copy = getCopy(language);
|
||||
const posthog = useSafeAnalytics();
|
||||
const pendingPlant = getPendingPlant();
|
||||
const screenBackground = isDarkMode
|
||||
? ONBOARDING_AUTH_BACKGROUND.dark
|
||||
: ONBOARDING_AUTH_BACKGROUND.light;
|
||||
const params = useLocalSearchParams<{ returnTo?: string; topup?: string }>();
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
const { height } = useWindowDimensions();
|
||||
const compact = height < 700;
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -41,10 +86,18 @@ export default function SignupScreen() {
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showPasswordConfirm, setShowPasswordConfirm] = useState(false);
|
||||
const [emailExpanded, setEmailExpanded] = useState(false);
|
||||
const [appleAvailable, setAppleAvailable] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const isExpoGo = Constants.appOwnership === 'expo';
|
||||
|
||||
const heroHeight = emailExpanded ? (compact ? 200 : 240) : (compact ? 320 : 400);
|
||||
const appleButtonHeight = emailExpanded ? 44 : 56;
|
||||
const inputHeight = emailExpanded && compact ? 46 : 52;
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('signup_screen_viewed', { context: 'onboarding' });
|
||||
}, [posthog]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpoGo) {
|
||||
@@ -64,6 +117,25 @@ export default function SignupScreen() {
|
||||
};
|
||||
}, [isExpoGo]);
|
||||
|
||||
const finishAuth = async (session: Awaited<ReturnType<typeof AuthService.signUp>>) => {
|
||||
await hydrateSession(session);
|
||||
if (session?.userId) {
|
||||
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
|
||||
}
|
||||
await AsyncStorage.setItem('greenlens_show_tour', 'true');
|
||||
const returnTo = Array.isArray(params.returnTo) ? params.returnTo[0] : params.returnTo;
|
||||
const topup = Array.isArray(params.topup) ? params.topup[0] : params.topup;
|
||||
if (returnTo === 'billing') {
|
||||
router.replace({ pathname: '/profile/billing', params: topup ? { topup } : undefined });
|
||||
return;
|
||||
}
|
||||
if (returnTo === 'paywall') {
|
||||
router.replace('/profile/billing?view=paywall&context=out_of_credits');
|
||||
return;
|
||||
}
|
||||
router.replace('/(tabs)');
|
||||
};
|
||||
|
||||
const validate = (): string | null => {
|
||||
if (!name.trim()) return t.errNameRequired;
|
||||
if (!email.trim() || !email.includes('@')) return t.errEmailInvalid;
|
||||
@@ -76,27 +148,21 @@ export default function SignupScreen() {
|
||||
const validationError = validate();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
setEmailExpanded(true);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const session = await AuthService.signUp(email, name, password);
|
||||
await hydrateSession(session);
|
||||
// Flag setzen: Tour beim nächsten App-Öffnen anzeigen
|
||||
await AsyncStorage.setItem('greenlens_show_tour', 'true');
|
||||
router.replace('/onboarding/source');
|
||||
await finishAuth(session);
|
||||
} catch (e: any) {
|
||||
if (e.message === 'EMAIL_TAKEN') {
|
||||
setError(t.errEmailTaken);
|
||||
} else if (e.message === 'BACKEND_URL_MISSING') {
|
||||
setError(t.errNetworkError);
|
||||
} else if (e.message === 'NETWORK_ERROR') {
|
||||
} else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') {
|
||||
setError(t.errNetworkError);
|
||||
} else if (e.message === 'SERVER_ERROR') {
|
||||
setError(t.errServerError);
|
||||
} else if (e.message === 'AUTH_ERROR') {
|
||||
setError(t.errAuthError);
|
||||
} else {
|
||||
setError(t.errAuthError);
|
||||
}
|
||||
@@ -131,21 +197,10 @@ export default function SignupScreen() {
|
||||
email: credential.email,
|
||||
name: fullName || undefined,
|
||||
});
|
||||
const billing = await hydrateSession(session);
|
||||
await AsyncStorage.setItem('greenlens_show_tour', 'true');
|
||||
posthog.capture('apple_login_succeeded', { surface: 'signup' });
|
||||
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
|
||||
if (session.isNewUser) {
|
||||
router.replace('/onboarding/source');
|
||||
} else {
|
||||
// Same routing as login: existing non-pro accounts go to the paywall
|
||||
// directly instead of bouncing through the root entitlement redirect.
|
||||
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
|
||||
}
|
||||
await finishAuth(session);
|
||||
} catch (e: any) {
|
||||
if (e?.code === 'ERR_REQUEST_CANCELED') {
|
||||
return;
|
||||
}
|
||||
if (e?.code === 'ERR_REQUEST_CANCELED') return;
|
||||
posthog.capture('apple_login_failed', {
|
||||
surface: 'signup',
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
@@ -160,72 +215,74 @@ export default function SignupScreen() {
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={[styles.flex, { backgroundColor: screenBackground }]}
|
||||
style={[styles.flex, { backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={[styles.backBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={20} color={colors.text} />
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => router.back()} activeOpacity={0.8}>
|
||||
<Ionicons name="arrow-back" size={20} color="#ffffff" />
|
||||
</TouchableOpacity>
|
||||
<Image
|
||||
source={require('../../assets/icon.png')}
|
||||
style={styles.logoIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={[styles.appName, { color: colors.text }]}>GreenLens</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
|
||||
{t.createAccount}
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
||||
<ImageBackground
|
||||
source={HERO_IMAGE}
|
||||
style={[styles.hero, { height: heroHeight, minHeight: heroHeight }]}
|
||||
imageStyle={styles.heroImage}
|
||||
>
|
||||
<View style={styles.heroOverlay} />
|
||||
<View style={styles.heroCopy}>
|
||||
<Text style={styles.heroTitle}>{copy.headline}</Text>
|
||||
<Text style={styles.heroSubline}>{copy.subline}</Text>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
|
||||
<View style={[styles.sheet, { backgroundColor: isDarkMode ? '#101a12' : '#fbfaf3' }]}>
|
||||
<View style={{ gap: emailExpanded ? 8 : 14 }}>
|
||||
{pendingPlant ? (
|
||||
<View style={[styles.pendingHint, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
|
||||
<Ionicons name="sparkles" size={18} color={colors.primary} />
|
||||
<Text style={[styles.pendingHintText, { color: colors.text }]}>
|
||||
{copy.savePlantPrefix} {pendingPlant.result.name}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Pending Plant Hint */}
|
||||
{pendingPlant && (
|
||||
<View style={[styles.pendingHint, { backgroundColor: `${colors.primarySoft}40`, borderColor: `${colors.primaryDark}40` }]}>
|
||||
<Ionicons name="sparkles" size={18} color={colors.primaryDark} />
|
||||
<Text style={[styles.pendingHintText, { color: colors.primaryDark }]}>
|
||||
{t.pendingPlantHint.replace('{0}', pendingPlant.result.name)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Card */}
|
||||
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.cardBorder, shadowColor: colors.cardShadow }]}>
|
||||
{appleAvailable ? (
|
||||
<AppleAuthentication.AppleAuthenticationButton
|
||||
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
|
||||
buttonStyle={isDarkMode
|
||||
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
|
||||
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
|
||||
cornerRadius={12}
|
||||
style={styles.appleButton}
|
||||
cornerRadius={14}
|
||||
style={[styles.appleButton, { height: appleButtonHeight }]}
|
||||
onPress={handleAppleSignIn}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{appleAvailable ? (
|
||||
<View style={styles.dividerRowCompact}>
|
||||
<View style={[styles.dividerRow, { marginVertical: emailExpanded ? 2 : 6 }]}>
|
||||
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
||||
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
|
||||
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Name */}
|
||||
<View style={styles.fieldGroup}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>Name</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
|
||||
{!emailExpanded ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.emailChoiceBtn, { backgroundColor: colors.surface, borderColor: colors.borderStrong }]}
|
||||
onPress={() => setEmailExpanded(true)}
|
||||
activeOpacity={0.84}
|
||||
>
|
||||
<Ionicons name="mail-outline" size={19} color={colors.primary} />
|
||||
<Text style={[styles.emailChoiceText, { color: colors.text }]}>{copy.emailCta}</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={[styles.form, { gap: emailExpanded && compact ? 8 : 12 }]}>
|
||||
<View style={[styles.fieldGroup, { gap: emailExpanded && compact ? 4 : 6 }]}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.nameLabel}</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border, height: inputHeight }]}>
|
||||
<Ionicons name="person-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
style={[styles.input, { color: colors.text, height: inputHeight }]}
|
||||
placeholder={t.namePlaceholder}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={name}
|
||||
@@ -237,13 +294,12 @@ export default function SignupScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Email */}
|
||||
<View style={styles.fieldGroup}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>E-Mail</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
|
||||
<View style={[styles.fieldGroup, { gap: emailExpanded && compact ? 4 : 6 }]}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.emailLabel}</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border, height: inputHeight }]}>
|
||||
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
style={[styles.input, { color: colors.text, height: inputHeight }]}
|
||||
placeholder={t.emailPlaceholder}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={email}
|
||||
@@ -256,13 +312,12 @@ export default function SignupScreen() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Password */}
|
||||
<View style={styles.fieldGroup}>
|
||||
<View style={[styles.fieldGroup, { gap: emailExpanded && compact ? 4 : 6 }]}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
|
||||
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border, height: inputHeight }]}>
|
||||
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
style={[styles.input, { color: colors.text, height: inputHeight }]}
|
||||
placeholder={t.passwordPlaceholder}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={password}
|
||||
@@ -272,28 +327,24 @@ export default function SignupScreen() {
|
||||
returnKeyType="next"
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={18}
|
||||
color={colors.textMuted}
|
||||
/>
|
||||
<Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Password Confirm */}
|
||||
<View style={styles.fieldGroup}>
|
||||
<View style={[styles.fieldGroup, { gap: emailExpanded && compact ? 4 : 6 }]}>
|
||||
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.confirmPasswordLabel}</Text>
|
||||
<View style={[
|
||||
styles.inputRow,
|
||||
{
|
||||
backgroundColor: colors.inputBg,
|
||||
borderColor: passwordConfirm && password !== passwordConfirm ? colors.danger : colors.inputBorder,
|
||||
backgroundColor: colors.surface,
|
||||
borderColor: passwordConfirm && password !== passwordConfirm ? colors.danger : colors.border,
|
||||
height: inputHeight,
|
||||
},
|
||||
]}>
|
||||
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
|
||||
<TextInput
|
||||
style={[styles.input, { color: colors.text }]}
|
||||
style={[styles.input, { color: colors.text, height: inputHeight }]}
|
||||
placeholder={t.confirmPasswordPlaceholder}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
value={passwordConfirm}
|
||||
@@ -304,78 +355,47 @@ export default function SignupScreen() {
|
||||
onSubmitEditing={handleSignup}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPasswordConfirm((v) => !v)} style={styles.eyeBtn}>
|
||||
<Ionicons
|
||||
name={showPasswordConfirm ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={18}
|
||||
color={colors.textMuted}
|
||||
/>
|
||||
<Ionicons name={showPasswordConfirm ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Password strength hint */}
|
||||
{password.length > 0 && (
|
||||
<View style={styles.strengthRow}>
|
||||
{[1, 2, 3, 4].map((level) => (
|
||||
<View
|
||||
key={level}
|
||||
style={[
|
||||
styles.strengthBar,
|
||||
{
|
||||
backgroundColor:
|
||||
password.length >= level * 3
|
||||
? level <= 1
|
||||
? colors.danger
|
||||
: level === 2
|
||||
? colors.warning
|
||||
: colors.success
|
||||
: colors.border,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
<Text style={[styles.strengthText, { color: colors.textMuted }]}>
|
||||
{password.length < 4
|
||||
? t.strengthTooShort
|
||||
: password.length < 7
|
||||
? t.strengthWeak
|
||||
: password.length < 10
|
||||
? t.strengthMedium
|
||||
: t.strengthStrong}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<View style={{ gap: 12, marginTop: 16 }}>
|
||||
{error ? (
|
||||
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
|
||||
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
|
||||
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
|
||||
<Text style={[styles.errorText, { color: colors.danger }]} selectable>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Signup Button */}
|
||||
{emailExpanded ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.7 : 1 }]}
|
||||
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.72 : 1 }]}
|
||||
onPress={handleSignup}
|
||||
activeOpacity={0.82}
|
||||
activeOpacity={0.84}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={colors.onPrimary} size="small" />
|
||||
) : (
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{t.onboardingRegister}</Text>
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{copy.createCta}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Login link */}
|
||||
<TouchableOpacity style={styles.loginLink} onPress={() => router.replace('/auth/login')}>
|
||||
<TouchableOpacity style={styles.loginLink} onPress={() => router.replace({ pathname: '/auth/login', params: { returnTo: params.returnTo, topup: params.topup } })} activeOpacity={0.78}>
|
||||
<Text style={[styles.loginLinkText, { color: colors.textSecondary }]}>
|
||||
{t.alreadyHaveAccount}{' '}
|
||||
<Text style={{ color: colors.primary, fontWeight: '600' }}>{t.onboardingLogin}</Text>
|
||||
{copy.already}{' '}
|
||||
<Text style={{ color: colors.primary, fontWeight: '800' }}>{copy.login}</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={[styles.legal, { color: colors.textMuted }]}>{copy.legal}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
@@ -383,160 +403,112 @@ export default function SignupScreen() {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
scroll: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
scroll: { flexGrow: 1 },
|
||||
hero: {
|
||||
minHeight: 330,
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 48,
|
||||
paddingTop: 56,
|
||||
paddingBottom: 34,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 32,
|
||||
heroImage: { resizeMode: 'cover' },
|
||||
heroOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(5, 12, 7, 0.46)',
|
||||
},
|
||||
backBtn: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
top: 54,
|
||||
left: 22,
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.28)',
|
||||
zIndex: 10,
|
||||
elevation: 10,
|
||||
},
|
||||
logoIcon: {
|
||||
width: 84,
|
||||
height: 84,
|
||||
borderRadius: 20,
|
||||
marginBottom: 16,
|
||||
heroCopy: { gap: 10 },
|
||||
heroTitle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 34,
|
||||
lineHeight: 39,
|
||||
fontWeight: '900',
|
||||
},
|
||||
appName: {
|
||||
fontSize: 30,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.5,
|
||||
marginBottom: 6,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '400',
|
||||
},
|
||||
card: {
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
padding: 24,
|
||||
gap: 14,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 1,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
},
|
||||
appleButton: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
marginBottom: 2,
|
||||
},
|
||||
dividerRowCompact: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginVertical: 2,
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
},
|
||||
dividerText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
fieldGroup: {
|
||||
gap: 6,
|
||||
},
|
||||
label: {
|
||||
fontSize: 13,
|
||||
fontWeight: '500',
|
||||
marginLeft: 2,
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 14,
|
||||
height: 50,
|
||||
},
|
||||
inputIcon: {
|
||||
marginRight: 10,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
height: 50,
|
||||
},
|
||||
eyeBtn: {
|
||||
padding: 4,
|
||||
marginLeft: 6,
|
||||
},
|
||||
strengthRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
marginTop: -4,
|
||||
},
|
||||
strengthBar: {
|
||||
flex: 1,
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
},
|
||||
strengthText: {
|
||||
fontSize: 11,
|
||||
marginLeft: 4,
|
||||
width: 40,
|
||||
},
|
||||
errorBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 13,
|
||||
flex: 1,
|
||||
},
|
||||
primaryBtn: {
|
||||
height: 52,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
primaryBtnText: {
|
||||
heroSubline: {
|
||||
color: 'rgba(255, 255, 255, 0.88)',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
lineHeight: 22,
|
||||
fontWeight: '700',
|
||||
},
|
||||
loginLink: {
|
||||
alignItems: 'center',
|
||||
marginTop: 24,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
loginLinkText: {
|
||||
fontSize: 15,
|
||||
sheet: {
|
||||
flex: 1,
|
||||
marginTop: -24,
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
paddingHorizontal: 22,
|
||||
paddingTop: 28,
|
||||
paddingBottom: 40,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
pendingHint: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
gap: 10,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
marginBottom: 20,
|
||||
gap: 12,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
pendingHintText: {
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
lineHeight: 18,
|
||||
pendingHintText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' },
|
||||
appleButton: { width: '100%', height: 56 },
|
||||
dividerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||
dividerLine: { flex: 1, height: 1 },
|
||||
dividerText: { fontSize: 12, fontWeight: '800' },
|
||||
emailChoiceBtn: {
|
||||
minHeight: 56,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
emailChoiceText: { fontSize: 16, fontWeight: '800' },
|
||||
form: { gap: 12 },
|
||||
fieldGroup: { gap: 6 },
|
||||
label: { fontSize: 13, fontWeight: '800', marginLeft: 2 },
|
||||
inputRow: {
|
||||
minHeight: 52,
|
||||
borderWidth: 1,
|
||||
borderRadius: 14,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
inputIcon: { marginRight: 10 },
|
||||
input: { flex: 1, minHeight: 52, fontSize: 15 },
|
||||
eyeBtn: { padding: 5, marginLeft: 6 },
|
||||
errorBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 7,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
errorText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' },
|
||||
primaryBtn: {
|
||||
minHeight: 56,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
primaryBtnText: { fontSize: 17, fontWeight: '900' },
|
||||
loginLink: { alignItems: 'center', paddingTop: 4, paddingBottom: 2 },
|
||||
loginLinkText: { fontSize: 15, fontWeight: '700' },
|
||||
legal: { textAlign: 'center', fontSize: 11.5, lineHeight: 16, paddingHorizontal: 12 },
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'expo-router';
|
||||
import { parseShareIntent, ShareIntentModule } from 'expo-share-intent';
|
||||
import * as ExpoLinking from 'expo-linking';
|
||||
import { SHARE_INTENT_KEY, storeSharedImageUri } from '../utils/shareHandoff';
|
||||
import { resolveSharedImageUri, summarizeShareIntent } from '../utils/shareIntent';
|
||||
import { isSharedImageResolutionFailure, resolveSharedImageUri, summarizeShareIntent } from '../utils/shareIntent';
|
||||
|
||||
const SHARE_INTENT_SCHEME = 'greenlens';
|
||||
const SHARE_INTENT_OPTIONS = {
|
||||
@@ -16,6 +16,8 @@ export default function ShareIntentCallbackScreen() {
|
||||
const router = useRouter();
|
||||
const [isWaiting, setIsWaiting] = React.useState(true);
|
||||
const [failureDetails, setFailureDetails] = React.useState<string | null>(null);
|
||||
const [failureTitle, setFailureTitle] = React.useState<string | null>(null);
|
||||
const [failureBody, setFailureBody] = React.useState<string | null>(null);
|
||||
const [previewUri, setPreviewUri] = React.useState<string | null>(null);
|
||||
const pendingKeyRef = React.useRef<string | null>(null);
|
||||
const getIntentCalledRef = React.useRef(false);
|
||||
@@ -33,11 +35,13 @@ export default function ShareIntentCallbackScreen() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const showFailure = (message: string) => {
|
||||
const showFailure = (message: string, title?: string, body?: string) => {
|
||||
settledRef.current = true;
|
||||
ShareIntentModule?.clearShareIntent(SHARE_INTENT_KEY);
|
||||
setPreviewUri(null);
|
||||
setIsWaiting(false);
|
||||
setFailureTitle(title ?? null);
|
||||
setFailureBody(body ?? null);
|
||||
setFailureDetails(message);
|
||||
};
|
||||
|
||||
@@ -45,13 +49,23 @@ export default function ShareIntentCallbackScreen() {
|
||||
try {
|
||||
setIsWaiting(true);
|
||||
setFailureDetails(null);
|
||||
setFailureTitle(null);
|
||||
setFailureBody(null);
|
||||
const shareIntent = parseShareIntent(event.value, SHARE_INTENT_OPTIONS);
|
||||
if (__DEV__) {
|
||||
console.debug('[ShareIntentCallback]', summarizeShareIntent(shareIntent));
|
||||
}
|
||||
const resolved = await resolveSharedImageUri(shareIntent);
|
||||
if (!resolved) {
|
||||
if (isSharedImageResolutionFailure(resolved)) {
|
||||
if (resolved.failureReason === 'login_wall') {
|
||||
showFailure(
|
||||
'Die geteilte Seite verlangt einen Login und hat das Bild nicht ausgeliefert.',
|
||||
'Instagram hat den Post nicht freigegeben',
|
||||
'Mach einen Screenshot vom Post und teile den Screenshot mit GreenLens — das funktioniert immer.',
|
||||
);
|
||||
} else {
|
||||
showFailure('Die Quelle hat keinen nutzbaren Bildanhang oder Bild-Link geliefert.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
settledRef.current = true;
|
||||
@@ -118,9 +132,10 @@ export default function ShareIntentCallbackScreen() {
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.messageBox}>
|
||||
<Text style={styles.title}>Bild konnte nicht geladen werden.</Text>
|
||||
<Text style={styles.title}>{failureTitle ?? 'Bild konnte nicht geladen werden.'}</Text>
|
||||
<Text style={styles.body}>
|
||||
Tippe und halte das Bild in Safari oder Instagram, wähle „Bild teilen" und teile es direkt mit GreenLens.
|
||||
{failureBody
|
||||
?? 'Tippe und halte das Bild in Safari oder Instagram, wähle „Bild teilen" und teile es direkt mit GreenLens.'}
|
||||
</Text>
|
||||
{failureDetails ? (
|
||||
<Text style={styles.detail}>{failureDetails}</Text>
|
||||
|
||||
@@ -1,165 +1,274 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Image,
|
||||
ImageBackground,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import Svg, { Path } from 'react-native-svg';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { useSafeAnalytics } from '../services/analytics';
|
||||
import { getWelcomeHeadlineVariant, WelcomeHeadlineVariant } from '../services/experiments';
|
||||
import { APP_STORE_RATING, getTestimonialForLanguage, translatedNote } from '../constants/socialProof';
|
||||
import { Language } from '../types';
|
||||
|
||||
type Feature = {
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
title: string;
|
||||
description: string;
|
||||
const getWelcomeCopy = (language: Language, variant: WelcomeHeadlineVariant) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
// Variante A (symptom): steigt direkt in Emmas akuten Moment ein.
|
||||
// Variante B (contrast): grenzt gegen kostenlose Identifikations-Apps ab.
|
||||
headline:
|
||||
variant === 'symptom'
|
||||
? 'Gelbe Blätter. Und jetzt?'
|
||||
: 'Andere Apps sagen dir, wie deine Pflanze heißt.',
|
||||
subline:
|
||||
variant === 'symptom'
|
||||
? 'GreenLens nennt dir die wahrscheinlichsten Ursachen, was du selbst prüfen solltest und die eine sichere Sache, die du jetzt tun kannst.'
|
||||
: 'GreenLens sagt dir, was mit ihr los ist: wahrscheinliche Ursache, was du prüfen sollst, was du als Nächstes tust - und ein Check, ob es gewirkt hat.',
|
||||
cta: 'Meine Pflanze prüfen',
|
||||
login: 'Ich habe schon ein Konto',
|
||||
demoScan: 'Erst ausprobieren - ohne Konto',
|
||||
legal: 'Mit dem Fortfahren akzeptierst du unsere Datenschutzerklärung und AGB.',
|
||||
ratingValue: APP_STORE_RATING.value.toLocaleString('de-DE', { minimumFractionDigits: 1 }),
|
||||
ratingCount: `${APP_STORE_RATING.count} Bewertungen`,
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
headline:
|
||||
variant === 'symptom'
|
||||
? 'Hojas amarillas. ¿Y ahora?'
|
||||
: 'Otras apps te dicen cómo se llama tu planta.',
|
||||
subline:
|
||||
variant === 'symptom'
|
||||
? 'GreenLens te da las causas más probables, qué revisar tú mismo y la única cosa segura que puedes hacer ahora.'
|
||||
: 'GreenLens te dice qué le pasa: causa probable, qué revisar, qué hacer después - y una comprobación de si funcionó.',
|
||||
cta: 'Revisar mi planta',
|
||||
login: 'Ya tengo cuenta',
|
||||
demoScan: 'Probar primero - sin cuenta',
|
||||
legal: 'Al continuar aceptas nuestra Política de privacidad y Términos.',
|
||||
ratingValue: APP_STORE_RATING.value.toLocaleString('es-ES', { minimumFractionDigits: 1 }),
|
||||
ratingCount: `${APP_STORE_RATING.count} valoraciones`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
headline:
|
||||
variant === 'symptom'
|
||||
? 'Yellow leaves. Now what?'
|
||||
: 'Other apps tell you what your plant is called.',
|
||||
subline:
|
||||
variant === 'symptom'
|
||||
? 'GreenLens gives you the most likely causes, what to check yourself, and the one safe thing to do right now.'
|
||||
: "GreenLens tells you what's wrong with it: likely cause, what to check, what to do next - and a follow-up on whether it worked.",
|
||||
cta: 'Check my plant',
|
||||
login: 'I already have an account',
|
||||
demoScan: 'Try it first - no account needed',
|
||||
legal: 'By continuing you agree to our Privacy Policy and Terms.',
|
||||
ratingValue: APP_STORE_RATING.value.toLocaleString('en-US', { minimumFractionDigits: 1 }),
|
||||
ratingCount: `${APP_STORE_RATING.count} ratings`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Proof-Punkte statt Testimonial: greifen, solange keine echten Store-Zitate
|
||||
* hinterlegt sind. Beide Zeilen sind Produktaussagen, keine Behauptungen ueber
|
||||
* Ergebnisse - sie halten damit die Positionierung "Triage statt Diagnose".
|
||||
*/
|
||||
const getProofPoints = (language: Language): string[] => {
|
||||
if (language === 'de') {
|
||||
return [
|
||||
'Wahrscheinlichste Ursachen - mit Angabe, wie sicher wir sind',
|
||||
'Konkrete Prüfschritte statt allgemeiner Pflegetipps',
|
||||
'Nach 7 Tagen fragen wir nach, ob es gewirkt hat',
|
||||
];
|
||||
}
|
||||
if (language === 'es') {
|
||||
return [
|
||||
'Causas más probables - indicando qué tan seguros estamos',
|
||||
'Pasos concretos de revisión, no consejos genéricos',
|
||||
'A los 7 días preguntamos si funcionó',
|
||||
];
|
||||
}
|
||||
return [
|
||||
'Most likely causes - with how confident we actually are',
|
||||
'Concrete things to check, not generic care tips',
|
||||
'After 7 days we ask whether it worked',
|
||||
];
|
||||
};
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const { t } = useApp();
|
||||
const { language } = useApp();
|
||||
const { height, width } = useWindowDimensions();
|
||||
const compact = height < 760;
|
||||
const sheetTop = compact ? 142 : 156;
|
||||
const waveHeight = compact ? 148 : 170;
|
||||
const bodyOffset = waveHeight - 2;
|
||||
const contentTop = compact ? 94 : 108;
|
||||
const compact = height < 700 || width < 380;
|
||||
const posthog = useSafeAnalytics();
|
||||
const insets = useSafeAreaInsets();
|
||||
// Der Hero bekommt den Rest der Hoehe, faellt aber nie unter diese Grenze.
|
||||
// Vorher war er fest auf 52% - dadurch hat die laengere Headline-Variante
|
||||
// den dritten Button aus dem Bildschirm geschoben.
|
||||
const heroMinHeight = Math.round(height * (compact ? 0.34 : 0.40));
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: 'scan-outline',
|
||||
title: t.welcomeFeatureIdentifyTitle,
|
||||
description: t.welcomeFeatureIdentifyDesc,
|
||||
},
|
||||
{
|
||||
icon: 'notifications-outline',
|
||||
title: t.welcomeFeatureReminderTitle,
|
||||
description: t.welcomeFeatureReminderDesc,
|
||||
},
|
||||
{
|
||||
icon: 'book-outline',
|
||||
title: t.welcomeFeatureLibraryTitle,
|
||||
description: t.welcomeFeatureLibraryDesc,
|
||||
},
|
||||
];
|
||||
const [variant, setVariant] = React.useState<WelcomeHeadlineVariant | null>(null);
|
||||
const copy = getWelcomeCopy(language, variant ?? 'symptom');
|
||||
const testimonial = getTestimonialForLanguage(language);
|
||||
const proofPoints = getProofPoints(language);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
void getWelcomeHeadlineVariant().then((assigned) => {
|
||||
if (!active) return;
|
||||
setVariant(assigned);
|
||||
posthog.capture('onboarding_welcome_viewed', { welcome_variant: assigned });
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [posthog]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ImageBackground
|
||||
source={require('../assets/welcome_botanical_hero.png')}
|
||||
style={styles.heroImage}
|
||||
imageStyle={styles.heroImageContent}
|
||||
<Image
|
||||
source={require('../assets/welcome_hero_vertical.png')}
|
||||
style={[{ position: 'absolute', top: 0, left: 0, right: 0, width: '100%' }, { height: compact ? '65%' : '70%' }]}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}
|
||||
alwaysBounceVertical={false}
|
||||
>
|
||||
<View style={styles.heroShadeTop} />
|
||||
<View style={styles.heroShadeBottom} />
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={[styles.brandRow, compact && styles.brandRowCompact]}>
|
||||
<View style={[styles.heroBlock, { minHeight: heroMinHeight }]}>
|
||||
<View style={[styles.heroSafe, { paddingTop: Math.max(insets.top, 16) }]}>
|
||||
<View style={styles.heroTopRow}>
|
||||
<View style={styles.brandRow}>
|
||||
<Image
|
||||
source={require('../assets/icon.png')}
|
||||
style={styles.logo}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<Text style={styles.brandName}>
|
||||
<Text
|
||||
style={styles.brandName}
|
||||
maxFontSizeMultiplier={1.2}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.82}
|
||||
>
|
||||
Green<Text style={styles.brandAccent}>Lens</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ImageBackground>
|
||||
|
||||
<View style={[styles.sheet, { top: sheetTop }]}>
|
||||
<Svg
|
||||
width={width}
|
||||
height={waveHeight}
|
||||
viewBox={`0 0 ${width} ${waveHeight}`}
|
||||
preserveAspectRatio="none"
|
||||
style={styles.sheetWave}
|
||||
>
|
||||
<Path
|
||||
d={`M0 34 C ${width * 0.08} 76 ${width * 0.14} 82 ${width * 0.24} 82 C ${width * 0.38} 82 ${width * 0.52} 82 ${width * 0.64} 82 C ${width * 0.78} 86 ${width * 0.88} 132 ${width} 156 L ${width} ${waveHeight} L 0 ${waveHeight} Z`}
|
||||
fill="#fbfaf3"
|
||||
/>
|
||||
</Svg>
|
||||
<View style={[styles.sheetBody, { top: bodyOffset }]} />
|
||||
<View
|
||||
style={[
|
||||
styles.sheetContent,
|
||||
{ top: contentTop },
|
||||
compact && styles.sheetContentCompact,
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.headline, compact && styles.headlineCompact]}>
|
||||
{t.welcomeHeadline}
|
||||
{/* Rating und Anzahl immer gemeinsam: 5,0 aus 6 Bewertungen ist
|
||||
ehrlich und ueberpruefbar - eine nackte Sternezahl waere es nicht. */}
|
||||
<View style={styles.ratingPill}>
|
||||
<Ionicons name="star" size={13} color="#f5c04e" />
|
||||
<Text style={styles.ratingText} maxFontSizeMultiplier={1.2} numberOfLines={1}>
|
||||
{copy.ratingValue}
|
||||
</Text>
|
||||
<Text style={styles.subheadline}>{t.welcomeSubheadline}</Text>
|
||||
|
||||
<View style={styles.features}>
|
||||
{features.map((feature, index) => (
|
||||
<View
|
||||
key={feature.title}
|
||||
style={[
|
||||
styles.featureRow,
|
||||
index === features.length - 1 && styles.featureRowLast,
|
||||
]}
|
||||
<Text
|
||||
style={styles.ratingCount}
|
||||
maxFontSizeMultiplier={1.2}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.72}
|
||||
>
|
||||
<View style={styles.featureIcon}>
|
||||
<Ionicons name={feature.icon} size={22} color="#a6d66f" />
|
||||
· {copy.ratingCount}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.featureCopy}>
|
||||
<Text style={styles.featureTitle}>{feature.title}</Text>
|
||||
<Text style={styles.featureDescription}>{feature.description}</Text>
|
||||
</View>
|
||||
|
||||
{testimonial ? (
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
<Text
|
||||
style={styles.testimonialText}
|
||||
numberOfLines={compact ? 3 : 4}
|
||||
maxFontSizeMultiplier={1.3}
|
||||
>
|
||||
„{testimonial.quote}"
|
||||
</Text>
|
||||
<View style={styles.testimonialMeta}>
|
||||
<Text style={styles.testimonialAuthor} maxFontSizeMultiplier={1.2}>
|
||||
{testimonial.author}
|
||||
{testimonial.isTranslated ? ` · ${translatedNote(language)}` : ''}
|
||||
</Text>
|
||||
<View style={styles.starsRow}>
|
||||
{Array.from({ length: testimonial.stars }).map((_, i) => (
|
||||
<Ionicons key={i} name="star" size={14} color="#f5c04e" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
/* Kein belegtes Zitat vorhanden → Argument statt erfundenem Social Proof. */
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
{proofPoints.map((point) => (
|
||||
<View key={point} style={styles.proofRow}>
|
||||
<Ionicons name="checkmark-circle" size={15} color="#a6d66f" />
|
||||
<Text style={styles.proofText} maxFontSizeMultiplier={1.3}>{point}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.sheet, { paddingBottom: Math.max(insets.bottom, 12) }]}>
|
||||
<View style={styles.sheetHandle} />
|
||||
<View style={styles.sheetContent}>
|
||||
{/* Erst rendern, wenn die Variante feststeht. Sonst blitzt fuer die
|
||||
Haelfte der Nutzer kurz die falsche Headline auf und wechselt dann -
|
||||
das sieht kaputt aus und verfaelscht den Test. */}
|
||||
<View style={{ opacity: variant ? 1 : 0 }}>
|
||||
{/* maxFontSizeMultiplier: groessere Systemschrift darf die Headline
|
||||
wachsen lassen, aber nicht die CTAs aus dem Screen schieben. */}
|
||||
<Text
|
||||
style={[styles.headline, compact && styles.headlineCompact]}
|
||||
maxFontSizeMultiplier={1.25}
|
||||
>
|
||||
{copy.headline}
|
||||
</Text>
|
||||
<Text style={styles.subline} maxFontSizeMultiplier={1.3}>{copy.subline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.demoButton}
|
||||
onPress={() => router.push('/scanner')}
|
||||
style={styles.cta}
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_started', { welcome_variant: variant ?? 'unassigned' });
|
||||
router.push('/onboarding/slides');
|
||||
}}
|
||||
activeOpacity={0.86}
|
||||
>
|
||||
<Ionicons name="scan" size={25} color="#f8f7ef" />
|
||||
<Text style={styles.demoButtonText}>{t.welcomeDemoScan}</Text>
|
||||
<Ionicons name="chevron-forward" size={26} color="#f8f7ef" />
|
||||
<Text style={styles.ctaText} maxFontSizeMultiplier={1.2}>{copy.cta}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.authRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.authButton}
|
||||
onPress={() => router.push('/auth/signup')}
|
||||
activeOpacity={0.82}
|
||||
>
|
||||
<Text style={styles.authButtonText}>{t.onboardingRegister}</Text>
|
||||
<TouchableOpacity onPress={() => router.push('/auth/login')} style={styles.loginLink}>
|
||||
<Text style={styles.loginText} maxFontSizeMultiplier={1.2}>{copy.login}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.authButton, styles.loginButton]}
|
||||
onPress={() => router.push('/auth/login')}
|
||||
activeOpacity={0.82}
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_demo_scan_started', {
|
||||
welcome_variant: variant ?? 'unassigned',
|
||||
});
|
||||
router.push('/scanner');
|
||||
}}
|
||||
style={styles.demoLink}
|
||||
>
|
||||
<Text style={[styles.authButtonText, styles.loginButtonText]}>
|
||||
{t.onboardingLogin}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.subscriptionLink}
|
||||
onPress={() => router.push('/profile/billing')}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="leaf-outline" size={21} color="#4b7c31" />
|
||||
<Text style={styles.subscriptionText}>{t.welcomeSubscriptionPlans}</Text>
|
||||
<Ionicons name="chevron-forward" size={20} color="#4b7c31" />
|
||||
<Ionicons name="scan-outline" size={18} color="#a6d66f" />
|
||||
<Text style={styles.demoText} maxFontSizeMultiplier={1.2}>{copy.demoScan}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.legalText}>{t.welcomeLegal}</Text>
|
||||
<Text style={styles.legal} maxFontSizeMultiplier={1.3}>{copy.legal}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -169,196 +278,235 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: '#0a110b',
|
||||
},
|
||||
heroImage: {
|
||||
height: '60%',
|
||||
minHeight: 430,
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
// Fuellt mindestens den Bildschirm. Wird der Inhalt hoeher (lange
|
||||
// Headline-Variante, grosse Systemschrift, kleines Geraet), scrollt der
|
||||
// Screen - statt die unteren Buttons abzuschneiden.
|
||||
flexGrow: 1,
|
||||
},
|
||||
heroBlock: {
|
||||
width: '100%',
|
||||
// Nimmt den Platz, den das Sheet uebrig laesst, faellt aber nie unter
|
||||
// heroMinHeight. Vorher fest 52% - das hat nur bei kurzer Copy gepasst.
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
},
|
||||
hero: {
|
||||
width: '100%',
|
||||
},
|
||||
heroImageContent: {
|
||||
backgroundColor: '#0a110b',
|
||||
transform: [{ scale: 1.04 }],
|
||||
},
|
||||
heroShadeTop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.08)',
|
||||
},
|
||||
heroShadeBottom: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 190,
|
||||
backgroundColor: 'rgba(7,12,7,0.2)',
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
brandRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 15,
|
||||
paddingHorizontal: 30,
|
||||
paddingTop: 62,
|
||||
},
|
||||
brandRowCompact: {
|
||||
paddingTop: 42,
|
||||
},
|
||||
logo: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
brandName: {
|
||||
color: '#f8f7ef',
|
||||
fontSize: 36,
|
||||
fontWeight: '900',
|
||||
},
|
||||
brandAccent: {
|
||||
color: '#9bc76e',
|
||||
},
|
||||
sheet: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
sheetWave: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: 120,
|
||||
backgroundColor: 'rgba(10,17,11,0.4)',
|
||||
},
|
||||
sheetBody: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: '#fbfaf3',
|
||||
},
|
||||
sheetContent: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
sheetContentCompact: {
|
||||
paddingHorizontal: 22,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
headline: {
|
||||
color: '#101c12',
|
||||
fontSize: 40,
|
||||
lineHeight: 43,
|
||||
fontWeight: '900',
|
||||
marginBottom: 6,
|
||||
maxWidth: 310,
|
||||
},
|
||||
headlineCompact: {
|
||||
fontSize: 34,
|
||||
lineHeight: 37,
|
||||
},
|
||||
subheadline: {
|
||||
color: '#5f625d',
|
||||
fontSize: 15,
|
||||
lineHeight: 19,
|
||||
fontWeight: '500',
|
||||
marginBottom: 11,
|
||||
},
|
||||
features: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
featureRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 11,
|
||||
paddingVertical: 6,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(16,28,18,0.14)',
|
||||
},
|
||||
featureRowLast: {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
featureIcon: {
|
||||
width: 46,
|
||||
height: 46,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#173817',
|
||||
},
|
||||
featureCopy: {
|
||||
heroSafe: {
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 24,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
featureTitle: {
|
||||
color: '#101c12',
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
marginBottom: 2,
|
||||
},
|
||||
featureDescription: {
|
||||
color: '#696b65',
|
||||
fontSize: 13,
|
||||
lineHeight: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
demoButton: {
|
||||
height: 60,
|
||||
borderRadius: 7,
|
||||
backgroundColor: '#437824',
|
||||
heroTopRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 18,
|
||||
marginBottom: 8,
|
||||
width: '100%',
|
||||
},
|
||||
demoButtonText: {
|
||||
color: '#f8f7ef',
|
||||
fontSize: 21,
|
||||
fontWeight: '800',
|
||||
},
|
||||
authRow: {
|
||||
brandRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
authButton: {
|
||||
flex: 1,
|
||||
height: 50,
|
||||
borderRadius: 7,
|
||||
borderWidth: 1.4,
|
||||
borderColor: '#4b7c31',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
flexShrink: 1,
|
||||
},
|
||||
loginButton: {
|
||||
borderColor: '#101c12',
|
||||
logo: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
authButtonText: {
|
||||
color: '#4b7c31',
|
||||
fontSize: 17,
|
||||
brandName: {
|
||||
flexShrink: 1,
|
||||
color: '#ffffff',
|
||||
fontSize: 24,
|
||||
fontWeight: '900',
|
||||
},
|
||||
brandAccent: {
|
||||
color: '#8ba885',
|
||||
},
|
||||
ratingPill: {
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginLeft: 10,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.25)',
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
ratingText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
loginButtonText: {
|
||||
color: '#101c12',
|
||||
ratingCount: {
|
||||
flexShrink: 1,
|
||||
color: '#c9d3c2',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
subscriptionLink: {
|
||||
minHeight: 24,
|
||||
proofRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
proofText: {
|
||||
flex: 1,
|
||||
color: '#e6efe2',
|
||||
fontSize: 13.5,
|
||||
lineHeight: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
testimonialCard: {
|
||||
backgroundColor: 'rgba(16, 26, 18, 0.75)',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.08)',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 14,
|
||||
elevation: 4,
|
||||
marginBottom: 8,
|
||||
},
|
||||
testimonialCardCompact: {
|
||||
padding: 12,
|
||||
},
|
||||
testimonialText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 14.5,
|
||||
lineHeight: 20,
|
||||
fontStyle: 'italic',
|
||||
marginBottom: 10,
|
||||
},
|
||||
testimonialMeta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
testimonialAuthor: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
marginRight: 8,
|
||||
color: '#a6d66f',
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
},
|
||||
starsRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 1,
|
||||
},
|
||||
sheet: {
|
||||
flexShrink: 0,
|
||||
backgroundColor: '#0c160d',
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
marginTop: -20,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
sheetHandle: {
|
||||
alignSelf: 'center',
|
||||
width: 44,
|
||||
height: 5,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
sheetContent: {
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
},
|
||||
headline: {
|
||||
color: '#ffffff',
|
||||
fontSize: 28,
|
||||
lineHeight: 34,
|
||||
fontWeight: '900',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
headlineCompact: {
|
||||
fontSize: 24,
|
||||
lineHeight: 29,
|
||||
},
|
||||
subline: {
|
||||
color: '#dae6d6',
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
},
|
||||
spacer: {
|
||||
height: 14,
|
||||
},
|
||||
cta: {
|
||||
minHeight: 58,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#437824',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
ctaText: {
|
||||
color: '#f8f7ef',
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
},
|
||||
loginLink: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 9,
|
||||
},
|
||||
loginText: {
|
||||
color: '#a6d66f',
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
},
|
||||
demoLink: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 7,
|
||||
minHeight: 50,
|
||||
paddingVertical: 13,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#a6d66f',
|
||||
backgroundColor: 'rgba(166, 214, 111, 0.05)',
|
||||
marginTop: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
subscriptionText: {
|
||||
color: '#4b7c31',
|
||||
demoText: {
|
||||
color: '#a6d66f',
|
||||
fontSize: 15,
|
||||
fontWeight: '800',
|
||||
textAlign: 'center',
|
||||
fontWeight: '700',
|
||||
},
|
||||
legalText: {
|
||||
color: '#6b6d68',
|
||||
legal: {
|
||||
color: '#8d9382',
|
||||
fontSize: 11,
|
||||
lineHeight: 14,
|
||||
fontWeight: '500',
|
||||
|
||||
@@ -55,21 +55,31 @@ export default function CustomizeOnboardingScreen() {
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<ThemeBackdrop colors={colors} />
|
||||
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={skipCustomization} style={[styles.iconBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Ionicons name="close" size={20} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.headerCopy}>
|
||||
<Text style={[styles.eyebrow, { color: colors.primary }]}>{t.onboardingChecklistIntro}</Text>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{t.customizeOnboardingTitle}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{t.customizeOnboardingSubtitle}</Text>
|
||||
<Text style={[styles.eyebrow, { color: colors.primary }]} maxFontSizeMultiplier={1.3}>
|
||||
{t.onboardingChecklistIntro}
|
||||
</Text>
|
||||
<Text style={[styles.title, { color: colors.text }]} maxFontSizeMultiplier={1.25}>
|
||||
{t.customizeOnboardingTitle}
|
||||
</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]} maxFontSizeMultiplier={1.3}>
|
||||
{t.customizeOnboardingSubtitle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<View style={[styles.previewCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.previewLabel, { color: colors.textMuted }]}>{t.customizeOnboardingPreview}</Text>
|
||||
<Text style={[styles.previewTitle, { color: colors.text }]}>{t.onboardingTagline}</Text>
|
||||
<Text style={[styles.previewLabel, { color: colors.textMuted }]} maxFontSizeMultiplier={1.3}>{t.customizeOnboardingPreview}</Text>
|
||||
<Text style={[styles.previewTitle, { color: colors.text }]} maxFontSizeMultiplier={1.3}>{t.onboardingTagline}</Text>
|
||||
<View style={styles.previewMeta}>
|
||||
<View style={[styles.previewChip, { backgroundColor: colors.primarySoft }]}>
|
||||
<Text style={[styles.previewChipText, { color: colors.primaryDark }]}>{appearanceMode}</Text>
|
||||
@@ -84,7 +94,7 @@ export default function CustomizeOnboardingScreen() {
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t.appearanceMode}</Text>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]} maxFontSizeMultiplier={1.3}>{t.appearanceMode}</Text>
|
||||
<View style={styles.segmentedControl}>
|
||||
{(['system', 'light', 'dark'] as AppearanceMode[]).map((mode) => {
|
||||
const isActive = appearanceMode === mode;
|
||||
@@ -96,7 +106,7 @@ export default function CustomizeOnboardingScreen() {
|
||||
style={[styles.segmentBtn, isActive && { backgroundColor: colors.primary }]}
|
||||
onPress={() => setAppearanceMode(mode)}
|
||||
>
|
||||
<Text style={[styles.segmentText, { color: isActive ? colors.onPrimary : colors.text }]}>
|
||||
<Text style={[styles.segmentText, { color: isActive ? colors.onPrimary : colors.text }]} maxFontSizeMultiplier={1.2} numberOfLines={2}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -106,7 +116,7 @@ export default function CustomizeOnboardingScreen() {
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t.colorPalette}</Text>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]} maxFontSizeMultiplier={1.3}>{t.colorPalette}</Text>
|
||||
<View style={styles.swatchContainer}>
|
||||
{(['forest', 'ocean', 'sunset', 'mono'] as ColorPalette[]).map((palette) => {
|
||||
const isActive = colorPalette === palette;
|
||||
@@ -127,7 +137,7 @@ export default function CustomizeOnboardingScreen() {
|
||||
onPress={() => setColorPalette(palette)}
|
||||
>
|
||||
<View style={[styles.swatch, { backgroundColor: swatch[0] }]} />
|
||||
<Text style={[styles.swatchLabel, { color: colors.text }]}>{label}</Text>
|
||||
<Text style={[styles.swatchLabel, { color: colors.text }]} maxFontSizeMultiplier={1.2} numberOfLines={2}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -135,7 +145,7 @@ export default function CustomizeOnboardingScreen() {
|
||||
</View>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]}>{t.language}</Text>
|
||||
<Text style={[styles.sectionTitle, { color: colors.text }]} maxFontSizeMultiplier={1.3}>{t.language}</Text>
|
||||
<View style={styles.languageRow}>
|
||||
{(['en', 'de', 'es'] as Language[]).map((lang) => {
|
||||
const isActive = language === lang;
|
||||
@@ -147,7 +157,10 @@ export default function CustomizeOnboardingScreen() {
|
||||
style={[styles.languageBtn, isActive && { backgroundColor: colors.primary }]}
|
||||
onPress={() => changeLanguage(lang)}
|
||||
>
|
||||
<Text style={{ color: isActive ? colors.onPrimary : colors.text, fontWeight: '600' }}>
|
||||
<Text
|
||||
style={{ color: isActive ? colors.onPrimary : colors.text, fontWeight: '600' }}
|
||||
maxFontSizeMultiplier={1.2}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -162,10 +175,10 @@ export default function CustomizeOnboardingScreen() {
|
||||
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
|
||||
onPress={skipCustomization}
|
||||
>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{t.customizeOnboardingSkip}</Text>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]} maxFontSizeMultiplier={1.2} numberOfLines={2}>{t.customizeOnboardingSkip}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colors.primary }]} onPress={finishCustomization}>
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{t.customizeOnboardingContinue}</Text>
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]} maxFontSizeMultiplier={1.2} numberOfLines={2}>{t.customizeOnboardingContinue}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
@@ -180,12 +193,13 @@ const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: 14,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
},
|
||||
iconBtn: {
|
||||
width: 40,
|
||||
@@ -218,6 +232,7 @@ const styles = StyleSheet.create({
|
||||
content: {
|
||||
padding: 20,
|
||||
gap: 16,
|
||||
paddingBottom: 12,
|
||||
},
|
||||
previewCard: {
|
||||
borderWidth: 1,
|
||||
@@ -317,7 +332,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
secondaryBtn: {
|
||||
flex: 1,
|
||||
height: 52,
|
||||
minHeight: 52,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1.5,
|
||||
alignItems: 'center',
|
||||
@@ -326,10 +343,13 @@ const styles = StyleSheet.create({
|
||||
secondaryBtnText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
primaryBtn: {
|
||||
flex: 1.3,
|
||||
height: 52,
|
||||
minHeight: 52,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -337,5 +357,6 @@ const styles = StyleSheet.create({
|
||||
primaryBtnText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,83 +1,44 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { OnboardingProgressService } from '../../services/onboardingProgressService';
|
||||
|
||||
const ONBOARDING_BACKGROUND = {
|
||||
light: '#fbfaf3',
|
||||
dark: '#0a110b',
|
||||
};
|
||||
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
|
||||
import { OnboardingQuestion, QuestionOption } from '../../components/OnboardingQuestion';
|
||||
|
||||
const EXPERIENCE_OPTIONS = [
|
||||
{ id: 'beginner', icon: 'leaf-outline' as const },
|
||||
{ id: 'intermediate', icon: 'sunny-outline' as const },
|
||||
{ id: 'advanced', icon: 'flask-outline' as const },
|
||||
{ id: 'beginner', emoji: '🌱' },
|
||||
{ id: 'intermediate', emoji: '☀️' },
|
||||
{ id: 'advanced', emoji: '🧪' },
|
||||
];
|
||||
|
||||
const getExperienceScreenCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
step: 'Schritt 3 von 4',
|
||||
heroBadge: 'Pflege-Tiefe',
|
||||
subtitles: {
|
||||
beginner: 'Klare Sprache, sichere Defaults, weniger Fachbegriffe.',
|
||||
intermediate: 'Praktische Schritte mit genug Kontext.',
|
||||
advanced: 'Mehr botanische Details und engere Diagnose.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (language === 'es') {
|
||||
return {
|
||||
step: 'Paso 3 de 4',
|
||||
heroBadge: 'Nivel de cuidado',
|
||||
subtitles: {
|
||||
beginner: 'Lenguaje claro y recomendaciones seguras.',
|
||||
intermediate: 'Pasos practicos con suficiente contexto.',
|
||||
advanced: 'Mas detalle botanico y diagnostico preciso.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
step: 'Step 3 of 4',
|
||||
heroBadge: 'Care depth',
|
||||
subtitles: {
|
||||
beginner: 'Clear language, fewer assumptions, safer defaults.',
|
||||
intermediate: 'Practical care steps with enough detail.',
|
||||
advanced: 'More botanical context and tighter diagnosis.',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function OnboardingExperienceScreen() {
|
||||
const router = useRouter();
|
||||
const posthog = useSafeAnalytics();
|
||||
const { session, isDarkMode, colorPalette, language, t } = useApp();
|
||||
const { session, isDarkMode, colorPalette, t } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;
|
||||
const [selectedLevel, setSelectedLevel] = useState<string | null>(null);
|
||||
const copy = getExperienceScreenCopy(language);
|
||||
|
||||
const levelLabels = useMemo(
|
||||
() => ({
|
||||
const levelLabels: Record<string, string> = {
|
||||
beginner: t.experienceOptionBeginner,
|
||||
intermediate: t.experienceOptionIntermediate,
|
||||
advanced: t.experienceOptionAdvanced,
|
||||
}),
|
||||
[t.experienceOptionAdvanced, t.experienceOptionBeginner, t.experienceOptionIntermediate],
|
||||
);
|
||||
};
|
||||
|
||||
const options: QuestionOption[] = EXPERIENCE_OPTIONS.map((option) => ({
|
||||
id: option.id,
|
||||
emoji: option.emoji,
|
||||
label: levelLabels[option.id],
|
||||
}));
|
||||
|
||||
const finish = (level: string | null) => {
|
||||
if (session?.userId && level) {
|
||||
OnboardingProgressService.setExperienceLevel(session.userId, level);
|
||||
}
|
||||
if (level) {
|
||||
void PreAuthOnboardingService.setAnswer('experienceLevel', level);
|
||||
}
|
||||
|
||||
posthog.capture('onboarding_experience_completed', {
|
||||
experience_level: level ?? 'skipped',
|
||||
@@ -86,113 +47,21 @@ export default function OnboardingExperienceScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: screenBackground }]}>
|
||||
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
|
||||
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
|
||||
</View>
|
||||
<ImageBackground
|
||||
source={require('../../assets/onboarding_experience_mockup.png')}
|
||||
style={[styles.heroPreview, { borderColor: colors.border }]}
|
||||
imageStyle={styles.heroImage}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<View style={[styles.heroOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.4)' : 'rgba(251, 250, 243, 0.24)' }]} />
|
||||
<View style={[styles.heroMetric, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Ionicons name="sparkles-outline" size={18} color={colors.primary} />
|
||||
<Text style={[styles.heroMetricText, { color: colors.text }]}>{copy.heroBadge}</Text>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{t.experienceOnboardingTitle}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{t.experienceOnboardingSubtitle}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.options}>
|
||||
{EXPERIENCE_OPTIONS.map((option) => {
|
||||
const isActive = selectedLevel === option.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.id}
|
||||
style={[
|
||||
styles.optionCard,
|
||||
{
|
||||
backgroundColor: isActive ? colors.primarySoft : colors.surface,
|
||||
borderColor: isActive ? colors.primary : colors.border,
|
||||
},
|
||||
]}
|
||||
onPress={() => setSelectedLevel(option.id)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={[styles.optionIcon, { backgroundColor: isActive ? colors.primary : colors.surfaceMuted }]}>
|
||||
<Ionicons name={option.icon} size={18} color={isActive ? colors.onPrimary : colors.textMuted} />
|
||||
</View>
|
||||
<View style={styles.optionCopy}>
|
||||
<Text style={[styles.optionLabel, { color: colors.text }]}>{levelLabels[option.id as keyof typeof levelLabels]}</Text>
|
||||
<Text style={[styles.optionSubtitle, { color: colors.textMuted }]}>
|
||||
{copy.subtitles[option.id as keyof typeof copy.subtitles]}
|
||||
</Text>
|
||||
</View>
|
||||
{isActive && <Ionicons name="checkmark-circle" size={18} color={colors.primary} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
|
||||
onPress={() => finish(null)}
|
||||
>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{t.experienceOnboardingSkip}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: selectedLevel ? colors.primary : colors.surfaceMuted }]}
|
||||
onPress={() => finish(selectedLevel)}
|
||||
disabled={!selectedLevel}
|
||||
>
|
||||
<Text style={[styles.primaryBtnText, { color: selectedLevel ? colors.onPrimary : colors.textMuted }]}>
|
||||
{t.experienceOnboardingContinue}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
<OnboardingQuestion
|
||||
colors={colors}
|
||||
isDarkMode={isDarkMode}
|
||||
step={3}
|
||||
totalSteps={4}
|
||||
title={t.experienceOnboardingTitle}
|
||||
subtitle={t.experienceOnboardingSubtitle}
|
||||
options={options}
|
||||
selectedId={selectedLevel}
|
||||
onSelect={setSelectedLevel}
|
||||
onContinue={() => finish(selectedLevel)}
|
||||
onBack={() => router.back()}
|
||||
continueLabel={t.experienceOnboardingContinue}
|
||||
skipLabel={t.experienceOnboardingSkip}
|
||||
onSkip={() => finish(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
safeArea: { flex: 1, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 14 },
|
||||
header: { alignItems: 'center', gap: 9, marginBottom: 14 },
|
||||
stepPill: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
|
||||
stepLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4 },
|
||||
heroPreview: { width: '100%', height: 175, borderRadius: 24, borderWidth: 1, overflow: 'hidden', justifyContent: 'flex-end', alignItems: 'flex-start' },
|
||||
heroImage: { borderRadius: 24 },
|
||||
heroOverlay: { ...StyleSheet.absoluteFillObject },
|
||||
heroMetric: { margin: 12, borderRadius: 999, borderWidth: 1, paddingHorizontal: 11, paddingVertical: 7, flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
heroMetricText: { fontSize: 12, fontWeight: '800' },
|
||||
title: { fontSize: 25, fontWeight: '800', textAlign: 'center', lineHeight: 29 },
|
||||
subtitle: { fontSize: 13, textAlign: 'center', lineHeight: 18, maxWidth: 320 },
|
||||
options: { gap: 8, flex: 1 },
|
||||
optionCard: {
|
||||
flex: 1,
|
||||
borderRadius: 15,
|
||||
borderWidth: 1.5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 16,
|
||||
gap: 10,
|
||||
},
|
||||
optionIcon: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center' },
|
||||
optionCopy: { flex: 1, gap: 3 },
|
||||
optionLabel: { fontSize: 14, fontWeight: '700' },
|
||||
optionSubtitle: { fontSize: 10.5, lineHeight: 14 },
|
||||
footer: { flexDirection: 'row', gap: 12, marginTop: 10 },
|
||||
secondaryBtn: { flex: 1, height: 50, borderRadius: 16, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
|
||||
secondaryBtnText: { fontSize: 15, fontWeight: '600' },
|
||||
primaryBtn: { flex: 1.2, height: 50, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
||||
primaryBtnText: { fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
|
||||
@@ -1,88 +1,46 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { OnboardingProgressService } from '../../services/onboardingProgressService';
|
||||
|
||||
const ONBOARDING_BACKGROUND = {
|
||||
light: '#fbfaf3',
|
||||
dark: '#0a110b',
|
||||
};
|
||||
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
|
||||
import { OnboardingQuestion, QuestionOption } from '../../components/OnboardingQuestion';
|
||||
|
||||
const GOAL_OPTIONS = [
|
||||
{ id: 'identify', icon: 'scan-outline' as const },
|
||||
{ id: 'care', icon: 'water-outline' as const },
|
||||
{ id: 'collection', icon: 'albums-outline' as const },
|
||||
{ id: 'learn', icon: 'book-outline' as const },
|
||||
{ id: 'identify', emoji: '🔍' },
|
||||
{ id: 'care', emoji: '💧' },
|
||||
{ id: 'collection', emoji: '🗂️' },
|
||||
{ id: 'learn', emoji: '📚' },
|
||||
];
|
||||
|
||||
const getGoalScreenCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
step: 'Schritt 2 von 4',
|
||||
heroBadge: 'Erstes Ziel',
|
||||
subtitles: {
|
||||
identify: 'Schnell erkennen, Pflege danach klaeren.',
|
||||
care: 'Aus Symptomen konkrete Schritte machen.',
|
||||
collection: 'Eine saubere Pflanzenbibliothek aufbauen.',
|
||||
learn: 'Pflanzenwissen einfacher einsortieren.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (language === 'es') {
|
||||
return {
|
||||
step: 'Paso 2 de 4',
|
||||
heroBadge: 'Primer objetivo',
|
||||
subtitles: {
|
||||
identify: 'Respuesta rapida primero, cuidado despues.',
|
||||
care: 'Convertir sintomas en pasos claros.',
|
||||
collection: 'Crear una biblioteca de plantas ordenada.',
|
||||
learn: 'Aprender plantas con explicaciones simples.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
step: 'Step 2 of 4',
|
||||
heroBadge: 'First goal',
|
||||
subtitles: {
|
||||
identify: 'Fast answer first, care details after.',
|
||||
care: 'Turn symptoms into a clear next step.',
|
||||
collection: 'Build a tidy plant library over time.',
|
||||
learn: 'Browse plants with simpler explanations.',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function OnboardingGoalScreen() {
|
||||
const router = useRouter();
|
||||
const posthog = useSafeAnalytics();
|
||||
const { session, isDarkMode, colorPalette, language, t } = useApp();
|
||||
const { session, isDarkMode, colorPalette, t } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;
|
||||
const [selectedGoal, setSelectedGoal] = useState<string | null>(null);
|
||||
const copy = getGoalScreenCopy(language);
|
||||
|
||||
const goalLabels = useMemo(
|
||||
() => ({
|
||||
const goalLabels: Record<string, string> = {
|
||||
identify: t.goalOptionIdentify,
|
||||
care: t.goalOptionCare,
|
||||
collection: t.goalOptionCollection,
|
||||
learn: t.goalOptionLearn,
|
||||
}),
|
||||
[t.goalOptionCare, t.goalOptionCollection, t.goalOptionIdentify, t.goalOptionLearn],
|
||||
);
|
||||
};
|
||||
|
||||
const options: QuestionOption[] = GOAL_OPTIONS.map((option) => ({
|
||||
id: option.id,
|
||||
emoji: option.emoji,
|
||||
label: goalLabels[option.id],
|
||||
}));
|
||||
|
||||
const finish = (goal: string | null) => {
|
||||
if (session?.userId && goal) {
|
||||
OnboardingProgressService.setPrimaryGoal(session.userId, goal);
|
||||
}
|
||||
if (goal) {
|
||||
void PreAuthOnboardingService.setAnswer('primaryGoal', goal);
|
||||
}
|
||||
|
||||
posthog.capture('onboarding_goal_completed', {
|
||||
goal: goal ?? 'skipped',
|
||||
@@ -91,113 +49,21 @@ export default function OnboardingGoalScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: screenBackground }]}>
|
||||
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
|
||||
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
|
||||
</View>
|
||||
<ImageBackground
|
||||
source={require('../../assets/onboarding_goal_mockup.png')}
|
||||
style={[styles.heroPreview, { borderColor: colors.border }]}
|
||||
imageStyle={styles.heroImage}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<View style={[styles.heroOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.22)' : 'rgba(251, 250, 243, 0.28)' }]} />
|
||||
<View style={[styles.heroBadge, { backgroundColor: colors.primary }]}>
|
||||
<Ionicons name="flag-outline" size={16} color={colors.onPrimary} />
|
||||
<Text style={[styles.heroBadgeText, { color: colors.onPrimary }]}>{copy.heroBadge}</Text>
|
||||
</View>
|
||||
</ImageBackground>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{t.goalOnboardingTitle}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{t.goalOnboardingSubtitle}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.options}>
|
||||
{GOAL_OPTIONS.map((option) => {
|
||||
const isActive = selectedGoal === option.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.id}
|
||||
style={[
|
||||
styles.optionCard,
|
||||
{
|
||||
backgroundColor: isActive ? colors.primarySoft : colors.surface,
|
||||
borderColor: isActive ? colors.primary : colors.border,
|
||||
},
|
||||
]}
|
||||
onPress={() => setSelectedGoal(option.id)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={[styles.optionIcon, { backgroundColor: isActive ? colors.primary : colors.surfaceMuted }]}>
|
||||
<Ionicons name={option.icon} size={18} color={isActive ? colors.onPrimary : colors.textMuted} />
|
||||
</View>
|
||||
<View style={styles.optionCopy}>
|
||||
<Text style={[styles.optionLabel, { color: colors.text }]}>{goalLabels[option.id as keyof typeof goalLabels]}</Text>
|
||||
<Text style={[styles.optionSubtitle, { color: colors.textMuted }]}>
|
||||
{copy.subtitles[option.id as keyof typeof copy.subtitles]}
|
||||
</Text>
|
||||
</View>
|
||||
{isActive && <Ionicons name="checkmark-circle" size={18} color={colors.primary} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
|
||||
onPress={() => finish(null)}
|
||||
>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{t.goalOnboardingSkip}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.primaryBtn, { backgroundColor: selectedGoal ? colors.primary : colors.surfaceMuted }]}
|
||||
onPress={() => finish(selectedGoal)}
|
||||
disabled={!selectedGoal}
|
||||
>
|
||||
<Text style={[styles.primaryBtnText, { color: selectedGoal ? colors.onPrimary : colors.textMuted }]}>
|
||||
{t.goalOnboardingContinue}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
<OnboardingQuestion
|
||||
colors={colors}
|
||||
isDarkMode={isDarkMode}
|
||||
step={2}
|
||||
totalSteps={4}
|
||||
title={t.goalOnboardingTitle}
|
||||
subtitle={t.goalOnboardingSubtitle}
|
||||
options={options}
|
||||
selectedId={selectedGoal}
|
||||
onSelect={setSelectedGoal}
|
||||
onContinue={() => finish(selectedGoal)}
|
||||
onBack={() => router.back()}
|
||||
continueLabel={t.goalOnboardingContinue}
|
||||
skipLabel={t.goalOnboardingSkip}
|
||||
onSkip={() => finish(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
safeArea: { flex: 1, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 14 },
|
||||
header: { alignItems: 'center', gap: 9, marginBottom: 14 },
|
||||
stepPill: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
|
||||
stepLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4 },
|
||||
heroPreview: { width: '100%', height: 175, borderRadius: 24, borderWidth: 1, overflow: 'hidden', justifyContent: 'flex-end', alignItems: 'flex-start' },
|
||||
heroImage: { borderRadius: 24 },
|
||||
heroOverlay: { ...StyleSheet.absoluteFillObject },
|
||||
heroBadge: { margin: 12, borderRadius: 999, paddingHorizontal: 11, paddingVertical: 7, flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
heroBadgeText: { fontSize: 12, fontWeight: '800' },
|
||||
title: { fontSize: 25, fontWeight: '800', textAlign: 'center', lineHeight: 29 },
|
||||
subtitle: { fontSize: 13, textAlign: 'center', lineHeight: 18, maxWidth: 320 },
|
||||
options: { gap: 8, flex: 1 },
|
||||
optionCard: {
|
||||
flex: 1,
|
||||
borderRadius: 15,
|
||||
borderWidth: 1.5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
gap: 10,
|
||||
},
|
||||
optionIcon: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center' },
|
||||
optionCopy: { flex: 1, gap: 3 },
|
||||
optionLabel: { fontSize: 14, fontWeight: '700' },
|
||||
optionSubtitle: { fontSize: 10.5, lineHeight: 14 },
|
||||
footer: { flexDirection: 'row', gap: 12, marginTop: 10 },
|
||||
secondaryBtn: { flex: 1, height: 50, borderRadius: 16, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
|
||||
secondaryBtnText: { fontSize: 15, fontWeight: '600' },
|
||||
primaryBtn: { flex: 1.2, height: 50, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
||||
primaryBtnText: { fontSize: 15, fontWeight: '700' },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -16,60 +16,84 @@ const ONBOARDING_BACKGROUND = {
|
||||
const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
step: 'Schritt 4 von 4',
|
||||
title: 'Wo ist der Health-Scan?',
|
||||
subtitle: 'Du findest ihn auf jeder gespeicherten Pflanze, direkt unter der Beschreibung.',
|
||||
buttonPreview: 'Health-Scan starten',
|
||||
cta: 'Weiter',
|
||||
skip: 'Spaeter',
|
||||
flow: ['Pflanze scannen', 'Speichern', 'Detailseite oeffnen', 'Health-Scan starten'],
|
||||
outputTitle: 'Was du danach bekommst',
|
||||
outputs: [
|
||||
'Gesundheits-Score mit Status: stabil, beobachten oder kritisch.',
|
||||
'Ausfuehrliche Analyse mit sichtbaren Hinweisen und Unsicherheit.',
|
||||
'Wahrscheinlichste Ursachen mit Confidence-Werten.',
|
||||
'Sofortmassnahmen plus konkreter 7-Tage-Pflegeplan.',
|
||||
title: 'So sieht eine Antwort aus',
|
||||
subtitle: 'Ein echtes Beispiel - damit du weißt, was du bekommst, bevor du etwas entscheidest.',
|
||||
exampleSymptomLabel: 'Symptom',
|
||||
exampleSymptom: 'Untere Blätter gelb, Erde feucht',
|
||||
causeLabel: 'Wahrscheinlichste Ursache',
|
||||
// Bewusst ein Fall MITTLERER Sicherheit, kein Vorzeige-Treffer - das ist
|
||||
// der Beweis fuer Belief 3. Die Werte summieren sich absichtlich NICHT auf
|
||||
// 100 %: der Backend-Prompt (server/lib/openai.js, likelyIssues) liefert je
|
||||
// Ursache eine unabhaengige Sicherheit, keine Wahrscheinlichkeitsverteilung.
|
||||
// Ein Beispiel, das sich auf 100 addiert, wuerde ein Verhalten zeigen, das
|
||||
// das Produkt nicht hat.
|
||||
causes: [
|
||||
{ name: 'Überwässerung', level: '64 %', tone: 'high' as const },
|
||||
{ name: 'Nährstoffmangel', level: '41 %', tone: 'mid' as const },
|
||||
{ name: 'Lichtmangel', level: '22 %', tone: 'low' as const },
|
||||
],
|
||||
guidanceNote: 'Tipp: Fotografiere die ganze Pflanze, die Blattunterseiten und die Erde. Je klarer das Foto, desto genauer wird der Plan.',
|
||||
// Schwelle 65 % entspricht der Kalibrierung im Backend-Prompt:
|
||||
// 0.65-0.84 "sehr wahrscheinlich", 0.40-0.64 "mehrdeutig".
|
||||
confidenceNote: 'Unter 65 % sagen wir dir das deutlich - dann sind die Prüfschritte wichtiger als die Ursache.',
|
||||
checkLabel: 'Prüfe zuerst',
|
||||
checks: ['Erde 3 cm tief anfassen', 'Hat der Topf Abzugslöcher?', 'Blattunterseiten ansehen'],
|
||||
actionLabel: 'Tu jetzt',
|
||||
action: 'Nicht gießen. In 5 Tagen erneut prüfen.',
|
||||
followUpLabel: 'In 7 Tagen',
|
||||
followUp: 'Wir fragen nach, ob es besser wird - und passen den Plan an, wenn nicht.',
|
||||
limitNote: 'Ein Foto zeigt keine Wurzeln, keine Erdfeuchte und keine Vorgeschichte. Deshalb bekommst du Wahrscheinlichkeiten und Prüfschritte - keine Diagnose.',
|
||||
guidanceNote: 'Tipp: Fotografiere die ganze Pflanze, die Blattunterseiten und die Erde. Je klarer das Foto, desto präziser der Plan.',
|
||||
cta: 'Weiter',
|
||||
skip: 'Später',
|
||||
};
|
||||
}
|
||||
|
||||
if (language === 'es') {
|
||||
return {
|
||||
step: 'Paso 4 de 4',
|
||||
title: 'Donde esta el health-scan?',
|
||||
subtitle: 'Lo encuentras en cada planta guardada, justo debajo de la descripcion.',
|
||||
buttonPreview: 'Iniciar health-scan',
|
||||
cta: 'Continuar',
|
||||
skip: 'Mas tarde',
|
||||
flow: ['Escanear planta', 'Guardar', 'Abrir detalle', 'Iniciar health-scan'],
|
||||
outputTitle: 'Que recibes despues',
|
||||
outputs: [
|
||||
'Puntaje de salud con estado: estable, observar o critico.',
|
||||
'Analisis detallado con senales visibles e incertidumbre.',
|
||||
'Causas probables con valores de confianza.',
|
||||
'Acciones inmediatas y plan concreto de 7 dias.',
|
||||
title: 'Así se ve una respuesta',
|
||||
subtitle: 'Un ejemplo real - para que sepas qué recibes antes de decidir nada.',
|
||||
exampleSymptomLabel: 'Síntoma',
|
||||
exampleSymptom: 'Hojas inferiores amarillas, sustrato húmedo',
|
||||
causeLabel: 'Causa más probable',
|
||||
causes: [
|
||||
{ name: 'Exceso de riego', level: '64 %', tone: 'high' as const },
|
||||
{ name: 'Falta de nutrientes', level: '41 %', tone: 'mid' as const },
|
||||
{ name: 'Falta de luz', level: '22 %', tone: 'low' as const },
|
||||
],
|
||||
guidanceNote: 'Consejo: fotografia la planta completa, el reverso de las hojas y el sustrato. Cuanto mas clara sea la foto, mas preciso sera el plan.',
|
||||
confidenceNote: 'Por debajo del 65 % te lo decimos claramente - entonces los pasos de revisión importan más que la causa.',
|
||||
checkLabel: 'Revisa primero',
|
||||
checks: ['Tocar el sustrato a 3 cm', '¿La maceta tiene drenaje?', 'Mirar el reverso de las hojas'],
|
||||
actionLabel: 'Haz ahora',
|
||||
action: 'No regar. Volver a revisar en 5 días.',
|
||||
followUpLabel: 'En 7 días',
|
||||
followUp: 'Preguntamos si va mejor - y ajustamos el plan si no.',
|
||||
limitNote: 'Una foto no muestra las raíces, la humedad del sustrato ni el historial. Por eso recibes probabilidades y pasos de revisión - no un diagnóstico.',
|
||||
guidanceNote: 'Consejo: fotografía la planta completa, el reverso de las hojas y el sustrato. Cuanto más clara sea la foto, más preciso será el plan.',
|
||||
cta: 'Continuar',
|
||||
skip: 'Más tarde',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
step: 'Step 4 of 4',
|
||||
title: 'Where is the health scan?',
|
||||
subtitle: 'It lives on every saved plant, directly below the plant description.',
|
||||
buttonPreview: 'Start health scan',
|
||||
title: 'This is what an answer looks like',
|
||||
subtitle: 'A real example - so you know what you get before you decide anything.',
|
||||
exampleSymptomLabel: 'Symptom',
|
||||
exampleSymptom: 'Lower leaves yellow, soil damp',
|
||||
causeLabel: 'Most likely cause',
|
||||
causes: [
|
||||
{ name: 'Overwatering', level: '64%', tone: 'high' as const },
|
||||
{ name: 'Nutrient deficiency', level: '41%', tone: 'mid' as const },
|
||||
{ name: 'Too little light', level: '22%', tone: 'low' as const },
|
||||
],
|
||||
confidenceNote: 'Below 65% we say so plainly - then the checks matter more than the cause.',
|
||||
checkLabel: 'Check first',
|
||||
checks: ['Feel the soil 3 cm down', 'Does the pot have drainage holes?', 'Look at the leaf undersides'],
|
||||
actionLabel: 'Do now',
|
||||
action: 'Do not water. Check again in 5 days.',
|
||||
followUpLabel: 'In 7 days',
|
||||
followUp: 'We ask whether it is improving - and adjust the plan if it is not.',
|
||||
limitNote: 'A photo cannot show roots, soil moisture or history. So you get probabilities and things to check - not a diagnosis.',
|
||||
guidanceNote: 'Tip: photograph the full plant, leaf undersides, and the soil. The clearer the photo, the more precise the plan.',
|
||||
cta: 'Continue',
|
||||
skip: 'Later',
|
||||
flow: ['Scan plant', 'Save', 'Open detail', 'Start health scan'],
|
||||
outputTitle: 'What you get after',
|
||||
outputs: [
|
||||
'Health score with stable, watch, or critical status.',
|
||||
'Detailed analysis with visible signals and uncertainty.',
|
||||
'Most likely causes with confidence values.',
|
||||
'Immediate actions plus a concrete 7-day care plan.',
|
||||
],
|
||||
guidanceNote: 'Tip: photograph the full plant, leaf undersides, and the soil. The clearer the photo, the more precise the plan.',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -86,54 +110,101 @@ export default function HealthCheckOnboardingScreen() {
|
||||
skipped,
|
||||
plan: billingSummary?.entitlement?.plan ?? 'free',
|
||||
});
|
||||
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
|
||||
&& billingSummary?.entitlement?.status === 'active';
|
||||
router.replace(hasActiveEntitlement ? '/(tabs)' : '/profile/billing');
|
||||
router.replace('/onboarding/personalizing');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: screenBackground }]}>
|
||||
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
|
||||
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: colors.surface }]}>
|
||||
<Ionicons name="arrow-back" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
<View style={[styles.progressTrack, { backgroundColor: colors.primarySoft }]}>
|
||||
<View style={[styles.progressFill, { backgroundColor: colors.primary, width: '100%' }]} />
|
||||
</View>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{copy.subtitle}</Text>
|
||||
<View style={styles.backBtn} />
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<ImageBackground
|
||||
source={require('../../assets/onboarding_health_scan_mockup.png')}
|
||||
style={[styles.illustration, { borderColor: colors.border }]}
|
||||
imageStyle={styles.illustrationImage}
|
||||
resizeMode="cover"
|
||||
{/* Header liegt im Scrollbereich: bei grosser Systemschrift wuchs er
|
||||
sonst nach unten und hat den Scrollbereich auf Null gequetscht. */}
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.content}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.illustrationOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.08)' : 'rgba(251, 250, 243, 0.04)' }]} />
|
||||
</ImageBackground>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: colors.text }]} maxFontSizeMultiplier={1.25}>{copy.title}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]} maxFontSizeMultiplier={1.3}>{copy.subtitle}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.flowCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
{copy.flow.map((item, index) => (
|
||||
<View key={item} style={styles.flowRow}>
|
||||
<View style={[styles.flowIndex, { backgroundColor: index === 3 ? colors.primary : colors.surfaceMuted }]}>
|
||||
<Text style={[styles.flowIndexText, { color: index === 3 ? colors.onPrimary : colors.textMuted }]}>
|
||||
{index + 1}
|
||||
{/* Worked Example statt Wegbeschreibung: ein echtes Ergebnis schlaegt
|
||||
jede Behauptung ueber die Qualitaet der Ergebnisse. */}
|
||||
<View style={[styles.exampleCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.exampleSymptomLabel}</Text>
|
||||
<Text style={[styles.exampleSymptom, { color: colors.text }]}>{copy.exampleSymptom}</Text>
|
||||
|
||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||
|
||||
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.causeLabel}</Text>
|
||||
{copy.causes.map((cause) => (
|
||||
<View key={cause.name} style={styles.causeRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.causeDot,
|
||||
{
|
||||
backgroundColor:
|
||||
cause.tone === 'high' ? colors.danger : cause.tone === 'mid' ? colors.warning : colors.textMuted,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.causeName,
|
||||
{ color: cause.tone === 'high' ? colors.text : colors.textSecondary },
|
||||
cause.tone === 'high' && styles.causeNamePrimary,
|
||||
]}
|
||||
>
|
||||
{cause.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.flowText, { color: colors.text }]}>{item}</Text>
|
||||
<Text style={[styles.causeLevel, { color: colors.textMuted }]}>{cause.level}</Text>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[styles.confidenceNote, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name="alert-circle-outline" size={14} color={colors.textMuted} />
|
||||
<Text style={[styles.confidenceNoteText, { color: colors.textSecondary }]}>{copy.confidenceNote}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.outputCard, { backgroundColor: colors.surface, borderColor: colors.border }]}>
|
||||
<Text style={[styles.outputTitle, { color: colors.text }]}>{copy.outputTitle}</Text>
|
||||
{copy.outputs.map((item) => (
|
||||
<View key={item} style={styles.outputRow}>
|
||||
<Ionicons name="checkmark-circle" size={16} color={colors.success} />
|
||||
<Text style={[styles.outputText, { color: colors.textSecondary }]}>{item}</Text>
|
||||
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
||||
|
||||
<Text style={[styles.exampleEyebrow, { color: colors.textMuted }]}>{copy.checkLabel}</Text>
|
||||
{copy.checks.map((check) => (
|
||||
<View key={check} style={styles.outputRow}>
|
||||
<Ionicons name="ellipse-outline" size={15} color={colors.primary} />
|
||||
<Text style={[styles.outputText, { color: colors.textSecondary }]}>{check}</Text>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={[styles.actionBox, { backgroundColor: colors.primarySoft }]}>
|
||||
<Text style={[styles.actionLabel, { color: colors.primaryDark }]}>{copy.actionLabel}</Text>
|
||||
<Text style={[styles.actionText, { color: colors.primaryDark }]}>{copy.action}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.followUpRow}>
|
||||
<Ionicons name="chatbubble-ellipses-outline" size={15} color={colors.textMuted} />
|
||||
<View style={styles.followUpCopy}>
|
||||
<Text style={[styles.followUpLabel, { color: colors.text }]}>{copy.followUpLabel}</Text>
|
||||
<Text style={[styles.followUpText, { color: colors.textSecondary }]}>{copy.followUp}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Die Grenze des Verfahrens steht bewusst direkt unter dem Beispiel,
|
||||
nicht im Kleingedruckten. Sie ist Teil des Verkaufsarguments. */}
|
||||
<View style={[styles.limitCard, { backgroundColor: colors.surfaceMuted, borderColor: colors.border }]}>
|
||||
<Ionicons name="information-circle-outline" size={17} color={colors.textMuted} />
|
||||
<Text style={[styles.limitText, { color: colors.textSecondary }]}>{copy.limitNote}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.guidanceCard, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
|
||||
@@ -147,10 +218,10 @@ export default function HealthCheckOnboardingScreen() {
|
||||
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
|
||||
onPress={() => finish(true)}
|
||||
>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{copy.skip}</Text>
|
||||
<Text style={[styles.secondaryBtnText, { color: colors.text }]} maxFontSizeMultiplier={1.2} numberOfLines={2}>{copy.skip}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.primaryBtn, { backgroundColor: colors.primary }]} onPress={() => finish(false)}>
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{copy.cta}</Text>
|
||||
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]} maxFontSizeMultiplier={1.2} numberOfLines={2}>{copy.cta}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
@@ -161,42 +232,53 @@ export default function HealthCheckOnboardingScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
safeArea: { flex: 1, paddingHorizontal: 20, paddingTop: 24, paddingBottom: 20 },
|
||||
header: { gap: 9, marginBottom: 18 },
|
||||
stepPill: { alignSelf: 'flex-start', borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
|
||||
stepLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4 },
|
||||
scroll: { flex: 1 },
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingVertical: 10 },
|
||||
backBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
|
||||
progressTrack: { flex: 1, height: 6, borderRadius: 3, overflow: 'hidden' },
|
||||
progressFill: { height: 6, borderRadius: 3 },
|
||||
header: { gap: 9, marginTop: 8, marginBottom: 4 },
|
||||
title: { fontSize: 30, lineHeight: 34, fontWeight: '900' },
|
||||
subtitle: { fontSize: 14, lineHeight: 20 },
|
||||
content: { gap: 14, paddingBottom: 12 },
|
||||
illustration: { height: 230, borderRadius: 28, borderWidth: 1, justifyContent: 'center', overflow: 'hidden' },
|
||||
illustrationImage: { borderRadius: 28 },
|
||||
illustrationOverlay: { ...StyleSheet.absoluteFillObject },
|
||||
phone: { width: 178, minHeight: 156, borderRadius: 26, borderWidth: 1, padding: 12, gap: 10, marginLeft: 16 },
|
||||
phoneHeader: { height: 58, borderRadius: 18, justifyContent: 'flex-end', padding: 10 },
|
||||
phoneTitle: { fontSize: 13, fontWeight: '800' },
|
||||
phoneRows: { gap: 8 },
|
||||
phoneRowLong: { height: 8, borderRadius: 999 },
|
||||
phoneRowShort: { width: '66%', height: 8, borderRadius: 999 },
|
||||
healthButtonPreview: { height: 34, borderRadius: 14, borderWidth: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 5 },
|
||||
healthButtonText: { fontSize: 10, fontWeight: '800' },
|
||||
scanCard: { position: 'absolute', right: 16, bottom: 20, width: 136, borderRadius: 20, borderWidth: 1, padding: 14, gap: 7 },
|
||||
scanScore: { fontSize: 25, lineHeight: 29, fontWeight: '900' },
|
||||
scanLabel: { fontSize: 11, fontWeight: '800', textTransform: 'uppercase' },
|
||||
scanLine: { height: 8, borderRadius: 999 },
|
||||
scanLineShort: { width: '68%', height: 8, borderRadius: 999 },
|
||||
flowCard: { borderRadius: 18, borderWidth: 1, padding: 14, gap: 10 },
|
||||
flowRow: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
||||
flowIndex: { width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
|
||||
flowIndexText: { fontSize: 12, fontWeight: '900' },
|
||||
flowText: { flex: 1, fontSize: 14, fontWeight: '700' },
|
||||
outputCard: { borderRadius: 18, borderWidth: 1, padding: 16, gap: 11 },
|
||||
outputTitle: { fontSize: 15, fontWeight: '800' },
|
||||
outputRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9 },
|
||||
outputRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9, marginBottom: 6 },
|
||||
exampleCard: { borderRadius: 20, borderWidth: 1, padding: 18, gap: 4 },
|
||||
exampleEyebrow: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase', marginBottom: 6 },
|
||||
exampleSymptom: { fontSize: 16, fontWeight: '700', lineHeight: 22 },
|
||||
divider: { height: 1, marginVertical: 14 },
|
||||
causeRow: { flexDirection: 'row', alignItems: 'center', gap: 9, marginBottom: 8 },
|
||||
causeDot: { width: 8, height: 8, borderRadius: 4 },
|
||||
causeName: { flex: 1, fontSize: 14.5, fontWeight: '600' },
|
||||
causeNamePrimary: { fontWeight: '800' },
|
||||
causeLevel: { fontSize: 12.5, fontWeight: '800', fontVariant: ['tabular-nums'] },
|
||||
confidenceNote: { flexDirection: 'row', alignItems: 'flex-start', gap: 7, borderRadius: 10, padding: 10, marginTop: 4 },
|
||||
confidenceNoteText: { flex: 1, fontSize: 12, lineHeight: 16.5, fontWeight: '500' },
|
||||
actionBox: { borderRadius: 14, padding: 13, marginTop: 12, gap: 3 },
|
||||
actionLabel: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase' },
|
||||
actionText: { fontSize: 14.5, fontWeight: '700', lineHeight: 20 },
|
||||
followUpRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 9, marginTop: 14 },
|
||||
followUpCopy: { flex: 1, gap: 2 },
|
||||
followUpLabel: { fontSize: 13, fontWeight: '800' },
|
||||
followUpText: { fontSize: 13, lineHeight: 18, fontWeight: '500' },
|
||||
limitCard: { borderRadius: 16, borderWidth: 1, padding: 14, flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
|
||||
limitText: { flex: 1, fontSize: 12.5, lineHeight: 18, fontWeight: '500' },
|
||||
outputText: { flex: 1, fontSize: 13, lineHeight: 18 },
|
||||
guidanceCard: { borderRadius: 18, borderWidth: 1, padding: 14, flexDirection: 'row', alignItems: 'flex-start', gap: 10 },
|
||||
guidanceText: { flex: 1, fontSize: 12, lineHeight: 18, fontWeight: '600' },
|
||||
footer: { flexDirection: 'row', gap: 12, marginTop: 12 },
|
||||
secondaryBtn: { flex: 1, height: 52, borderRadius: 16, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
|
||||
secondaryBtnText: { fontSize: 15, fontWeight: '600' },
|
||||
primaryBtn: { flex: 1.3, height: 52, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
||||
primaryBtnText: { fontSize: 15, fontWeight: '700' },
|
||||
// minHeight statt height: sonst wird der Label-Text bei groesserer
|
||||
// Systemschrift oben und unten abgeschnitten.
|
||||
secondaryBtn: { flex: 1, minHeight: 52, paddingVertical: 14, paddingHorizontal: 8, borderRadius: 16, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
|
||||
secondaryBtnText: { fontSize: 15, fontWeight: '600', textAlign: 'center' },
|
||||
primaryBtn: { flex: 1.3, minHeight: 52, paddingVertical: 14, paddingHorizontal: 8, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
|
||||
primaryBtnText: { fontSize: 15, fontWeight: '700', textAlign: 'center' },
|
||||
});
|
||||
|
||||
272
app/onboarding/personalizing.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Animated, Easing, Image, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import Svg, { Circle } from 'react-native-svg';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { PreAuthOnboardingService, PreAuthAnswers } from '../../services/preAuthOnboardingService';
|
||||
import { Language } from '../../types';
|
||||
|
||||
const getCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
status: 'Dein Plan wird zusammengestellt…',
|
||||
// Schritte beschreiben den Nutzen fuer den Nutzer, nicht unsere Prozesse.
|
||||
steps: [
|
||||
'Deine Antworten werden ausgewertet',
|
||||
'Mögliche Ursachen für dein Symptom werden gewichtet',
|
||||
'Prüfschritte für deine Lichtsituation werden ausgewählt',
|
||||
'Dein 7-Tage-Plan steht',
|
||||
],
|
||||
summaryTitle: 'Dein Plan basiert auf',
|
||||
noAnswers: 'Deinen Angaben aus dem Gespräch',
|
||||
situations: {
|
||||
acute: 'Akutes Problem',
|
||||
slow: 'Langsame Veränderung',
|
||||
prevention: 'Vorbeugung',
|
||||
} as Record<string, string>,
|
||||
symptoms: {
|
||||
yellow: 'Gelbe Blätter',
|
||||
brown_tips: 'Braune Spitzen',
|
||||
drooping: 'Hängende Blätter',
|
||||
spots: 'Flecken oder Belag',
|
||||
pests: 'Schädlinge',
|
||||
other: 'Anderes Symptom',
|
||||
} as Record<string, string>,
|
||||
lights: {
|
||||
bright_indirect: 'Helles, indirektes Licht',
|
||||
low_light: 'Wenig Licht',
|
||||
direct_sunlight: 'Direkte Sonne',
|
||||
} as Record<string, string>,
|
||||
experiences: {
|
||||
beginner: 'Einsteigerin',
|
||||
intermediate: 'Etwas Erfahrung',
|
||||
advanced: 'Erfahren',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
status: 'Preparando tu plan…',
|
||||
steps: [
|
||||
'Analizando tus respuestas',
|
||||
'Ponderando las causas posibles de tu síntoma',
|
||||
'Eligiendo pasos de revisión para tu luz',
|
||||
'Tu plan de 7 días está listo',
|
||||
],
|
||||
summaryTitle: 'Tu plan se basa en',
|
||||
noAnswers: 'Lo que nos contaste en el chat',
|
||||
situations: {
|
||||
acute: 'Problema agudo',
|
||||
slow: 'Cambio lento',
|
||||
prevention: 'Prevención',
|
||||
} as Record<string, string>,
|
||||
symptoms: {
|
||||
yellow: 'Hojas amarillas',
|
||||
brown_tips: 'Puntas marrones',
|
||||
drooping: 'Hojas caídas',
|
||||
spots: 'Manchas o residuo',
|
||||
pests: 'Plagas',
|
||||
other: 'Otro síntoma',
|
||||
} as Record<string, string>,
|
||||
lights: {
|
||||
bright_indirect: 'Luz brillante indirecta',
|
||||
low_light: 'Poca luz',
|
||||
direct_sunlight: 'Sol directo',
|
||||
} as Record<string, string>,
|
||||
experiences: {
|
||||
beginner: 'Principiante',
|
||||
intermediate: 'Algo de experiencia',
|
||||
advanced: 'Con experiencia',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'Putting your plan together…',
|
||||
steps: [
|
||||
'Reviewing your answers',
|
||||
'Weighing possible causes for your symptom',
|
||||
'Picking checks for your light situation',
|
||||
'Your 7-day plan is ready',
|
||||
],
|
||||
summaryTitle: 'Your plan is based on',
|
||||
noAnswers: 'What you told us in the chat',
|
||||
situations: {
|
||||
acute: 'Acute problem',
|
||||
slow: 'Slow change',
|
||||
prevention: 'Prevention',
|
||||
} as Record<string, string>,
|
||||
symptoms: {
|
||||
yellow: 'Yellow leaves',
|
||||
brown_tips: 'Brown tips',
|
||||
drooping: 'Drooping leaves',
|
||||
spots: 'Spots or residue',
|
||||
pests: 'Pests',
|
||||
other: 'Other symptom',
|
||||
} as Record<string, string>,
|
||||
lights: {
|
||||
bright_indirect: 'Bright, indirect light',
|
||||
low_light: 'Low light',
|
||||
direct_sunlight: 'Direct sun',
|
||||
} as Record<string, string>,
|
||||
experiences: {
|
||||
beginner: 'Beginner',
|
||||
intermediate: 'Some experience',
|
||||
advanced: 'Experienced',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
};
|
||||
|
||||
const STEP_THRESHOLDS = [25, 50, 75, 95];
|
||||
const RING_SIZE = 150;
|
||||
const RING_STROKE_WIDTH = 7;
|
||||
const RING_RADIUS = (RING_SIZE - RING_STROKE_WIDTH) / 2;
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
|
||||
|
||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||
|
||||
export default function OnboardingPersonalizingScreen() {
|
||||
const { language, isDarkMode, colorPalette } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const posthog = useSafeAnalytics();
|
||||
const copy = getCopy(language);
|
||||
const progress = useRef(new Animated.Value(0)).current;
|
||||
const [percent, setPercent] = useState(0);
|
||||
const [answers, setAnswers] = useState<PreAuthAnswers>({});
|
||||
const navigated = useRef(false);
|
||||
|
||||
// Die eigenen Angaben zurueckspiegeln beweist "es geht um MEINE Pflanze"
|
||||
// (Belief 4) durch Demonstration statt durch Behauptung.
|
||||
useEffect(() => {
|
||||
void PreAuthOnboardingService.getAnswers().then(setAnswers);
|
||||
}, []);
|
||||
|
||||
const summaryChips = [
|
||||
answers.situation ? copy.situations[answers.situation] : null,
|
||||
answers.symptom ? copy.symptoms[answers.symptom] : null,
|
||||
answers.lightLevel ? copy.lights[answers.lightLevel] : null,
|
||||
answers.experienceLevel ? copy.experiences[answers.experienceLevel] : null,
|
||||
].filter((chip): chip is string => Boolean(chip));
|
||||
|
||||
const strokeDashoffset = progress.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: [RING_CIRCUMFERENCE, 0],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
posthog.capture('onboarding_personalizing_viewed');
|
||||
const listener = progress.addListener(({ value }) => setPercent(Math.round(value)));
|
||||
Animated.timing(progress, {
|
||||
toValue: 100,
|
||||
duration: 6000,
|
||||
easing: Easing.inOut(Easing.cubic),
|
||||
useNativeDriver: false,
|
||||
}).start(({ finished }) => {
|
||||
if (finished && !navigated.current) {
|
||||
navigated.current = true;
|
||||
setTimeout(() => {
|
||||
posthog.capture('paywall_opened', { source: 'onboarding' });
|
||||
router.replace('/profile/billing?view=paywall&context=onboarding');
|
||||
}, 450);
|
||||
}
|
||||
});
|
||||
return () => progress.removeListener(listener);
|
||||
}, [progress, posthog]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}>
|
||||
{/* Scrollbar, damit die Zusammenfassungs-Karte auf kleinen Geraeten und
|
||||
bei grosser Systemschrift nicht unten wegfaellt. */}
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text style={[styles.percent, { color: colors.primary }]} maxFontSizeMultiplier={1.2}>{percent}%</Text>
|
||||
<View style={styles.ringWrap}>
|
||||
<Svg width={RING_SIZE} height={RING_SIZE} style={StyleSheet.absoluteFill}>
|
||||
<Circle
|
||||
cx={RING_SIZE / 2}
|
||||
cy={RING_SIZE / 2}
|
||||
r={RING_RADIUS}
|
||||
stroke={colors.primarySoft}
|
||||
strokeWidth={RING_STROKE_WIDTH}
|
||||
fill="none"
|
||||
/>
|
||||
<AnimatedCircle
|
||||
cx={RING_SIZE / 2}
|
||||
cy={RING_SIZE / 2}
|
||||
r={RING_RADIUS}
|
||||
stroke={colors.primary}
|
||||
strokeWidth={RING_STROKE_WIDTH}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${RING_CIRCUMFERENCE}, ${RING_CIRCUMFERENCE}`}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
rotation="-90"
|
||||
originX={RING_SIZE / 2}
|
||||
originY={RING_SIZE / 2}
|
||||
/>
|
||||
</Svg>
|
||||
<Image source={require('../../assets/paywall_scan_background.png')} style={styles.ringImage} />
|
||||
</View>
|
||||
<View style={[styles.statusPill, { backgroundColor: colors.surfaceMuted }]}>
|
||||
<Ionicons name="sync-outline" size={15} color={colors.textSecondary} />
|
||||
<Text style={[styles.statusText, { color: colors.textSecondary }]} maxFontSizeMultiplier={1.3}>{copy.status}</Text>
|
||||
</View>
|
||||
<View style={styles.checklist}>
|
||||
{copy.steps.map((label, index) => {
|
||||
const done = percent >= STEP_THRESHOLDS[index];
|
||||
return (
|
||||
<View key={label} style={styles.checkRow}>
|
||||
<Ionicons
|
||||
name={done ? 'checkmark-circle' : 'ellipse-outline'}
|
||||
size={24}
|
||||
color={done ? colors.primary : colors.border}
|
||||
/>
|
||||
<Text style={[styles.checkLabel, { color: done ? colors.text : colors.textMuted }]} maxFontSizeMultiplier={1.3}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
<View style={[styles.testimonialCard, { backgroundColor: colors.surface }]}>
|
||||
<Text style={[styles.summaryTitle, { color: colors.textMuted }]}>{copy.summaryTitle}</Text>
|
||||
<View style={styles.summaryChips}>
|
||||
{summaryChips.length > 0 ? (
|
||||
summaryChips.map((chip) => (
|
||||
<View key={chip} style={[styles.summaryChip, { backgroundColor: colors.primarySoft }]}>
|
||||
<Text style={[styles.summaryChipText, { color: colors.primaryDark }]}>{chip}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text style={[styles.testimonialText, { color: colors.textSecondary }]}>{copy.noAnswers}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1 },
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: { flexGrow: 1, alignItems: 'center', paddingHorizontal: 24, paddingTop: 30, paddingBottom: 16 },
|
||||
percent: { fontSize: 56, fontWeight: '900', marginBottom: 16 },
|
||||
ringWrap: { width: 150, height: 150, alignItems: 'center', justifyContent: 'center', marginBottom: 22 },
|
||||
ringImage: { width: 112, height: 112, borderRadius: 56 },
|
||||
statusPill: { flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 999, paddingHorizontal: 18, paddingVertical: 11, marginBottom: 26 },
|
||||
statusText: { fontSize: 14.5, fontWeight: '800' },
|
||||
checklist: { alignSelf: 'stretch', gap: 15, marginBottom: 26, paddingHorizontal: 8 },
|
||||
checkRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||
checkLabel: { fontSize: 16.5, fontWeight: '700' },
|
||||
testimonialCard: { alignSelf: 'stretch', borderRadius: 18, padding: 16, marginBottom: 14 },
|
||||
testimonialText: { fontSize: 14, lineHeight: 20 },
|
||||
summaryTitle: { fontSize: 10.5, fontWeight: '900', letterSpacing: 0.9, textTransform: 'uppercase', marginBottom: 10 },
|
||||
summaryChips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||
summaryChip: { borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
|
||||
summaryChipText: { fontSize: 12.5, fontWeight: '700' },
|
||||
});
|
||||
562
app/onboarding/slides.tsx
Normal file
@@ -0,0 +1,562 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Image, ScrollView, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSafeAnalytics } from '../../services/analytics';
|
||||
import { useColors } from '../../constants/Colors';
|
||||
import { useApp } from '../../context/AppContext';
|
||||
import { Language } from '../../types';
|
||||
|
||||
type ColorsType = ReturnType<typeof useColors>;
|
||||
|
||||
const getSlidesCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
slides: [
|
||||
{
|
||||
// Slide 1 = Triage. Bewusst NICHT die Identifikation: die kann Google
|
||||
// Lens gratis. Der Pitch startet erst nach der Identifikation.
|
||||
title: 'Was ist mit meiner Pflanze los?',
|
||||
body: 'Fotografiere das Symptom. Du bekommst die wahrscheinlichsten Ursachen - und was du prüfen sollst.',
|
||||
},
|
||||
{
|
||||
title: 'Ein konkreter Plan. Für diese Pflanze.',
|
||||
body: 'Sofortmaßnahme, 7-Tage-Plan, und danach die Frage: Hat es gewirkt?',
|
||||
},
|
||||
{
|
||||
// Der Screen, den keine Konkurrenz-App hat. Beantwortet den Einwand
|
||||
// "Eine andere App hat mir selbstbewusst etwas Falsches gesagt."
|
||||
title: 'Wir sagen dir auch, wenn wir unsicher sind.',
|
||||
body: 'Ein Foto zeigt keine Wurzeln und keine Erdfeuchte. Deshalb Wahrscheinlichkeiten statt erfundener Gewissheit.',
|
||||
},
|
||||
],
|
||||
causeTitle: 'Wahrscheinlichste Ursache',
|
||||
causePrimary: 'Überwässerung',
|
||||
causePrimaryLevel: '64 %',
|
||||
causeSecondary: 'Nährstoffmangel',
|
||||
causeSecondaryLevel: '41 %',
|
||||
checkLabel: 'Prüfe zuerst',
|
||||
checkItems: 'Erde 3 cm tief · Abzugslöcher · Blattunterseiten',
|
||||
planLabel: '7-Tage-Plan',
|
||||
planNow: 'Heute: nicht gießen',
|
||||
planFollowUp: 'Tag 7: Wir fragen nach, ob es besser wird',
|
||||
uncertainTitle: 'Was ein Foto nicht zeigt',
|
||||
uncertainItems: ['Wurzeln', 'Erdfeuchte', 'Vorgeschichte'],
|
||||
uncertainNote: 'Deshalb: Wahrscheinlichkeiten statt Diagnose.',
|
||||
continueLabel: 'Weiter',
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
slides: [
|
||||
{
|
||||
title: '¿Qué le pasa a mi planta?',
|
||||
body: 'Fotografía el síntoma. Recibes las causas más probables - y qué revisar.',
|
||||
},
|
||||
{
|
||||
title: 'Un plan concreto. Para esta planta.',
|
||||
body: 'Acción inmediata, plan de 7 días, y luego la pregunta: ¿funcionó?',
|
||||
},
|
||||
{
|
||||
title: 'También te decimos cuándo no estamos seguros.',
|
||||
body: 'Una foto no muestra las raíces ni la humedad. Por eso probabilidades, no certezas inventadas.',
|
||||
},
|
||||
],
|
||||
causeTitle: 'Causa más probable',
|
||||
causePrimary: 'Exceso de riego',
|
||||
causePrimaryLevel: '64 %',
|
||||
causeSecondary: 'Falta de nutrientes',
|
||||
causeSecondaryLevel: '41 %',
|
||||
checkLabel: 'Revisa primero',
|
||||
checkItems: 'Sustrato a 3 cm · Drenaje · Reverso de las hojas',
|
||||
planLabel: 'Plan de 7 días',
|
||||
planNow: 'Hoy: no regar',
|
||||
planFollowUp: 'Día 7: preguntamos si va mejor',
|
||||
uncertainTitle: 'Lo que una foto no muestra',
|
||||
uncertainItems: ['Raíces', 'Humedad', 'Historial'],
|
||||
uncertainNote: 'Por eso: probabilidades, no diagnóstico.',
|
||||
continueLabel: 'Continuar',
|
||||
};
|
||||
}
|
||||
return {
|
||||
slides: [
|
||||
{
|
||||
title: "What's wrong with my plant?",
|
||||
body: 'Photograph the symptom. You get the most likely causes - and what to check.',
|
||||
},
|
||||
{
|
||||
title: 'A concrete plan. For this plant.',
|
||||
body: 'Immediate action, a 7-day plan, and then the question: did it work?',
|
||||
},
|
||||
{
|
||||
title: "We also tell you when we're not sure.",
|
||||
body: 'A photo cannot show roots or soil moisture. So: probabilities, not invented certainty.',
|
||||
},
|
||||
],
|
||||
causeTitle: 'Most likely cause',
|
||||
causePrimary: 'Overwatering',
|
||||
causePrimaryLevel: '64%',
|
||||
causeSecondary: 'Nutrient deficiency',
|
||||
causeSecondaryLevel: '41%',
|
||||
checkLabel: 'Check first',
|
||||
checkItems: 'Soil 3 cm deep · Drainage holes · Leaf undersides',
|
||||
planLabel: '7-day plan',
|
||||
planNow: 'Today: do not water',
|
||||
planFollowUp: 'Day 7: we ask whether it improved',
|
||||
uncertainTitle: 'What a photo cannot show',
|
||||
uncertainItems: ['Roots', 'Soil moisture', 'History'],
|
||||
uncertainNote: 'So: probabilities, not diagnosis.',
|
||||
continueLabel: 'Continue',
|
||||
};
|
||||
};
|
||||
|
||||
type SlidesCopy = ReturnType<typeof getSlidesCopy>;
|
||||
const OVERLAY_TEXT_MAX_MULTIPLIER = 1.2;
|
||||
|
||||
/** Slide 1: Ursachen-Ranking statt Prozent-Behauptung. */
|
||||
function CauseRankingOverlay({ copy, colors }: { copy: SlidesCopy; colors: ColorsType }) {
|
||||
return (
|
||||
<>
|
||||
<View style={styles.scanFrameWrap} pointerEvents="none">
|
||||
<View style={[styles.cornerTL, { borderColor: colors.primary }]} />
|
||||
<View style={[styles.cornerTR, { borderColor: colors.primary }]} />
|
||||
<View style={[styles.cornerBL, { borderColor: colors.primary }]} />
|
||||
<View style={[styles.cornerBR, { borderColor: colors.primary }]} />
|
||||
</View>
|
||||
<View style={styles.healthCard}>
|
||||
<Text style={styles.overlayEyebrow} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.causeTitle}
|
||||
</Text>
|
||||
<View style={[styles.healthRow, styles.healthRowWarning]}>
|
||||
<Ionicons name="alert-circle" size={15} color="#C62828" />
|
||||
<Text style={styles.healthRowWarningText} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.causePrimary} · {copy.causePrimaryLevel}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.healthRow, styles.healthRowNeutral]}>
|
||||
<Ionicons name="help-circle-outline" size={15} color="#6b7280" />
|
||||
<Text style={styles.healthRowNeutralText} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.causeSecondary} · {copy.causeSecondaryLevel}
|
||||
</Text>
|
||||
</View>
|
||||
{/* In derselben Karte statt als zweites absolutes Element: zwei frei
|
||||
positionierte Overlays ueberlappen sich, sobald Uebersetzungen die
|
||||
Karte wachsen lassen. */}
|
||||
<View style={styles.checkInline}>
|
||||
<Text style={styles.checkChipLabel} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.checkLabel}
|
||||
</Text>
|
||||
<Text style={styles.checkChipText} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.checkItems}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Slide 2: der Plan inklusive Follow-up - das ist der Kaufgrund. */
|
||||
function PlanOverlay({ copy }: { copy: SlidesCopy }) {
|
||||
return (
|
||||
<View style={styles.healthCard}>
|
||||
<View style={styles.healthCardHeader}>
|
||||
<View style={styles.healthCardIcon}>
|
||||
<Ionicons name="calendar" size={16} color="#2e7d32" />
|
||||
</View>
|
||||
<Text style={styles.healthCardTitle} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.planLabel}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.healthRow, styles.healthRowSuccess]}>
|
||||
<Ionicons name="water-outline" size={15} color="#2e7d32" />
|
||||
<Text style={styles.healthRowSuccessText} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.planNow}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[styles.healthRow, styles.healthRowNeutral]}>
|
||||
<Ionicons name="chatbubble-ellipses-outline" size={15} color="#6b7280" />
|
||||
<Text style={styles.healthRowNeutralText} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.planFollowUp}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/** Slide 3: die Grenzen des Verfahrens, sichtbar gemacht. */
|
||||
function UncertaintyOverlay({ copy }: { copy: SlidesCopy }) {
|
||||
return (
|
||||
<View style={styles.healthCard}>
|
||||
<View style={styles.healthCardHeader}>
|
||||
<View style={styles.healthCardIcon}>
|
||||
<Ionicons name="eye-off-outline" size={16} color="#6b7280" />
|
||||
</View>
|
||||
<Text style={styles.healthCardTitle} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.uncertainTitle}
|
||||
</Text>
|
||||
</View>
|
||||
{copy.uncertainItems.map((item) => (
|
||||
<View key={item} style={[styles.healthRow, styles.healthRowNeutral]}>
|
||||
<Ionicons name="close-circle-outline" size={15} color="#6b7280" />
|
||||
<Text style={styles.healthRowNeutralText} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{item}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
<Text style={styles.overlayNote} maxFontSizeMultiplier={OVERLAY_TEXT_MAX_MULTIPLIER}>
|
||||
{copy.uncertainNote}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OnboardingSlidesScreen() {
|
||||
const router = useRouter();
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const { height } = useWindowDimensions();
|
||||
const insets = useSafeAreaInsets();
|
||||
const compact = height < 700;
|
||||
// Das Bild bekommt den Platz, den das Sheet uebrig laesst - aber nie weniger
|
||||
// als diese Untergrenze. Feste Prozenthoehen haben bei groesserer
|
||||
// System-Schriftgroesse den CTA aus dem Bildschirm geschoben.
|
||||
const imageMinHeight = Math.round(height * (compact ? 0.46 : 0.42));
|
||||
const { language, isDarkMode, colorPalette } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const posthog = useSafeAnalytics();
|
||||
const [page, setPage] = useState(0);
|
||||
const copy = getSlidesCopy(language);
|
||||
const slide = copy.slides[page];
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ y: 0, animated: false });
|
||||
posthog.capture('onboarding_slide_viewed', { index: page, slide: ['triage', 'plan', 'uncertainty'][page] });
|
||||
}, [page, posthog]);
|
||||
|
||||
const next = () => {
|
||||
if (page < copy.slides.length - 1) {
|
||||
setPage(page + 1);
|
||||
} else {
|
||||
router.replace('/onboarding/source');
|
||||
}
|
||||
};
|
||||
|
||||
const back = () => {
|
||||
if (page > 0) {
|
||||
setPage(page - 1);
|
||||
} else {
|
||||
router.back();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
style={[styles.container, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}
|
||||
alwaysBounceVertical={false}
|
||||
>
|
||||
<View style={[styles.imageArea, { minHeight: imageMinHeight }]}>
|
||||
<Image
|
||||
source={
|
||||
page === 0
|
||||
? require('../../assets/onboarding_health_scan_mockup_vertical.png')
|
||||
: page === 1
|
||||
? require('../../assets/paywall_scan_background.png')
|
||||
: require('../../assets/welcome_botanical_header.png')
|
||||
}
|
||||
style={styles.image}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<SafeAreaView style={styles.imageSafeArea} edges={['top']}>
|
||||
<TouchableOpacity onPress={back} style={styles.backBtn} activeOpacity={0.85}>
|
||||
<Ionicons name="arrow-back" size={20} color="#1f2520" />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
{page === 0 && <CauseRankingOverlay copy={copy} colors={colors} />}
|
||||
{page === 1 && <PlanOverlay copy={copy} />}
|
||||
{page === 2 && <UncertaintyOverlay copy={copy} />}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.sheet,
|
||||
{ backgroundColor: colors.surface, paddingBottom: Math.max(insets.bottom, 16) + 8 },
|
||||
]}
|
||||
>
|
||||
{/* maxFontSizeMultiplier: die Systemschriftgroesse darf die Headline
|
||||
vergroessern, aber nicht so weit, dass sie das halbe Sheet frisst. */}
|
||||
<Text style={[styles.title, { color: colors.text }]} maxFontSizeMultiplier={1.25}>
|
||||
{slide.title}
|
||||
</Text>
|
||||
<Text style={[styles.body, { color: colors.textSecondary }]} maxFontSizeMultiplier={1.35}>
|
||||
{slide.body}
|
||||
</Text>
|
||||
<View style={styles.dots}>
|
||||
{copy.slides.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.dot,
|
||||
index === page
|
||||
? [styles.dotActive, { backgroundColor: colors.primary }]
|
||||
: { backgroundColor: colors.border },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.cta, { backgroundColor: colors.primary }]}
|
||||
onPress={next}
|
||||
activeOpacity={0.86}
|
||||
>
|
||||
<Text style={[styles.ctaText, { color: colors.onPrimary }]} maxFontSizeMultiplier={1.2}>
|
||||
{copy.continueLabel}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
// Fuellt mindestens den Bildschirm; wird der Inhalt bei sehr grosser
|
||||
// Schrift hoeher, scrollt der Screen statt den CTA abzuschneiden.
|
||||
flexGrow: 1,
|
||||
},
|
||||
imageArea: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
image: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
imageSafeArea: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 2,
|
||||
},
|
||||
backBtn: {
|
||||
marginLeft: 16,
|
||||
marginTop: 8,
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
backgroundColor: 'rgba(255,255,255,0.85)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
// Scan frame overlay (slide 1)
|
||||
scanFrameWrap: {
|
||||
position: 'absolute',
|
||||
top: '22%',
|
||||
left: '20%',
|
||||
right: '20%',
|
||||
bottom: '26%',
|
||||
},
|
||||
cornerTL: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderTopWidth: 4,
|
||||
borderLeftWidth: 4,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
cornerTR: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderTopWidth: 4,
|
||||
borderRightWidth: 4,
|
||||
borderTopRightRadius: 8,
|
||||
},
|
||||
cornerBL: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderBottomWidth: 4,
|
||||
borderLeftWidth: 4,
|
||||
borderBottomLeftRadius: 8,
|
||||
},
|
||||
cornerBR: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderBottomWidth: 4,
|
||||
borderRightWidth: 4,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
// Health card overlay (slide 2)
|
||||
healthCard: {
|
||||
position: 'absolute',
|
||||
bottom: 34,
|
||||
left: 16,
|
||||
right: 16,
|
||||
zIndex: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.96)',
|
||||
borderRadius: 18,
|
||||
padding: 14,
|
||||
gap: 8,
|
||||
},
|
||||
healthCardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
healthCardIcon: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#fdeaea',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
healthCardTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '800',
|
||||
color: '#1f2520',
|
||||
},
|
||||
healthRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderRadius: 10,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
healthRowWarning: {
|
||||
backgroundColor: '#fdeaea',
|
||||
},
|
||||
healthRowWarningText: {
|
||||
fontSize: 13.5,
|
||||
fontWeight: '700',
|
||||
color: '#C62828',
|
||||
},
|
||||
healthRowSuccess: {
|
||||
backgroundColor: '#e8f3e3',
|
||||
},
|
||||
healthRowSuccessText: {
|
||||
fontSize: 13.5,
|
||||
fontWeight: '700',
|
||||
color: '#2e7d32',
|
||||
},
|
||||
// Neutrale Zeile: bewusst grau, damit "weniger wahrscheinlich" und
|
||||
// "was wir nicht sehen" optisch NICHT wie eine Zusage aussehen.
|
||||
healthRowNeutral: {
|
||||
backgroundColor: '#f1f2f0',
|
||||
},
|
||||
healthRowNeutralText: {
|
||||
fontSize: 13.5,
|
||||
fontWeight: '600',
|
||||
color: '#5b6158',
|
||||
},
|
||||
overlayEyebrow: {
|
||||
fontSize: 10.5,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 0.9,
|
||||
textTransform: 'uppercase',
|
||||
color: '#6b7280',
|
||||
marginBottom: 8,
|
||||
},
|
||||
overlayNote: {
|
||||
fontSize: 11.5,
|
||||
lineHeight: 15,
|
||||
fontWeight: '600',
|
||||
color: '#6b7280',
|
||||
marginTop: 8,
|
||||
},
|
||||
checkInline: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(0,0,0,0.08)',
|
||||
paddingTop: 9,
|
||||
marginTop: 2,
|
||||
},
|
||||
// Ein Stack statt zwei absoluter Elemente: die Bloecke koennen sich nicht
|
||||
// mehr ueberlappen, egal wie lang der uebersetzte Text wird.
|
||||
checkChipLabel: {
|
||||
fontSize: 10,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 0.8,
|
||||
textTransform: 'uppercase',
|
||||
color: '#6b7280',
|
||||
marginBottom: 3,
|
||||
},
|
||||
checkChipText: {
|
||||
fontSize: 12.5,
|
||||
lineHeight: 17,
|
||||
fontWeight: '700',
|
||||
color: '#1f2520',
|
||||
},
|
||||
// Reminder chips overlay (slide 3)
|
||||
// Bottom sheet
|
||||
sheet: {
|
||||
// Hoehe folgt dem Inhalt statt einem festen Prozentwert - sonst haengt es
|
||||
// von Textlaenge und Systemschriftgroesse ab, ob der CTA noch passt.
|
||||
flexShrink: 0,
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
marginTop: -24,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 30,
|
||||
fontWeight: '900',
|
||||
textAlign: 'center',
|
||||
marginBottom: 10,
|
||||
},
|
||||
body: {
|
||||
fontSize: 15.5,
|
||||
lineHeight: 22,
|
||||
textAlign: 'center',
|
||||
maxWidth: 320,
|
||||
marginBottom: 20,
|
||||
},
|
||||
dots: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 22,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
dotActive: {
|
||||
width: 26,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
cta: {
|
||||
alignSelf: 'stretch',
|
||||
minHeight: 58,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
ctaText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '800',
|
||||
},
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Modal,
|
||||
Alert,
|
||||
Share,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -462,7 +463,10 @@ export default function PlantDetailScreen() {
|
||||
if (!plant.notificationsEnabled) {
|
||||
const granted = await requestPermissions();
|
||||
if (!granted) {
|
||||
Alert.alert(t.reminder, t.reminderPermissionNeeded);
|
||||
Alert.alert(t.reminder, t.reminderPermissionNeeded, [
|
||||
{ text: t.cancel, style: 'cancel' },
|
||||
{ text: t.openSettings, onPress: () => Linking.openSettings() },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
await scheduleWateringReminder(plant);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useColors } from '../../constants/Colors';
|
||||
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
|
||||
import { Language } from '../../types';
|
||||
import { AuthService } from '../../services/authService';
|
||||
import { resetToFreshInstall } from '../../services/devReset';
|
||||
|
||||
const getDataCopy = (language: Language) => {
|
||||
if (language === 'de') {
|
||||
@@ -115,6 +116,28 @@ export default function DataScreen() {
|
||||
]);
|
||||
};
|
||||
|
||||
// Nur im Dev-Build: setzt die App auf "frisch installiert" zurueck. Noetig,
|
||||
// weil ein Expo-Reload weder Keychain noch SQLite loescht - man landet sonst
|
||||
// immer wieder direkt im Dashboard statt im Onboarding.
|
||||
const handleDevReset = () => {
|
||||
Alert.alert(
|
||||
'Onboarding neu starten?',
|
||||
'Session, Onboarding-Status und A/B-Zuweisung werden zurückgesetzt. Du landest wieder auf dem Welcome-Screen.',
|
||||
[
|
||||
{ text: 'Abbrechen', style: 'cancel' },
|
||||
{
|
||||
text: 'Zurücksetzen',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await resetToFreshInstall();
|
||||
await signOut();
|
||||
router.replace('/onboarding');
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
Alert.alert(copy.deleteConfirmTitle, copy.deleteConfirmMessage, [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
@@ -177,6 +200,23 @@ export default function DataScreen() {
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{__DEV__ ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionRow, { backgroundColor: colors.cardBg, borderColor: colors.border }]}
|
||||
onPress={handleDevReset}
|
||||
>
|
||||
<View style={styles.actionIcon}>
|
||||
<Ionicons name="refresh-circle-outline" size={24} color={colors.primary} />
|
||||
</View>
|
||||
<View style={styles.actionTextContainer}>
|
||||
<Text style={[styles.actionTitle, { color: colors.text }]}>Onboarding neu starten (DEV)</Text>
|
||||
<Text style={[styles.actionHint, { color: `${colors.text}80` }]}>
|
||||
Setzt Session, Onboarding-Status und A/B-Variante zurück.
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
<View style={{ marginTop: 24 }}>
|
||||
<TouchableOpacity style={[styles.actionRow, { backgroundColor: '#FF3B3015', borderColor: '#FF3B3050', marginBottom: 0 }]} onPress={handleDeleteAccount}>
|
||||
<View style={[styles.actionIcon, { backgroundColor: '#FF3B3020' }]}>
|
||||
|
||||
146
app/scanner.tsx
@@ -22,19 +22,15 @@ import { backendApiClient, isInsufficientCreditsError, isNetworkError, isTimeout
|
||||
import { isBackendApiError } from '../services/backend/contracts';
|
||||
import { createIdempotencyKey } from '../utils/idempotency';
|
||||
import { AuthService } from '../services/authService';
|
||||
import { getMockPlantByImage } from '../services/backend/mockCatalog';
|
||||
import { consumeSharedImageUri, SHARE_INTENT_KEY } from '../utils/shareHandoff';
|
||||
import { OutOfCreditsSheet } from '../components/OutOfCreditsSheet';
|
||||
|
||||
const HEALTH_CHECK_CREDIT_COST = 2;
|
||||
const DEMO_SCAN_LIMIT = 5;
|
||||
const DEMO_SCAN_LIMIT = 1;
|
||||
|
||||
const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'de') {
|
||||
return {
|
||||
creditsLabel: 'Credits',
|
||||
noCreditsTitle: 'Keine Credits mehr',
|
||||
noCreditsMessage: 'Du hast keine Credits mehr fuer KI-Scans. Upgrade oder Top-up im Profil.',
|
||||
healthNoCreditsMessage: `Du brauchst ${HEALTH_CHECK_CREDIT_COST} Credits fuer den Health-Check.`,
|
||||
managePlan: 'Plan verwalten',
|
||||
dismiss: 'Schliessen',
|
||||
genericErrorTitle: 'Fehler',
|
||||
@@ -46,6 +42,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
retryLabel: 'Erneut versuchen',
|
||||
notAPlantTitle: 'Keine Pflanze erkannt',
|
||||
notAPlantMessage: 'Das Bild zeigt keine erkennbare Pflanze. Bitte fotografiere eine Pflanze und versuche es erneut.',
|
||||
notAPlantSharedMessage: 'Das geteilte Bild zeigt keine erkennbare Pflanze. Möglicherweise wurde nicht der Original-Post geladen. Mach einen Screenshot vom Post und teile den Screenshot — das funktioniert immer.',
|
||||
providerErrorMessage: 'KI-Scan gerade nicht verfügbar. Bitte versuche es erneut.',
|
||||
healthProviderErrorMessage: 'KI-Health-Check gerade nicht verfügbar. Bitte versuche es erneut.',
|
||||
healthTitle: 'Health Check',
|
||||
@@ -55,8 +52,9 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoTitle: 'Rettungsplan bereit',
|
||||
demoMessage: 'Wir haben mögliche Ursachen erkannt. Schalte die vollständige KI-Diagnose und deinen 7-Tage-Rettungsplan frei.',
|
||||
demoNoCreditsTitle: 'Demo-Scans aufgebraucht',
|
||||
demoNoCreditsMessage: 'Du hast deine 5 kostenlosen Demo-Scans auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
|
||||
demoCreditsRemaining: (count: number) => `${count} Demo-Scans übrig`,
|
||||
demoNoCreditsMessage: 'Du hast deinen kostenlosen Demo-Scan auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
|
||||
demoCreditsRemaining: (count: number) => (count === 1 ? '1 Demo-Scan übrig' : `${count} Demo-Scans übrig`),
|
||||
creditsRemaining: (count: number) => `${count} Scans übrig`,
|
||||
appleCta: 'Mit Apple fortfahren',
|
||||
emailCta: 'Mit E-Mail fortfahren',
|
||||
unlockCta: 'Vollständige Diagnose freischalten',
|
||||
@@ -66,9 +64,6 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
if (language === 'es') {
|
||||
return {
|
||||
creditsLabel: 'Creditos',
|
||||
noCreditsTitle: 'Sin creditos',
|
||||
noCreditsMessage: 'No tienes creditos para escaneos AI. Actualiza o compra top-up en Perfil.',
|
||||
healthNoCreditsMessage: `Necesitas ${HEALTH_CHECK_CREDIT_COST} creditos para el health-check.`,
|
||||
managePlan: 'Gestionar plan',
|
||||
dismiss: 'Cerrar',
|
||||
genericErrorTitle: 'Error',
|
||||
@@ -80,6 +75,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
retryLabel: 'Reintentar',
|
||||
notAPlantTitle: 'No es una planta',
|
||||
notAPlantMessage: 'La imagen no muestra una planta reconocible. Por favor fotografía una planta e inténtalo de nuevo.',
|
||||
notAPlantSharedMessage: 'La imagen compartida no muestra una planta reconocible. Es posible que no se haya cargado la publicación original. Haz una captura de pantalla de la publicación y compártela — eso siempre funciona.',
|
||||
providerErrorMessage: 'Escaneo IA no disponible ahora. Inténtalo de nuevo.',
|
||||
healthProviderErrorMessage: 'Health-check IA no disponible ahora. Inténtalo de nuevo.',
|
||||
healthTitle: 'Health Check',
|
||||
@@ -89,8 +85,9 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoTitle: 'Plan de rescate listo',
|
||||
demoMessage: 'Detectamos posibles causas. Desbloquea el diagnóstico completo con IA y tu plan de rescate de 7 días.',
|
||||
demoNoCreditsTitle: 'Escaneos demo agotados',
|
||||
demoNoCreditsMessage: 'Ya usaste tus 5 escaneos demo gratuitos en este dispositivo. Inicia Pro para seguir escaneando plantas.',
|
||||
demoCreditsRemaining: (count: number) => `${count} escaneos demo restantes`,
|
||||
demoNoCreditsMessage: 'Ya usaste tu escaneo demo gratuito en este dispositivo. Inicia Pro para seguir escaneando plantas.',
|
||||
demoCreditsRemaining: (count: number) => (count === 1 ? '1 escaneo demo restante' : `${count} escaneos demo restantes`),
|
||||
creditsRemaining: (count: number) => `${count} escaneos restantes`,
|
||||
appleCta: 'Continuar con Apple',
|
||||
emailCta: 'Continuar con email',
|
||||
unlockCta: 'Desbloquear diagnóstico completo',
|
||||
@@ -99,9 +96,6 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
|
||||
return {
|
||||
creditsLabel: 'Credits',
|
||||
noCreditsTitle: 'No credits left',
|
||||
noCreditsMessage: 'You have no AI scan credits left. Upgrade or buy a top-up in Profile.',
|
||||
healthNoCreditsMessage: `You need ${HEALTH_CHECK_CREDIT_COST} credits for the health check.`,
|
||||
managePlan: 'Manage plan',
|
||||
dismiss: 'Close',
|
||||
genericErrorTitle: 'Error',
|
||||
@@ -113,6 +107,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
retryLabel: 'Try again',
|
||||
notAPlantTitle: 'No plant detected',
|
||||
notAPlantMessage: 'The image does not show a recognizable plant. Please photograph a plant and try again.',
|
||||
notAPlantSharedMessage: 'The shared image does not show a recognizable plant. The original post may not have loaded. Take a screenshot of the post and share the screenshot — that always works.',
|
||||
providerErrorMessage: 'AI scan is currently unavailable. Please try again.',
|
||||
healthProviderErrorMessage: 'AI health check is currently unavailable. Please try again.',
|
||||
healthTitle: 'Health Check',
|
||||
@@ -122,8 +117,9 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
|
||||
demoTitle: 'Rescue plan ready',
|
||||
demoMessage: 'We found possible causes. Unlock the full AI diagnosis and your 7-day rescue plan.',
|
||||
demoNoCreditsTitle: 'Demo scans used',
|
||||
demoNoCreditsMessage: 'You used your 5 free demo scans on this device. Start Pro to keep scanning plants.',
|
||||
demoCreditsRemaining: (count: number) => `${count} demo scans left`,
|
||||
demoNoCreditsMessage: 'You used your free demo scan on this device. Start Pro to keep scanning plants.',
|
||||
demoCreditsRemaining: (count: number) => (count === 1 ? '1 demo scan left' : `${count} demo scans left`),
|
||||
creditsRemaining: (count: number) => `${count} scans left`,
|
||||
appleCta: 'Continue with Apple',
|
||||
emailCta: 'Continue with email',
|
||||
unlockCta: 'Unlock full diagnosis',
|
||||
@@ -149,6 +145,7 @@ export default function ScannerScreen() {
|
||||
setPendingPlant,
|
||||
guestScanCount,
|
||||
incrementGuestScanCount,
|
||||
hasCompletedOnboarding,
|
||||
} = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
const router = useRouter();
|
||||
@@ -167,8 +164,8 @@ export default function ScannerScreen() {
|
||||
: params.sharedImageKey;
|
||||
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
|
||||
&& billingSummary?.entitlement?.status === 'active';
|
||||
const isDemoMode = !hasActiveEntitlement;
|
||||
const availableCredits = hasActiveEntitlement ? (billingSummary?.credits.available ?? 0) : 0;
|
||||
const isDemoMode = !session; // guests get limited AI demo scans; signed-in users burn real credits
|
||||
const availableCredits = billingSummary?.credits.available ?? 0;
|
||||
const demoScansRemaining = Math.max(0, DEMO_SCAN_LIMIT - guestScanCount);
|
||||
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
@@ -179,6 +176,7 @@ export default function ScannerScreen() {
|
||||
const [analysisProgress, setAnalysisProgress] = useState(0);
|
||||
const [analysisResult, setAnalysisResult] = useState<IdentificationResult | null>(null);
|
||||
const [demoResultVisible, setDemoResultVisible] = useState(false);
|
||||
const [outOfCreditsVisible, setOutOfCreditsVisible] = useState(false);
|
||||
const cameraRef = useRef<CameraView>(null);
|
||||
const scanLineProgress = useRef(new Animated.Value(0)).current;
|
||||
const scanPulse = useRef(new Animated.Value(0)).current;
|
||||
@@ -204,6 +202,7 @@ export default function ScannerScreen() {
|
||||
|
||||
const lastProcessedShareToken = useRef<string | null>(null);
|
||||
const sharedAnalysisInFlightToken = useRef<string | null>(null);
|
||||
const analysisFromShareRef = useRef(false);
|
||||
const resizeForAnalysisRef = useRef<(uri: string) => Promise<string>>(async (uri) => uri);
|
||||
const analyzeImageRef = useRef<(imageUri: string, galleryImageUri?: string) => Promise<void>>(async () => {});
|
||||
|
||||
@@ -267,32 +266,18 @@ export default function ScannerScreen() {
|
||||
if (isAnalyzing) return;
|
||||
|
||||
if (isDemoMode && guestScanCount >= DEMO_SCAN_LIMIT) {
|
||||
Alert.alert(
|
||||
billingCopy.demoNoCreditsTitle,
|
||||
billingCopy.demoNoCreditsMessage,
|
||||
[
|
||||
{ text: billingCopy.dismiss, style: 'cancel' },
|
||||
{
|
||||
text: billingCopy.managePlan,
|
||||
onPress: () => router.replace('/profile/billing'),
|
||||
},
|
||||
],
|
||||
);
|
||||
posthog.capture('out_of_credits_shown', { trigger: 'demo_limit', scan_type: 'identification' });
|
||||
setOutOfCreditsVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDemoMode && availableCredits <= 0) {
|
||||
Alert.alert(
|
||||
billingCopy.noCreditsTitle,
|
||||
isHealthMode ? billingCopy.healthNoCreditsMessage : billingCopy.noCreditsMessage,
|
||||
[
|
||||
{ text: billingCopy.dismiss, style: 'cancel' },
|
||||
{
|
||||
text: billingCopy.managePlan,
|
||||
onPress: () => router.replace('/profile/billing'),
|
||||
},
|
||||
],
|
||||
);
|
||||
// Only pre-block when the billing summary is actually known; if it's null
|
||||
// (fetch failed / still loading) let the request proceed — the server 402
|
||||
// path opens the sheet as the safety net.
|
||||
const requiredCredits = isHealthMode ? 2 : 1;
|
||||
if (!isDemoMode && billingSummary && availableCredits < requiredCredits) {
|
||||
posthog.capture('out_of_credits_shown', { trigger: 'pre_check', scan_type: isHealthMode ? 'health_check' : 'identification' });
|
||||
setOutOfCreditsVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,11 +305,12 @@ export default function ScannerScreen() {
|
||||
demo_scans_used: guestScanCount,
|
||||
demo_scans_remaining: demoScansRemaining,
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2100));
|
||||
const demoResult = await PlantRecognitionService.identify(imageUri, language, {
|
||||
idempotencyKey: createIdempotencyKey('demo-scan-plant'),
|
||||
});
|
||||
setAnalysisProgress(100);
|
||||
await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
await new Promise(resolve => setTimeout(resolve, 350));
|
||||
const demoResult = getMockPlantByImage(galleryImageUri || imageUri, language, true);
|
||||
incrementGuestScanCount();
|
||||
setAnalysisResult(demoResult);
|
||||
posthog.capture('demo_scan_completed', {
|
||||
@@ -340,6 +326,11 @@ export default function ScannerScreen() {
|
||||
credits_available: availableCredits,
|
||||
});
|
||||
|
||||
// Für aggregierte Auswertungen: welche Art bzw. welcher Befund kam heraus.
|
||||
// Nur Art-/Befundnamen, keine Bilddaten.
|
||||
let scannedSpecies: string | null = null;
|
||||
let healthStatus: string | null = null;
|
||||
|
||||
if (isHealthMode) {
|
||||
if (!healthPlant) {
|
||||
Alert.alert(billingCopy.genericErrorTitle, billingCopy.genericErrorMessage);
|
||||
@@ -366,6 +357,9 @@ export default function ScannerScreen() {
|
||||
latency_ms: Date.now() - startTime,
|
||||
});
|
||||
|
||||
scannedSpecies = healthPlant.botanicalName || healthPlant.name || null;
|
||||
healthStatus = response.healthCheck?.status || null;
|
||||
|
||||
const currentGallery = healthPlant.gallery || [];
|
||||
const existingChecks = healthPlant.healthChecks || [];
|
||||
const updatedChecks = [response.healthCheck, ...existingChecks].slice(0, 6);
|
||||
@@ -386,6 +380,8 @@ export default function ScannerScreen() {
|
||||
latency_ms: Date.now() - startTime,
|
||||
});
|
||||
|
||||
scannedSpecies = result.botanicalName || result.name || null;
|
||||
|
||||
setAnalysisResult(result);
|
||||
}
|
||||
setAnalysisProgress(100);
|
||||
@@ -393,6 +389,8 @@ export default function ScannerScreen() {
|
||||
posthog.capture('paid_scan_completed', {
|
||||
scan_type: isHealthMode ? 'health_check' : 'identification',
|
||||
latency_ms: Date.now() - startTime,
|
||||
species: scannedSpecies,
|
||||
health_status: healthStatus,
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setIsAnalyzing(false);
|
||||
@@ -412,17 +410,8 @@ export default function ScannerScreen() {
|
||||
});
|
||||
|
||||
if (isInsufficientCreditsError(error)) {
|
||||
Alert.alert(
|
||||
billingCopy.noCreditsTitle,
|
||||
isHealthMode ? billingCopy.healthNoCreditsMessage : billingCopy.noCreditsMessage,
|
||||
[
|
||||
{ text: billingCopy.dismiss, style: 'cancel' },
|
||||
{
|
||||
text: billingCopy.managePlan,
|
||||
onPress: () => router.replace('/profile/billing'),
|
||||
},
|
||||
],
|
||||
);
|
||||
posthog.capture('out_of_credits_shown', { trigger: 'server_402', scan_type: isHealthMode ? 'health_check' : 'identification' });
|
||||
setOutOfCreditsVisible(true);
|
||||
} else if (isTimeoutError(error)) {
|
||||
Alert.alert(
|
||||
billingCopy.timeoutTitle,
|
||||
@@ -444,7 +433,7 @@ export default function ScannerScreen() {
|
||||
} else if (isBackendApiError(error) && error.code === 'NOT_A_PLANT') {
|
||||
Alert.alert(
|
||||
billingCopy.notAPlantTitle,
|
||||
billingCopy.notAPlantMessage,
|
||||
analysisFromShareRef.current ? billingCopy.notAPlantSharedMessage : billingCopy.notAPlantMessage,
|
||||
[{ text: billingCopy.dismiss, style: 'cancel' }],
|
||||
);
|
||||
} else if (isBackendApiError(error) && error.code === 'PROVIDER_ERROR') {
|
||||
@@ -496,6 +485,7 @@ export default function ScannerScreen() {
|
||||
if (cancelled || sharedAnalysisInFlightToken.current !== shareToken) return;
|
||||
setDemoResultVisible(false);
|
||||
setSelectedImage(analysisUri);
|
||||
analysisFromShareRef.current = true;
|
||||
await analyzeImageRef.current(analysisUri, nextSharedImageUri);
|
||||
} finally {
|
||||
if (sharedAnalysisInFlightToken.current === shareToken) {
|
||||
@@ -517,6 +507,7 @@ export default function ScannerScreen() {
|
||||
const analysisUri = await resizeForAnalysis(photo.uri);
|
||||
setDemoResultVisible(false);
|
||||
setSelectedImage(analysisUri);
|
||||
analysisFromShareRef.current = false;
|
||||
analyzeImage(analysisUri, photo.uri);
|
||||
}
|
||||
};
|
||||
@@ -534,6 +525,7 @@ export default function ScannerScreen() {
|
||||
const analysisUri = await resizeForAnalysis(asset.uri);
|
||||
setDemoResultVisible(false);
|
||||
setSelectedImage(asset.uri);
|
||||
analysisFromShareRef.current = false;
|
||||
analyzeImage(analysisUri, asset.uri);
|
||||
}
|
||||
};
|
||||
@@ -628,7 +620,7 @@ export default function ScannerScreen() {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.replace('/onboarding');
|
||||
router.replace(hasCompletedOnboarding ? '/(tabs)' : '/onboarding');
|
||||
};
|
||||
|
||||
const controlsPaddingBottom = Math.max(20, insets.bottom + 10);
|
||||
@@ -689,7 +681,11 @@ export default function ScannerScreen() {
|
||||
<View style={[styles.creditBadge, { backgroundColor: colors.heroButton, borderColor: colors.heroButtonBorder }]}>
|
||||
<Ionicons name={isDemoMode ? 'sparkles-outline' : 'wallet-outline'} size={12} color={colors.text} />
|
||||
<Text style={[styles.creditBadgeText, { color: colors.text }]}>
|
||||
{isDemoMode ? billingCopy.demoCreditsRemaining(demoScansRemaining) : `${billingCopy.creditsLabel}: ${availableCredits}`}
|
||||
{isDemoMode
|
||||
? billingCopy.demoCreditsRemaining(demoScansRemaining)
|
||||
: !hasActiveEntitlement
|
||||
? billingCopy.creditsRemaining(availableCredits)
|
||||
: `${billingCopy.creditsLabel}: ${availableCredits}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -865,6 +861,38 @@ export default function ScannerScreen() {
|
||||
<Text style={[styles.controlLabel, { color: colors.textMuted }]}>{t.help}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<OutOfCreditsSheet
|
||||
visible={outOfCreditsVisible}
|
||||
language={language}
|
||||
colors={colors}
|
||||
isPro={hasActiveEntitlement}
|
||||
renewsAtIso={billingSummary?.credits.cycleEndsAt}
|
||||
onSeePlans={() => {
|
||||
setOutOfCreditsVisible(false);
|
||||
if (!session) {
|
||||
posthog.capture('paywall_opened', { source: 'out_of_credits', authenticated: false });
|
||||
router.replace('/profile/billing?view=paywall&context=out_of_credits');
|
||||
return;
|
||||
}
|
||||
posthog.capture('billing_opened', { source: 'out_of_credits', authenticated: true });
|
||||
router.replace('/profile/billing');
|
||||
}}
|
||||
onTopup={(productId) => {
|
||||
setOutOfCreditsVisible(false);
|
||||
posthog.capture('topup_prompt_opened', { source: 'out_of_credits', product_id: productId, authenticated: Boolean(session) });
|
||||
if (session) {
|
||||
router.replace({ pathname: '/profile/billing', params: { topup: productId } });
|
||||
return;
|
||||
}
|
||||
router.replace({ pathname: '/auth/signup', params: { returnTo: 'billing', topup: productId } });
|
||||
}}
|
||||
onDismiss={() => {
|
||||
posthog.capture('paywall_dismissed', { source: 'out_of_credits_sheet' });
|
||||
setOutOfCreditsVisible(false);
|
||||
handleClose();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
assets/new_welcoming_hero.png
Normal file
|
After Width: | Height: | Size: 799 KiB |
BIN
assets/onboarding_health_scan_mockup_vertical.png
Normal file
|
After Width: | Height: | Size: 736 KiB |
BIN
assets/robot_monstera_avatar.jpg
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
assets/welcome_hero_vertical.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
417
audits/Copy_Audit_Onboarding_Paywall.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# GreenLens — Copy-Audit: Onboarding & Paywall
|
||||
|
||||
> **Status: umgesetzt am 27.07.2026.** Alle Empfehlungen aus diesem Dokument sind im Code implementiert
|
||||
> (DE/EN/ES). TypeScript-Check läuft fehlerfrei. Offene Punkte stehen in Abschnitt 6.
|
||||
>
|
||||
> Alle 6 echten App-Store-Bewertungen sind in `constants/socialProof.ts` hinterlegt.
|
||||
> Confidence-Werte im Worked Example spiegeln echtes Produktverhalten; die Kalibrierung im
|
||||
> Health-Prompt (`server/lib/openai.js`) wurde dafür ergänzt (Abschnitt 6.2).
|
||||
|
||||
**Audit:** 26.07.2026 · **Umsetzung:** 27.07.2026
|
||||
**Grundlage:** Deep Research Report, Avatar Sheet (Emma / "Anxious Beginner"), Offer Brief, Necessary Beliefs Doc
|
||||
**Geprüfte Screens:** `app/onboarding.tsx`, `app/onboarding/slides.tsx`, `source.tsx`, `health-check.tsx`, `personalizing.tsx`, `customize.tsx`, `app/profile/billing.tsx` (Paywall + Cancel-Flow), `components/OutOfCreditsSheet.tsx`, `utils/translations.ts`
|
||||
|
||||
---
|
||||
|
||||
## 0. Executive Summary
|
||||
|
||||
Der Flow ist handwerklich sauber gebaut (Progress, Analytics, 3 Sprachen, Chat-Interaktion). **Das Problem ist nicht die Ausführung, sondern das Argument.**
|
||||
|
||||
Drei Befunde, alles andere ist Detail:
|
||||
|
||||
1. **Der gesamte Flow verkauft Identifikation. Das Produkt verkauft Triage.**
|
||||
Offer Brief, wörtlich: *"Start the pitch after identification, never on identification."* Der aktuelle Flow tut exakt das Gegenteil — Screen 1 bis 3 heißen „Pflanzen-Scanner", „Scanne jede Pflanze", „Monstera · 98%". Das ist genau das, was Google Lens gratis kann. Damit wird **Belief 5** („Ein Abo ist mir mehr wert als weiter googeln") nie adressiert — sie wird sogar aktiv widerlegt.
|
||||
|
||||
2. **Es gibt keinen einzigen Satz über Emmas Schmerz.**
|
||||
Der Avatar kommt mit einem *akuten Symptom*, Schuldgefühl und Angst („I'll be so discouraged if I kill another one"). Im gesamten Onboarding fällt kein Wort über gelbe Blätter, braune Spitzen, hängende Triebe oder „hab ich zu viel gegossen?". Der Flow ist Feature-Tour, nicht Argument. *Pain is the pitch* — der Pitch fehlt.
|
||||
|
||||
3. **Die Paywall kommt nach null erlebtem Wert.**
|
||||
`personalizing.tsx` → direkt `billing?view=paywall`. Der Nutzer hat zu diesem Zeitpunkt keinen Scan gemacht, keine Ausgabe gesehen, kein Symptom eingegeben. Offer Brief benennt das als **den einzelnen höchsten Hebel überhaupt**: *"A meaningful preview of the triage output before the paywall."*
|
||||
|
||||
Dazu kommen **fünf akute Credibility-Risiken** (Abschnitt 2), von denen mindestens zwei rechtlich und App-Store-relevant sind.
|
||||
|
||||
---
|
||||
|
||||
## 1. Belief-Coverage-Matrix
|
||||
|
||||
Wo im Flow wird welcher der sechs notwendigen Beliefs bearbeitet?
|
||||
|
||||
### Vorher
|
||||
|
||||
| # | Notwendiger Belief | Wo adressiert? | Status |
|
||||
|---|---|---|---|
|
||||
| 1 | Ein Foto kann die Ursache genug eingrenzen | nirgends | fehlt |
|
||||
| 2 | Die App sagt mir konkret, was zu prüfen und zu tun ist (**Anker**) | nur health-check, als UI-Tutorial | schwach |
|
||||
| 3 | Die App sagt ehrlich, wenn sie unsicher ist | nirgends — „98%" behauptet Sicherheit | kontraproduktiv |
|
||||
| 4 | Die Empfehlung gilt für *meine* Pflanze | behauptet, nie belegt | schwach |
|
||||
| 5 | Abo > weiter googeln | nirgends | fehlt |
|
||||
| 6 | Preis, Verlängerung, Kündigung vorab klar | Preis + „Jederzeit kündbar" | erfüllt |
|
||||
|
||||
### Nachher — alle sechs bedient
|
||||
|
||||
| # | Belief | Umgesetzt in | Konkret |
|
||||
|---|---|---|---|
|
||||
| 1 | Foto grenzt ein | `slides.tsx` Slide 1, `health-check.tsx` | „GreenLens gewichtet die wahrscheinlichsten Ursachen und sagt dir, was du prüfen sollst, um sie zu bestätigen oder auszuschließen." + Ursachen-Ranking im Beispiel |
|
||||
| 2 | Konkret prüfen & tun | `health-check.tsx`, `slides.tsx`, Paywall-Bullets | Worked Example mit „Prüfe zuerst" (3 Schritte) und „Tu jetzt" (eine Handlung) |
|
||||
| 3 | Ehrlich bei Unsicherheit | `slides.tsx` Slide 3, `health-check.tsx` limitNote, Paywall-Bullet 1 | Eigener Slide „Wir sagen dir auch, wenn wir unsicher sind" + graue Confidence-Stufen statt Prozentzahl |
|
||||
| 4 | Gilt für *meine* Pflanze | `slides.tsx` Slide 2, `personalizing.tsx` | „Ein konkreter Plan. Für diese Pflanze." + Rückspiegelung der eigenen Chat-Antworten als Chips |
|
||||
| 5 | Besser als googeln | Welcome-Variante B, **Paywall `paywallCompare`** | „Kostenlose Tools sagen dir, wie deine Pflanze heißt. Sie sagen dir nicht, was zu prüfen ist, was zu tun ist oder ob es gewirkt hat." — bewusst auch auf der Paywall, damit der Belief unabhängig von der A/B-Variante greift |
|
||||
| 6 | Preis & Kündigung vorab | `billing.tsx` | Monatsäquivalent des Jahrespreises, Trial-Erinnerung 2 Tage vorher, konkreter Kündigungsweg — alles vor dem CTA |
|
||||
|
||||
Der Anker-Belief (2) war ursprünglich an *einer* Stelle bedient — dort als Wegbeschreibung getarnt („Wo ist der Health-Scan?") statt als Kaufargument. Dieser Screen ist jetzt das Worked Example und trägt drei Beliefs gleichzeitig.
|
||||
|
||||
---
|
||||
|
||||
## 2. Akute Credibility- und Compliance-Risiken
|
||||
|
||||
**Diese fünf Punkte vor allen Copy-Verbesserungen klären.**
|
||||
|
||||
### 2.1 Testimonials ohne belegte Herkunft — `onboarding.tsx:23`, `personalizing.tsx:17`
|
||||
```
|
||||
„Endlich überleben meine Pflanzen! Absolute Empfehlung." — Anna M.
|
||||
„GreenLens hat meine Geigenfeige gerettet..." — Elena R.
|
||||
```
|
||||
Wenn diese Zitate nicht von realen, dokumentierten Nutzern stammen: UWG-Verstoß (§5 Irreführung), App-Store-Guideline-Risiko, und im Schadensfall genau die Vertrauensfrage, auf der die gesamte Positionierung ruht. **Entweder belegen oder entfernen.** Es gibt starke Alternativen ohne Testimonial (siehe 3.1).
|
||||
|
||||
### 2.2 „4,8 APP-STORE-BEWERTUNG" hardcoded — `personalizing.tsx:19`
|
||||
Der Deep Research Report stellt fest, dass das App-Store-Listing **keine Reviews** hat. Eine fest einprogrammierte Sternebewertung, die nicht aus einer realen Quelle stammt, ist derselbe Tatbestand wie 2.1 — nur prominenter platziert.
|
||||
|
||||
### 2.3 „Monstera · 98%" — `slides.tsx:30`
|
||||
Offer Brief, Verbotsliste: *"Not a specific accuracy percentage — self-reported figures read as marketing noise and are unverifiable."* Zusätzlich zerstört diese Zahl aktiv **Belief 3** (Ehrlichkeit über Unsicherheit), der die gesamte Differenzierung gegenüber „App XY hat mir selbstbewusst was Falsches gesagt" trägt.
|
||||
|
||||
### 2.4 Direkter Widerspruch auf der Paywall — `billing.tsx:156–159`
|
||||
```
|
||||
Headline: „Unbegrenzter Zugriff"
|
||||
Sub: „Unbegrenzte Scans, Health-Checks…"
|
||||
Card-Body: „…, 100 Credits/Monat"
|
||||
```
|
||||
„Unbegrenzt" und „100 Credits/Monat" stehen auf demselben Screen, drei Zeilen auseinander. Für einen Avatar, dessen dokumentierte Kernangst *Abo-Intransparenz* ist, ist das der schlimmstmögliche Fehler an der schlimmstmöglichen Stelle. Er zerstört den einzigen Belief, der aktuell erfüllt ist (Nr. 6).
|
||||
|
||||
### 2.5 Datenschutz-Aussage prüfen — `translations.ts:254`
|
||||
```
|
||||
„Deine Daten bleiben privat und lokal auf deinem Gerät."
|
||||
```
|
||||
Laut Architektur gehen Scans an OpenAI und Bilder an MinIO/S3. Diese Aussage ist dann **sachlich falsch**. Bitte durch etwas Wahres ersetzen, z. B.: *„Deine Fotos werden nur zur Analyse verarbeitet und nicht weiterverkauft."* (nur wenn zutreffend).
|
||||
|
||||
**Nebenbefund — Umlaute:** In `health-check.tsx` (DE) steht durchgängig ASCII-Ersatz: „Spaeter", „oeffnen", „Ausfuehrliche", „Sofortmassnahmen", „Wo ist der Health-Scan?" mit „Donde" statt „Dónde" im ES. Auf einem Screen, auf dem Zahlungsbereitschaft entsteht, liest sich das als unfertiges Produkt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Screen für Screen — Diagnose & Rewrite
|
||||
|
||||
### 3.1 Welcome — `app/onboarding.tsx`
|
||||
|
||||
**Aktuell**
|
||||
> Willkommen bei GreenLens!
|
||||
> Pflanzen erkennen, verstehen und pflegen — ganz einfach.
|
||||
> „Endlich überleben meine Pflanzen!" — Anna M.
|
||||
> [Los geht's]
|
||||
|
||||
**Diagnose**
|
||||
- „Willkommen bei GreenLens!" ist eine Begrüßung, keine Headline. Sie kostet den wertvollsten Platz im gesamten Produkt und sagt nichts.
|
||||
- „ganz einfach" ist ein subjektives Adjektiv ohne Beleg — *point, don't talk*.
|
||||
- Die Subline ist eine Kategoriebeschreibung, die auf jede der zwölf Konkurrenz-Apps passt. Keine Pattern Interruption.
|
||||
- One-Mississippi-Test: nach zwei Sekunden weiß der Nutzer, dass es um Pflanzen geht. Sonst nichts.
|
||||
|
||||
**Rewrite A — Symptom-Einstieg (empfohlen, adressiert Belief 1+2+5)**
|
||||
> **Gelbe Blätter. Und jetzt?**
|
||||
> GreenLens sagt dir die wahrscheinlichsten Ursachen, was du selbst prüfen sollst und die eine sichere Sache, die du jetzt tun kannst.
|
||||
> [Meine Pflanze prüfen] · Schon dabei? Anmelden
|
||||
|
||||
**Rewrite B — Abgrenzung zur Gratis-Alternative (adressiert Belief 5 frontal)**
|
||||
> **Andere Apps sagen dir, wie deine Pflanze heißt.**
|
||||
> **GreenLens sagt dir, was mit ihr los ist.**
|
||||
> Wahrscheinliche Ursache, was du prüfen sollst, was du als Nächstes tust — und ein Check, ob es gewirkt hat.
|
||||
> [Pflanze prüfen]
|
||||
|
||||
Beide ersetzen das Testimonial durch Argument. Wenn belegte Testimonials existieren, gehören sie *nach* das Argument, nicht davor.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Slides — `app/onboarding/slides.tsx`
|
||||
|
||||
**Aktuell:** „Scanne jede Pflanze" → „Health Check & Pflegeplan" → „Nie mehr Gießen vergessen"
|
||||
|
||||
**Diagnose**
|
||||
- **Falsche Reihenfolge.** Slide 1 ist die Gratis-Funktion. Slide 2 ist das Produkt. Der stärkste Grund zu zahlen steht an Position 2 von 3 — und der Nutzer, der nach Slide 1 aussteigt, hat ihn nie gesehen.
|
||||
- „Nie mehr Gießen vergessen" verkauft an einen anderen Avatar (den organisierten Sammler). Emma hat kein Erinnerungsproblem, sie hat ein *Diagnoseproblem*. Slide 3 wechselt mitten im Flow das Publikum.
|
||||
- „Monstera · 98%" → siehe 2.3.
|
||||
- Slide 2 ist inhaltlich richtig, aber unterspezifiziert: „erkennt Probleme früh und erstellt deinen Rettungsplan" — was steht in dem Plan? Genau das weiß `health-check.tsx` bereits sehr gut. Der Text existiert schon, nur am falschen Screen.
|
||||
|
||||
**Rewrite — 3 Slides, neu geordnet**
|
||||
|
||||
**Slide 1 (war 2) — das Produkt**
|
||||
> **Was ist mit meiner Pflanze los?**
|
||||
> Fotografiere das Symptom. GreenLens rankt die wahrscheinlichsten Ursachen und sagt dir, was du prüfen sollst, um sie zu bestätigen oder auszuschließen.
|
||||
> *Visual-Chip:* „Wahrscheinlichste Ursache: Überwässerung · Prüfe: Erde 3 cm tief, Blattunterseiten"
|
||||
|
||||
**Slide 2 — der Plan (Belief 2 + 4)**
|
||||
> **Ein konkreter Plan. Für diese Pflanze.**
|
||||
> Sofortmaßnahme, 7-Tage-Plan, und nach 7 Tagen die Frage: Hat es gewirkt? Wenn nicht, geht es weiter.
|
||||
|
||||
**Slide 3 — Ehrlichkeit als Feature (Belief 3 — aktuell komplett unbesetzt)**
|
||||
> **Wir sagen dir auch, wenn wir unsicher sind.**
|
||||
> Ein Foto zeigt keine Wurzeln, keine Erdfeuchte, keine Vorgeschichte. Deshalb bekommst du Wahrscheinlichkeiten und Prüfschritte — keine erfundene Gewissheit.
|
||||
|
||||
Slide 3 ist der Screen, den keine Konkurrenz-App hat. Er beantwortet „Eine andere App hat mir selbstbewusst was Falsches gesagt" — laut Offer Brief eine der Top-Einwände.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Chat-Flow — `app/onboarding/source.tsx`
|
||||
|
||||
**Aktuell:** Monstera-Bot → „Wie hast du von GreenLens gehört?" → Ziel → Erfahrung → Licht
|
||||
|
||||
**Diagnose**
|
||||
- **Die erste Frage dient dir, nicht ihm.** Attribution („Wie hast du von GreenLens gehört?") ist eine Marketing-Frage an der Stelle mit der höchsten Abbruchgefahr. Sie signalisiert dem Nutzer: dieses Gespräch ist für die Firma. Verschieben — nach dem ersten Erfolgserlebnis oder in die Profil-Settings.
|
||||
- **Die Fiktion kollidiert mit der Positionierung.** „Hi! Ich bin deine neue Monstera 🌿" ist charmant, aber der Nutzer hat evtl. gar keine Monstera — und die gesamte Differenzierung basiert auf *Ehrlichkeit und Nüchternheit*. Eine sprechende Pflanze an der Stelle, an der Vertrauen entstehen soll, arbeitet gegen Belief 3.
|
||||
- **Es wird nie nach dem Symptom gefragt** — der einzigen Information, die für Emmas Job-to-be-done tatsächlich zählt.
|
||||
- Positiv: Die Lichtfrage ist gut motiviert („damit ich glücklich bleibe"). Genau dieses Muster — *warum ich das frage* — fehlt bei allen anderen Fragen.
|
||||
|
||||
**Rewrite — Fragenreihenfolge**
|
||||
|
||||
| # | Frage | Warum |
|
||||
|---|---|---|
|
||||
| 1 | „Hat gerade eine deiner Pflanzen ein Problem?" (Ja, akut / Ja, schleichend / Nein, vorbeugend) | Sofortige Relevanz, segmentiert Emma vs. Sammler |
|
||||
| 2 | „Was siehst du?" (Gelbe Blätter · Braune Spitzen · Hängt · Flecken/Belag · Krabbeltiere · Etwas anderes) | Das ist der P.I.G.-Moment. Hier erkennt sie sich wieder. |
|
||||
| 3 | „Wie viel Licht bekommt sie?" | bleibt — gut gemacht |
|
||||
| 4 | „Wie sicher fühlst du dich bei Pflanzenpflege?" | bleibt |
|
||||
| — | „Wie hast du von uns gehört?" | **raus aus dem Flow** |
|
||||
|
||||
Jede Frage mit einem Halbsatz begründen, warum sie gestellt wird — so wie es die Lichtfrage bereits tut.
|
||||
|
||||
Wenn der Monstera-Charakter bleiben soll: als *Beispielpflanze zur Demo* rahmen, nicht als Persona, die vorgibt, die Pflanze des Nutzers zu sein.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Health-Check-Screen — `app/onboarding/health-check.tsx`
|
||||
|
||||
**Das ist der stärkste Screen im Flow — und er ist als Bedienungsanleitung verkleidet.**
|
||||
|
||||
Diese vier Zeilen sind das gesamte Verkaufsargument:
|
||||
> Gesundheits-Score mit Status: stabil, beobachten oder kritisch.
|
||||
> Ausführliche Analyse mit sichtbaren Hinweisen **und Unsicherheit**.
|
||||
> Wahrscheinlichste Ursachen **mit Confidence-Werten**.
|
||||
> Sofortmaßnahmen plus konkreter 7-Tage-Pflegeplan.
|
||||
|
||||
Das bedient Belief 1, 2, 3 und 4 gleichzeitig. Es steht aber unter der Überschrift **„Wo ist der Health-Scan?"** — also unter einer Wegbeschreibung, an Position 5 von 6, kurz vor der Paywall.
|
||||
|
||||
**Empfehlung:** Inhalt hochziehen (Slides 1+2, siehe 3.2). Den Screen selbst zu einem **konkreten Beispiel** umbauen — laut Offer Brief *"the single highest-leverage proof asset available today"*:
|
||||
|
||||
> **So sieht eine Antwort aus**
|
||||
>
|
||||
> *Symptom:* Gelbe untere Blätter, Erde feucht
|
||||
> **Wahrscheinlichste Ursache: Überwässerung** (hoch)
|
||||
> Weniger wahrscheinlich: Nährstoffmangel (mittel) · Lichtmangel (niedrig)
|
||||
>
|
||||
> **Prüfe zuerst:** Erde 3 cm tief · Abzugslöcher im Topf · Blattunterseiten
|
||||
> **Tu jetzt:** Nicht gießen. In 5 Tagen erneut prüfen.
|
||||
> **In 7 Tagen:** Wir fragen nach, ob es besser wird.
|
||||
>
|
||||
> *Ein Foto zeigt keine Wurzeln. Deshalb Wahrscheinlichkeiten statt Diagnose.*
|
||||
|
||||
Ein echtes Beispiel schlägt jede Behauptung. Und es beantwortet den Einwand „Die Tipps sind eh generisch" ohne ein einziges Adjektiv.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Personalizing — `app/onboarding/personalizing.tsx`
|
||||
|
||||
**Diagnose**
|
||||
- Loading-Screens, die Arbeit simulieren, sind branchenüblich — aber „Antworten werden analysiert / Pflegeplan wird erstellt / Scan-Credits werden vorbereitet / Plan wird finalisiert" beschreibt **deine** Prozesse, nicht ihren Nutzen. Es ist Wartezeit ohne Überzeugungsarbeit an der Stelle direkt vor der Kaufentscheidung.
|
||||
- Testimonial + „4,8 App-Store-Bewertung": siehe 2.1 / 2.2.
|
||||
- „Scan-Credits werden vorbereitet" führt einen Begriff ein, der nie erklärt wurde — und der zwei Screens später zum Widerspruch mit „unbegrenzt" führt.
|
||||
|
||||
**Rewrite — Schritte auf ihren Nutzen umschreiben**
|
||||
> Deine Antworten werden ausgewertet
|
||||
> Passende Ursachen für dein Symptom werden gewichtet
|
||||
> Prüfschritte für deine Lichtsituation werden ausgewählt
|
||||
> Dein 7-Tage-Plan steht
|
||||
|
||||
Statt Testimonial an dieser Stelle: die **Zusammenfassung ihrer eigenen Angaben** einblenden („Wenig Licht · Gelbe Blätter · Anfängerin"). Das beweist Belief 4 (*es geht um meine Pflanze*) durch Demonstration statt Behauptung.
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Paywall — `app/profile/billing.tsx`
|
||||
|
||||
**Aktuell**
|
||||
> GREENLENS PRO
|
||||
> **Unbegrenzter Zugriff**
|
||||
> Unbegrenzte Scans, Health-Checks und dein persönlicher Pflegeplan.
|
||||
> GreenLens Pro — Unbegrenzte KI-Scans, Gesundheitsdiagnose, 7-Tage-Rettungspläne, 100 Credits/Monat
|
||||
> 7 Tage gratis, dann 39,99 €/Jahr · [Gratis testen] · Jederzeit kündbar
|
||||
|
||||
**Diagnose**
|
||||
1. **„Unbegrenzter Zugriff" vs. „100 Credits/Monat"** — siehe 2.4. Muss weg, unabhängig von allem anderen.
|
||||
2. **Headline verkauft Menge, nicht Ergebnis.** „Unbegrenzt" ist eine Mengenaussage. Emma will kein Volumen, sie will *wissen, was zu tun ist*. Value Equation: Der Traumzustand wird nicht benannt.
|
||||
3. **„Gesundheitsdiagnose"** — laut Offer Brief explizit nicht versprechbar („Not a guaranteed or lab-grade diagnosis"). Wording: *Ursachen-Analyse* oder *Triage*.
|
||||
4. **Kein Risk Reversal jenseits von „Jederzeit kündbar".** Was honest garantierbar ist: transparente Abrechnung + klarer Kündigungsweg. Beides gehört *sichtbar auf den Screen*, nicht in eine Fußnote.
|
||||
5. **Kein Preisanker.** 39,99 €/Jahr steht allein im Raum. Vergleich fehlt (eine ersetzte Pflanze; ein Nursery-Besuch).
|
||||
6. **Nichts erinnert an den Schmerz.** Zwischen Symptomfrage und Kaufentscheidung liegen drei Screens, auf denen ihr Problem nie wieder erwähnt wurde.
|
||||
|
||||
**Rewrite**
|
||||
> **GREENLENS PRO**
|
||||
> ## Nicht mehr raten, was deiner Pflanze fehlt.
|
||||
> Bei jedem Symptom: wahrscheinlichste Ursache, was du prüfen sollst, was du jetzt tust — und ein Check nach 7 Tagen, ob es gewirkt hat.
|
||||
>
|
||||
> ✓ Ursachen-Ranking statt einer Behauptung — inklusive dem, was unsicher ist
|
||||
> ✓ Konkrete Prüfschritte, keine allgemeinen Pflegetexte
|
||||
> ✓ 7-Tage-Plan pro Problem, plus Nachfrage, ob es besser wurde
|
||||
> ✓ 100 Scans & Nachfragen im Monat *(Pro-Modell)*
|
||||
> ✓ Deine komplette Pflanzen-Historie
|
||||
>
|
||||
> **7 Tage gratis. Danach 39,99 €/Jahr (3,33 €/Monat).**
|
||||
> Wir erinnern dich 2 Tage vor Ablauf. Kündigen in 2 Taps in den iOS-Einstellungen — Anleitung in der App.
|
||||
>
|
||||
> [7 Tage gratis testen]
|
||||
> *Heute fällig: 0,00 € · Erste Abbuchung am 2. August*
|
||||
|
||||
Änderungen im Detail:
|
||||
- „Unbegrenzt" ersatzlos gestrichen → interner Widerspruch aufgelöst
|
||||
- Headline benennt den Zustand, nicht die Menge
|
||||
- „Gesundheitsdiagnose" → „Ursachen-Ranking" (compliant mit Offer Brief)
|
||||
- Unsicherheits-Bullet als **Feature** → Belief 3 zum ersten Mal im ganzen Flow bedient
|
||||
- Trial-Erinnerung + konkreter Kündigungsweg → Belief 6 von „erfüllt" auf „übererfüllt"
|
||||
- Monatsäquivalent des Jahrespreises sichtbar (3,33 €) → senkt die wahrgenommene Hürde bei einem preissensiblen Avatar
|
||||
|
||||
**Zusätzlich, höchster Hebel:** Vor die Paywall einen echten Scan setzen. Ein Foto, ein Ergebnis, dann Paywall. Der Offer Brief nennt das die wirksamste verfügbare Einzelmaßnahme — und der aktuelle Flow ist genau eine `router.replace`-Zeile davon entfernt (`personalizing.tsx:75`).
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Out-of-Credits Sheet — `components/OutOfCreditsSheet.tsx`
|
||||
|
||||
**Aktuell**
|
||||
> Deine Gratis-Scans sind aufgebraucht.
|
||||
> Hol dir Pro für unbegrenztes Scannen und deinen 7-Tage-Rettungsplan.
|
||||
|
||||
**Diagnose:** Reine Verlustmeldung. Der Nutzer wollte gerade etwas tun und wird blockiert. Das ist der Moment mit der höchsten *Intention* im ganzen Produkt und er wird mit einer Fehlermeldung eröffnet. Außerdem wieder „unbegrenzt" (siehe 2.4).
|
||||
|
||||
**Rewrite**
|
||||
> **Diese Pflanze schauen wir uns noch an.**
|
||||
> Mit Pro läuft dein Scan sofort — plus Ursachen-Ranking, Prüfschritte und dein 7-Tage-Plan.
|
||||
> [Diesen Scan mit Pro starten] · [Einzelne Credits kaufen] · Später
|
||||
|
||||
Der CTA soll **die angefangene Aufgabe zu Ende bringen**, nicht ein Abo verkaufen. Der Kauf ist das Mittel, nicht das Angebot.
|
||||
|
||||
---
|
||||
|
||||
### 3.8 Cancel-Flow — `billing.tsx:141–150`
|
||||
|
||||
**Gut gemacht:** Grund vor Angebot abfragen, gestaffeltes Save-Offer, klarer Abbruchweg. Struktur stimmt.
|
||||
|
||||
**Zwei Verbesserungen**
|
||||
- „Schade, dass du gehst" / „Ein Geschenk für dich!" — der Ton ist unternehmensseitig („wir sind traurig"). Besser nutzerseitig: *„Bevor du gehst — was hat nicht gepasst?"*
|
||||
- Das Save-Offer ist grundsegmentiert falsch: Wer „Ich nutze die App zu selten" wählt, bekommt einen Rabatt, obwohl der Preis nicht sein Problem ist. Bei *zu selten* wäre die passende Antwort **Pause statt Rabatt** („Abo für 3 Monate pausieren") — und das rettet die Beziehung, ohne Marge zu verschenken.
|
||||
|
||||
---
|
||||
|
||||
## 4. Umsetzungsstand
|
||||
|
||||
### Erledigt (27.07.2026)
|
||||
|
||||
| # | Maßnahme | Datei |
|
||||
|---|---|---|
|
||||
| 1 | Erfundene Testimonials entfernt, echte Store-Zitate als einzige Quelle | `constants/socialProof.ts` (neu), `onboarding.tsx`, `personalizing.tsx` |
|
||||
| 2 | Rating auf belegbare **5,0 · 6 Bewertungen** — Wert und Anzahl immer gemeinsam | `constants/socialProof.ts`, `onboarding.tsx` |
|
||||
| 3 | „Unbegrenzt / unlimited / ilimitado" überall entfernt, wo Credits limitiert sind | `billing.tsx`, `OutOfCreditsSheet.tsx` |
|
||||
| 4 | „98%" ersetzt durch Ursachen-Ranking mit hoch/mittel/niedrig | `slides.tsx`, `health-check.tsx` |
|
||||
| 5 | Datenschutz-Aussage korrigiert (kein „bleibt lokal auf deinem Gerät" mehr) | `translations.ts` (DE/EN/ES) |
|
||||
| 6 | Umlaute und Akzente in `health-check.tsx` korrigiert | `health-check.tsx` |
|
||||
| 7 | Welcome-Rewrite mit A/B-Test (Symptom vs. Abgrenzung) | `onboarding.tsx`, `services/experiments.ts` (neu) |
|
||||
| 8 | Slides neu geordnet: Triage → Plan → Unsicherheit | `slides.tsx` |
|
||||
| 9 | Chat: Attributionsfrage ans Ende, Symptomfrage nach vorn | `source.tsx` |
|
||||
| 10 | Health-Check-Screen zum Worked Example umgebaut | `health-check.tsx` |
|
||||
| 11 | Personalizing: Nutzensprache + Rückspiegelung der eigenen Antworten | `personalizing.tsx` |
|
||||
| 12 | Paywall-Rewrite inkl. Bullets, Vergleich, Monatsäquivalent, Trial-Erinnerung, Kündigungsweg | `billing.tsx` |
|
||||
| 13 | Out-of-Credits als „Aufgabe zu Ende bringen" reframed | `OutOfCreditsSheet.tsx` |
|
||||
| 14 | Cancel-Flow: Pause statt Rabatt bei „nutze zu selten" | `billing.tsx` |
|
||||
| 15 | „Pro scans with GPT-5.4" entfernt — Modellname ist kein Nutzen | `billing.tsx` |
|
||||
| 16 | Hardcodierte englische Fragen-Überschriften im Chat lokalisiert | `source.tsx` |
|
||||
|
||||
**Verifikation:** `npx tsc --noEmit` → 0 Fehler. Grep-Gegenprobe auf „unbegrenzt/unlimited/ilimitado", „Anna M.", „Elena R.", „4,8", „98%", „GPT-5" → keine Treffer außer erklärenden Kommentaren.
|
||||
|
||||
### Neue Dateien
|
||||
|
||||
- **`constants/socialProof.ts`** — einzige Quelle für Sterne, Anzahl und Zitate. Regel im Kopf der Datei: nur was im Store nachweisbar ist. Solange `TESTIMONIALS` leer ist, rendert das UI Argument-Copy statt eines Zitats.
|
||||
- **`services/experiments.ts`** — lokale, persistierte 50/50-Zuweisung. Siehe Abschnitt 5.
|
||||
|
||||
---
|
||||
|
||||
## 5. A/B-Test: wie er funktioniert und wie du ihn auswertest
|
||||
|
||||
**Warum nicht PostHog Feature Flags:** Der PostHog-Client läuft bewusst ohne Provider (`services/analytics.ts` — die Provider-Variante hat früher den App-Start zerlegt). Flags wären erst nach einem Netzwerk-Roundtrip da, der Welcome-Screen rendert aber sofort. Die Zuweisung passiert deshalb lokal, wird in AsyncStorage persistiert und an PostHog gemeldet.
|
||||
|
||||
**Was passiert:**
|
||||
|
||||
1. Beim ersten Öffnen des Welcome-Screens wird 50/50 gewürfelt: `symptom` oder `contrast`.
|
||||
2. Die Variante wird persistiert — derselbe Nutzer sieht immer dieselbe.
|
||||
3. `experiment_assigned` wird gefeuert, plus `register()` auf dem PostHog-Client. Dadurch hängt `experiment_welcome_headline_v1` **an jedem folgenden Event** dieses Nutzers.
|
||||
|
||||
**Auswertung in PostHog:** Funnel bauen über `onboarding_welcome_viewed` → `onboarding_started` → `onboarding_chat_completed` → `paywall_opened` → `subscription_started`/`trial_started`, dann *Breakdown by* `experiment_welcome_headline_v1`. Kein Backend-Umbau nötig — die Property liegt auf allen Events.
|
||||
|
||||
**Aussagekraft:** Bei 6 Bewertungen im Store bist du noch in einer Größenordnung, in der ein Headline-Test lange braucht, um signifikant zu werden. Bis dahin ist der Test vor allem eine saubere Infrastruktur — die Entscheidung darüber, welche Variante gewinnt, sollte nicht auf den ersten 50 Nutzern getroffen werden.
|
||||
|
||||
**QA:** `forceVariant('welcome_headline_v1', 'contrast')` erzwingt eine Variante zum Testen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Offene Punkte
|
||||
|
||||
### 6.1 Store-Bewertungen — eingetragen ✅
|
||||
|
||||
Alle 6 echten Bewertungen liegen in `constants/socialProof.ts`, Wortlaut und Anzeigename unverändert. Jede hat ein `priority`-Feld — die Auswahl im UI richtet sich danach, wie stark eine Bewertung die Positionierung stützt, nicht nach Reihenfolge.
|
||||
|
||||
**Priorität 1 ist „Exactly what I needed" (Chrispetersssss, EN)** und die ist ein Glücksfall: *"Snap a photo and it tells you what's wrong and how to fix it - no more guessing why the leaves are turning yellow."* Das ist die Triage-Positionierung, formuliert von einem echten Nutzer — inklusive „gelbe Blätter", also exakt dem Symptom aus der Welcome-Variante A. Diese Bewertung ist wertvoller als jede Headline, die wir selbst schreiben könnten.
|
||||
|
||||
Priorität 2 („Die Pflanzenanalyse toppt echt jede andere App") stützt Belief 5, Priorität 3 („weil ich keinen grünen Daumen habe") trifft den Avatar wörtlich.
|
||||
|
||||
Alle Bewertungen haben freigegebene Übersetzungen für die jeweils anderen Sprachen. Spanische Nutzer sehen die englische Top-Bewertung übersetzt — mit sichtbarem „Traducido"-Hinweis, weil ein fremdsprachiges Original nie unkommentiert als muttersprachlich ausgegeben werden darf.
|
||||
|
||||
**Pflege:** `APP_STORE_RATING` bei jeder Listing-Änderung nachziehen; `verifiedOn` dokumentiert die letzte Gegenprüfung.
|
||||
|
||||
### 6.2 Belief 3 — gedeckt, nach einem Fund im Backend-Prompt ✅
|
||||
|
||||
Bei der Gegenprüfung in `server/lib/openai.js` sind zwei Dinge aufgefallen, die die Copy betroffen hätten.
|
||||
|
||||
**Fund 1: Die Confidence-Skala galt nur für die Arterkennung.**
|
||||
|
||||
`buildScanPrompt` (Arterkennung) hatte eine saubere Kalibrierung:
|
||||
|
||||
```
|
||||
0.85–0.95 Art klar erkennbar
|
||||
0.65–0.84 sehr wahrscheinlich, einzelne Merkmale verdeckt
|
||||
0.40–0.64 mehrdeutig, mehrere Arten passen
|
||||
< 0.40 überwiegend geraten
|
||||
```
|
||||
|
||||
`buildHealthPrompt` (die Ursachen — also genau das, was das Worked Example zeigt) hatte **keine**. Dort stand nur: `"confidence": float 0.05–0.99 reflecting visual certainty`. Eine freie Zahl ohne definierte Bedeutung. Die App zeigt sie aber als Prozentwert an, und die neue Copy verspricht, dass wir Unsicherheit ehrlich ausweisen — dieses Versprechen hing damit an einem uncalibrierten Wert.
|
||||
|
||||
**Behoben:** Dieselbe Skala ist jetzt auch im Health-Prompt hinterlegt, plus die explizite Anweisung *"Do not inflate confidence to appear decisive. If the photo cannot settle it, a value below 0.65 is the correct and expected answer."* Damit ist Belief 3 nicht nur Copy, sondern Modellverhalten.
|
||||
|
||||
**Die Schwelle liegt bei 65 %, nicht 70 %.** Deine Einschätzung war der Sache nach richtig — 65–84 % ist der „sehr wahrscheinlich"-Bereich, darunter wird es mehrdeutig. Die exakte Kante liegt aber bei 0,65. Die Beispiel-Notiz sagt jetzt „Unter 65 %" und stimmt damit mit dem Prompt überein. Wenn du die Schwelle im Prompt änderst, ist die Copy in `health-check.tsx` (`confidenceNote`, alle drei Sprachen) mitzuziehen.
|
||||
|
||||
**Fund 2: Die Beispielwerte hätten ein Verhalten gezeigt, das das Produkt nicht hat.**
|
||||
|
||||
Meine ersten Zahlen waren 58 / 27 / 11 — das summiert sich auf ~96 % und liest sich damit als Wahrscheinlichkeitsverteilung. Das Produkt liefert aber **unabhängige** Sicherheiten je Ursache; zwei Ursachen können beide bei 0,8 liegen. Ein Nutzer, der das Beispiel gesehen hat und dann echte Werte bekommt, die sich auf 140 % addieren, hätte zu Recht das Gefühl, dass etwas nicht stimmt.
|
||||
|
||||
**Geändert auf 64 / 41 / 22.** Summiert sich bewusst nicht auf 100, spiegelt echtes Verhalten und behält den entscheidenden Punkt: der Spitzenwert liegt knapp unter der Sicherheitsschwelle, das Beispiel zeigt also einen ehrlich unsicheren Fall. Im Prompt steht die Nicht-Summierung jetzt ebenfalls explizit (`these are NOT probabilities and must NOT sum to 1`), damit das Modell es nicht selbst normalisiert.
|
||||
|
||||
**Warum das der stärkere Beweis ist:** Ein Beispiel mit 94 % würde aussehen wie jede Konkurrenz-App, der die Nutzer nicht mehr glauben. 64 % plus „unter 65 % sagen wir das deutlich" beweist die Behauptung im selben Atemzug, in dem sie aufgestellt wird — und macht die Prüfschritte zur logischen Konsequenz statt zum Beiwerk.
|
||||
|
||||
**Abgrenzung zum entfernten „98 %":** Der Offer Brief verbietet *Accuracy-Claims* über das Produkt insgesamt („zu 98 % genau") — eine unbelegbare Marketing-Zahl. Ein *fallbezogener Confidence-Wert* im Ergebnis ist das Gegenteil: keine Werbeaussage, sondern eine Auskunft über die Grenzen dieser einen Einschätzung. Das alte „Monstera · 98 %" war Ersteres, die 64 % im Beispiel sind Letzteres.
|
||||
|
||||
**Noch zu beobachten:** Der Prompt ist geändert, aber noch nicht gegen echte Scans validiert. Nach dem nächsten Deploy ein paar Health-Scans mit uneindeutigen Fotos laufen lassen und prüfen, ob die Werte tatsächlich in den unteren Bereich fallen — ein Modell, das trotz Anweisung durchgehend 0,9 ausgibt, wäre wieder eine Produkt-, keine Copy-Lücke.
|
||||
|
||||
### 6.3 Scan vor der Paywall
|
||||
|
||||
Du hast angemerkt, dass der Demo-Scan auf dem Welcome-Screen bereits existiert. Er ist jetzt als **„Erst ausprobieren — ohne Konto"** formuliert und wird mit `onboarding_demo_scan_started` getrackt.
|
||||
|
||||
Der Flow bleibt bewusst unverändert: `personalizing.tsx` → Paywall. Grund: Wer den Demo-Scan nimmt, umgeht das Onboarding komplett und landet nie bei der Paywall — wer das Onboarding durchläuft, sieht keinen Scan. Beide Wege haben eine Lücke, und das Zusammenführen berührt Credits- und Auth-Logik, nicht Copy.
|
||||
|
||||
**Empfehlung als eigener Schritt:** Nach `personalizing.tsx` auf den Scanner leiten („Deine erste Analyse ist inklusive"), Paywall erst nach dem ersten Ergebnis. Der Offer Brief nennt das den höchsten verfügbaren Einzelhebel. In PostHog lässt sich vorher vergleichen, wie die Demo-Scan-Nutzer gegen die Onboarding-Nutzer konvertieren — die Daten dafür laufen ab jetzt.
|
||||
|
||||
### 6.4 Cancel-Flow: Pause technisch umsetzen
|
||||
|
||||
Die Pause-Option ist in der UI umgesetzt und wird als `cancel_save_offer_accepted` mit `offer: 'pause_3_months'` getrackt. Die tatsächliche Umsetzung am Store-Abo steht noch aus — im Code als `TODO(billing)` markiert. Aktuell zeigt sie einen Mock-Alert, genau wie der Rabatt vorher auch.
|
||||
|
||||
### 6.5 Symptom-Antwort in die Personalisierung führen
|
||||
|
||||
`situation` und `symptom` aus dem Chat werden jetzt gespeichert (`PreAuthOnboardingService`) und auf dem Personalizing-Screen zurückgespiegelt. Sie fließen aber noch nicht in den tatsächlich generierten Plan ein. Das wäre der nächste Schritt, damit Belief 4 nicht nur demonstriert, sondern eingelöst wird.
|
||||
140
blogpost-ideen-30.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 30 Blogpost-Ideen für GreenLens
|
||||
|
||||
Basis: GSC-Export vom 03.08.2026 (904 Queries, 5.183 Impressionen) plus die Cluster, die im Export noch gar nicht auftauchen, weil es dafür keine Seite gibt.
|
||||
|
||||
**Vorab zur Priorisierung.** Die Impressionszahlen unten sind klein — das ist normal für eine junge Domain und kein Grund, ein Thema zu verwerfen. Aussagekräftiger ist die Kombination aus Position und Wettbewerbsdruck: Ein Thema auf Position 30 mit 20 Impressionen ist näher an Klicks als eins auf Position 70 mit 80.
|
||||
|
||||
---
|
||||
|
||||
## Vor dem ersten neuen Post: die Kannibalisierungsregel
|
||||
|
||||
Du hast jetzt zwei Seitentypen, die sich überschneiden können. Damit sie sich nicht gegenseitig schlagen, gilt eine feste Trennung:
|
||||
|
||||
| Typ | URL-Muster | Intent | Titel-Muster |
|
||||
|---|---|---|---|
|
||||
| SEO-Landingpage | `/plant-identifier-app` | kommerziell — „womit mache ich das" | App, Tool, best, free, download |
|
||||
| Blogpost | `/blog/<slug>` | informational — „wie geht das, warum passiert das" | how, why, when, which, guide |
|
||||
|
||||
**Regel:** Ein Blogpost bekommt nie ein Keyword als Haupttarget, das schon eine Landingpage hat. Wenn du merkst, dass ein Blogtitel „best plant identification app" heißen will, gehört der Inhalt auf die Landingpage, nicht in den Blog.
|
||||
|
||||
Jeder Post unten verlinkt intern auf genau eine Landingpage. Diese Verlinkung ist Pflicht, nicht Kür — sie ist das Signal, welche Seite kommerziell ranken soll.
|
||||
|
||||
---
|
||||
|
||||
## Cluster A — Symptome und Diagnose (11 Posts)
|
||||
|
||||
Dein stärkster Cluster, weil Symptomsuchen konkret, dringlich und wenig umkämpft sind. `/pflanzen-diagnose` und `/blog/plant-health-diagnosis-guide` sind die Hubs, alle folgenden Posts verlinken dorthin.
|
||||
|
||||
| # | Titel | Haupt-Keyword | Imp. | Pos. | Sprache |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Why Are My Plant Leaves Turning Yellow? 6 Causes | why are plant leaves yellow | – | – | EN |
|
||||
| 2 | Brown Leaf Tips: Water Quality or Humidity? | brown leaf tips houseplant | – | – | EN |
|
||||
| 3 | Blattkrankheiten bestimmen: Flecken richtig lesen | blattkrankheiten bestimmen | 6 | 68 | DE |
|
||||
| 4 | Leaf Disease Identification: Spot Patterns Explained | leaf disease identification | 8 | 80 | EN |
|
||||
| 5 | Fungus Gnats in Houseplants: Why They Appear | fungus gnats plant soil | – | – | EN |
|
||||
| 6 | Spider Mites on Indoor Plants: Early Signs | spider mites houseplant | – | – | EN |
|
||||
| 7 | Mealybugs and Scale: The Wax Problem | mealybugs houseplant | – | – | EN |
|
||||
| 8 | Thrips: The Pest You See Last | thrips houseplant | – | – | EN |
|
||||
| 9 | Overwatered or Underwatered? Same Droop, Opposite Fix | overwatered vs underwatered | – | – | EN |
|
||||
| 10 | Root Rot: Catching It Before It Is Too Late | root rot houseplant | – | – | EN |
|
||||
| 11 | Powdery Mildew Indoors: Why It Comes Back | powdery mildew houseplant | – | – | EN |
|
||||
|
||||
Zu 5–8: Der Schädlings-Cluster hat in deinem Export **null Impressionen**. Das heißt nicht, dass niemand sucht — es heißt, dass du für kein Schädlings-Keyword auch nur in den Top 100 auftauchst. Das ist eine echte Lücke, kein fehlendes Volumen.
|
||||
|
||||
---
|
||||
|
||||
## Cluster B — Bestimmung nach Pflanzengruppe (7 Posts)
|
||||
|
||||
Deine Bestimmungsseiten ranken breit, aber flach (Position 45–65). Spezifische Gruppen sind leichter zu gewinnen als das generische „Pflanzen bestimmen".
|
||||
|
||||
| # | Titel | Haupt-Keyword | Imp. | Pos. | Sprache |
|
||||
|---|---|---|---|---|---|
|
||||
| 12 | Identify a Plant by Its Leaf Alone | identify plant by leaf picture | 18 | 68 | EN |
|
||||
| 13 | Succulent Identification: Rosette, Leaf and Growth Form | succulent identification app | 7 | 21 | EN |
|
||||
| 14 | Cactus Identification: Ribs, Spines and Areoles | cactus identification app | 8 | 36 | EN |
|
||||
| 15 | Identify Shrubs by Picture: What to Photograph | identify shrubs by picture | 28 | 71 | EN |
|
||||
| 16 | Bäume per Foto bestimmen: Rinde, Blatt, Knospe | baumerkennung durch foto | 3 | 32 | DE |
|
||||
| 17 | Gartenpflanzen bestimmen: Beet, Balkon, Rabatte | gartenpflanzen bestimmen | 4 | 62 | DE |
|
||||
| 18 | Großblättrige Zimmerpflanzen richtig zuordnen | großblättrige zimmerpflanzen bestimmen | 2 | 63 | DE |
|
||||
|
||||
Nr. 13 steht auf Position 21 — die beste Ausgangslage im ganzen Set und der Post, den ich als ersten aus diesem Cluster schreiben würde.
|
||||
|
||||
---
|
||||
|
||||
## Cluster C — Pflege und Standort (6 Posts)
|
||||
|
||||
Im Export praktisch unsichtbar, weil du dafür kaum Seiten hast. Gleichzeitig der Cluster mit der höchsten Bindung: Wer Pflegefragen stellt, besitzt schon Pflanzen.
|
||||
|
||||
| # | Titel | Haupt-Keyword | Sprache |
|
||||
|---|---|---|---|
|
||||
| 19 | How Much Light Does Your Houseplant Actually Need? | houseplant light requirements | EN |
|
||||
| 20 | Repotting: When, and When Not To | when to repot houseplant | EN |
|
||||
| 21 | Zimmerpflanzen düngen: Wann, wie viel, wann gar nicht | zimmerpflanzen düngen | DE |
|
||||
| 22 | Winter Care: What Changes and What Does Not | houseplant winter care | EN |
|
||||
| 23 | Humidity for Houseplants: What Actually Works | houseplant humidity | EN |
|
||||
| 24 | Urlaub: Pflanzen zwei Wochen allein lassen | pflanzen urlaub gießen | DE |
|
||||
|
||||
Zu 23: Hier kannst du etwas tun, was fast niemand macht — sauber schreiben, dass Sprühen die Luftfeuchte kaum messbar erhöht. Ratgeber schreiben das Gegenteil voneinander ab. Ein belegter Widerspruch ist zitierfähiger als eine weitere Bestätigung.
|
||||
|
||||
---
|
||||
|
||||
## Cluster D — Sicherheit und Haushalt (3 Posts)
|
||||
|
||||
| # | Titel | Haupt-Keyword | Sprache |
|
||||
|---|---|---|---|
|
||||
| 25 | Toxic Houseplants for Cats: The Common Ones | toxic houseplants cats | EN |
|
||||
| 26 | Giftige Zimmerpflanzen für Kinder erkennen | giftige zimmerpflanzen kinder | DE |
|
||||
| 27 | Pet-Safe Houseplants That Are Actually Easy | pet safe houseplants | EN |
|
||||
|
||||
Dieser Cluster ist der stärkste Kandidat für Zitierungen durch AI-Antwortsysteme, weil Sicherheitsfragen dort bevorzugt mit Quellen beantwortet werden. Bedingung: ASPCA als Beleg verlinken und nirgends behaupten, eine App könne Giftigkeit sicher klären.
|
||||
|
||||
---
|
||||
|
||||
## Cluster E — Vergleiche und Methodik (3 Posts)
|
||||
|
||||
| # | Titel | Haupt-Keyword | Sprache |
|
||||
|---|---|---|---|
|
||||
| 28 | We Tested 50 Plants Across 4 ID Apps: Results | plant identification app accuracy | EN |
|
||||
| 29 | Google Lens vs. dedizierte Pflanzen-Apps | google lens pflanzen erkennen | DE |
|
||||
| 30 | Why Plant ID Apps Disagree With Each Other | plant identification apps different results | EN |
|
||||
|
||||
**Nr. 28 ist der wichtigste Post der ganzen Liste.** Das ist der Benchmark aus Phase 2 — der einzige Beitrag hier, für den du Daten hättest, die kein Wettbewerber kopieren kann. Alle anderen 29 sind gut recherchierte Synthese; dieser wäre eine Primärquelle. Er ist auch der einzige, der sich natürlich verlinken lässt.
|
||||
|
||||
Bedingung bleibt: Die Fälle veröffentlichen, in denen GreenLens verliert.
|
||||
|
||||
---
|
||||
|
||||
## Syndication: so bleibt greenlenspro.com die Quelle
|
||||
|
||||
Du willst die Posts zweitveröffentlichen. Das funktioniert, wenn die Reihenfolge stimmt.
|
||||
|
||||
**1. Immer zuerst auf greenlenspro.com veröffentlichen.** Warte, bis die URL in der Search Console als indexiert erscheint (meist 1–7 Tage). Erst danach syndizieren. Wird die Kopie zuerst gefunden, kann sie als Original gewertet werden — der Canonical repariert das nicht zuverlässig.
|
||||
|
||||
**2. Auf der Zweitplattform `rel=canonical` auf deine URL setzen.** Das ist bereits vorbereitet: Deine Blogposts haben jetzt einen selbstreferenzierenden Canonical, und Next.js löst ihn über `metadataBase` zur absoluten URL auf.
|
||||
|
||||
| Plattform | Canonical-Unterstützung | Wo |
|
||||
|---|---|---|
|
||||
| Medium | ja, zuverlässig | Import-Tool nutzen, setzt Canonical automatisch |
|
||||
| DEV.to | ja, zuverlässig | Frontmatter `canonical_url:` |
|
||||
| Hashnode | ja | Post-Einstellungen, „Original article URL" |
|
||||
| LinkedIn Articles | **nein** | nur Auszug posten und verlinken |
|
||||
| Substack | **nein** | nur Auszug posten und verlinken |
|
||||
|
||||
**3. Wo kein Canonical möglich ist, nicht den Volltext posten.** Bei LinkedIn und Substack die ersten zwei Absätze plus Link. Ein vollständiger Duplikat ohne Canonical konkurriert mit deinem Original.
|
||||
|
||||
**4. Nicht alles überall.** Die Dev-Plattformen sind für deinen Pflanzen-Content das falsche Publikum — dort gehören die Beiträge über `greenlens-python`, `greenlenspro-cli` und die VS-Code-Extension hin, wie vorhin besprochen. Ein Gießplan-Artikel auf DEV.to bringt Entwickler, die keine Pflanzen-App kaufen.
|
||||
|
||||
---
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
| Reihenfolge | Posts | Warum |
|
||||
|---|---|---|
|
||||
| 1 | 13, 14 | beste Positionen (21 und 36), schnellster Gewinn |
|
||||
| 2 | 1, 2, 9 | Symptom-Longtail, stützt den Diagnose-Hub |
|
||||
| 3 | 5–8 | Schädlingscluster, aktuell komplett unbesetzt |
|
||||
| 4 | 28 | Benchmark, sobald die Daten stehen |
|
||||
| 5 | 25–27 | Sicherheitscluster, hohe Zitierchance |
|
||||
| 6 | Rest | nach Kapazität |
|
||||
|
||||
Zwei Posts pro Woche ist realistischer als sechs — bei diesem Anspruch an Tiefe schlägt Frequenz sonst in Dünne um, und dünne Seiten sind laut `siteFocusScore` schlechter als keine Seiten.
|
||||
222
blogpost-plan-2026-08.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# Blogpost-Plan GreenLens – 3 neue Ratgeberseiten (Stand 03.08.2026)
|
||||
|
||||
**Format:** Keine neue Blog-Infrastruktur. Alle drei Inhalte laufen als `SeoPageProfile` in `lib/seoPages.ts` – identisch zu den bestehenden Symptomseiten (`braune-blattspitzen`, `wurzelfaeule-erkennen`). Das liefert FAQ- und HowTo-Schema out of the box, was für AI Overviews entscheidend ist.
|
||||
|
||||
**Umsetzung pro Seite (3 Schritte, siehe `lib/seoPageFactory.tsx`):**
|
||||
|
||||
1. Profil in `lib/seoPages.ts` anlegen
|
||||
2. Slug in `germanSeoPageSlugs` eintragen (→ Sitemap + `/de/<slug>`-Redirect automatisch)
|
||||
3. `app/<slug>/page.tsx` mit `buildSeoPageMetadata` + `createSeoPage` anlegen
|
||||
4. `relatedLinks` bestehender Profile auf die neue Seite zeigen lassen
|
||||
|
||||
---
|
||||
|
||||
## Post 1 — Gießerinnerung & Gießplan
|
||||
|
||||
**Priorität: 1** (beste Ausgangspositionen, schnellster Ranking-Gewinn)
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| `slug` | `giessplan-zimmerpflanzen` |
|
||||
| `locale` | `de` |
|
||||
| `templateIntent` | `watering` |
|
||||
| `canonical` | `/giessplan-zimmerpflanzen` |
|
||||
| `includeAppSchema` | `true` |
|
||||
|
||||
**metaTitle:** `Gießplan für Zimmerpflanzen: Intervalle nach Pflanzenart | GreenLens`
|
||||
|
||||
**metaDescription:** `Wie oft welche Pflanze gießen? Gießintervalle für 20 Zimmerpflanzen, Fingerprobe statt Kalender – und wann eine Erinnerungs-App wirklich hilft.`
|
||||
|
||||
**h1:** `Gießplan für Zimmerpflanzen: Wie oft welche Pflanze wirklich Wasser braucht`
|
||||
|
||||
**tagline:** `Jeden Sonntag alle Pflanzen gießen klingt ordentlich. Für die Hälfte davon ist es zu viel.`
|
||||
|
||||
**directAnswer** (die ersten 60 Wörter, AI-Overview-Ziel):
|
||||
> Es gibt kein festes Gießintervall, das für alle Zimmerpflanzen funktioniert. Sukkulenten brauchen alle 2–4 Wochen Wasser, ein Farn alle 2–3 Tage. Der zuverlässigste Test ist die Fingerprobe: Erde bis zum zweiten Fingerglied prüfen – fühlt sie sich trocken an, wird gegossen. Eine Erinnerungs-App ersetzt diese Prüfung nicht, sondern erinnert daran, sie zu machen.
|
||||
|
||||
**Ziel-Keywords**
|
||||
|
||||
| Keyword | Imp. | akt. Pos. |
|
||||
|---|---|---|
|
||||
| pflanzen gießen erinnerung | 235 | 27 |
|
||||
| app erinnerung pflanzen gießen | 71 | 18 |
|
||||
| gießerinnerung app | 14 | 13 |
|
||||
| pflanzen gießen app | 12 | 31 |
|
||||
| pflegekarte pflanzen | 12 | 16 |
|
||||
| gieß erinnerung app | 10 | 17 |
|
||||
| pflanzenpflege erinnerung | 4 | 10 |
|
||||
| gieß erinnerung | 3 | 8 |
|
||||
|
||||
**contentSections**
|
||||
|
||||
1. *Selbst prüfen* — „Die Fingerprobe schlägt jeden Kalender": Warum feste Intervalle scheitern (Jahreszeit, Heizung, Topfgröße, Substrat). Bullets mit den 4 Einflussfaktoren.
|
||||
2. *Nach Pflanzenart* — „Gießintervalle für 20 häufige Zimmerpflanzen": **Tabelle** Pflanze / Sommer / Winter / Erkennungsmerkmal Durst. Monstera, Ficus, Sukkulenten, Orchidee, Bogenhanf, Efeutute, Grünlilie, Farn, Kaktus, Palme, Gummibaum, Einblatt, Aloe, Philodendron, Kalanchoe, Yucca, Dieffenbachie, Zamioculcas, Alocasia, Calathea.
|
||||
3. *Zum Ausdrucken* — „Pflegekarte anlegen": Was auf eine Pflegekarte gehört (Art, Standort, Intervall Sommer/Winter, letztes Umtopfen, Düngerhythmus). → deckt `pflegekarte pflanzen` (Pos. 16) mit ab.
|
||||
4. *Erinnerung einrichten* — Handy-Wecker vs. Kalender vs. App vs. Feuchtigkeitssensor, ehrlich verglichen inkl. Nachteilen.
|
||||
|
||||
**featureTable** — `alternativeLabel: 'Kalender-Erinnerung'`
|
||||
Zeilen: Intervall pro Pflanze · Anpassung an Jahreszeit · Reaktion auf Standortwechsel · Was tun, wenn übergossen · Historie
|
||||
|
||||
**howToName:** `Gießbedarf einer Zimmerpflanze bestimmen`
|
||||
**howToSteps:** Fingerprobe (2 cm tief) → Topfgewicht vergleichen → Blattzustand prüfen (schlaff vs. weich) → Abzugsloch kontrollieren → Intervall notieren
|
||||
|
||||
**faqs** (mind. 6, jeweils Frage in Suchsprache)
|
||||
- Wie oft muss ich Zimmerpflanzen gießen?
|
||||
- Woran erkenne ich, dass eine Pflanze zu viel Wasser bekommen hat?
|
||||
- Muss ich im Winter weniger gießen?
|
||||
- Welche App erinnert ans Pflanzengießen?
|
||||
- Funktioniert eine Gießerinnerung auch ohne Internet?
|
||||
- Wie oft gießt man Sukkulenten?
|
||||
|
||||
**relatedLinks:** `/giess-erinnerung-app`, `/ueberwaessert-oder-zu-trocken`, `/pflanzen-pflege-app`, `/wurzelfaeule-erkennen`
|
||||
**Rückverlinkung setzen von:** `/giess-erinnerung-app`, `/pflanzen-pflege-app`, `/ueberwaessert-oder-zu-trocken`
|
||||
|
||||
---
|
||||
|
||||
## Post 2 — Pflanzendiagnose: Krankheiten & Schädlinge mit Bildern
|
||||
|
||||
**Priorität: 2** (größtes Volumen, aber Positionen 30–86 → längster Weg)
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| `slug` | `pflanzen-diagnose` |
|
||||
| `locale` | `de` |
|
||||
| `templateIntent` | `diagnosis` |
|
||||
| `canonical` | `/pflanzen-diagnose` |
|
||||
|
||||
**metaTitle:** `Pflanzendiagnose: Krankheiten & Schädlinge mit Bildern erkennen | GreenLens`
|
||||
|
||||
**metaDescription:** `Gelbe Blätter, weiße Beläge, kleine Fliegen? 15 häufige Pflanzenkrankheiten und Schädlinge mit Bildern, Symptomtabelle und Behandlung – kostenlos.`
|
||||
|
||||
**h1:** `Pflanzendiagnose: 15 Krankheiten und Schädlinge an Symptomen erkennen`
|
||||
|
||||
**tagline:** `Die meisten Pflanzen sterben nicht an der Krankheit, sondern an der falschen Behandlung.`
|
||||
|
||||
**directAnswer:**
|
||||
> Eine Pflanzendiagnose beginnt beim sichtbaren Symptom, nicht bei der Krankheit. Gelbe Blätter, braune Ränder, weiße Beläge, klebrige Stellen und kleine Fliegen führen jeweils zu einer anderen Ursachengruppe. Die wichtigste Unterscheidung ist Pflegefehler (Wasser, Licht, Nährstoffe) gegen Erreger (Pilz, Bakterien, Schädlinge) – die Maßnahmen sind gegensätzlich.
|
||||
|
||||
**Ziel-Keywords**
|
||||
|
||||
| Keyword | Imp. | akt. Pos. |
|
||||
|---|---|---|
|
||||
| pflanzen diagnose | 79 | 42 |
|
||||
| pflanzenkrankheiten erkennen | 38 | 30 |
|
||||
| pflanzenschädlinge diagnose | 33 | 86 |
|
||||
| pflanzenkrankheiten erkennen und behandeln | 24 | 55 |
|
||||
| pflanzenkrankheiten bilder | 14 | 41 |
|
||||
| pflanzen krankheiten bilder | 11 | 65 |
|
||||
| pflanzenkrankheiten bestimmen | 11 | 33 |
|
||||
| diagnose von pflanzenkrankheiten | 7 | 34 |
|
||||
| pflanzenkrankheiten erkennen online | 7 | 38 |
|
||||
| blattkrankheiten bestimmen | 6 | 68 |
|
||||
|
||||
> **Wichtig:** `…bilder` sind eigene Keywords mit zusammen 25+ Impressionen. Ohne echte Bildergalerie mit sauberen Alt-Texten und `heroImage` ist dieser Post nicht konkurrenzfähig. Bilder sind hier der Arbeitsaufwand, nicht der Text.
|
||||
|
||||
**contentSections**
|
||||
|
||||
1. *Erster Schritt* — „Pflegefehler oder Erreger?": Die Weiche, an der sich alles entscheidet. Bullets mit den Unterscheidungsmerkmalen.
|
||||
2. *Symptomtabelle* — Vollbreite **Tabelle**: Symptom / wahrscheinlichste Ursache / Gegenprobe / Sofortmaßnahme. Zeilen: gelbe Blätter · braune Spitzen · braune Flecken mit Hof · weißer mehliger Belag · weiße Watte in Blattachseln · klebriger Film · feine Gespinste · kleine schwarze Fliegen · schwarze Punkte Blattunterseite · Blattfall · weiche schwarze Stängelbasis · verkrüppelter Neuaustrieb · silbrige Sprenkel · Löcher im Blatt · Pflanze wächst nicht.
|
||||
3. *Schädlinge* — Trauermücken, Spinnmilben, Blattläuse, Wollläuse, Thripse: je Erkennungsmerkmal + Behandlung. → deckt `pflanzenschädlinge diagnose` (Pos. 86!) ab.
|
||||
4. *Pilzkrankheiten* — Mehltau, Grauschimmel, Blattfleckenkrankheit, Wurzelfäule.
|
||||
5. *Wann es zu spät ist* — ehrlicher Abschnitt: woran man erkennt, dass eine Pflanze nicht mehr zu retten ist. Baut Vertrauen und wird in AI-Antworten gern zitiert.
|
||||
|
||||
**howToName:** `Pflanzenkrankheit an Symptomen bestimmen`
|
||||
**howToSteps:** Symptom lokalisieren (Blatt/Stängel/Wurzel) → betroffene Stelle anfassen (trocken/weich) → Blattunterseite mit Lupe prüfen → Erde und Wurzelbereich kontrollieren → Ausbreitung über 3 Tage beobachten
|
||||
|
||||
**faqs**
|
||||
- Wie erkenne ich, welche Krankheit meine Pflanze hat?
|
||||
- Kann man Pflanzenkrankheiten per Foto bestimmen?
|
||||
- Was sind die häufigsten Pflanzenkrankheiten bei Zimmerpflanzen?
|
||||
- Woher kommen die kleinen Fliegen in der Blumenerde?
|
||||
- Ist eine kranke Pflanze für andere Pflanzen ansteckend?
|
||||
- Gibt es eine kostenlose App für Pflanzenkrankheiten?
|
||||
|
||||
**relatedLinks:** `/pflanzen-krankheiten-erkennen`, `/gelbe-blaetter-zimmerpflanze`, `/braune-blattspitzen`, `/trauermuecken-blumenerde`, `/wurzelfaeule-erkennen`
|
||||
**Rückverlinkung setzen von:** allen fünf Symptomseiten (die ranken derzeit auf Pos. 60–80 und brauchen den internen Link-Fluss)
|
||||
|
||||
---
|
||||
|
||||
## Post 3 — „Welche Pflanze ist das?"
|
||||
|
||||
**Priorität: 3** (kleinstes Volumen, aber komplett unbesetzt und ideal für AI Overviews)
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| `slug` | `welche-pflanze-ist-das` |
|
||||
| `locale` | `de` |
|
||||
| `templateIntent` | `identification` |
|
||||
| `canonical` | `/welche-pflanze-ist-das` |
|
||||
|
||||
**metaTitle:** `Welche Pflanze ist das? Pflanze per Foto bestimmen – 6 Wege | GreenLens`
|
||||
|
||||
**metaDescription:** `Unbekannte Pflanze gefunden? So bestimmst du sie per Foto – Apps, Google Lens und Bestimmungsmerkmale im Vergleich. Was wirklich funktioniert.`
|
||||
|
||||
**h1:** `Welche Pflanze ist das? So bestimmst du sie per Foto`
|
||||
|
||||
**tagline:** `Du hast ein Foto. Du willst einen Namen. Der Weg dahin ist kürzer, als die meisten Ratgeber tun.`
|
||||
|
||||
**directAnswer:**
|
||||
> Eine unbekannte Pflanze bestimmst du am schnellsten per Foto-App: Blatt, Blüte oder ganze Pflanze bei Tageslicht vor ruhigem Hintergrund fotografieren, App liefert den Artnamen. Für Zimmerpflanzen und häufige Gartenpflanzen liegen Foto-Apps meist richtig. Bei seltenen oder regionalen Arten lohnt zusätzlich eine Bestimmung über Blattform, Blattstellung und Blütenaufbau.
|
||||
|
||||
**Ziel-Keywords**
|
||||
|
||||
| Keyword | Imp. | akt. Pos. |
|
||||
|---|---|---|
|
||||
| pflanzensuche | 71 | 60 |
|
||||
| welche pflanze ist das | 38 | 61 |
|
||||
| pflanzen suche | 29 | 66 |
|
||||
| pflanzensuchmaschine | 14 | 62 |
|
||||
| was ist das für eine pflanze foto | 11 | 51 |
|
||||
| pflanzensuche bild / mit foto / per bild | 16 | 24–36 |
|
||||
| welche pflanze ist das foto | 6 | 47 |
|
||||
| welche pflanze ist das google | 6 | 21 |
|
||||
| was für eine pflanze ist das | 7 | 70 |
|
||||
| zimmerpflanzen namen mit bild | 3 | 41 |
|
||||
|
||||
**contentSections**
|
||||
|
||||
1. *In 30 Sekunden* — „Das richtige Foto machen": Tageslicht, ruhiger Hintergrund, Blatt UND Blüte, Nahaufnahme statt Übersicht. Der häufigste Fehler ist das Ganzkörperfoto.
|
||||
2. *6 Wege im Vergleich* — **Tabelle**: Foto-App / Google Lens / Bestimmungsschlüssel / Botanik-Forum / Gärtnerei fragen / Reverse Image Search. Spalten: Wie schnell / Wie genau / Wofür geeignet / Kosten. → fängt `pflanzensuchmaschine` und `pflanzensuche bild`.
|
||||
3. *Ohne App* — „An welchen Merkmalen du eine Pflanze selbst erkennst": Blattform, Blattrand, Blattstellung (wechsel-/gegenständig/quirlig), Blütenaufbau, Wuchsform, Milchsaft. Mit Bildern.
|
||||
4. *Zimmerpflanzen* — „Die 15 häufigsten Zimmerpflanzen mit Bild und Namen": Bildraster mit Namen. → deckt `zimmerpflanzen namen mit bild` ab und ist ein starker Verweilzeit-Baustein.
|
||||
5. *Vorsicht giftig* — Kurzabschnitt: Bei unbekannten Pflanzen mit Kindern/Haustieren erst Giftigkeit prüfen. Wird in AI-Antworten überdurchschnittlich zitiert.
|
||||
|
||||
**featureTable** — `alternativeLabel: 'Google Lens'`
|
||||
Zeilen: Artname · Pflegeinfos danach · Gesundheitscheck · Offline nutzbar · Sammlung speichern
|
||||
|
||||
**howToName:** `Unbekannte Pflanze per Foto bestimmen`
|
||||
**howToSteps:** Bei Tageslicht fotografieren → einzelnes Blatt formatfüllend aufnehmen → falls vorhanden Blüte separat → in App hochladen → Ergebnis über zweites Merkmal gegenprüfen
|
||||
|
||||
**faqs**
|
||||
- Wie finde ich heraus, welche Pflanze ich habe?
|
||||
- Kann Google eine Pflanze anhand eines Fotos erkennen?
|
||||
- Welche App erkennt Pflanzen am besten?
|
||||
- Wie fotografiere ich eine Pflanze richtig zur Bestimmung?
|
||||
- Kann man Pflanzen auch ohne App bestimmen?
|
||||
- Was ist das für eine Pflanze – gibt es eine kostenlose Suche?
|
||||
|
||||
**relatedLinks:** `/pflanzen-bestimmen`, `/zimmerpflanzen-bestimmen`, `/vs/google-lens`, `/pflanzen-erkennen-kostenlos`
|
||||
**Rückverlinkung setzen von:** `/pflanzen-bestimmen`, `/zimmerpflanzen-bestimmen`, `/vs/google-lens`, `/blumen-scanner`
|
||||
|
||||
---
|
||||
|
||||
## Umsetzungsreihenfolge
|
||||
|
||||
| # | Aufgabe | Aufwand | Erwarteter Effekt |
|
||||
|---|---|---|---|
|
||||
| 0 | Titles/Metas der Top-10-Seiten überarbeiten (`blumen-scanner` zuerst) | 0,5 Tag | Sofort, größter Hebel |
|
||||
| 1 | Post 1 Gießplan | 1 Tag Text | Pos. 27 → Top 10 realistisch |
|
||||
| 2 | Post 3 Welche Pflanze ist das | 1 Tag Text + Bildraster | Neues Cluster, AI-Overview-Chance |
|
||||
| 3 | Post 2 Pflanzendiagnose | 2 Tage + Bildbeschaffung | Größtes Volumen, längster Weg |
|
||||
| 4 | Interne Verlinkung aller `relatedLinks` beidseitig setzen | 0,5 Tag | Hebt die schwachen Symptomseiten mit |
|
||||
| 5 | `app/sitemap.ts` prüfen (DE läuft automatisch über `germanSeoPageSlugs`) | 15 Min | – |
|
||||
|
||||
**Gemeinsame Regeln für alle drei Posts**
|
||||
|
||||
- `directAnswer` beantwortet die Titelfrage in den ersten 60 Wörtern – das ist der Baustein, den AI Overviews ziehen.
|
||||
- Mindestens 6 FAQs, Fragen wörtlich in Suchsprache (nicht „Bewässerungsintervalle", sondern „Wie oft muss ich gießen?").
|
||||
- `howToSteps` immer setzen: liefert HowTo-Schema, das für „Wie erkenne ich …"-Fragen bevorzugt wird.
|
||||
- `lastUpdated` / `lastUpdatedIso` auf `August 2026` / `2026-08-XX`.
|
||||
- Eigenes `heroImage` pro Seite mit beschreibendem `heroImageAlt` – nicht `/hero.jpg` wiederverwenden.
|
||||
- `notBestIf` ehrlich ausfüllen. Die bestehenden Seiten machen das gut, und es ist ein Grund, warum sie zitiert werden.
|
||||
- Der `offer`-Block bleibt am Seitenende und kurz – Informationsteil nicht verwässern.
|
||||
114
components/OnboardingQuestion.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useColors } from '../constants/Colors';
|
||||
|
||||
type ColorsType = ReturnType<typeof useColors>;
|
||||
|
||||
export type QuestionOption = { id: string; emoji: string; label: string; subtitle?: string };
|
||||
|
||||
type Props = {
|
||||
colors: ColorsType;
|
||||
isDarkMode: boolean;
|
||||
step: number; // 1-based
|
||||
totalSteps: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
options: QuestionOption[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onContinue: () => void;
|
||||
onBack?: () => void;
|
||||
continueLabel: string;
|
||||
skipLabel?: string;
|
||||
onSkip?: () => void;
|
||||
};
|
||||
|
||||
export function OnboardingQuestion({
|
||||
colors, isDarkMode, step, totalSteps, title, subtitle, options,
|
||||
selectedId, onSelect, onContinue, onBack, continueLabel, skipLabel, onSkip,
|
||||
}: Props) {
|
||||
return (
|
||||
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]} edges={['top', 'left', 'right', 'bottom']}>
|
||||
<View style={styles.topBar}>
|
||||
{onBack ? (
|
||||
<TouchableOpacity onPress={onBack} style={[styles.backBtn, { backgroundColor: colors.surface }]}>
|
||||
<Ionicons name="arrow-back" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
) : <View style={styles.backBtn} />}
|
||||
<View style={[styles.progressTrack, { backgroundColor: colors.primarySoft }]}>
|
||||
<View style={[styles.progressFill, { backgroundColor: colors.primary, width: `${Math.round((step / totalSteps) * 100)}%` }]} />
|
||||
</View>
|
||||
<View style={styles.backBtn} />
|
||||
</View>
|
||||
{/* Scrollbar statt fixem Block: bei grosser Systemschriftgroesse wuchsen
|
||||
Titel und Karten frueher ueber den Footer und haben den CTA verdeckt. */}
|
||||
<ScrollView
|
||||
style={styles.optionsScroll}
|
||||
contentContainerStyle={styles.options}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text style={[styles.title, { color: colors.text }]} maxFontSizeMultiplier={1.25}>{title}</Text>
|
||||
<Text style={[styles.subtitle, { color: colors.textSecondary }]} maxFontSizeMultiplier={1.3}>{subtitle}</Text>
|
||||
{options.map((option) => {
|
||||
const active = selectedId === option.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.id}
|
||||
onPress={() => onSelect(option.id)}
|
||||
activeOpacity={0.85}
|
||||
style={[styles.card, {
|
||||
backgroundColor: active ? colors.primarySoft : colors.surface,
|
||||
borderColor: active ? colors.primary : 'transparent',
|
||||
}]}
|
||||
>
|
||||
<Text style={styles.emoji}>{option.emoji}</Text>
|
||||
<View style={styles.cardCopy}>
|
||||
<Text style={[styles.cardLabel, { color: active ? colors.primary : colors.text }]} maxFontSizeMultiplier={1.3}>{option.label}</Text>
|
||||
{option.subtitle ? <Text style={[styles.cardSubtitle, { color: colors.textMuted }]} maxFontSizeMultiplier={1.3}>{option.subtitle}</Text> : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
<View style={styles.footer}>
|
||||
{skipLabel && onSkip ? (
|
||||
<TouchableOpacity onPress={onSkip} style={styles.skipBtn}>
|
||||
<Text style={[styles.skipText, { color: colors.textMuted }]}>{skipLabel}</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
onPress={onContinue}
|
||||
disabled={!selectedId}
|
||||
activeOpacity={0.86}
|
||||
style={[styles.cta, { backgroundColor: selectedId ? colors.primary : colors.surfaceMuted }]}
|
||||
>
|
||||
<Text style={[styles.ctaText, { color: selectedId ? colors.onPrimary : colors.textMuted }]} maxFontSizeMultiplier={1.2}>{continueLabel}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, paddingHorizontal: 22 },
|
||||
topBar: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingVertical: 10 },
|
||||
backBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
|
||||
progressTrack: { flex: 1, height: 6, borderRadius: 3, overflow: 'hidden' },
|
||||
progressFill: { height: 6, borderRadius: 3 },
|
||||
title: { fontSize: 30, lineHeight: 35, fontWeight: '900', textAlign: 'center', marginTop: 18, marginBottom: 8 },
|
||||
subtitle: { fontSize: 15, lineHeight: 20, textAlign: 'center', marginBottom: 22 },
|
||||
optionsScroll: { flex: 1 },
|
||||
options: { gap: 12, paddingBottom: 12 },
|
||||
card: { flexDirection: 'row', alignItems: 'center', gap: 14, borderRadius: 16, borderWidth: 2, paddingHorizontal: 16, paddingVertical: 18 },
|
||||
emoji: { fontSize: 26 },
|
||||
cardCopy: { flex: 1, gap: 2 },
|
||||
cardLabel: { fontSize: 17, fontWeight: '800' },
|
||||
cardSubtitle: { fontSize: 12.5, lineHeight: 16 },
|
||||
footer: { gap: 8, paddingBottom: 6 },
|
||||
skipBtn: { alignItems: 'center', paddingVertical: 6 },
|
||||
skipText: { fontSize: 14, fontWeight: '700' },
|
||||
cta: { minHeight: 56, paddingVertical: 16, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
|
||||
ctaText: { fontSize: 17, fontWeight: '800' },
|
||||
});
|
||||
174
components/OutOfCreditsSheet.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { Image, Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Language } from '../types';
|
||||
import { useColors } from '../constants/Colors';
|
||||
|
||||
type ColorsType = ReturnType<typeof useColors>;
|
||||
|
||||
const getCopy = (language: Language, isPro: boolean) => {
|
||||
// Framing: der Nutzer wollte gerade etwas tun und wurde gestoppt. Der CTA
|
||||
// bringt die angefangene Aufgabe zu Ende - das Abo ist das Mittel, nicht das
|
||||
// Angebot. Kein "unbegrenzt": das Pro-Kontingent ist 100 Scans pro Monat.
|
||||
if (language === 'de') {
|
||||
return {
|
||||
title: 'Diese Pflanze schauen wir uns noch an.',
|
||||
body: (date: string) => (isPro
|
||||
? `Deine Credits erneuern sich am ${date}. Mit einem Credit-Paket läuft dieser Scan sofort.`
|
||||
: `Deine 3 Gratis-Scans erneuern sich am ${date}. Mit Pro läuft dieser Scan sofort - plus Ursachen-Ranking, Prüfschritte und dein 7-Tage-Plan.`),
|
||||
bodyNoDate: isPro
|
||||
? 'Mit einem Credit-Paket läuft dieser Scan sofort.'
|
||||
: 'Mit Pro läuft dieser Scan sofort - plus Ursachen-Ranking, Prüfschritte und dein 7-Tage-Plan.',
|
||||
cta: isPro ? 'Credits aufladen' : 'Diesen Scan mit Pro starten',
|
||||
topupsLabel: 'Oder einzelne Credits kaufen',
|
||||
later: 'Später',
|
||||
best: 'BESTE WAHL',
|
||||
credits: 'Credits',
|
||||
};
|
||||
}
|
||||
if (language === 'es') {
|
||||
return {
|
||||
title: 'Vamos a mirar esta planta.',
|
||||
body: (date: string) => (isPro
|
||||
? `Tus créditos se renuevan el ${date}. Con un paquete de créditos este escaneo sale ya.`
|
||||
: `Tus 3 escaneos gratis se renuevan el ${date}. Con Pro este escaneo sale ya - más ranking de causas, pasos de revisión y tu plan de 7 días.`),
|
||||
bodyNoDate: isPro
|
||||
? 'Con un paquete de créditos este escaneo sale ya.'
|
||||
: 'Con Pro este escaneo sale ya - más ranking de causas, pasos de revisión y tu plan de 7 días.',
|
||||
cta: isPro ? 'Recargar créditos' : 'Hacer este escaneo con Pro',
|
||||
topupsLabel: 'O compra créditos sueltos',
|
||||
later: 'Más tarde',
|
||||
best: 'MEJOR OPCIÓN',
|
||||
credits: 'créditos',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Let's still look at this plant.",
|
||||
body: (date: string) => (isPro
|
||||
? `Your credits renew on ${date}. With a credit pack this scan runs right now.`
|
||||
: `Your 3 free scans renew on ${date}. With Pro this scan runs right now - plus a cause ranking, things to check and your 7-day plan.`),
|
||||
bodyNoDate: isPro
|
||||
? 'With a credit pack this scan runs right now.'
|
||||
: 'With Pro this scan runs right now - plus a cause ranking, things to check and your 7-day plan.',
|
||||
cta: isPro ? 'Top up credits' : 'Run this scan with Pro',
|
||||
topupsLabel: 'Or buy single credits',
|
||||
later: 'Later',
|
||||
best: 'BEST',
|
||||
credits: 'credits',
|
||||
};
|
||||
};
|
||||
|
||||
const formatRenewalDate = (iso: string | null | undefined, language: Language): string | null => {
|
||||
if (!iso) return null;
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
const locale = language === 'de' ? 'de-DE' : language === 'es' ? 'es-ES' : 'en-US';
|
||||
return date.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
|
||||
};
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
language: Language;
|
||||
colors: ColorsType;
|
||||
isPro?: boolean;
|
||||
renewsAtIso?: string | null;
|
||||
onSeePlans: () => void;
|
||||
onTopup: (productId: 'topup_small' | 'topup_medium' | 'topup_large') => void;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
const TOPUPS = [
|
||||
{ id: 'topup_small' as const, amount: 30, best: false },
|
||||
{ id: 'topup_medium' as const, amount: 100, best: false },
|
||||
{ id: 'topup_large' as const, amount: 250, best: true },
|
||||
];
|
||||
|
||||
export function OutOfCreditsSheet({ visible, language, colors, isPro = false, renewsAtIso, onSeePlans, onTopup, onDismiss }: Props) {
|
||||
const copy = getCopy(language, isPro);
|
||||
const renewalDate = formatRenewalDate(renewsAtIso, language);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="slide" onRequestClose={onDismiss}>
|
||||
<View style={styles.backdrop}>
|
||||
<TouchableOpacity
|
||||
style={styles.backdropTouchable}
|
||||
activeOpacity={1}
|
||||
onPress={onDismiss}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={copy.later}
|
||||
/>
|
||||
<View style={[styles.sheet, { backgroundColor: colors.surface }]} accessibilityViewIsModal>
|
||||
<View style={[styles.handle, { backgroundColor: colors.border }]} />
|
||||
<View style={styles.iconWrap}>
|
||||
<View style={[styles.iconCircle, { backgroundColor: colors.primarySoft, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' }]}>
|
||||
<Image
|
||||
source={require('../assets/icon.png')}
|
||||
style={{ width: 44, height: 44, borderRadius: 10 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.zeroBadge, { backgroundColor: colors.danger }]}>
|
||||
<Text style={styles.zeroBadgeText}>0</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
|
||||
<Text style={[styles.body, { color: colors.textSecondary }]}>
|
||||
{renewalDate ? copy.body(renewalDate) : copy.bodyNoDate}
|
||||
</Text>
|
||||
{!isPro && (
|
||||
<TouchableOpacity style={[styles.cta, { backgroundColor: colors.primary }]} onPress={onSeePlans} activeOpacity={0.86}>
|
||||
<Ionicons name="ribbon-outline" size={19} color={colors.onPrimary} />
|
||||
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.cta}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={[styles.topupsLabel, { color: colors.textMuted }]}>{copy.topupsLabel.toUpperCase()}</Text>
|
||||
<View style={styles.topupRow}>
|
||||
{TOPUPS.map((topup) => (
|
||||
<TouchableOpacity
|
||||
key={topup.id}
|
||||
style={[styles.topupChip, { borderColor: topup.best ? colors.primary : colors.border, backgroundColor: colors.surfaceMuted }]}
|
||||
onPress={() => onTopup(topup.id)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{topup.best && (
|
||||
<View style={[styles.bestBadge, { backgroundColor: colors.primary }]}>
|
||||
<Text style={[styles.bestBadgeText, { color: colors.onPrimary }]}>{copy.best}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={[styles.topupAmount, { color: colors.primary }]}>+{topup.amount}</Text>
|
||||
<Text style={[styles.topupUnit, { color: colors.textSecondary }]}>{copy.credits}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
<TouchableOpacity onPress={onDismiss} style={styles.laterBtn}>
|
||||
<Text style={[styles.laterText, { color: colors.primary }]}>{copy.later}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backdrop: { flex: 1, backgroundColor: 'rgba(10,17,11,0.45)', justifyContent: 'flex-end' },
|
||||
backdropTouchable: { flex: 1 },
|
||||
sheet: { borderTopLeftRadius: 26, borderTopRightRadius: 26, paddingHorizontal: 24, paddingTop: 10, paddingBottom: 34, alignItems: 'center' },
|
||||
handle: { width: 44, height: 5, borderRadius: 3, marginBottom: 18 },
|
||||
iconWrap: { marginBottom: 14 },
|
||||
iconCircle: { width: 76, height: 76, borderRadius: 38, alignItems: 'center', justifyContent: 'center' },
|
||||
zeroBadge: { position: 'absolute', top: -2, right: -4, width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
|
||||
zeroBadgeText: { color: '#fff', fontSize: 13, fontWeight: '900' },
|
||||
title: { fontSize: 24, fontWeight: '900', textAlign: 'center', marginBottom: 8 },
|
||||
body: { fontSize: 15, lineHeight: 21, textAlign: 'center', marginBottom: 18, maxWidth: 320 },
|
||||
cta: { alignSelf: 'stretch', height: 56, borderRadius: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 16 },
|
||||
ctaText: { fontSize: 17, fontWeight: '800' },
|
||||
topupsLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 0.8, marginBottom: 10 },
|
||||
topupRow: { flexDirection: 'row', gap: 10, alignSelf: 'stretch', marginBottom: 14 },
|
||||
topupChip: { flex: 1, borderWidth: 1.5, borderRadius: 14, paddingVertical: 14, alignItems: 'center', overflow: 'hidden' },
|
||||
bestBadge: { position: 'absolute', top: 0, left: 0, right: 0, paddingVertical: 3, alignItems: 'center' },
|
||||
bestBadgeText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.6 },
|
||||
topupAmount: { fontSize: 22, fontWeight: '900', marginTop: 6 },
|
||||
topupUnit: { fontSize: 12, fontWeight: '600' },
|
||||
laterBtn: { paddingVertical: 8 },
|
||||
laterText: { fontSize: 15, fontWeight: '800' },
|
||||
});
|
||||
187
constants/socialProof.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { Language } from '../types';
|
||||
|
||||
/**
|
||||
* EINZIGE QUELLE FUER SOCIAL PROOF IN DER APP.
|
||||
*
|
||||
* Regel: Hier steht ausschliesslich, was im App Store / Play Store nachweisbar ist.
|
||||
* Keine erfundenen Zitate, keine geschaetzten Sterne, keine Rundungen nach oben.
|
||||
* Wenn eine Angabe nicht belegbar ist, wird sie hier entfernt - nicht im UI kaschiert.
|
||||
*
|
||||
* Stand: 27.07.2026 - 6 Bewertungen, Durchschnitt 5,0.
|
||||
* Bei jeder Aktualisierung des Store-Listings hier nachziehen.
|
||||
*/
|
||||
export const APP_STORE_RATING = {
|
||||
value: 5.0,
|
||||
count: 6,
|
||||
/** Datum der letzten Verifikation gegen App Store Connect. */
|
||||
verifiedOn: '2026-07-27',
|
||||
};
|
||||
|
||||
export type Testimonial = {
|
||||
/** Titel der Bewertung im Store. */
|
||||
title: string;
|
||||
/** Wortlaut exakt wie im Store veroeffentlicht (gekuerzt nur mit […]). */
|
||||
quote: string;
|
||||
/** Store-Anzeigename - kein erfundener Klarname. */
|
||||
author: string;
|
||||
stars: 1 | 2 | 3 | 4 | 5;
|
||||
/** Sprache, in der die Bewertung tatsaechlich verfasst wurde. */
|
||||
sourceLanguage: Language;
|
||||
/** Veroeffentlichungsdatum im Store (ISO), fuer die Nachvollziehbarkeit. */
|
||||
publishedOn: string;
|
||||
/**
|
||||
* Wie stark die Bewertung die Positionierung stuetzt (1 = staerkste).
|
||||
* Kriterium: benennt sie Triage ("sagt mir, was los ist und was ich tun soll")
|
||||
* oder nur allgemeine Zufriedenheit? Erstere ueberzeugt, letztere ist Rauschen.
|
||||
*/
|
||||
priority: number;
|
||||
/** Freigegebene Uebersetzungen. Wird im UI als Uebersetzung gekennzeichnet. */
|
||||
translations?: Partial<Record<Language, string>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* ECHTE Store-Bewertungen, Stand 27.07.2026 (6 von 6, alle 5 Sterne).
|
||||
* Wortlaut, Anzeigename und Sternezahl unveraendert uebernommen.
|
||||
*/
|
||||
export const TESTIMONIALS: Testimonial[] = [
|
||||
{
|
||||
// Staerkste Bewertung im gesamten Bestand: beschreibt exakt die Positionierung
|
||||
// (Triage statt Identifikation) in den Worten eines echten Nutzers.
|
||||
title: 'Exactly what I needed',
|
||||
quote:
|
||||
"Finally an app that actually helps me keep my plants alive. Snap a photo and it tells you what's wrong and how to fix it - no more guessing why the leaves are turning yellow. Super easy to use and the care reminders are a nice touch. Highly recommend if you're bad at keeping plants alive like me!",
|
||||
author: 'Chrispetersssss',
|
||||
stars: 5,
|
||||
sourceLanguage: 'en',
|
||||
publishedOn: '2026-07-22',
|
||||
priority: 1,
|
||||
translations: {
|
||||
de: 'Endlich eine App, die mir wirklich hilft, meine Pflanzen am Leben zu halten. Foto machen und sie sagt dir, was nicht stimmt und wie du es behebst - kein Rätselraten mehr, warum die Blätter gelb werden. Sehr einfach zu bedienen, und die Pflege-Erinnerungen sind ein netter Zusatz. Klare Empfehlung, wenn du wie ich schlecht darin bist, Pflanzen am Leben zu halten!',
|
||||
es: 'Por fin una app que de verdad me ayuda a mantener vivas mis plantas. Haces una foto y te dice qué le pasa y cómo arreglarlo: se acabó adivinar por qué se ponen amarillas las hojas. Muy fácil de usar, y los recordatorios de cuidado son un buen extra. Muy recomendable si se te dan mal las plantas, como a mí.',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Direkter Wettbewerbsvergleich - stuetzt Belief 5 (Abo statt Alternativen).
|
||||
title: 'Wow',
|
||||
quote:
|
||||
'Die Pflanzenanalyse toppt echt jede andere App die ich bisher ausprobiert habe! Der Hammer!',
|
||||
author: 'warumistjedernamevergeben',
|
||||
stars: 5,
|
||||
sourceLanguage: 'de',
|
||||
publishedOn: '2026-04-12',
|
||||
priority: 2,
|
||||
translations: {
|
||||
en: 'The plant analysis genuinely beats every other app I have tried so far! Amazing!',
|
||||
es: '¡El análisis de plantas supera de verdad a cualquier otra app que haya probado! ¡Increíble!',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Trifft den Avatar woertlich ("keinen gruenen Daumen").
|
||||
title: 'Mega',
|
||||
quote:
|
||||
'Ich habe schon viele Apps ausprobiert, weil ich keinen grünen Daumen habe. Keine hat mich bisher so überzeugt wie diese ! Kann sie jedem ans Herz legen',
|
||||
author: 'Annastasia2104',
|
||||
stars: 5,
|
||||
sourceLanguage: 'de',
|
||||
publishedOn: '2026-07-02',
|
||||
priority: 3,
|
||||
translations: {
|
||||
en: 'I have tried a lot of apps because I do not have a green thumb. None has convinced me as much as this one! I can recommend it to anyone.',
|
||||
es: 'He probado muchas apps porque no tengo mano para las plantas. ¡Ninguna me había convencido tanto como esta! Se la recomiendo a cualquiera.',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Beste App',
|
||||
quote: 'Bisher mit Abstand die beste App für meine Pflanzen!',
|
||||
author: 'lina_einhorn',
|
||||
stars: 5,
|
||||
sourceLanguage: 'de',
|
||||
publishedOn: '2026-07-02',
|
||||
priority: 4,
|
||||
translations: {
|
||||
en: 'By far the best app for my plants so far!',
|
||||
es: '¡Con diferencia, la mejor app para mis plantas hasta ahora!',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Endlich ....',
|
||||
quote:
|
||||
'… vergesse ich nicht, meine Pflanzen zu gießen, um zu pflegen!!! Einfache Bedienung - top App',
|
||||
author: 'ClaKnu',
|
||||
stars: 5,
|
||||
sourceLanguage: 'de',
|
||||
publishedOn: '2026-07-02',
|
||||
priority: 5,
|
||||
translations: {
|
||||
en: '… I no longer forget to water and care for my plants!!! Easy to use - top app',
|
||||
es: '… ¡ya no me olvido de regar y cuidar mis plantas!!! Fácil de usar, app estupenda',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tolle App',
|
||||
quote: 'Ich bin richtig zufrieden ! Tolle App',
|
||||
author: 'annabelle_gouz',
|
||||
stars: 5,
|
||||
sourceLanguage: 'de',
|
||||
publishedOn: '2026-07-04',
|
||||
priority: 6,
|
||||
translations: {
|
||||
en: 'I am really happy with it! Great app',
|
||||
es: '¡Estoy muy contenta! Gran app',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export type DisplayTestimonial = {
|
||||
title: string;
|
||||
quote: string;
|
||||
author: string;
|
||||
stars: 1 | 2 | 3 | 4 | 5;
|
||||
/** true, wenn der angezeigte Text eine Uebersetzung des Originals ist. */
|
||||
isTranslated: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Waehlt die staerkste Bewertung fuer die UI-Sprache.
|
||||
*
|
||||
* Vorrang hat eine im Original passende Sprache; gibt es keine, wird die
|
||||
* hoechstpriorisierte Bewertung mit freigegebener Uebersetzung genommen und
|
||||
* im UI als Uebersetzung gekennzeichnet. Nie wird ein fremdsprachiges
|
||||
* Original unkommentiert als muttersprachlich ausgegeben.
|
||||
*/
|
||||
export const getTestimonialForLanguage = (language: Language): DisplayTestimonial | null => {
|
||||
if (TESTIMONIALS.length === 0) return null;
|
||||
|
||||
const byPriority = [...TESTIMONIALS].sort((a, b) => a.priority - b.priority);
|
||||
|
||||
const native = byPriority.find((t) => t.sourceLanguage === language);
|
||||
if (native) {
|
||||
return {
|
||||
title: native.title,
|
||||
quote: native.quote,
|
||||
author: native.author,
|
||||
stars: native.stars,
|
||||
isTranslated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const translated = byPriority.find((t) => t.translations?.[language]);
|
||||
if (translated) {
|
||||
return {
|
||||
title: translated.title,
|
||||
quote: translated.translations![language]!,
|
||||
author: translated.author,
|
||||
stars: translated.stars,
|
||||
isTranslated: true,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Hinweis, der erscheint, wenn eine Bewertung nicht in der UI-Sprache verfasst wurde. */
|
||||
export const translatedNote = (language: Language): string => {
|
||||
if (language === 'de') return 'Übersetzt';
|
||||
if (language === 'es') return 'Traducido';
|
||||
return 'Translated';
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../services/backend/contracts';
|
||||
import { createIdempotencyKey } from '../utils/idempotency';
|
||||
import { AuthService, AuthSession } from '../services/authService';
|
||||
import { Analytics } from '../services/analytics';
|
||||
import { PlantsDb, SettingsDb, LexiconHistoryDb, AppMetaDb } from '../services/database';
|
||||
|
||||
interface AppState {
|
||||
@@ -63,6 +64,8 @@ interface AppState {
|
||||
getPendingPlant: () => { result: IdentificationResult; imageUri: string } | null;
|
||||
guestScanCount: number;
|
||||
incrementGuestScanCount: () => void;
|
||||
hasCompletedOnboarding: boolean;
|
||||
markOnboardingCompleted: () => void;
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppState | null>(null);
|
||||
@@ -84,7 +87,7 @@ const isColorPalette = (v: string): v is ColorPalette =>
|
||||
v === 'forest' || v === 'ocean' || v === 'sunset' || v === 'mono';
|
||||
const isLanguage = (v: string): v is Language => v === 'de' || v === 'en' || v === 'es';
|
||||
const REVENUECAT_PRO_ENTITLEMENT_ID = (process.env.EXPO_PUBLIC_REVENUECAT_PRO_ENTITLEMENT_ID || 'pro').trim() || 'pro';
|
||||
const SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS = new Set<PurchaseProductId>(['monthly_pro', 'yearly_pro']);
|
||||
const SUPPORTED_REVENUECAT_SUBSCRIPTION_PRODUCTS = new Set<PurchaseProductId>(['weekly_pro', 'monthly_pro', 'yearly_pro']);
|
||||
|
||||
const summarizeRevenueCatCustomerInfo = (customerInfo: RevenueCatCustomerInfo) => {
|
||||
const activeEntitlements = customerInfo?.entitlements?.active || {};
|
||||
@@ -153,6 +156,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [profileImageUri, setProfileImageUri] = useState<string | null>(null);
|
||||
const [pendingPlant, setPendingPlantState] = useState<{ result: IdentificationResult; imageUri: string } | null>(null);
|
||||
const [guestScanCount, setGuestScanCount] = useState(0);
|
||||
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(false);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [isLoadingPlants, setIsLoadingPlants] = useState(true);
|
||||
const [billingSummary, setBillingSummary] = useState<BillingSummary | null>(null);
|
||||
@@ -190,8 +194,13 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setBillingSummary(null);
|
||||
setPlants([]);
|
||||
setLanguage(getDeviceLanguage());
|
||||
setAppearanceModeState('system');
|
||||
setColorPaletteState('forest');
|
||||
// Theme ist eine GERAETE-Einstellung, keine Kontodaten. Beim Ausloggen oder
|
||||
// beim Start ohne Session darf sie deshalb nicht auf die Defaults fallen -
|
||||
// sonst landet ein Gast nach jedem Neustart wieder auf Dark/Forest.
|
||||
const deviceAppearance = AppMetaDb.get('appearance_mode');
|
||||
setAppearanceModeState(deviceAppearance && isAppearanceMode(deviceAppearance) ? deviceAppearance : 'system');
|
||||
const devicePalette = AppMetaDb.get('color_palette');
|
||||
setColorPaletteState(devicePalette && isColorPalette(devicePalette) ? devicePalette : 'forest');
|
||||
setProfileNameState('');
|
||||
setProfileImageUri(null);
|
||||
setIsLoadingPlants(false);
|
||||
@@ -298,6 +307,10 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const hydrateSession = useCallback(async (nextSession: AuthSession) => {
|
||||
setSession(nextSession);
|
||||
setProfileNameState(nextSession.name);
|
||||
Analytics.identify(nextSession.serverUserId, {
|
||||
email: nextSession.email,
|
||||
name: nextSession.name,
|
||||
});
|
||||
setIsLoadingPlants(true);
|
||||
setIsLoadingBilling(true);
|
||||
// The previous (guest or prior account) summary is meaningless for this
|
||||
@@ -359,6 +372,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
await AuthService.logout();
|
||||
Analytics.reset();
|
||||
resetStateForSignedOutUser();
|
||||
}, [resetStateForSignedOutUser]);
|
||||
|
||||
@@ -372,6 +386,22 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setGuestScanCount(parseInt(savedCount, 10) || 0);
|
||||
}
|
||||
|
||||
if (AppMetaDb.get('onboarding_completed') === '1') {
|
||||
setHasCompletedOnboarding(true);
|
||||
}
|
||||
|
||||
// Theme sofort aus dem Geraetespeicher setzen - vor Session-Restore und
|
||||
// vor dem Server-Roundtrip. Danach ueberschreibt hydrateSession ggf. mit
|
||||
// den nutzerspezifischen Werten, die aber identisch sein sollten.
|
||||
const storedAppearance = AppMetaDb.get('appearance_mode');
|
||||
if (storedAppearance && isAppearanceMode(storedAppearance)) {
|
||||
setAppearanceModeState(storedAppearance);
|
||||
}
|
||||
const storedPalette = AppMetaDb.get('color_palette');
|
||||
if (storedPalette && isColorPalette(storedPalette)) {
|
||||
setColorPaletteState(storedPalette);
|
||||
}
|
||||
|
||||
const s = await AuthService.getSession();
|
||||
if (!s) {
|
||||
resetStateForSignedOutUser();
|
||||
@@ -433,7 +463,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
cycleStartedAt: now.toISOString(),
|
||||
cycleEndsAt: renewsAt || new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
availableProducts: ['monthly_pro', 'yearly_pro', 'topup_small', 'topup_medium', 'topup_large'],
|
||||
availableProducts: ['weekly_pro', 'monthly_pro', 'yearly_pro', 'topup_small', 'topup_medium', 'topup_large'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -503,11 +533,17 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const setAppearanceMode = useCallback((mode: AppearanceMode) => {
|
||||
setAppearanceModeState(mode);
|
||||
// Geraeteweit speichern, unabhaengig von einer Session: sonst geht die
|
||||
// Auswahl fuer Gaeste beim Neustart verloren, und eingeloggte Nutzer sehen
|
||||
// beim Start kurz das Default-Theme, weil die Nutzer-Settings erst nach
|
||||
// hydrateSession (inkl. Server-Roundtrip) geladen werden.
|
||||
AppMetaDb.set('appearance_mode', mode);
|
||||
if (session) SettingsDb.setAppearanceMode(session.userId, mode);
|
||||
}, [session]);
|
||||
|
||||
const setColorPalette = useCallback((palette: ColorPalette) => {
|
||||
setColorPaletteState(palette);
|
||||
AppMetaDb.set('color_palette', palette);
|
||||
if (session) SettingsDb.setColorPalette(session.userId, palette);
|
||||
}, [session]);
|
||||
|
||||
@@ -574,6 +610,11 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
});
|
||||
}, []);
|
||||
|
||||
const markOnboardingCompleted = useCallback(() => {
|
||||
AppMetaDb.set('onboarding_completed', '1');
|
||||
setHasCompletedOnboarding(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
session,
|
||||
@@ -613,6 +654,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
getPendingPlant,
|
||||
guestScanCount,
|
||||
incrementGuestScanCount,
|
||||
hasCompletedOnboarding,
|
||||
markOnboardingCompleted,
|
||||
}}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
|
||||
160
design/stitch-onboarding/botanical_vitality/DESIGN.md
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
name: Botanical Vitality
|
||||
colors:
|
||||
surface: '#f8fbef'
|
||||
surface-dim: '#d8dbd0'
|
||||
surface-bright: '#f8fbef'
|
||||
surface-container-lowest: '#ffffff'
|
||||
surface-container-low: '#f2f5e9'
|
||||
surface-container: '#ecefe4'
|
||||
surface-container-high: '#e7e9de'
|
||||
surface-container-highest: '#e1e4d9'
|
||||
on-surface: '#191d16'
|
||||
on-surface-variant: '#42493c'
|
||||
inverse-surface: '#2e322a'
|
||||
inverse-on-surface: '#eff2e7'
|
||||
outline: '#72796a'
|
||||
outline-variant: '#c2c9b7'
|
||||
surface-tint: '#366a17'
|
||||
primary: '#2b5f0b'
|
||||
on-primary: '#ffffff'
|
||||
primary-container: '#437824'
|
||||
on-primary-container: '#c1ff99'
|
||||
inverse-primary: '#9bd776'
|
||||
secondary: '#446741'
|
||||
on-secondary: '#ffffff'
|
||||
secondary-container: '#c2eaba'
|
||||
on-secondary-container: '#486b45'
|
||||
tertiary: '#375d00'
|
||||
on-tertiary: '#ffffff'
|
||||
tertiary-container: '#4d771b'
|
||||
on-tertiary-container: '#cbfd91'
|
||||
error: '#ba1a1a'
|
||||
on-error: '#ffffff'
|
||||
error-container: '#ffdad6'
|
||||
on-error-container: '#93000a'
|
||||
primary-fixed: '#b6f390'
|
||||
primary-fixed-dim: '#9bd776'
|
||||
on-primary-fixed: '#092100'
|
||||
on-primary-fixed-variant: '#205100'
|
||||
secondary-fixed: '#c5edbd'
|
||||
secondary-fixed-dim: '#aad1a2'
|
||||
on-secondary-fixed: '#012104'
|
||||
on-secondary-fixed-variant: '#2d4e2b'
|
||||
tertiary-fixed: '#c0f287'
|
||||
tertiary-fixed-dim: '#a5d56e'
|
||||
on-tertiary-fixed: '#0f2000'
|
||||
on-tertiary-fixed-variant: '#2e4f00'
|
||||
background: '#f8fbef'
|
||||
on-background: '#191d16'
|
||||
surface-variant: '#e1e4d9'
|
||||
typography:
|
||||
display:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 34px
|
||||
fontWeight: '800'
|
||||
lineHeight: 42px
|
||||
letterSpacing: -0.02em
|
||||
headline-lg:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 28px
|
||||
fontWeight: '800'
|
||||
lineHeight: 36px
|
||||
letterSpacing: -0.01em
|
||||
headline-md:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 22px
|
||||
fontWeight: '700'
|
||||
lineHeight: 28px
|
||||
body-lg:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 17px
|
||||
fontWeight: '500'
|
||||
lineHeight: 26px
|
||||
body-md:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 15px
|
||||
fontWeight: '400'
|
||||
lineHeight: 22px
|
||||
label-lg:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 13px
|
||||
fontWeight: '700'
|
||||
lineHeight: 18px
|
||||
letterSpacing: 0.05em
|
||||
label-md:
|
||||
fontFamily: Plus Jakarta Sans
|
||||
fontSize: 12px
|
||||
fontWeight: '600'
|
||||
lineHeight: 16px
|
||||
rounded:
|
||||
sm: 0.25rem
|
||||
DEFAULT: 0.5rem
|
||||
md: 0.75rem
|
||||
lg: 1rem
|
||||
xl: 1.5rem
|
||||
full: 9999px
|
||||
spacing:
|
||||
base: 4px
|
||||
xs: 8px
|
||||
sm: 12px
|
||||
md: 16px
|
||||
lg: 24px
|
||||
xl: 32px
|
||||
container-margin: 20px
|
||||
gutter: 16px
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
The design system is centered on the concept of "Gentle Precision." It balances the organic, living nature of botany with a high-end, structured educational interface. The target audience is urban plant enthusiasts who seek a calm, focused environment to manage their indoor jungles.
|
||||
|
||||
The style is **Premium Minimalist with a Tactile Twist**. It draws inspiration from modern learning apps by using high whitespace, generous hit areas, and soft, approachable geometry. While the typography is bold and authoritative, the color palette and rounded corners keep the experience friendly and accessible. The interface should feel like a premium lifestyle magazine: clean, airy, and centered entirely on high-quality botanical photography.
|
||||
|
||||
## Colors
|
||||
This design system utilizes a "Nature-In" palette. The foundation is a warm cream background that reduces eye strain and feels more organic than pure white.
|
||||
|
||||
- **Primary Green (#437824):** Used for primary actions, success states, and key navigational highlights.
|
||||
- **Deep Forest (#173817):** Reserved for secondary buttons, information chips, and high-contrast accents that require a sense of grounded authority.
|
||||
- **Spring Accent (#A6D66F):** Used for progress bars, illustrative accents, and soft background fills for selected states.
|
||||
- **Typography:** Headlines use a high-contrast near-black to ensure readability against the cream background, while body text uses a muted gray-green to maintain the "calm" brand pillar.
|
||||
|
||||
## Typography
|
||||
We use **Plus Jakarta Sans** for its exceptional readability and friendly, rounded terminals that mirror the organic curves of plant life.
|
||||
|
||||
Headlines are unapologetically bold (Extra-Bold 800), creating a strong visual hierarchy and a confident editorial feel. For mobile, we prioritize vertical rhythm with generous line heights to ensure the "high whitespace" aesthetic is maintained even within text-heavy sections. Display styles utilize negative letter-spacing to keep the bold weights feeling tight and professional on iOS displays.
|
||||
|
||||
## Layout & Spacing
|
||||
This design system follows a fluid-width logic tailored for iOS devices, specifically optimized for the iPhone 15 aspect ratio.
|
||||
|
||||
- **Grid:** A standard 4-column mobile grid with 20px side margins.
|
||||
- **Vertical Rhythm:** Content blocks are separated by 24px or 32px to ensure the UI feels "airy."
|
||||
- **Safe Areas:** Strict adherence to iOS safe areas for the status bar and home indicator.
|
||||
- **Content Spacing:** Elements within cards use a 12px or 16px internal padding to maintain a compact but breathable internal structure.
|
||||
|
||||
## Elevation & Depth
|
||||
Depth is achieved through **Tonal Layering** and soft, natural shadows. We avoid harsh dropshadows.
|
||||
|
||||
- **Level 0 (Background):** The warm cream (#FBFAF3) base.
|
||||
- **Level 1 (Cards/Surface):** Pure white (#FFFFFF) surfaces with a very soft, diffused shadow (0px 4px 20px rgba(16, 28, 18, 0.06)). This makes cards appear as if they are resting lightly on the cream surface.
|
||||
- **Level 2 (Interactive):** When pressed, buttons or cards may subtly scale down (0.98) or the shadow may tighten to simulate physical contact.
|
||||
- **Glassmorphism:** Used sparingly for top navigation bars or floating action buttons, employing a 20px backdrop blur to allow the botanical photography to peek through.
|
||||
|
||||
## Shapes
|
||||
The shape language is defined by "Friendly Geometry."
|
||||
|
||||
- **Standard Radius:** 16px for cards and containers to create a soft, approachable frame for photography.
|
||||
- **Button Radius:** 14px specifically for the 56px height buttons, creating a balanced, slightly "squircle" feel.
|
||||
- **Chip Radius:** Fully rounded (pill-shaped) for tags and small indicators.
|
||||
- **Images:** All plant photography must feature rounded corners (minimum 12px) to prevent the "sharpness" of the images from clashing with the soft UI.
|
||||
|
||||
## Components
|
||||
- **Buttons:**
|
||||
- *Primary:* 56px height, #437824 background, white text (Bold), 14px radius.
|
||||
- *Secondary:* 56px height, #173817 background, white text (Bold), 14px radius.
|
||||
- *Outline:* 56px height, 2px border (#437824), #437824 text.
|
||||
- **Cards:** White background, 16px radius, soft 6% opacity shadow. Used for plant profiles, "Care Tasks," and "Reminders."
|
||||
- **Chips:** Small, pill-shaped tags used for plant difficulty (e.g., "Easy Care") or water requirements. Use #173817 with white text for high contrast.
|
||||
- **Progress Bars:** Thick 8px tracks using the #A6D66F accent color on a #E6EADF (muted version of cream) track.
|
||||
- **Input Fields:** 56px height, white background, 14px radius, subtle 1px border in #D1D4CC.
|
||||
- **Care Icons:** Use thick-stroke (2pt) icons with rounded caps to match the typography's softness.
|
||||
- **Imagery:** Use high-resolution photography with natural lighting. Ensure the subject (the plant) is centered with a clean or naturally blurred background.
|
||||
189
design/stitch-onboarding/greenlens_pro_paywall/code.html
Normal file
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens Pro - Paywall</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
|
||||
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
|
||||
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
|
||||
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
|
||||
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
|
||||
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface antialiased min-h-screen flex flex-col relative font-body-md overflow-x-hidden selection:bg-primary-container selection:text-on-primary-container">
|
||||
<!-- Hero Header Background -->
|
||||
<div class="absolute top-0 left-0 w-full h-80 z-0 pointer-events-none">
|
||||
<div class="w-full h-full bg-cover bg-center" data-alt="A soft, premium botanical photograph of a lush Monstera deliciosa leaf bathed in warm morning sunlight. The lighting is natural and high-key, creating a bright, airy, light-mode aesthetic. The background softly blurs out, allowing the vibrant, organic greens of the plant to contrast beautifully against a warm, minimalist cream backdrop. The mood is calm, upscale, and inviting." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDvuJKth4pDT75-D0SOCZSHfyxOmTSYlp2wVfMF_cvw7EojCdrM7InYI9z_zZhxaCkB68Hc8oG_lw0h1lJfm3_8So6u5vT8WQmYsQbx0Vgt0ObahJ5JVfSYz89qYFLbo24xIoTZgLEMAhUTfnCqFSArnc727JG0cvbuWGISnP-BgzC5LS7E2kIIRkq9a2T2wbcXdZlTXWYJ9CoV7_nYSum-sfOI4-yAif6n-VpKO_motQM_ZBL5TOeI6PlhTBjS1zrS-NfT19ziw70')"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-surface/10 via-surface/60 to-surface"></div>
|
||||
</div>
|
||||
<!-- Top Action Bar -->
|
||||
<div class="absolute top-0 left-0 w-full px-container-margin pt-14 pb-4 flex justify-between items-center z-20">
|
||||
<button class="w-10 h-10 rounded-full bg-surface-container-lowest/60 backdrop-blur-md flex items-center justify-center text-on-surface shadow-sm hover:bg-surface-container-lowest/80 transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
<button class="font-label-lg text-label-lg text-on-surface hover:opacity-80 transition-opacity drop-shadow-sm">
|
||||
Restore
|
||||
</button>
|
||||
</div>
|
||||
<!-- Scrollable Content Canvas -->
|
||||
<main class="relative z-10 flex-1 flex flex-col px-container-margin pt-56 pb-12 w-full max-w-md mx-auto">
|
||||
<!-- Typography Header -->
|
||||
<div class="flex flex-col items-center text-center mb-xl">
|
||||
<span class="font-label-lg text-label-lg text-primary mb-sm tracking-[0.1em] uppercase">GreenLens Pro</span>
|
||||
<h1 class="font-display text-display text-on-surface font-extrabold mb-sm leading-tight">Get Unlimited Access</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant max-w-[280px]">Unlimited scans, health checks and your personal care plan.</p>
|
||||
</div>
|
||||
<!-- Main Plan Card -->
|
||||
<div class="bg-surface-container rounded-xl p-lg mb-lg shadow-[0px_4px_20px_rgba(16,28,18,0.06)] flex flex-col gap-sm border border-outline-variant/20">
|
||||
<h2 class="font-headline-md text-headline-md text-on-surface">GreenLens Pro</h2>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed">Unlimited AI scans, health diagnosis, 7-day rescue plans, 100 credits/month</p>
|
||||
<div class="font-label-lg text-label-lg text-on-surface mt-base pt-sm border-t border-outline-variant/30">
|
||||
Free for 7 days, then €39.99/year
|
||||
</div>
|
||||
</div>
|
||||
<!-- Toggle Row -->
|
||||
<div class="bg-surface-container-lowest rounded-xl px-md py-sm mb-xl flex justify-between items-center shadow-[0px_4px_20px_rgba(16,28,18,0.04)]">
|
||||
<span class="font-label-lg text-label-lg text-on-surface">Free Trial Enabled</span>
|
||||
<!-- iOS Style Toggle ON -->
|
||||
<button class="w-14 h-8 bg-primary-container rounded-full relative transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-surface">
|
||||
<div class="absolute right-1 top-1 w-6 h-6 bg-surface-container-lowest rounded-full shadow-sm transition-all"></div>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Timeline / Schedule -->
|
||||
<div class="flex flex-col gap-lg pl-sm mb-xl relative">
|
||||
<!-- Connecting Line -->
|
||||
<div class="absolute left-[19px] top-4 bottom-8 w-0.5 bg-surface-variant z-0"></div>
|
||||
<!-- Step 1: Active -->
|
||||
<div class="flex items-start gap-md relative z-10">
|
||||
<div class="w-4 h-4 rounded-full bg-primary mt-1 border-[3px] border-surface shadow-sm shrink-0"></div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-lg text-label-lg text-primary">Due today — 7 days free</span>
|
||||
<span class="font-body-md text-body-md text-primary mt-base">€0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Step 2: Future -->
|
||||
<div class="flex items-start gap-md relative z-10">
|
||||
<div class="w-4 h-4 rounded-full bg-surface-variant mt-1 border-[3px] border-surface shadow-sm shrink-0"></div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-body-md text-body-md text-on-surface-variant">Due July 13</span>
|
||||
<span class="font-label-lg text-label-lg text-on-surface mt-base">€39.99</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sticky Bottom Actions -->
|
||||
<div class="mt-auto flex flex-col items-center gap-md pt-md w-full">
|
||||
<button class="w-full h-14 bg-primary-container text-on-primary rounded-xl font-label-lg text-label-lg flex items-center justify-center shadow-lg hover:bg-tertiary-container hover:shadow-xl active:scale-[0.98] transition-all duration-200">
|
||||
Try Free
|
||||
</button>
|
||||
<div class="flex flex-col items-center gap-sm">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">Cancel Anytime</span>
|
||||
<div class="flex items-center gap-sm text-outline-variant">
|
||||
<a class="font-label-md text-label-md text-outline hover:text-on-surface transition-colors" href="#">Privacy</a>
|
||||
<span class="font-label-md text-label-md">|</span>
|
||||
<a class="font-label-md text-label-md text-outline hover:text-on-surface transition-colors" href="#">Terms</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/greenlens_pro_paywall/screen.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
199
design/stitch-onboarding/health_check_care_plan/code.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
|
||||
<title>GreenLens Onboarding - Health Check</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px",
|
||||
"2xl": "1rem",
|
||||
"3xl": "1.5rem"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
|
||||
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
|
||||
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
|
||||
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
|
||||
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
|
||||
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
|
||||
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.material-symbols-outlined.fill {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
/* Soft, natural shadow for cards */
|
||||
.shadow-soft {
|
||||
box-shadow: 0px 4px 20px rgba(16, 28, 18, 0.06);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background h-screen w-full flex justify-center items-center overflow-hidden antialiased">
|
||||
<!-- Mobile Device Container (iPhone 15 Aspect Ratio roughly 19.5:9) -->
|
||||
<main class="w-full h-full max-w-md mx-auto relative flex flex-col bg-surface-container-lowest md:rounded-[40px] md:h-[852px] md:w-[393px] md:shadow-2xl overflow-hidden">
|
||||
<!-- Top 60% Image Area -->
|
||||
<div class="relative h-[60%] w-full">
|
||||
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A close-up, high-resolution lifestyle photograph of an indoor houseplant with a few visibly yellowing, slightly droopy leaves, set against a bright, airy cream-colored background. Soft, natural daylight streams in from an unseen window, creating a calm, minimalist aesthetic. The focus is sharp on the plant, embodying a premium, organic visual style typical of high-end botanical magazines." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDpIiAXrV02PGrV2kch25aBf36rzqZdzaUc3ve1x0O45wHQPSl-ZOiRyEK995UDMBRzUpAbTluxb-uEwFy83lmwfoXWhd7vjABtHpIatmirSuuR_22Nn5ixXze3DthIbP-ZKdfS-x7HBrSgcZ3oASanUHs4CUJc56uZvW_QIZX1IQnz2v32cl-Ro1jRK58xBuyWAfbgxmk3UnM1ibKRpufE6HKt0pXZjoqYKmnlYv2303rmMJENdBE-PaUu1BCAuPg3YKXCBpX6y4w')"></div>
|
||||
<!-- Safe Area Overlay / Gradient -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/20 via-transparent to-black/10"></div>
|
||||
<!-- Diagnosis Card Overlay -->
|
||||
<div class="absolute inset-x-container-margin bottom-6">
|
||||
<div class="bg-surface-container-lowest/95 backdrop-blur-xl rounded-2xl p-sm shadow-soft animate-[slideUp_0.5s_ease-out_forwards]">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center gap-xs mb-sm">
|
||||
<div class="bg-error-container text-error rounded-full p-2 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[18px]">medical_services</span>
|
||||
</div>
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Health Check</h3>
|
||||
</div>
|
||||
<!-- Diagnosis Rows -->
|
||||
<div class="flex flex-col gap-xs">
|
||||
<!-- Problem Row -->
|
||||
<div class="flex items-center justify-between bg-error-container/30 px-sm py-xs rounded-lg">
|
||||
<div class="flex items-center gap-xs">
|
||||
<span class="material-symbols-outlined text-error text-[20px] fill">warning</span>
|
||||
<span class="font-body-md text-body-md text-error font-medium">Overwatering detected</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Solution Row -->
|
||||
<div class="flex items-center justify-between bg-surface-container-low px-sm py-xs rounded-lg">
|
||||
<div class="flex items-center gap-xs">
|
||||
<span class="material-symbols-outlined text-primary text-[20px] fill">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface font-medium">7-day rescue plan ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 40% Content Area -->
|
||||
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-3xl -mt-6 z-10 flex flex-col justify-between p-container-margin shadow-[0px_-8px_24px_rgba(0,0,0,0.04)]">
|
||||
<!-- Text Content -->
|
||||
<div class="mt-xs">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-xs">Health Check & Care Plan</h2>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant">GreenLens spots problems early and builds a rescue plan for you.</p>
|
||||
</div>
|
||||
<!-- Footer Controls -->
|
||||
<div class="flex flex-col gap-lg pb-safe">
|
||||
<!-- Progress Dots -->
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
|
||||
<!-- Active Dot -->
|
||||
<div class="w-8 h-2 rounded-full bg-primary-container"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
|
||||
</div>
|
||||
<!-- Primary Action -->
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center active:scale-95 transition-transform duration-150">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<style>
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/health_check_care_plan/screen.png
Normal file
|
After Width: | Height: | Size: 535 KiB |
@@ -0,0 +1,270 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
|
||||
<title>GreenLens Onboarding - Health Check</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=Manrope:wght@400&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px",
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
],
|
||||
"label-lg": [
|
||||
"Epilogue"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"label-lg": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.4",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.material-symbols-outlined.fill {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
/* Soft, natural shadow for cards */
|
||||
.shadow-soft {
|
||||
box-shadow: 0px 4px 20px rgba(16, 28, 18, 0.06);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background h-screen w-full flex justify-center items-center overflow-hidden antialiased">
|
||||
<!-- Mobile Device Container (iPhone 15 Aspect Ratio roughly 19.5:9) -->
|
||||
<main class="w-full h-full max-w-md mx-auto relative flex flex-col bg-surface-container-lowest md:rounded-[40px] md:h-[852px] md:w-[393px] md:shadow-2xl overflow-hidden">
|
||||
<!-- Top 60% Image Area -->
|
||||
<div class="relative h-[60%] w-full">
|
||||
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A close-up, high-resolution lifestyle photograph of an indoor houseplant with a few visibly yellowing, slightly droopy leaves, set against a bright, airy cream-colored background. Soft, natural daylight streams in from an unseen window, creating a calm, minimalist aesthetic. The focus is sharp on the plant, embodying a premium, organic visual style typical of high-end botanical magazines." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDpIiAXrV02PGrV2kch25aBf36rzqZdzaUc3ve1x0O45wHQPSl-ZOiRyEK995UDMBRzUpAbTluxb-uEwFy83lmwfoXWhd7vjABtHpIatmirSuuR_22Nn5ixXze3DthIbP-ZKdfS-x7HBrSgcZ3oASanUHs4CUJc56uZvW_QIZX1IQnz2v32cl-Ro1jRK58xBuyWAfbgxmk3UnM1ibKRpufE6HKt0pXZjoqYKmnlYv2303rmMJENdBE-PaUu1BCAuPg3YKXCBpX6y4w')"></div>
|
||||
<!-- Safe Area Overlay / Gradient -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/50 via-transparent to-black/60"></div>
|
||||
<!-- Diagnosis Card Overlay -->
|
||||
<div class="absolute inset-x-container-margin bottom-6">
|
||||
<div class="bg-surface-container-lowest/95 backdrop-blur-xl rounded-2xl p-sm shadow-soft animate-[slideUp_0.5s_ease-out_forwards]">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center gap-xs mb-sm">
|
||||
<div class="bg-error-container text-error rounded-full p-2 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-[18px]">medical_services</span>
|
||||
</div>
|
||||
<h3 class="font-headline-md text-headline-md text-on-surface">Health Check</h3>
|
||||
</div>
|
||||
<!-- Diagnosis Rows -->
|
||||
<div class="flex flex-col gap-xs">
|
||||
<!-- Problem Row -->
|
||||
<div class="flex items-center justify-between bg-error-container/30 px-sm py-xs rounded-lg">
|
||||
<div class="flex items-center gap-xs">
|
||||
<span class="material-symbols-outlined text-error text-[20px] fill">warning</span>
|
||||
<span class="font-body-md text-body-md text-error font-medium">Overwatering detected</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Solution Row -->
|
||||
<div class="flex items-center justify-between bg-surface-container-low px-sm py-xs rounded-lg">
|
||||
<div class="flex items-center gap-xs">
|
||||
<span class="material-symbols-outlined text-primary text-[20px] fill">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface font-medium">7-day rescue plan ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 40% Content Area -->
|
||||
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-3xl -mt-6 z-10 flex flex-col justify-between p-container-margin shadow-[0px_-8px_24px_rgba(0,0,0,0.2)]">
|
||||
<!-- Text Content -->
|
||||
<div class="mt-xs">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-xs">Health Check & Care Plan</h2>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant">GreenLens spots problems early and builds a rescue plan for you.</p>
|
||||
</div>
|
||||
<!-- Footer Controls -->
|
||||
<div class="flex flex-col gap-lg pb-safe">
|
||||
<!-- Progress Dots -->
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
|
||||
<!-- Active Dot -->
|
||||
<div class="w-8 h-2 rounded-full bg-primary-container"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
|
||||
</div>
|
||||
<!-- Primary Action -->
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center active:scale-95 transition-transform duration-150">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<style>
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 542 KiB |
343
design/stitch-onboarding/login_to_greenlens/code.html
Normal file
@@ -0,0 +1,343 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
|
||||
<title>GreenLens - Login</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": [
|
||||
"Plus Jakarta Sans"
|
||||
],
|
||||
"body-lg": [
|
||||
"Plus Jakarta Sans"
|
||||
],
|
||||
"label-lg": [
|
||||
"Plus Jakarta Sans"
|
||||
],
|
||||
"body-md": [
|
||||
"Plus Jakarta Sans"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Plus Jakarta Sans"
|
||||
],
|
||||
"display": [
|
||||
"Plus Jakarta Sans"
|
||||
],
|
||||
"headline-md": [
|
||||
"Plus Jakarta Sans"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "16px",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"17px",
|
||||
{
|
||||
"lineHeight": "26px",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"label-lg": [
|
||||
"13px",
|
||||
{
|
||||
"lineHeight": "18px",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"15px",
|
||||
{
|
||||
"lineHeight": "22px",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "36px",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "800"
|
||||
}
|
||||
],
|
||||
"display": [
|
||||
"34px",
|
||||
{
|
||||
"lineHeight": "42px",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "800"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"22px",
|
||||
{
|
||||
"lineHeight": "28px",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background-color: #000; /* For framing */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.iphone-frame {
|
||||
width: 100%;
|
||||
max-width: 393px; /* iPhone 15 width */
|
||||
height: 100vh;
|
||||
max-height: 852px; /* iPhone 15 height */
|
||||
background-color: #FBFAF3; /* surface */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-col;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.iphone-frame {
|
||||
height: 852px;
|
||||
border-radius: 54px;
|
||||
box-shadow: 0 0 0 10px #000, 0 20px 40px rgba(0,0,0,0.5);
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
.hero-section {
|
||||
height: 35%;
|
||||
position: relative;
|
||||
}
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.hero-gradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(251,250,243,0) 50%, #FBFAF3 100%);
|
||||
}
|
||||
.form-sheet {
|
||||
height: 65%;
|
||||
background-color: #FBFAF3;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
margin-top: -24px; /* Pull up to overlap hero */
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
/* Custom Input Styling matching guidelines */
|
||||
.input-field {
|
||||
height: 56px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #D1D4CC;
|
||||
padding: 0 16px;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #191d16;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.input-field:focus {
|
||||
border-color: #437824;
|
||||
box-shadow: 0 0 0 2px rgba(67, 120, 36, 0.2);
|
||||
}
|
||||
.input-field::placeholder {
|
||||
color: #72796a;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Button Styling */
|
||||
.btn-primary {
|
||||
height: 56px;
|
||||
background-color: #437824;
|
||||
border-radius: 14px;
|
||||
color: #FFFFFF;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
}
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-apple {
|
||||
height: 56px;
|
||||
background-color: #000000;
|
||||
border-radius: 14px;
|
||||
color: #FFFFFF;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
.btn-apple:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
height: 1px;
|
||||
background-color: #e1e4d9;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div class="iphone-frame flex flex-col relative">
|
||||
<!-- Hero Image Area (35%) -->
|
||||
<div class="hero-section">
|
||||
<img alt="Botanical hero image" class="hero-image" data-alt="A lush, high-resolution close-up of a Monstera Deliciosa leaf, softly backlit by warm morning sunlight. The botanical photography is premium, airy, and calming, featuring rich vibrant greens against a slightly blurred, clean background. The aesthetic is modern minimalist with a tactile organic feel, perfectly suited for a high-end plant care app." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAZ52RoNjzx9-A72eQKmdGgnrltylpxouotb1w9T0WHGj_3lAW7G9L6cV6MgKptlxFZiiYT6AobZ5G7R3rvT86jhDTo5J87nsGwWrcqHnBgUOQ4enGmQ7WSStk2vV4zdWdSHCBfPSndpOUj_DRF3od8ASsFMDcA6VS02INwlqEmewyvYC1lpHPrr1ZrsHut8BAc0tWBa3GB8MKSNcQo03Da-KD06wwLK6kNBFyEHQqt77xxescSLi2CZgi_Zq5c2hNhs9Qd993DV-o"/>
|
||||
<div class="hero-gradient"></div>
|
||||
<div class="absolute bottom-10 left-5">
|
||||
<h1 class="font-display text-display text-on-surface">Welcome back!</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Sheet Area (65%) -->
|
||||
<div class="form-sheet">
|
||||
<!-- Apple Login -->
|
||||
<button class="btn-apple mb-lg">
|
||||
<svg fill="currentColor" height="20" viewbox="0 0 24 24" width="20">
|
||||
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.62-1.496 3.603-2.998 1.156-1.682 1.633-3.314 1.655-3.398-.035-.015-3.182-1.22-3.216-4.858-.026-3.04 2.484-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.693.792-1.353 2.235-1.169 3.603 1.343.104 2.61-.715 3.456-1.59z"></path>
|
||||
</svg>
|
||||
Continue with Apple
|
||||
</button>
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center gap-4 mb-lg">
|
||||
<div class="divider-line"></div>
|
||||
<span class="font-label-md text-label-md text-outline">OR</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
<!-- Inputs -->
|
||||
<div class="flex flex-col gap-sm mb-md">
|
||||
<input autocomplete="email" class="input-field" placeholder="Email address" type="email"/>
|
||||
<input autocomplete="current-password" class="input-field" placeholder="Password" type="password"/>
|
||||
</div>
|
||||
<!-- Forgot Password -->
|
||||
<div class="flex justify-end mb-lg">
|
||||
<a class="font-body-md text-body-md text-primary-container font-semibold hover:opacity-80 transition-opacity" href="#">Forgot password?</a>
|
||||
</div>
|
||||
<!-- Submit Button -->
|
||||
<button class="btn-primary mb-lg">
|
||||
Log in
|
||||
</button>
|
||||
<!-- Spacer -->
|
||||
<div class="flex-grow"></div>
|
||||
<!-- Sign Up Link -->
|
||||
<div class="text-center pb-8">
|
||||
<span class="font-body-md text-body-md text-on-surface-variant">New here? </span>
|
||||
<a class="font-body-md text-body-md text-primary-container font-bold hover:opacity-80 transition-opacity" href="#">Create account</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/login_to_greenlens/screen.png
Normal file
|
After Width: | Height: | Size: 419 KiB |
341
design/stitch-onboarding/login_to_greenlens_dark_mode/code.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
|
||||
<title>GreenLens - Login</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=JetBrains+Mono:wght@500&family=Manrope:wght@400&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background-color: #000; /* For framing */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.iphone-frame {
|
||||
width: 100%;
|
||||
max-width: 393px; /* iPhone 15 width */
|
||||
height: 100vh;
|
||||
max-height: 852px; /* iPhone 15 height */
|
||||
background-color: #0c160d; /* surface */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-col;
|
||||
}
|
||||
@media (min-width: 400px) {
|
||||
.iphone-frame {
|
||||
height: 852px;
|
||||
border-radius: 54px;
|
||||
box-shadow: 0 0 0 10px #000, 0 20px 40px rgba(0,0,0,0.5);
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
.hero-section {
|
||||
height: 35%;
|
||||
position: relative;
|
||||
}
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.hero-gradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(12,22,13,0) 50%, #0c160d 100%);
|
||||
}
|
||||
.form-sheet {
|
||||
height: 65%;
|
||||
background-color: #0c160d;
|
||||
border-top-left-radius: 24px;
|
||||
border-top-right-radius: 24px;
|
||||
margin-top: -24px; /* Pull up to overlap hero */
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
/* Custom Input Styling matching guidelines */
|
||||
.input-field {
|
||||
height: 56px;
|
||||
background-color: #141e14;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #43493a;
|
||||
padding: 0 16px;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
color: #dae6d6;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.input-field:focus {
|
||||
border-color: #c1f388;
|
||||
box-shadow: 0 0 0 2px rgba(193, 243, 136, 0.2);
|
||||
}
|
||||
.input-field::placeholder {
|
||||
color: #8d9382;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Button Styling */
|
||||
.btn-primary {
|
||||
height: 56px;
|
||||
background-color: #a6d66f;
|
||||
border-radius: 14px;
|
||||
color: #365d00;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, opacity 0.2s;
|
||||
}
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-apple {
|
||||
height: 56px;
|
||||
background-color: #dae6d6;
|
||||
border-radius: 14px;
|
||||
color: #0c160d;
|
||||
font-family: 'Manrope', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
.btn-apple:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
height: 1px;
|
||||
background-color: #2d372d;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-black">
|
||||
<div class="iphone-frame flex flex-col relative">
|
||||
<!-- Hero Image Area (35%) -->
|
||||
<div class="hero-section">
|
||||
<img alt="Botanical hero image" class="hero-image" data-alt="A lush, high-resolution close-up of a Monstera Deliciosa leaf, softly backlit by warm morning sunlight. The botanical photography is premium, airy, and calming, featuring rich vibrant greens against a slightly blurred, clean background. The aesthetic is modern minimalist with a tactile organic feel, perfectly suited for a high-end plant care app." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAZ52RoNjzx9-A72eQKmdGgnrltylpxouotb1w9T0WHGj_3lAW7G9L6cV6MgKptlxFZiiYT6AobZ5G7R3rvT86jhDTo5J87nsGwWrcqHnBgUOQ4enGmQ7WSStk2vV4zdWdSHCBfPSndpOUj_DRF3od8ASsFMDcA6VS02INwlqEmewyvYC1lpHPrr1ZrsHut8BAc0tWBa3GB8MKSNcQo03Da-KD06wwLK6kNBFyEHQqt77xxescSLi2CZgi_Zq5c2hNhs9Qd993DV-o"/>
|
||||
<div class="hero-gradient"></div>
|
||||
<div class="absolute bottom-10 left-5">
|
||||
<h1 class="font-display text-display text-on-surface">Welcome back!</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Sheet Area (65%) -->
|
||||
<div class="form-sheet">
|
||||
<!-- Apple Login -->
|
||||
<button class="btn-apple mb-6">
|
||||
<svg fill="currentColor" height="20" viewbox="0 0 24 24" width="20">
|
||||
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.62-1.496 3.603-2.998 1.156-1.682 1.633-3.314 1.655-3.398-.035-.015-3.182-1.22-3.216-4.858-.026-3.04 2.484-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.693.792-1.353 2.235-1.169 3.603 1.343.104 2.61-.715 3.456-1.59z"></path>
|
||||
</svg>
|
||||
Continue with Apple
|
||||
</button>
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="divider-line"></div>
|
||||
<span class="font-label-sm text-label-sm text-outline">OR</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
<!-- Inputs -->
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<input autocomplete="email" class="input-field" placeholder="Email address" type="email"/>
|
||||
<input autocomplete="current-password" class="input-field" placeholder="Password" type="password"/>
|
||||
</div>
|
||||
<!-- Forgot Password -->
|
||||
<div class="flex justify-end mb-6">
|
||||
<a class="font-body-md text-body-md text-primary-container font-semibold hover:opacity-80 transition-opacity" href="#">Forgot password?</a>
|
||||
</div>
|
||||
<!-- Submit Button -->
|
||||
<button class="btn-primary mb-6">
|
||||
Log in
|
||||
</button>
|
||||
<!-- Spacer -->
|
||||
<div class="flex-grow"></div>
|
||||
<!-- Sign Up Link -->
|
||||
<div class="text-center pb-8">
|
||||
<span class="font-body-md text-body-md text-on-surface-variant">New here? </span>
|
||||
<a class="font-body-md text-body-md text-primary-container font-bold hover:opacity-80 transition-opacity" href="#">Create account</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/login_to_greenlens_dark_mode/screen.png
Normal file
|
After Width: | Height: | Size: 400 KiB |
178
design/stitch-onboarding/never_forget_watering/code.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>GreenLens - Onboarding</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@400..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px",
|
||||
"sheet": "2rem"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
|
||||
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
|
||||
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
|
||||
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
|
||||
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
|
||||
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.icon-fill {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 600;
|
||||
}
|
||||
|
||||
.fade-in-up-1 { animation: fadeInUp 0.8s ease-out 0.2s both; }
|
||||
.fade-in-up-2 { animation: fadeInUp 0.8s ease-out 0.4s both; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(15px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface h-screen w-full flex flex-col overflow-hidden antialiased text-on-surface selection:bg-primary-container selection:text-on-primary">
|
||||
<!-- Top 60% Image Section -->
|
||||
<div class="relative h-[574px] w-full flex-shrink-0">
|
||||
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A high-end, softly lit interior photography shot of a sleek wooden shelf filled with lush, healthy indoor houseplants including a vibrant Monstera and a small potted Basil. The setting is bright and airy, illuminated by natural morning sunlight creating a warm, modern aesthetic. The color palette emphasizes fresh natural greens against a soft, creamy background, fitting a premium lifestyle magazine layout." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuA4uAMcvLFEJ2VgQ1TjVJ58xBi4YAa_V5XtYVfbGSpZAqonYJu56TJFY--BKMWxfBnecwSfeXvmKv9EwJKMH5eLLoIsP87j7tqZGUN4axa1C8Zk8Rvblw4tgpygjPRn634PK4nS5yHRkrxk0JXMu0nSl-yIlM4KQmbX_QuNVJaGPm-0RuY4jkSOMaIGg5UUeCuefUZeAhgp50PjNFeRqsXCj4rQPVCyP1BedEFOTx8Tvu-4PXol7WD2DqO-w8IhpHkrdzXMImLPiks')">
|
||||
</div>
|
||||
<!-- Gradient Overlay for contrast at the top/bottom -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-surface/40"></div>
|
||||
<!-- Floating Reminder Chips -->
|
||||
<div class="absolute inset-0 p-container-margin pt-16 flex flex-col gap-4 relative">
|
||||
<!-- Chip 1 -->
|
||||
<div class="fade-in-up-1 self-end mt-12 bg-surface-container-lowest/90 backdrop-blur-md px-sm py-2 rounded-full shadow-[0px_4px_20px_rgba(16,28,18,0.08)] flex items-center gap-2 border border-surface-variant max-w-max">
|
||||
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center text-on-secondary-container">
|
||||
<span class="material-symbols-outlined icon-fill text-[18px]">water_drop</span>
|
||||
</div>
|
||||
<div class="pr-2">
|
||||
<p class="font-label-md text-label-md text-on-surface">Water Monstera</p>
|
||||
<p class="font-label-md text-[10px] text-tertiary-container mt-0.5">Today</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chip 2 -->
|
||||
<div class="fade-in-up-2 self-start mt-8 ml-4 bg-surface-container-lowest/90 backdrop-blur-md px-sm py-2 rounded-full shadow-[0px_4px_20px_rgba(16,28,18,0.08)] flex items-center gap-2 border border-surface-variant max-w-max">
|
||||
<div class="w-8 h-8 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed">
|
||||
<span class="material-symbols-outlined icon-fill text-[18px]">eco</span>
|
||||
</div>
|
||||
<div class="pr-2">
|
||||
<p class="font-label-md text-label-md text-on-surface">Fertilize Basil</p>
|
||||
<p class="font-label-md text-[10px] text-on-surface-variant mt-0.5">In 3 days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 40% Content Sheet -->
|
||||
<div class="relative flex-1 bg-surface-container-lowest rounded-t-[32px] -mt-8 z-10 px-container-margin pt-xl pb-10 flex flex-col justify-between shadow-[0px_-8px_30px_rgba(16,28,18,0.04)]">
|
||||
<!-- Text Content -->
|
||||
<div class="flex flex-col items-center text-center px-4">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-3">Never Forget Watering</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant">Smart reminders and your personal plant library keep everything on track.</p>
|
||||
</div>
|
||||
<!-- Pagination & Actions -->
|
||||
<div class="flex flex-col items-center w-full gap-lg mt-auto">
|
||||
<!-- Progress Dots -->
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
|
||||
<div class="w-8 h-2 rounded-full bg-primary transition-all duration-300"></div>
|
||||
</div>
|
||||
<!-- Primary Button -->
|
||||
<button class="w-full h-[56px] bg-primary-container hover:bg-primary text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center transition-all duration-200 active:scale-95 shadow-sm">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/never_forget_watering/screen.png
Normal file
|
After Width: | Height: | Size: 642 KiB |
@@ -0,0 +1,235 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>GreenLens - Onboarding</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@400..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.icon-fill {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 600;
|
||||
}
|
||||
|
||||
.fade-in-up-1 { animation: fadeInUp 0.8s ease-out 0.2s both; }
|
||||
.fade-in-up-2 { animation: fadeInUp 0.8s ease-out 0.4s both; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(15px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface h-screen w-full flex flex-col overflow-hidden antialiased text-on-surface selection:bg-primary-container selection:text-on-primary">
|
||||
<!-- Top 60% Image Section -->
|
||||
<div class="relative h-[574px] w-full flex-shrink-0">
|
||||
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A high-end, softly lit interior photography shot of a sleek wooden shelf filled with lush, healthy indoor houseplants including a vibrant Monstera and a small potted Basil. The setting is bright and airy, illuminated by natural morning sunlight creating a warm, modern aesthetic. The color palette emphasizes fresh natural greens against a soft, creamy background, fitting a premium lifestyle magazine layout." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuA4uAMcvLFEJ2VgQ1TjVJ58xBi4YAa_V5XtYVfbGSpZAqonYJu56TJFY--BKMWxfBnecwSfeXvmKv9EwJKMH5eLLoIsP87j7tqZGUN4axa1C8Zk8Rvblw4tgpygjPRn634PK4nS5yHRkrxk0JXMu0nSl-yIlM4KQmbX_QuNVJaGPm-0RuY4jkSOMaIGg5UUeCuefUZeAhgp50PjNFeRqsXCj4rQPVCyP1BedEFOTx8Tvu-4PXol7WD2DqO-w8IhpHkrdzXMImLPiks')">
|
||||
</div>
|
||||
<!-- Gradient Overlay for contrast at the top/bottom -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/40 via-transparent to-surface"></div>
|
||||
<!-- Floating Reminder Chips -->
|
||||
<div class="absolute inset-0 p-margin-mobile pt-16 flex flex-col gap-4 relative">
|
||||
<!-- Chip 1 -->
|
||||
<div class="fade-in-up-1 self-end mt-12 bg-surface-container-low/90 backdrop-blur-md px-3 py-2 rounded-full shadow-[0px_4px_20px_rgba(0,0,0,0.2)] flex items-center gap-2 border border-surface-variant max-w-max">
|
||||
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center text-on-secondary-container">
|
||||
<span class="material-symbols-outlined icon-fill text-[18px]">water_drop</span>
|
||||
</div>
|
||||
<div class="pr-2">
|
||||
<p class="font-label-sm text-label-sm text-on-surface">Water Monstera</p>
|
||||
<p class="font-label-sm text-[10px] text-tertiary-container mt-0.5">Today</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Chip 2 -->
|
||||
<div class="fade-in-up-2 self-start mt-8 ml-4 bg-surface-container-low/90 backdrop-blur-md px-3 py-2 rounded-full shadow-[0px_4px_20px_rgba(0,0,0,0.2)] flex items-center gap-2 border border-surface-variant max-w-max">
|
||||
<div class="w-8 h-8 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed">
|
||||
<span class="material-symbols-outlined icon-fill text-[18px]">eco</span>
|
||||
</div>
|
||||
<div class="pr-2">
|
||||
<p class="font-label-sm text-label-sm text-on-surface">Fertilize Basil</p>
|
||||
<p class="font-label-sm text-[10px] text-on-surface-variant mt-0.5">In 3 days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 40% Content Sheet -->
|
||||
<div class="relative flex-1 bg-surface-container-lowest rounded-t-[32px] -mt-8 z-10 px-margin-mobile pt-8 pb-10 flex flex-col justify-between shadow-[0px_-8px_30px_rgba(0,0,0,0.1)]">
|
||||
<!-- Text Content -->
|
||||
<div class="flex flex-col items-center text-center px-4">
|
||||
<h1 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface mb-3">Never Forget Watering</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant">Smart reminders and your personal plant library keep everything on track.</p>
|
||||
</div>
|
||||
<!-- Pagination & Actions -->
|
||||
<div class="flex flex-col items-center w-full gap-6 mt-auto">
|
||||
<!-- Progress Dots -->
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
|
||||
<div class="w-8 h-2 rounded-full bg-primary transition-all duration-300"></div>
|
||||
</div>
|
||||
<!-- Primary Button -->
|
||||
<button class="w-full h-[56px] bg-primary-container hover:bg-primary text-on-primary-container font-label-sm text-label-sm rounded-lg flex items-center justify-center transition-all duration-200 active:scale-95 shadow-sm">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 629 KiB |
149
design/stitch-onboarding/nocturnal_botanical/DESIGN.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: Nocturnal Botanical
|
||||
colors:
|
||||
surface: '#0c160d'
|
||||
surface-dim: '#0c160d'
|
||||
surface-bright: '#313c31'
|
||||
surface-container-lowest: '#071008'
|
||||
surface-container-low: '#141e14'
|
||||
surface-container: '#182218'
|
||||
surface-container-high: '#222d22'
|
||||
surface-container-highest: '#2d372d'
|
||||
on-surface: '#dae6d6'
|
||||
on-surface-variant: '#c3c9b6'
|
||||
inverse-surface: '#dae6d6'
|
||||
inverse-on-surface: '#283328'
|
||||
outline: '#8d9382'
|
||||
outline-variant: '#43493a'
|
||||
surface-tint: '#a5d56e'
|
||||
primary: '#c1f388'
|
||||
on-primary: '#1e3700'
|
||||
primary-container: '#a6d66f'
|
||||
on-primary-container: '#365d00'
|
||||
inverse-primary: '#41690c'
|
||||
secondary: '#b7ccb5'
|
||||
on-secondary: '#233424'
|
||||
secondary-container: '#394b39'
|
||||
on-secondary-container: '#a6bba4'
|
||||
tertiary: '#dfe6d8'
|
||||
on-tertiary: '#2c3229'
|
||||
tertiary-container: '#c3cabc'
|
||||
on-tertiary-container: '#4e554a'
|
||||
error: '#ffb4ab'
|
||||
on-error: '#690005'
|
||||
error-container: '#93000a'
|
||||
on-error-container: '#ffdad6'
|
||||
primary-fixed: '#c0f287'
|
||||
primary-fixed-dim: '#a5d56e'
|
||||
on-primary-fixed: '#0f2000'
|
||||
on-primary-fixed-variant: '#2e4f00'
|
||||
secondary-fixed: '#d3e8d0'
|
||||
secondary-fixed-dim: '#b7ccb5'
|
||||
on-secondary-fixed: '#0f1f10'
|
||||
on-secondary-fixed-variant: '#394b39'
|
||||
tertiary-fixed: '#dee5d6'
|
||||
tertiary-fixed-dim: '#c2c9bb'
|
||||
on-tertiary-fixed: '#171d15'
|
||||
on-tertiary-fixed-variant: '#42493e'
|
||||
background: '#0c160d'
|
||||
on-background: '#dae6d6'
|
||||
surface-variant: '#2d372d'
|
||||
typography:
|
||||
display:
|
||||
fontFamily: Epilogue
|
||||
fontSize: 64px
|
||||
fontWeight: '700'
|
||||
lineHeight: '1.1'
|
||||
letterSpacing: -0.02em
|
||||
headline-lg:
|
||||
fontFamily: Epilogue
|
||||
fontSize: 40px
|
||||
fontWeight: '600'
|
||||
lineHeight: '1.2'
|
||||
letterSpacing: -0.01em
|
||||
headline-lg-mobile:
|
||||
fontFamily: Epilogue
|
||||
fontSize: 32px
|
||||
fontWeight: '600'
|
||||
lineHeight: '1.2'
|
||||
headline-md:
|
||||
fontFamily: Epilogue
|
||||
fontSize: 28px
|
||||
fontWeight: '500'
|
||||
lineHeight: '1.3'
|
||||
body-lg:
|
||||
fontFamily: Manrope
|
||||
fontSize: 18px
|
||||
fontWeight: '400'
|
||||
lineHeight: '1.6'
|
||||
body-md:
|
||||
fontFamily: Manrope
|
||||
fontSize: 16px
|
||||
fontWeight: '400'
|
||||
lineHeight: '1.6'
|
||||
label-sm:
|
||||
fontFamily: JetBrains Mono
|
||||
fontSize: 12px
|
||||
fontWeight: '500'
|
||||
lineHeight: '1.0'
|
||||
letterSpacing: 0.05em
|
||||
rounded:
|
||||
sm: 0.25rem
|
||||
DEFAULT: 0.5rem
|
||||
md: 0.75rem
|
||||
lg: 1rem
|
||||
xl: 1.5rem
|
||||
full: 9999px
|
||||
spacing:
|
||||
unit: 8px
|
||||
container-max: 1280px
|
||||
gutter: 24px
|
||||
margin-mobile: 16px
|
||||
margin-desktop: 64px
|
||||
---
|
||||
|
||||
## Brand & Style
|
||||
This design system captures the essence of a moonlit conservatory, blending premium health and wellness with a sophisticated, late-night aesthetic. It targets an audience that values high-end botanical science, luxury self-care, and deep vitality.
|
||||
|
||||
The style is a fusion of **Minimalism** and **Glassmorphism**, utilizing deep, organic layers and sharp typography to create a sense of mystery and scientific precision. The UI should evoke a sense of calm, renewal, and premium exclusivity. Large-scale botanical photography should be treated with low-exposure and high-contrast to integrate seamlessly into the dark environment.
|
||||
|
||||
## Colors
|
||||
The palette is rooted in deep, anaerobic greens to establish a luxurious dark mode experience.
|
||||
|
||||
- **Primary:** A vibrant, desaturated lime-green used for high-impact actions and essential highlights.
|
||||
- **Surface Tiers:** Depth is created through subtle shifts in forest green values rather than pure grays, maintaining the botanical theme throughout the interface.
|
||||
- **Typography:** Headlines utilize an off-white cream to ensure maximum legibility and a "glow" effect against the dark background, while body text uses a muted sage to reduce eye strain and establish hierarchy.
|
||||
|
||||
## Typography
|
||||
The typography system uses a bold, geometric sans-serif for headlines to convey authority and modern style. Body text is set in a refined, highly legible sans-serif to maintain a professional and calm tone.
|
||||
|
||||
For technical data or botanical classifications, a monospaced font is used in all-caps to provide a "lab-certified" aesthetic. Ensure all display text has a slight negative letter-spacing to tighten the visual impact of the bold weights.
|
||||
|
||||
## Layout & Spacing
|
||||
The layout follows a **Fluid Grid** model with generous outer margins to simulate the feeling of an editorial magazine.
|
||||
|
||||
- **Desktop:** 12-column grid with a 1280px max-width. Use 64px margins to allow the content to breathe.
|
||||
- **Tablet:** 8-column grid with 32px margins.
|
||||
- **Mobile:** 4-column grid with 16px margins.
|
||||
|
||||
Spacing should follow a strict 8px base unit. Use larger vertical gaps (80px+) between sections to emphasize the minimalist, premium feel.
|
||||
|
||||
## Elevation & Depth
|
||||
Depth is achieved through **Tonal Layering** and **Backdrop Blurs**. Avoid traditional drop shadows; instead, use slightly lighter green surface colors to indicate elevation.
|
||||
|
||||
When an element needs to float (such as a modal or dropdown), apply a high-density background blur (20px+) to the surface and a 1px border using a low-opacity version of the Primary color (#A6D66F at 10% opacity) to define the edge. This "Inner Glow" technique replaces shadows to maintain the clean, organic feel.
|
||||
|
||||
## Shapes
|
||||
The shape language is consistently **Rounded**, reflecting the soft, organic curves found in nature.
|
||||
|
||||
- Standard components (buttons, inputs) use a 0.5rem (8px) radius.
|
||||
- Larger containers and cards use a 1.5rem (24px) radius to create a soft, inviting frame for botanical photography.
|
||||
- Avoid sharp 90-degree angles to keep the UI feeling approachable and natural.
|
||||
|
||||
## Components
|
||||
- **Buttons:** Primary buttons use the Primary Green background with dark charcoal text for maximum contrast. Secondary buttons use a ghost style with a 1px border.
|
||||
- **Cards:** Use the Surface Container color (#1D2E1E) with no shadow. Imagery within cards should always have a subtle vignette to blend into the dark container.
|
||||
- **Input Fields:** Filled style using the Surface color. The bottom border should animate to the Primary color on focus.
|
||||
- **Chips/Badges:** Use a subtle tint of the primary color (15% opacity) for the background and the full-strength primary color for the text.
|
||||
- **Progress Indicators:** Use thin, elegant lines. The track should be the neutral color, and the indicator should be the primary green.
|
||||
- **Botanical Hero:** Full-bleed background images should have a dark gradient overlay (top to bottom) starting at 0% and ending at the background color (#0A140B) to ensure text legibility at the base of the section.
|
||||
190
design/stitch-onboarding/out_of_credits/code.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Out of Credits</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px",
|
||||
"modal": "24px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
|
||||
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
|
||||
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
|
||||
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
|
||||
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
|
||||
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.icon-fill {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background antialiased h-screen w-screen overflow-hidden flex justify-center items-center">
|
||||
<!-- Mobile Frame Simulation -->
|
||||
<div class="relative w-full h-full max-w-[430px] max-h-[932px] bg-black md:rounded-[48px] md:border-[8px] md:border-black overflow-hidden shadow-2xl">
|
||||
<!-- Background (Blurred Camera Feed) -->
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
<img alt="Blurred plant background" class="w-full h-full object-cover blur-md scale-105" data-alt="A slightly blurred view of a lush, vibrant Monstera Deliciosa plant sitting in a modern terracotta pot indoors. The lighting is bright and natural, streaming from a nearby window, creating soft shadows. The overall mood is calm and botanical, fitting a premium plant care app. The focus is soft to act as a background canvas." src="https://lh3.googleusercontent.com/aida-public/AB6AXuA6czRRINu7xjGgvdD-yoc0YmUs2v89tYRPsgr4-uCdRpBWm0bqnfhTbV5KIx_KIm8yGs1AXeEQMoCV1dBUioReXPHv2-Jprro-G33clj1-lW3XLQgYg4ax6Y5Lsy_BJCVYM4uJ1kPHEi6OqGBzjV9M_jfJ3-bazQ2sIdUNJACfgRKoWOjibazJxke5zbQ9-TqG5UUJWRYzUR3UxpgCI0kqUjudZZvHlLPGuUekVZ2XwfdIrGxCip4b3roPP-1ZCFIJSdq1P06eRp8"/>
|
||||
<!-- Dark Overlay for better contrast with the bottom sheet -->
|
||||
<div class="absolute inset-0 bg-black/40"></div>
|
||||
</div>
|
||||
<!-- Top Status Bar Area (Safe Area) -->
|
||||
<div class="absolute top-0 w-full h-12 z-20 flex justify-between items-center px-6 text-white">
|
||||
<span class="font-label-md text-label-md">9:41</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[16px]">signal_cellular_4_bar</span>
|
||||
<span class="material-symbols-outlined text-[16px]">wifi</span>
|
||||
<span class="material-symbols-outlined text-[16px]">battery_full</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Sheet Modal -->
|
||||
<div class="absolute bottom-0 w-full bg-surface-container-lowest rounded-t-[32px] shadow-[0px_-8px_40px_rgba(0,0,0,0.15)] flex flex-col items-center pt-4 pb-10 px-gutter transform transition-transform duration-300 translate-y-0">
|
||||
<!-- Drag Handle -->
|
||||
<div class="w-12 h-1.5 bg-surface-variant rounded-full mb-8"></div>
|
||||
<!-- Zero Scans Icon -->
|
||||
<div class="relative mb-6">
|
||||
<div class="w-20 h-20 bg-secondary-container rounded-full flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-4xl text-on-secondary-container icon-fill">eco</span>
|
||||
</div>
|
||||
<!-- Badge -->
|
||||
<div class="absolute -top-1 -right-1 w-8 h-8 bg-error text-on-error rounded-full flex items-center justify-center font-headline-md text-headline-md shadow-sm border-2 border-surface-container-lowest">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Content -->
|
||||
<div class="text-center mb-8 px-4">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-3">You're out of free scans</h2>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Your 3 free scans renew on August 1. Upgrade to Pro for unlimited scanning.</p>
|
||||
</div>
|
||||
<!-- Primary Action -->
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary-container font-label-lg text-label-lg rounded-xl mb-4 hover:opacity-90 active:scale-[0.98] transition-all flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-xl">workspace_premium</span>
|
||||
See Pro Plans
|
||||
</button>
|
||||
<!-- Credit Top-ups -->
|
||||
<div class="w-full mb-6">
|
||||
<p class="font-label-md text-label-md text-outline mb-3 text-center uppercase tracking-wider">Or buy single credits</p>
|
||||
<div class="flex justify-between gap-3">
|
||||
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
|
||||
<span class="font-headline-md text-headline-md text-primary">+30</span>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">credits</span>
|
||||
</button>
|
||||
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
|
||||
<span class="font-headline-md text-headline-md text-primary">+100</span>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">credits</span>
|
||||
</button>
|
||||
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors relative overflow-hidden">
|
||||
<!-- Best Value Badge -->
|
||||
<div class="absolute top-0 w-full bg-secondary text-on-secondary text-[10px] font-bold py-0.5 text-center">BEST</div>
|
||||
<span class="font-headline-md text-headline-md text-primary mt-3">+250</span>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">credits</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Secondary Action -->
|
||||
<button class="font-label-lg text-label-lg text-primary hover:text-primary-container transition-colors py-2 px-4">
|
||||
Maybe later
|
||||
</button>
|
||||
</div>
|
||||
<!-- Bottom Home Indicator Area -->
|
||||
<div class="absolute bottom-2 w-full flex justify-center z-50">
|
||||
<div class="w-32 h-1 bg-on-surface rounded-full opacity-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/out_of_credits/screen.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
245
design/stitch-onboarding/out_of_credits_dark_mode/code.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Out of Credits</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=Manrope:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px",
|
||||
"modal": "24px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.icon-fill {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background antialiased h-screen w-screen overflow-hidden flex justify-center items-center">
|
||||
<!-- Mobile Frame Simulation -->
|
||||
<div class="relative w-full h-full max-w-[430px] max-h-[932px] bg-black md:rounded-[48px] md:border-[8px] md:border-black overflow-hidden shadow-2xl">
|
||||
<!-- Background (Blurred Camera Feed) -->
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
<img alt="Blurred plant background" class="w-full h-full object-cover blur-md scale-105" data-alt="A slightly blurred view of a lush, vibrant Monstera Deliciosa plant sitting in a modern terracotta pot indoors. The lighting is bright and natural, streaming from a nearby window, creating soft shadows. The overall mood is calm and botanical, fitting a premium plant care app. The focus is soft to act as a background canvas." src="https://lh3.googleusercontent.com/aida-public/AB6AXuA6czRRINu7xjGgvdD-yoc0YmUs2v89tYRPsgr4-uCdRpBWm0bqnfhTbV5KIx_KIm8yGs1AXeEQMoCV1dBUioReXPHv2-Jprro-G33clj1-lW3XLQgYg4ax6Y5Lsy_BJCVYM4uJ1kPHEi6OqGBzjV9M_jfJ3-bazQ2sIdUNJACfgRKoWOjibazJxke5zbQ9-TqG5UUJWRYzUR3UxpgCI0kqUjudZZvHlLPGuUekVZ2XwfdIrGxCip4b3roPP-1ZCFIJSdq1P06eRp8"/>
|
||||
<!-- Dark Overlay for better contrast with the bottom sheet -->
|
||||
<div class="absolute inset-0 bg-black/60"></div>
|
||||
</div>
|
||||
<!-- Top Status Bar Area (Safe Area) -->
|
||||
<div class="absolute top-0 w-full h-12 z-20 flex justify-between items-center px-6 text-white">
|
||||
<span class="font-label-sm text-label-sm">9:41</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[16px]">signal_cellular_4_bar</span>
|
||||
<span class="material-symbols-outlined text-[16px]">wifi</span>
|
||||
<span class="material-symbols-outlined text-[16px]">battery_full</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Sheet Modal -->
|
||||
<div class="absolute bottom-0 w-full bg-surface-container-lowest rounded-t-[32px] shadow-[0px_-8px_40px_rgba(0,0,0,0.15)] flex flex-col items-center pt-4 pb-10 px-gutter transform transition-transform duration-300 translate-y-0">
|
||||
<!-- Drag Handle -->
|
||||
<div class="w-12 h-1.5 bg-surface-variant rounded-full mb-8"></div>
|
||||
<!-- Zero Scans Icon -->
|
||||
<div class="relative mb-6">
|
||||
<div class="w-20 h-20 bg-secondary-container rounded-full flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-4xl text-on-secondary-container icon-fill">eco</span>
|
||||
</div>
|
||||
<!-- Badge -->
|
||||
<div class="absolute -top-1 -right-1 w-8 h-8 bg-error text-on-error rounded-full flex items-center justify-center font-headline-md text-headline-md shadow-sm border-2 border-surface-container-lowest">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Content -->
|
||||
<div class="text-center mb-8 px-4">
|
||||
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-3">You're out of free scans</h2>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Your 3 free scans renew on August 1. Upgrade to Pro for unlimited scanning.</p>
|
||||
</div>
|
||||
<!-- Primary Action -->
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary-container font-body-md text-body-md font-bold rounded-xl mb-4 hover:opacity-90 active:scale-[0.98] transition-all flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-xl">workspace_premium</span>
|
||||
See Pro Plans
|
||||
</button>
|
||||
<!-- Credit Top-ups -->
|
||||
<div class="w-full mb-6">
|
||||
<p class="font-label-sm text-label-sm text-outline mb-3 text-center uppercase tracking-wider">Or buy single credits</p>
|
||||
<div class="flex justify-between gap-3">
|
||||
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
|
||||
<span class="font-headline-md text-headline-md text-primary">+30</span>
|
||||
<span class="font-label-sm text-label-sm text-on-surface-variant mt-1">credits</span>
|
||||
</button>
|
||||
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
|
||||
<span class="font-headline-md text-headline-md text-primary">+100</span>
|
||||
<span class="font-label-sm text-label-sm text-on-surface-variant mt-1">credits</span>
|
||||
</button>
|
||||
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors relative overflow-hidden">
|
||||
<!-- Best Value Badge -->
|
||||
<div class="absolute top-0 w-full bg-secondary text-on-secondary text-[10px] font-bold py-0.5 text-center">BEST</div>
|
||||
<span class="font-headline-md text-headline-md text-primary mt-3">+250</span>
|
||||
<span class="font-label-sm text-label-sm text-on-surface-variant mt-1">credits</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Secondary Action -->
|
||||
<button class="font-body-md text-body-md font-bold text-primary hover:text-primary-container transition-colors py-2 px-4">
|
||||
Maybe later
|
||||
</button>
|
||||
</div>
|
||||
<!-- Bottom Home Indicator Area -->
|
||||
<div class="absolute bottom-2 w-full flex justify-center z-50">
|
||||
<div class="w-32 h-1 bg-on-surface rounded-full opacity-50"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/out_of_credits_dark_mode/screen.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
227
design/stitch-onboarding/personalization_question/code.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="h-full" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
|
||||
<title>GreenLens - Onboarding Plant Count</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
|
||||
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
|
||||
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
|
||||
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
|
||||
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
|
||||
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
|
||||
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #FBFAF3; /* Enforced from style guidance level 0 */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Custom styles for precise matching */
|
||||
.card-shadow {
|
||||
box-shadow: 0px 4px 20px rgba(16, 28, 18, 0.06);
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
background-color: #E6EADF;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
background-color: #A6D66F;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.option-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.selected-card {
|
||||
border-color: #437824;
|
||||
background-color: rgba(166, 214, 111, 0.15); /* Tint derived from #A6D66F */
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full flex justify-center bg-[#FBFAF3] font-body-md text-on-surface antialiased">
|
||||
<!-- iPhone 15 Frame Simulator -->
|
||||
<div class="relative w-full max-w-[393px] h-[852px] bg-[#FBFAF3] sm:rounded-[3rem] sm:shadow-2xl sm:overflow-hidden sm:my-8 flex flex-col">
|
||||
<!-- Status Bar Area (Safe Area Top) -->
|
||||
<div class="h-12 w-full shrink-0"></div>
|
||||
<!-- Top Navigation Area -->
|
||||
<div class="px-container-margin pt-2 pb-4 flex items-center shrink-0 relative">
|
||||
<button aria-label="Go back" class="w-10 h-10 flex items-center justify-center rounded-full bg-surface-container-lowest card-shadow active:scale-95 transition-transform text-primary absolute left-container-margin">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">arrow_back</span>
|
||||
</button>
|
||||
<div class="w-full flex justify-center">
|
||||
<div class="w-24 h-2 rounded-full progress-track overflow-hidden">
|
||||
<div class="h-full rounded-full progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 flex flex-col px-container-margin overflow-y-auto pb-24 pt-4">
|
||||
<!-- Header Text -->
|
||||
<div class="text-center mb-xl">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-xs">
|
||||
How many plants do you have?
|
||||
</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant px-sm">
|
||||
This helps us personalize your care plan.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Options List -->
|
||||
<div class="flex flex-col gap-sm">
|
||||
<!-- Option 1 -->
|
||||
<button class="option-card w-full text-left bg-surface-container-lowest card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
|
||||
<span class="text-[28px] leading-none select-none">🌱</span>
|
||||
<span class="font-body-lg text-body-lg text-on-surface">Just starting (1-3)</span>
|
||||
</button>
|
||||
<!-- Option 2 -->
|
||||
<button class="option-card w-full text-left bg-surface-container-lowest card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
|
||||
<span class="text-[28px] leading-none select-none">🪴</span>
|
||||
<span class="font-body-lg text-body-lg text-on-surface">A few (4-10)</span>
|
||||
</button>
|
||||
<!-- Option 3 (Selected) -->
|
||||
<button class="option-card w-full text-left card-shadow rounded-[16px] p-lg border-2 flex items-center gap-md selected-card">
|
||||
<span class="text-[28px] leading-none select-none">🌿</span>
|
||||
<span class="font-body-lg text-body-lg text-primary-container font-bold">Quite a jungle (11-25)</span>
|
||||
</button>
|
||||
<!-- Option 4 -->
|
||||
<button class="option-card w-full text-left bg-surface-container-lowest card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
|
||||
<span class="text-[28px] leading-none select-none">🌳</span>
|
||||
<span class="font-body-lg text-body-lg text-on-surface">Full jungle (25+)</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-[32px]"></div>
|
||||
<!-- Floating Decorative Element (Abstract Blur to keep it visually interesting per styling guidelines) -->
|
||||
<div class="absolute bottom-32 -right-16 w-64 h-64 bg-tertiary-fixed-dim/20 rounded-full blur-[60px] pointer-events-none z-0"></div>
|
||||
</main>
|
||||
<!-- Bottom Action Area -->
|
||||
<div class="px-container-margin pb-10 pt-4 bg-[#FBFAF3]/90 backdrop-blur-md shrink-0 relative z-10 w-full rounded-b-[3rem]">
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary rounded-[14px] font-label-lg text-label-lg active:scale-98 transition-transform shadow-[0px_8px_16px_rgba(67,120,36,0.2)] flex items-center justify-center">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Simple JS for interaction demonstration
|
||||
document.querySelectorAll('.option-card').forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
// Remove selected class from all
|
||||
document.querySelectorAll('.option-card').forEach(c => {
|
||||
c.classList.remove('selected-card');
|
||||
c.classList.add('bg-surface-container-lowest', 'border-transparent');
|
||||
c.querySelector('span:last-child').classList.remove('text-primary-container', 'font-bold');
|
||||
c.querySelector('span:last-child').classList.add('text-on-surface');
|
||||
});
|
||||
|
||||
// Add to clicked
|
||||
this.classList.add('selected-card');
|
||||
this.classList.remove('bg-surface-container-lowest', 'border-transparent');
|
||||
|
||||
let textSpan = this.querySelector('span:last-child');
|
||||
textSpan.classList.remove('text-on-surface');
|
||||
textSpan.classList.add('text-primary-container', 'font-bold');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/personalization_question/screen.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
@@ -0,0 +1,291 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="h-full dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
|
||||
<title>GreenLens - Onboarding Plant Count</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=JetBrains+Mono:wght@500&family=Manrope:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px",
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
],
|
||||
"label-lg": ["JetBrains Mono"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"label-lg": ["14px", {"lineHeight": "1.4", "fontWeight": "600"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #0c160d; /* Enforced dark mode background */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* Custom styles for precise matching */
|
||||
.card-shadow {
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
background-color: #222d22;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
background-color: #c1f388;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.option-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.selected-card {
|
||||
border-color: #c1f388;
|
||||
background-color: rgba(193, 243, 136, 0.15); /* Tint derived from #c1f388 */
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full flex justify-center bg-background font-body-md text-on-surface antialiased">
|
||||
<!-- iPhone 15 Frame Simulator -->
|
||||
<div class="relative w-full max-w-[393px] h-[852px] bg-background sm:rounded-[3rem] sm:shadow-2xl sm:overflow-hidden sm:my-8 flex flex-col">
|
||||
<!-- Status Bar Area (Safe Area Top) -->
|
||||
<div class="h-12 w-full shrink-0"></div>
|
||||
<!-- Top Navigation Area -->
|
||||
<div class="px-container-margin pt-2 pb-4 flex items-center shrink-0 relative">
|
||||
<button aria-label="Go back" class="w-10 h-10 flex items-center justify-center rounded-full bg-surface-container card-shadow active:scale-95 transition-transform text-primary absolute left-container-margin">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">arrow_back</span>
|
||||
</button>
|
||||
<div class="w-full flex justify-center">
|
||||
<div class="w-24 h-2 rounded-full progress-track overflow-hidden">
|
||||
<div class="h-full rounded-full progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 flex flex-col px-container-margin overflow-y-auto pb-24 pt-4">
|
||||
<!-- Header Text -->
|
||||
<div class="text-center mb-xl">
|
||||
<h1 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface mb-xs">
|
||||
How many plants do you have?
|
||||
</h1>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant px-sm">
|
||||
This helps us personalize your care plan.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Options List -->
|
||||
<div class="flex flex-col gap-sm">
|
||||
<!-- Option 1 -->
|
||||
<button class="option-card w-full text-left bg-surface-container card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
|
||||
<span class="text-[28px] leading-none select-none">🌱</span>
|
||||
<span class="font-body-lg text-body-lg text-on-surface">Just starting (1-3)</span>
|
||||
</button>
|
||||
<!-- Option 2 -->
|
||||
<button class="option-card w-full text-left bg-surface-container card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
|
||||
<span class="text-[28px] leading-none select-none">🪴</span>
|
||||
<span class="font-body-lg text-body-lg text-on-surface">A few (4-10)</span>
|
||||
</button>
|
||||
<!-- Option 3 (Selected) -->
|
||||
<button class="option-card w-full text-left card-shadow rounded-[16px] p-lg border-2 flex items-center gap-md selected-card">
|
||||
<span class="text-[28px] leading-none select-none">🌿</span>
|
||||
<span class="font-body-lg text-body-lg text-primary font-bold">Quite a jungle (11-25)</span>
|
||||
</button>
|
||||
<!-- Option 4 -->
|
||||
<button class="option-card w-full text-left bg-surface-container card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
|
||||
<span class="text-[28px] leading-none select-none">🌳</span>
|
||||
<span class="font-body-lg text-body-lg text-on-surface">Full jungle (25+)</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 min-h-[32px]"></div>
|
||||
<!-- Floating Decorative Element (Abstract Blur to keep it visually interesting per styling guidelines) -->
|
||||
<div class="absolute bottom-32 -right-16 w-64 h-64 bg-primary/10 rounded-full blur-[60px] pointer-events-none z-0"></div>
|
||||
</main>
|
||||
<!-- Bottom Action Area -->
|
||||
<div class="px-container-margin pb-10 pt-4 bg-background/90 backdrop-blur-md shrink-0 relative z-10 w-full rounded-b-[3rem]">
|
||||
<button class="w-full h-[56px] bg-primary text-on-primary rounded-[14px] font-label-lg text-label-lg active:scale-98 transition-transform shadow-[0px_8px_16px_rgba(193,243,136,0.1)] flex items-center justify-center">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Simple JS for interaction demonstration
|
||||
document.querySelectorAll('.option-card').forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
// Remove selected class from all
|
||||
document.querySelectorAll('.option-card').forEach(c => {
|
||||
c.classList.remove('selected-card');
|
||||
c.classList.add('bg-surface-container', 'border-transparent');
|
||||
c.querySelector('span:last-child').classList.remove('text-primary', 'font-bold');
|
||||
c.querySelector('span:last-child').classList.add('text-on-surface');
|
||||
});
|
||||
|
||||
// Add to clicked
|
||||
this.classList.add('selected-card');
|
||||
this.classList.remove('bg-surface-container', 'border-transparent');
|
||||
|
||||
let textSpan = this.querySelector('span:last-child');
|
||||
textSpan.classList.remove('text-on-surface');
|
||||
textSpan.classList.add('text-primary', 'font-bold');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 178 KiB |
213
design/stitch-onboarding/personalizing_your_plan/code.html
Normal file
@@ -0,0 +1,213 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens - Progress</title>
|
||||
<!-- Material Symbols -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
|
||||
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
|
||||
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
|
||||
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
|
||||
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
|
||||
"display": ["48px", { "lineHeight": "56px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
|
||||
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Custom animations to enhance the "Gentle Precision" feel */
|
||||
@keyframes subtle-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.02); opacity: 0.95; }
|
||||
}
|
||||
.animate-subtle-pulse {
|
||||
animation: subtle-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float-up {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
.animate-float-up {
|
||||
animation: float-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface antialiased min-h-screen flex flex-col font-sans overflow-x-hidden selection:bg-primary-container selection:text-on-primary-container">
|
||||
<!-- Main Canvas Content -->
|
||||
<main class="flex-1 flex flex-col px-gutter pt-16 pb-12 w-full max-w-md mx-auto relative h-screen max-h-[900px]">
|
||||
<!-- Top Section: Progress Display -->
|
||||
<section class="flex flex-col items-center mt-4 mb-12 animate-float-up" style="animation-delay: 0.1s;">
|
||||
<div class="animate-subtle-pulse flex flex-col items-center">
|
||||
<h1 class="font-display text-display text-primary-container mb-8">73%</h1>
|
||||
<div class="relative w-40 h-40 flex items-center justify-center mb-8">
|
||||
<!-- Progress Ring Background -->
|
||||
<svg class="absolute inset-0 w-full h-full transform -rotate-90" viewbox="0 0 120 120">
|
||||
<circle class="stroke-surface-variant" cx="60" cy="60" fill="none" r="54" stroke-width="6"></circle>
|
||||
<!-- Progress Ring Active (73% of 339.29 circumference = 247.6) -->
|
||||
<circle class="stroke-primary-container transition-all duration-[2000ms] ease-out" cx="60" cy="60" fill="none" r="54" stroke-dasharray="339.29" stroke-dashoffset="91.6" stroke-linecap="round" stroke-width="6"></circle>
|
||||
</svg>
|
||||
<!-- Center Image -->
|
||||
<div class="w-28 h-28 rounded-full overflow-hidden shadow-[0px_4px_20px_rgba(16,28,18,0.06)] relative z-10 border-4 border-surface-container-lowest bg-surface-container-low">
|
||||
<img class="w-full h-full object-cover" data-alt="A beautifully lit, highly detailed macro photograph of a fresh, healthy Monstera Deliciosa leaf node emerging. The setting is bright, featuring soft, diffused natural daylight synonymous with premium lifestyle indoor plant photography. The background is a clean, minimal cream wall softly out of focus, allowing the lush green textures to command attention." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDD8Ij49YnpXP0wrHVlraHqQPLHbq5nMP0HJHT-VhpkShra9HVYXZZa0OupKWUhBvR9N4-GIVlLrO55A41z_t3WZQBIupmEkYcnZj9OTKrDeZ5ZAwm1ONH4ZZila5Pww-Njw8Hn0oFBgAtnUZQb0VQi70qgX6xtUzNCLyIUX3OQopUcYBAfEMVKyPExSWfr1X2Kj4YWyP57xnqG5_672eJ2OzoAnrZPMDSBCLx0gqvmmBLurrxksL5O0fTM2afAEQOy-fReEwPLZCM"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Pill -->
|
||||
<div class="bg-surface-variant/60 backdrop-blur-md rounded-full px-5 py-2.5 flex items-center gap-3 shadow-sm">
|
||||
<span class="material-symbols-outlined text-on-surface-variant text-[18px] animate-spin" style="animation-duration: 3s;">sync</span>
|
||||
<span class="font-label-lg text-label-lg text-on-surface-variant">Personalizing your care plan…</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Middle Section: Vertical Checklist -->
|
||||
<section class="flex-1 w-full max-w-[280px] mx-auto flex flex-col justify-center gap-5 animate-float-up" style="animation-delay: 0.3s;">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary-container text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">Analyzing your answers</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary-container text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">Building your care plan</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary-container text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">Preparing your scan credits</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex items-center justify-center w-6 h-6">
|
||||
<span class="material-symbols-outlined text-outline-variant text-[24px] absolute">radio_button_unchecked</span>
|
||||
<span class="w-1.5 h-1.5 bg-outline-variant rounded-full animate-pulse"></span>
|
||||
</div>
|
||||
<span class="font-body-md text-body-md text-on-surface-variant opacity-80">Finalizing your plan</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Bottom Section: Social Proof & Badge -->
|
||||
<section class="mt-auto flex flex-col items-center animate-float-up w-full" style="animation-delay: 0.5s;">
|
||||
<!-- Overlapping Testimonial Cards -->
|
||||
<div class="relative w-full h-[140px] mb-6">
|
||||
<!-- Background Card (Muted) -->
|
||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[90%] bg-surface-container-highest rounded-xl h-24 shadow-[0px_4px_20px_rgba(16,28,18,0.06)] transform -translate-y-3 scale-95 opacity-50 z-0"></div>
|
||||
<!-- Foreground Card (Active) -->
|
||||
<div class="absolute top-0 left-0 w-full bg-surface-container-lowest rounded-xl shadow-[0px_8px_30px_rgba(16,28,18,0.08)] p-4 flex flex-col gap-2 z-10 border border-surface-variant/50">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<img class="w-10 h-10 rounded-full object-cover bg-surface-variant" data-alt="A bright, professional headshot of a smiling woman in her early thirties, bathed in warm, soft natural sunlight. The aesthetic is clean, approachable, and highly polished, fitting a premium digital service. She is framed intimately against a gently blurred background of subtle green foliage." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCwP_g6-fpeohdJjnhG1R_yA-RypkiO6-NENOC640_fORV6ygSF9wlJvjXnQwnUzWnNyLN3AnOErHziNl_el3scfiHFQ0MjcJbOruxYdE0bhxkrPIGHNrH4Dl0nS0WxfeiNPSSiZpuSMM4D-0ZnTZRd5hR6-DnM04dT_mc3beAjFEYeedIrFrSKhi5IsZvK8gTu0x6k5UN627ijnpIaO3WBSeJ1waNUMaZ2OD_tj2pbPohgmE92SunvsOw8qVTP8zPPm79KJgIuniY"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-lg text-label-lg text-on-surface flex items-center gap-1">Elena R. <span class="text-lg">🇺🇸</span></span>
|
||||
<div class="flex text-tertiary-fixed-dim">
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed line-clamp-2 italic">
|
||||
"GreenLens completely saved my Fiddle Leaf Fig. The daily routines feel incredibly precise and easy."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Store Badge -->
|
||||
<div class="flex items-center justify-center gap-2 text-primary-container bg-surface-container-low px-4 py-2 rounded-full border border-primary-container/20">
|
||||
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">workspace_premium</span>
|
||||
<span class="font-label-lg text-label-lg uppercase tracking-widest text-[11px]">4.8 App Store Rating</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/personalizing_your_plan/screen.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
@@ -0,0 +1,275 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens - Progress</title>
|
||||
<!-- Material Symbols -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=JetBrains+Mono:wght@500&family=Manrope:wght@400&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px",
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Custom animations to enhance the "Gentle Precision" feel */
|
||||
@keyframes subtle-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.02); opacity: 0.95; }
|
||||
}
|
||||
.animate-subtle-pulse {
|
||||
animation: subtle-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float-up {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
.animate-float-up {
|
||||
animation: float-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface antialiased min-h-screen flex flex-col font-sans overflow-x-hidden selection:bg-primary selection:text-on-primary">
|
||||
<!-- Main Canvas Content -->
|
||||
<main class="flex-1 flex flex-col px-gutter pt-16 pb-12 w-full max-w-md mx-auto relative h-screen max-h-[900px]">
|
||||
<!-- Top Section: Progress Display -->
|
||||
<section class="flex flex-col items-center mt-4 mb-12 animate-float-up" style="animation-delay: 0.1s;">
|
||||
<div class="animate-subtle-pulse flex flex-col items-center">
|
||||
<h1 class="font-display text-display text-primary mb-8">73%</h1>
|
||||
<div class="relative w-40 h-40 flex items-center justify-center mb-8">
|
||||
<!-- Progress Ring Background -->
|
||||
<svg class="absolute inset-0 w-full h-full transform -rotate-90" viewbox="0 0 120 120">
|
||||
<circle class="stroke-surface-variant" cx="60" cy="60" fill="none" r="54" stroke-width="6"></circle>
|
||||
<!-- Progress Ring Active (73% of 339.29 circumference = 247.6) -->
|
||||
<circle class="stroke-primary transition-all duration-[2000ms] ease-out" cx="60" cy="60" fill="none" r="54" stroke-dasharray="339.29" stroke-dashoffset="91.6" stroke-linecap="round" stroke-width="6"></circle>
|
||||
</svg>
|
||||
<!-- Center Image -->
|
||||
<div class="w-28 h-28 rounded-full overflow-hidden shadow-[0px_4px_20px_rgba(16,28,18,0.06)] relative z-10 border-4 border-surface-container-lowest bg-surface-container-low">
|
||||
<img class="w-full h-full object-cover" data-alt="A beautifully lit, highly detailed macro photograph of a fresh, healthy Monstera Deliciosa leaf node emerging. The setting is bright, featuring soft, diffused natural daylight synonymous with premium lifestyle indoor plant photography. The background is a clean, minimal cream wall softly out of focus, allowing the lush green textures to command attention." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDD8Ij49YnpXP0wrHVlraHqQPLHbq5nMP0HJHT-VhpkShra9HVYXZZa0OupKWUhBvR9N4-GIVlLrO55A41z_t3WZQBIupmEkYcnZj9OTKrDeZ5ZAwm1ONH4ZZila5Pww-Njw8Hn0oFBgAtnUZQb0VQi70qgX6xtUzNCLyIUX3OQopUcYBAfEMVKyPExSWfr1X2Kj4YWyP57xnqG5_672eJ2OzoAnrZPMDSBCLx0gqvmmBLurrxksL5O0fTM2afAEQOy-fReEwPLZCM"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Pill -->
|
||||
<div class="bg-surface-variant/60 backdrop-blur-md rounded-full px-5 py-2.5 flex items-center gap-3 shadow-sm">
|
||||
<span class="material-symbols-outlined text-on-surface-variant text-[18px] animate-spin" style="animation-duration: 3s;">sync</span>
|
||||
<span class="font-label-sm text-label-sm text-on-surface-variant">Personalizing your care plan…</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Middle Section: Vertical Checklist -->
|
||||
<section class="flex-1 w-full max-w-[280px] mx-auto flex flex-col justify-center gap-5 animate-float-up" style="animation-delay: 0.3s;">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">Analyzing your answers</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">Building your care plan</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
<span class="font-body-md text-body-md text-on-surface">Preparing your scan credits</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex items-center justify-center w-6 h-6">
|
||||
<span class="material-symbols-outlined text-outline-variant text-[24px] absolute">radio_button_unchecked</span>
|
||||
<span class="w-1.5 h-1.5 bg-outline-variant rounded-full animate-pulse"></span>
|
||||
</div>
|
||||
<span class="font-body-md text-body-md text-on-surface-variant opacity-80">Finalizing your plan</span>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Bottom Section: Social Proof & Badge -->
|
||||
<section class="mt-auto flex flex-col items-center animate-float-up w-full" style="animation-delay: 0.5s;">
|
||||
<!-- Overlapping Testimonial Cards -->
|
||||
<div class="relative w-full h-[140px] mb-6">
|
||||
<!-- Background Card (Muted) -->
|
||||
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[90%] bg-surface-container-highest rounded-xl h-24 shadow-[0px_4px_20px_rgba(16,28,18,0.06)] transform -translate-y-3 scale-95 opacity-50 z-0"></div>
|
||||
<!-- Foreground Card (Active) -->
|
||||
<div class="absolute top-0 left-0 w-full bg-surface-container-lowest rounded-xl shadow-[0px_8px_30px_rgba(16,28,18,0.08)] p-4 flex flex-col gap-2 z-10 border border-surface-variant/50">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<img class="w-10 h-10 rounded-full object-cover bg-surface-variant" data-alt="A bright, professional headshot of a smiling woman in her early thirties, bathed in warm, soft natural sunlight. The aesthetic is clean, approachable, and highly polished, fitting a premium digital service. She is framed intimately against a gently blurred background of subtle green foliage." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCwP_g6-fpeohdJjnhG1R_yA-RypkiO6-NENOC640_fORV6ygSF9wlJvjXnQwnUzWnNyLN3AnOErHziNl_el3scfiHFQ0MjcJbOruxYdE0bhxkrPIGHNrH4Dl0nS0WxfeiNPSSiZpuSMM4D-0ZnTZRd5hR6-DnM04dT_mc3beAjFEYeedIrFrSKhi5IsZvK8gTu0x6k5UN627ijnpIaO3WBSeJ1waNUMaZ2OD_tj2pbPohgmE92SunvsOw8qVTP8zPPm79KJgIuniY"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-sm text-label-sm text-on-surface flex items-center gap-1">Elena R. <span class="text-lg">🇺🇸</span></span>
|
||||
<div class="flex text-tertiary-fixed-dim">
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed line-clamp-2 italic">
|
||||
"GreenLens completely saved my Fiddle Leaf Fig. The daily routines feel incredibly precise and easy."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Store Badge -->
|
||||
<div class="flex items-center justify-center gap-2 text-primary bg-surface-container-low px-4 py-2 rounded-full border border-primary/20">
|
||||
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">workspace_premium</span>
|
||||
<span class="font-label-sm text-label-sm uppercase tracking-widest">4.8 App Store Rating</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 229 KiB |
155
design/stitch-onboarding/scan_any_plant/code.html
Normal file
@@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>GreenLens - Onboarding</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
|
||||
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
|
||||
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
|
||||
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
|
||||
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
|
||||
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
|
||||
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface-dim flex items-center justify-center min-h-screen">
|
||||
<div class="w-full max-w-md h-[884px] md:h-[844px] md:max-h-[839px] bg-background md:rounded-[40px] md:shadow-2xl relative overflow-hidden flex flex-col mx-auto border-0 md:border-[8px] md:border-black">
|
||||
<!-- Top 60% Image Area -->
|
||||
<div class="relative flex-grow h-[60%] w-full" data-alt="A high-resolution, bright, natural lighting photograph of a smartphone held in front of a lush, large green Monstera leaf. The style is premium minimalist lifestyle photography, emphasizing crisp details and fresh greenery. Soft cream and white blurred background behind the plant. A clear focus on the phone screen framing the vibrant leaf." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAUZPCiN64BDhHkPH2NIYPISrIugWbCp_EkjOkaMh3fiNS7ryUPfGspVCfH2uBadQkMHwjferaQCp08IRybFzVTWEwlamSXufp2jVmvwmyX95-xy2emX1T_vA22-KJ47FEjXNZXdiUrcXXe2IcpYPEJKr3kXqYMWWUcIscNC5ML8XENdrXkQHq9gi9oIewApl-N0tazA5Go8VUUuz3bl_OQKf74TXzd38fEAw7m0V4OMkx5rJhirV4ngl-afOe9exyyM2kmWWqfACg'); background-size: cover; background-position: center;">
|
||||
<!-- Scan Overlay Frame -->
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none pb-12">
|
||||
<div class="w-56 h-56 border-2 border-primary-fixed/50 rounded-2xl relative shadow-[0_0_40px_rgba(43,95,11,0.2)]">
|
||||
<!-- Corner Accents -->
|
||||
<div class="absolute -top-[2px] -left-[2px] w-8 h-8 border-t-[3px] border-l-[3px] border-primary-fixed rounded-tl-2xl"></div>
|
||||
<div class="absolute -top-[2px] -right-[2px] w-8 h-8 border-t-[3px] border-r-[3px] border-primary-fixed rounded-tr-2xl"></div>
|
||||
<div class="absolute -bottom-[2px] -left-[2px] w-8 h-8 border-b-[3px] border-l-[3px] border-primary-fixed rounded-bl-2xl"></div>
|
||||
<div class="absolute -bottom-[2px] -right-[2px] w-8 h-8 border-b-[3px] border-r-[3px] border-primary-fixed rounded-br-2xl"></div>
|
||||
<!-- Scanning line animation simulation -->
|
||||
<div class="absolute top-1/2 left-0 w-full h-[2px] bg-primary-fixed shadow-[0_0_8px_rgba(182,243,144,0.8)] opacity-70"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result Chip -->
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 translate-y-[140px] bg-surface-container-lowest/90 backdrop-blur-xl px-4 py-2.5 rounded-full shadow-[0px_8px_24px_rgba(16,28,18,0.12)] flex items-center space-x-2.5 border border-surface-variant">
|
||||
<span class="material-symbols-outlined text-primary text-[18px]" style="font-variation-settings: 'FILL' 1;">temp_preferences_eco</span>
|
||||
<span class="font-label-md text-label-md text-on-surface">Monstera identified · 98%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 40% Sheet -->
|
||||
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-[32px] w-full px-gutter py-8 flex flex-col justify-between z-10 shadow-[0px_-8px_32px_rgba(16,28,18,0.06)] -mt-8">
|
||||
<!-- Text Content -->
|
||||
<div class="flex flex-col space-y-4 text-center mt-4">
|
||||
<h1 class="font-display text-display text-on-surface">Scan Any Plant</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant max-w-[300px] mx-auto">
|
||||
Point your camera at a plant and GreenLens identifies it in seconds.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Bottom Controls -->
|
||||
<div class="flex flex-col space-y-8 items-center w-full pb-6">
|
||||
<!-- Progress Dots -->
|
||||
<div class="flex space-x-2.5">
|
||||
<div class="w-8 h-2 bg-primary rounded-full transition-all duration-300"></div>
|
||||
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
|
||||
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
|
||||
</div>
|
||||
<!-- Action Button -->
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary rounded-[14px] font-label-lg text-label-lg flex items-center justify-center hover:opacity-90 active:scale-95 transition-all shadow-sm">
|
||||
CONTINUE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/scan_any_plant/screen.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
210
design/stitch-onboarding/scan_any_plant_dark_mode/code.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>GreenLens - Onboarding</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=JetBrains+Mono:wght@500&family=Manrope:wght@400&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-surface-dim flex items-center justify-center min-h-screen">
|
||||
<div class="w-full max-w-md h-[884px] md:h-[844px] md:max-h-[839px] bg-background md:rounded-[40px] md:shadow-2xl relative overflow-hidden flex flex-col mx-auto border-0 md:border-[8px] md:border-black">
|
||||
<!-- Top 60% Image Area -->
|
||||
<div class="relative flex-grow h-[60%] w-full" data-alt="A high-resolution, bright, natural lighting photograph of a smartphone held in front of a lush, large green Monstera leaf. The style is premium minimalist lifestyle photography, emphasizing crisp details and fresh greenery. Soft cream and white blurred background behind the plant. A clear focus on the phone screen framing the vibrant leaf." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAUZPCiN64BDhHkPH2NIYPISrIugWbCp_EkjOkaMh3fiNS7ryUPfGspVCfH2uBadQkMHwjferaQCp08IRybFzVTWEwlamSXufp2jVmvwmyX95-xy2emX1T_vA22-KJ47FEjXNZXdiUrcXXe2IcpYPEJKr3kXqYMWWUcIscNC5ML8XENdrXkQHq9gi9oIewApl-N0tazA5Go8VUUuz3bl_OQKf74TXzd38fEAw7m0V4OMkx5rJhirV4ngl-afOe9exyyM2kmWWqfACg'); background-size: cover; background-position: center;">
|
||||
<!-- Scan Overlay Frame -->
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none pb-12">
|
||||
<div class="w-56 h-56 border-2 border-primary-fixed/50 rounded-2xl relative shadow-[0_0_40px_rgba(43,95,11,0.2)]">
|
||||
<!-- Corner Accents -->
|
||||
<div class="absolute -top-[2px] -left-[2px] w-8 h-8 border-t-[3px] border-l-[3px] border-primary-fixed rounded-tl-2xl"></div>
|
||||
<div class="absolute -top-[2px] -right-[2px] w-8 h-8 border-t-[3px] border-r-[3px] border-primary-fixed rounded-tr-2xl"></div>
|
||||
<div class="absolute -bottom-[2px] -left-[2px] w-8 h-8 border-b-[3px] border-l-[3px] border-primary-fixed rounded-bl-2xl"></div>
|
||||
<div class="absolute -bottom-[2px] -right-[2px] w-8 h-8 border-b-[3px] border-r-[3px] border-primary-fixed rounded-br-2xl"></div>
|
||||
<!-- Scanning line animation simulation -->
|
||||
<div class="absolute top-1/2 left-0 w-full h-[2px] bg-primary-fixed shadow-[0_0_8px_rgba(182,243,144,0.8)] opacity-70"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Result Chip -->
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 translate-y-[140px] bg-surface-container-lowest/90 backdrop-blur-xl px-4 py-2.5 rounded-full shadow-[0px_8px_24px_rgba(16,28,18,0.12)] flex items-center space-x-2.5 border border-surface-variant">
|
||||
<span class="material-symbols-outlined text-primary text-[18px]" style="font-variation-settings: 'FILL' 1;">temp_preferences_eco</span>
|
||||
<span class="font-label-sm text-label-sm text-on-surface uppercase tracking-wider">Monstera identified · 98%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 40% Sheet -->
|
||||
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-[32px] w-full px-gutter py-8 flex flex-col justify-between z-10 shadow-[0px_-8px_32px_rgba(16,28,18,0.06)] -mt-8">
|
||||
<!-- Text Content -->
|
||||
<div class="flex flex-col space-y-4 text-center mt-4">
|
||||
<h1 class="font-display text-headline-lg-mobile md:text-display text-on-surface">Scan Any Plant</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant max-w-[300px] mx-auto">
|
||||
Point your camera at a plant and GreenLens identifies it in seconds.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Bottom Controls -->
|
||||
<div class="flex flex-col space-y-8 items-center w-full pb-6">
|
||||
<!-- Progress Dots -->
|
||||
<div class="flex space-x-2.5">
|
||||
<div class="w-8 h-2 bg-primary rounded-full transition-all duration-300"></div>
|
||||
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
|
||||
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
|
||||
</div>
|
||||
<!-- Action Button -->
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary-container rounded-[14px] font-body-md text-body-md font-bold uppercase tracking-widest flex items-center justify-center hover:opacity-90 active:scale-95 transition-all shadow-sm">
|
||||
CONTINUE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/scan_any_plant_dark_mode/screen.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
166
design/stitch-onboarding/sign_up_for_greenlens/code.html
Normal file
@@ -0,0 +1,166 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens - Sign Up</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"2xl": "1rem",
|
||||
"3xl": "1.5rem",
|
||||
"4xl": "2rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "20px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
|
||||
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
|
||||
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
|
||||
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
|
||||
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
|
||||
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
|
||||
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Plus Jakarta Sans', sans-serif; }
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#111111] flex items-center justify-center min-h-screen">
|
||||
<!-- iPhone 15 Frame -->
|
||||
<main class="w-full h-[884px] md:h-[852px] md:w-[393px] bg-surface relative overflow-hidden md:rounded-[55px] shadow-2xl md:border-[8px] md:border-black">
|
||||
<!-- Top 45% - Botanical Hero -->
|
||||
<div class="absolute top-0 left-0 w-full h-[50%] z-0">
|
||||
<div class="w-full h-full bg-cover bg-center" data-alt="A moody, high-end close-up photograph of vibrant indoor Monstera foliage. Deep, rich shadows contrast with soft, natural highlights reflecting off the glossy dark green leaves. The aesthetic is premium, minimalist, and calming, designed to serve as a cinematic backdrop with a stark light-mode UI overlay." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuB2FpbdibIeASSKYhIYDfwjgsD7FN1aOn1UfzEAbbg8U0xFmZkvNp0NX7FWPO8OYv6Vjzi46sLAmz5HjkexgD3c08MRpBnEmbf1nMU5CkJ8oNIQfMpta-gPg-h9cvojERpIFJcLWD5TKYuwwLJWd0tOqlV8MrqMkS32vRy8hO1m_AOTjVVoVLmW1sUlg8P-e6ojyA96g22IUckm0C9NcJCnoeYRo4u1f2lg-TZIWS4HJ61XG3u1DIrMCmsUmVYgGnz2qjixP46amm0')"></div>
|
||||
<!-- Dark Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/20 via-black/50 to-[#121A10]"></div>
|
||||
<!-- Hero Text Content -->
|
||||
<div class="absolute bottom-[15%] left-0 w-full px-gutter text-white z-10 flex flex-col gap-1">
|
||||
<h1 class="font-display text-display text-white">Anna,</h1>
|
||||
<h2 class="font-headline-md text-headline-md text-white/90">Let's finish your setup!</h2>
|
||||
<p class="font-body-md text-body-md text-white/80 mt-2 max-w-[90%]">
|
||||
Create an account to save your plants and 3 free scans per month.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 55% - Cream Sheet -->
|
||||
<div class="absolute bottom-0 left-0 w-full h-[55%] bg-surface rounded-t-4xl z-20 flex flex-col px-gutter pt-8 pb-10 shadow-[0px_-8px_30px_rgba(0,0,0,0.12)]">
|
||||
<!-- Drag Handle (Visual only) -->
|
||||
<div class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
|
||||
<div class="flex-1 flex flex-col gap-4 mt-4">
|
||||
<!-- Apple Button -->
|
||||
<button class="w-full h-[56px] bg-on-surface text-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-on-surface/90 transition-colors active:scale-[0.98]">
|
||||
<svg class="w-5 h-5 fill-current" viewbox="0 0 384 512"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 24 184.8 8.8 245.4c-21.7 86.8 5.4 191.6 37.1 237.9 19.3 28.2 44.9 57.2 75.3 56.1 29.2-1.1 41.5-18.9 76.5-18.9 34.9 0 46.5 18.9 76.9 18.2 31.2-.7 53.9-26.8 72.8-55.4 23.3-35.1 32.8-69.2 33.3-71-1.3-.4-62.8-24.1-62-93.6zm-41-118c15.6-20.2 26.6-47.6 23.7-75.1-23.7 1-52.9 16.2-69.2 35.8-13.6 16.4-26.6 44.4-23 71.3 26 2 51.5-12.7 68.5-32z"></path></svg>
|
||||
<span class="font-label-lg text-label-lg">Continue with Apple</span>
|
||||
</button>
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center gap-4 my-2 opacity-60">
|
||||
<div class="flex-1 h-px bg-outline-variant"></div>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant uppercase">OR</span>
|
||||
<div class="flex-1 h-px bg-outline-variant"></div>
|
||||
</div>
|
||||
<!-- Email Button -->
|
||||
<button class="w-full h-[56px] bg-transparent border-2 border-outline-variant text-on-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-surface-variant/50 transition-colors active:scale-[0.98]">
|
||||
<span class="material-symbols-outlined" data-icon="mail" data-weight="fill" style="font-variation-settings: 'FILL' 1;">mail</span>
|
||||
<span class="font-label-lg text-label-lg">Continue with Email</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Bottom Actions & Legal -->
|
||||
<div class="mt-auto flex flex-col items-center gap-6">
|
||||
<p class="font-body-md text-body-md text-on-surface">
|
||||
Already have an account?
|
||||
<a class="text-primary-container font-label-lg text-label-lg hover:underline ml-1" href="#">Log in</a>
|
||||
</p>
|
||||
<p class="text-[11px] leading-tight text-on-surface-variant text-center px-4">
|
||||
By continuing, you agree to GreenLens's <a class="underline" href="#">Terms of Service</a> and <a class="underline" href="#">Privacy Policy</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/sign_up_for_greenlens/screen.png
Normal file
|
After Width: | Height: | Size: 501 KiB |
@@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="dark" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens - Sign Up</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&family=JetBrains+Mono:wght@500&family=Manrope:wght@400&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px",
|
||||
"2xl": "1rem",
|
||||
"3xl": "1.5rem",
|
||||
"4xl": "2rem"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
],
|
||||
"label-lg": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"label-md": [
|
||||
"JetBrains Mono"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"label-lg": [
|
||||
"14px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"label-md": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Manrope', sans-serif; }
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#111111] flex items-center justify-center min-h-screen">
|
||||
<!-- iPhone 15 Frame -->
|
||||
<main class="w-full h-[884px] md:h-[852px] md:w-[393px] bg-surface relative overflow-hidden md:rounded-[55px] shadow-2xl md:border-[8px] md:border-black">
|
||||
<!-- Top 45% - Botanical Hero -->
|
||||
<div class="absolute top-0 left-0 w-full h-[50%] z-0">
|
||||
<div class="w-full h-full bg-cover bg-center" data-alt="A moody, high-end close-up photograph of vibrant indoor Monstera foliage. Deep, rich shadows contrast with soft, natural highlights reflecting off the glossy dark green leaves. The aesthetic is premium, minimalist, and calming, designed to serve as a cinematic backdrop with a stark light-mode UI overlay." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuB2FpbdibIeASSKYhIYDfwjgsD7FN1aOn1UfzEAbbg8U0xFmZkvNp0NX7FWPO8OYv6Vjzi46sLAmz5HjkexgD3c08MRpBnEmbf1nMU5CkJ8oNIQfMpta-gPg-h9cvojERpIFJcLWD5TKYuwwLJWd0tOqlV8MrqMkS32vRy8hO1m_AOTjVVoVLmW1sUlg8P-e6ojyA96g22IUckm0C9NcJCnoeYRo4u1f2lg-TZIWS4HJ61XG3u1DIrMCmsUmVYgGnz2qjixP46amm0')"></div>
|
||||
<!-- Dark Gradient Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/20 via-black/50 to-[#121A10]"></div>
|
||||
<!-- Hero Text Content -->
|
||||
<div class="absolute bottom-[15%] left-0 w-full px-gutter text-white z-10 flex flex-col gap-1">
|
||||
<h1 class="font-display text-display text-white">Anna,</h1>
|
||||
<h2 class="font-headline-md text-headline-md text-white/90">Let's finish your setup!</h2>
|
||||
<p class="font-body-md text-body-md text-white/80 mt-2 max-w-[90%]">
|
||||
Create an account to save your plants and 3 free scans per month.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 55% - Cream Sheet -->
|
||||
<div class="absolute bottom-0 left-0 w-full h-[55%] bg-surface rounded-t-4xl z-20 flex flex-col px-gutter pt-8 pb-10 shadow-[0px_-8px_30px_rgba(0,0,0,0.12)]">
|
||||
<!-- Drag Handle (Visual only) -->
|
||||
<div class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
|
||||
<div class="flex-1 flex flex-col gap-4 mt-4">
|
||||
<!-- Apple Button -->
|
||||
<button class="w-full h-[56px] bg-on-surface text-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-on-surface/90 transition-colors active:scale-[0.98]">
|
||||
<svg class="w-5 h-5 fill-current" viewbox="0 0 384 512"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 24 184.8 8.8 245.4c-21.7 86.8 5.4 191.6 37.1 237.9 19.3 28.2 44.9 57.2 75.3 56.1 29.2-1.1 41.5-18.9 76.5-18.9 34.9 0 46.5 18.9 76.9 18.2 31.2-.7 53.9-26.8 72.8-55.4 23.3-35.1 32.8-69.2 33.3-71-1.3-.4-62.8-24.1-62-93.6zm-41-118c15.6-20.2 26.6-47.6 23.7-75.1-23.7 1-52.9 16.2-69.2 35.8-13.6 16.4-26.6 44.4-23 71.3 26 2 51.5-12.7 68.5-32z"></path></svg>
|
||||
<span class="font-label-lg text-label-lg">Continue with Apple</span>
|
||||
</button>
|
||||
<!-- Divider -->
|
||||
<div class="flex items-center gap-4 my-2 opacity-60">
|
||||
<div class="flex-1 h-px bg-outline-variant"></div>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant uppercase">OR</span>
|
||||
<div class="flex-1 h-px bg-outline-variant"></div>
|
||||
</div>
|
||||
<!-- Email Button -->
|
||||
<button class="w-full h-[56px] bg-transparent border-2 border-outline-variant text-on-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-surface-variant/50 transition-colors active:scale-[0.98]">
|
||||
<span class="material-symbols-outlined" data-icon="mail" data-weight="fill" style="font-variation-settings: 'FILL' 1;">mail</span>
|
||||
<span class="font-label-lg text-label-lg">Continue with Email</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Bottom Actions & Legal -->
|
||||
<div class="mt-auto flex flex-col items-center gap-6">
|
||||
<p class="font-body-md text-body-md text-on-surface">
|
||||
Already have an account?
|
||||
<a class="text-primary font-label-lg text-label-lg hover:underline ml-1" href="#">Log in</a>
|
||||
</p>
|
||||
<p class="text-[11px] leading-tight text-on-surface-variant text-center px-4">
|
||||
By continuing, you agree to GreenLens's <a class="underline" href="#">Terms of Service</a> and <a class="underline" href="#">Privacy Policy</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 505 KiB |
174
design/stitch-onboarding/welcome_to_greenlens/code.html
Normal file
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens - Welcome</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary": "#375d00",
|
||||
"on-surface": "#191d16",
|
||||
"on-secondary-fixed": "#012104",
|
||||
"on-secondary-fixed-variant": "#2d4e2b",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-low": "#f2f5e9",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary": "#446741",
|
||||
"outline": "#72796a",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-container": "#ecefe4",
|
||||
"on-error": "#ffffff",
|
||||
"on-surface-variant": "#42493c",
|
||||
"background": "#f8fbef",
|
||||
"error": "#ba1a1a",
|
||||
"outline-variant": "#c2c9b7",
|
||||
"on-secondary-container": "#486b45",
|
||||
"secondary-fixed": "#c5edbd",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary": "#2b5f0b",
|
||||
"surface-dim": "#d8dbd0",
|
||||
"primary-container": "#437824",
|
||||
"tertiary-fixed-dim": "#a5d56e",
|
||||
"inverse-on-surface": "#eff2e7",
|
||||
"surface-container-high": "#e7e9de",
|
||||
"surface-bright": "#f8fbef",
|
||||
"inverse-surface": "#2e322a",
|
||||
"on-background": "#191d16",
|
||||
"on-primary-fixed": "#092100",
|
||||
"secondary-container": "#c2eaba",
|
||||
"on-tertiary-container": "#cbfd91",
|
||||
"primary-fixed": "#b6f390",
|
||||
"tertiary-container": "#4d771b",
|
||||
"primary-fixed-dim": "#9bd776",
|
||||
"tertiary-fixed": "#c0f287",
|
||||
"surface-tint": "#366a17",
|
||||
"secondary-fixed-dim": "#aad1a2",
|
||||
"surface-variant": "#e1e4d9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed-variant": "#2e4f00",
|
||||
"on-primary": "#ffffff",
|
||||
"on-primary-container": "#c1ff99",
|
||||
"surface-container-highest": "#e1e4d9",
|
||||
"on-tertiary-fixed": "#0f2000",
|
||||
"on-primary-fixed-variant": "#205100",
|
||||
"inverse-primary": "#9bd776",
|
||||
"surface": "#f8fbef"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"gutter": "16px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Plus Jakarta Sans"],
|
||||
"body-lg": ["Plus Jakarta Sans"],
|
||||
"label-lg": ["Plus Jakarta Sans"],
|
||||
"body-md": ["Plus Jakarta Sans"],
|
||||
"headline-lg": ["Plus Jakarta Sans"],
|
||||
"display": ["Plus Jakarta Sans"],
|
||||
"headline-md": ["Plus Jakarta Sans"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
|
||||
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
|
||||
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
|
||||
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
|
||||
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
|
||||
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
|
||||
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface h-screen w-full overflow-hidden flex flex-col font-body-md select-none">
|
||||
<!-- Top 55%: Hero Image Area -->
|
||||
<div class="h-[440px] md:h-[480px] w-full relative shrink-0">
|
||||
<!-- Hero Background Image -->
|
||||
<div class="absolute inset-0 bg-cover bg-center" data-alt="A stunning, high-resolution botanical photograph of a lush indoor jungle featuring large Monstera deliciosa and Calathea plants. The composition is shot from a slightly elevated angle, capturing the vibrant green, textured leaves bathed in soft, natural morning light pouring through a nearby window. The background features a subtle, warm cream-colored wall that perfectly aligns with a premium minimalist light-mode app aesthetic. The depth of field is shallow, keeping the foreground leaves razor-sharp while gently blurring the background, creating a calm, focused, and high-end lifestyle magazine mood." style="background-image: url('../../../assets/welcome_hero_vertical.png')"></div>
|
||||
<!-- Subtle Top Gradient for Text Legibility -->
|
||||
<div class="absolute top-0 left-0 w-full h-32 bg-gradient-to-b from-on-surface/50 to-transparent"></div>
|
||||
<!-- Header: Logo & App Store Badge -->
|
||||
<div class="absolute top-12 left-0 w-full px-container-margin flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-surface-container-lowest">
|
||||
<span class="material-symbols-outlined text-[28px]" style="font-variation-settings: 'FILL' 1;">spa</span>
|
||||
<span class="font-display text-display tracking-tight leading-none text-surface-container-lowest pt-1">Green<span class="text-[#8ba885]">Lens</span></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 bg-surface-container-lowest/20 backdrop-blur-md border border-surface-container-lowest/10 rounded-full px-3 py-1.5 shadow-sm">
|
||||
<span class="material-symbols-outlined text-[14px] text-surface-container-lowest" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="font-label-md text-label-md text-surface-container-lowest">4.8</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Floating Testimonial Card -->
|
||||
<div class="absolute bottom-12 left-container-margin right-container-margin bg-surface-container-lowest/75 backdrop-blur-md rounded-[16px] p-4 shadow-[0px_4px_20px_rgba(16,28,18,0.12)] border border-surface-variant/50">
|
||||
<p class="font-body-md text-body-md text-on-surface mb-3 italic leading-relaxed">"Finally my plants stay alive! Highly recommend."</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<img class="w-8 h-8 rounded-full object-cover shadow-sm" data-alt="A professional, high-quality portrait photograph of a young woman with a warm, friendly smile, designed to be used as a circular avatar in a premium app interface. She has natural, effortless styling and is softly lit by diffused natural daylight. The background is a very soft, out-of-focus hint of an indoor plant shop or greenhouse, featuring muted greens and warm cream tones to perfectly match a sophisticated, minimalist light-mode aesthetic. The image exudes trust and approachability." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCqKHPbHjvPxBBSroxRXwyNuYGln0ydKqum11jR-kIoUoBeWzrSqByhgdt-maLp0b9HbdyDabUsv2PBnqZy8oIBAGSjHmtG9PhPiWARa9AcQrlRAtKWOAVYdD3y03G9Y0ilbQMz4MvFDBv57IqGO7xcp4cEAddUgjS1sgPskTA6Rl-9vXibnYMo2ScPYD6r0ch02eNjaCfxNq1sP0OGSBIksgW1o1CUuw6Brzl0WQbmoVaRaflVdiDp25qFXfG1woIYJw0qF6LZ3_E"/>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">Anna M.</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 text-[#F5B041]">
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 45%: Cream Sheet Area -->
|
||||
<div class="flex-1 bg-background rounded-t-[32px] -mt-6 relative z-10 px-container-margin pt-10 pb-8 flex flex-col items-center text-center shadow-[0px_-8px_30px_rgba(0,0,0,0.04)]">
|
||||
<!-- Sheet Handle (Subtle UI Detail) -->
|
||||
<div class="absolute top-4 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-3 mt-2">Welcome to GreenLens!</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant mb-auto px-4 max-w-sm">Identify, understand and care for your plants — effortlessly.</p>
|
||||
<!-- Actions -->
|
||||
<div class="w-full mt-8 mb-6">
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center transition-transform hover:scale-[0.98] active:scale-95 shadow-[0px_8px_16px_rgba(67,120,36,0.15)] mb-4">
|
||||
Let's Go
|
||||
</button>
|
||||
<button class="w-full h-[44px] font-label-lg text-label-lg text-primary-container hover:opacity-80 transition-opacity flex items-center justify-center">
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
<!-- Legal -->
|
||||
<p class="font-label-md text-label-md text-outline px-4 max-w-xs">
|
||||
By continuing you agree to our <a class="underline hover:text-on-surface transition-colors" href="#">Privacy Policy</a> and <a class="underline hover:text-on-surface transition-colors" href="#">Terms</a>
|
||||
</p>
|
||||
</div>
|
||||
</body></html>
|
||||
BIN
design/stitch-onboarding/welcome_to_greenlens/screen.png
Normal file
|
After Width: | Height: | Size: 647 KiB |
@@ -0,0 +1,244 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
|
||||
<title>GreenLens - Welcome</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-primary-fixed-variant": "#2e4f00",
|
||||
"inverse-primary": "#41690c",
|
||||
"surface": "#0c160d",
|
||||
"on-tertiary-fixed": "#171d15",
|
||||
"primary-container": "#a6d66f",
|
||||
"on-error-container": "#ffdad6",
|
||||
"surface-dim": "#0c160d",
|
||||
"surface-container-lowest": "#071008",
|
||||
"secondary-fixed-dim": "#b7ccb5",
|
||||
"secondary": "#b7ccb5",
|
||||
"secondary-container": "#394b39",
|
||||
"on-surface": "#dae6d6",
|
||||
"primary-fixed": "#c0f287",
|
||||
"primary": "#c1f388",
|
||||
"on-primary": "#1e3700",
|
||||
"surface-bright": "#313c31",
|
||||
"tertiary-fixed": "#dee5d6",
|
||||
"error-container": "#93000a",
|
||||
"background": "#0c160d",
|
||||
"on-tertiary-fixed-variant": "#42493e",
|
||||
"on-tertiary": "#2c3229",
|
||||
"on-surface-variant": "#c3c9b6",
|
||||
"inverse-surface": "#dae6d6",
|
||||
"on-secondary": "#233424",
|
||||
"on-primary-container": "#365d00",
|
||||
"inverse-on-surface": "#283328",
|
||||
"surface-variant": "#2d372d",
|
||||
"surface-container-low": "#141e14",
|
||||
"surface-container-highest": "#2d372d",
|
||||
"surface-container-high": "#222d22",
|
||||
"tertiary-fixed-dim": "#c2c9bb",
|
||||
"tertiary-container": "#c3cabc",
|
||||
"outline": "#8d9382",
|
||||
"on-background": "#dae6d6",
|
||||
"on-secondary-fixed": "#0f1f10",
|
||||
"on-error": "#690005",
|
||||
"secondary-fixed": "#d3e8d0",
|
||||
"outline-variant": "#43493a",
|
||||
"error": "#ffb4ab",
|
||||
"on-secondary-fixed-variant": "#394b39",
|
||||
"on-tertiary-container": "#4e554a",
|
||||
"surface-tint": "#a5d56e",
|
||||
"tertiary": "#dfe6d8",
|
||||
"surface-container": "#182218",
|
||||
"on-secondary-container": "#a6bba4",
|
||||
"primary-fixed-dim": "#a5d56e",
|
||||
"on-primary-fixed": "#0f2000"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.25rem",
|
||||
"lg": "0.5rem",
|
||||
"xl": "0.75rem",
|
||||
"full": "9999px"
|
||||
},
|
||||
"spacing": {
|
||||
"unit": "8px",
|
||||
"gutter": "24px",
|
||||
"margin-mobile": "16px",
|
||||
"margin-desktop": "64px",
|
||||
"container-max": "1280px",
|
||||
"xs": "8px",
|
||||
"xl": "32px",
|
||||
"base": "4px",
|
||||
"sm": "12px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"container-margin": "20px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display": [
|
||||
"Epilogue"
|
||||
],
|
||||
"headline-md": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-lg": [
|
||||
"Manrope"
|
||||
],
|
||||
"headline-lg": [
|
||||
"Epilogue"
|
||||
],
|
||||
"label-sm": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"Epilogue"
|
||||
],
|
||||
"body-md": [
|
||||
"Manrope"
|
||||
],
|
||||
"label-md": [
|
||||
"JetBrains Mono"
|
||||
],
|
||||
"label-lg": [
|
||||
"JetBrains Mono"
|
||||
]
|
||||
},
|
||||
"fontSize": {
|
||||
"display": [
|
||||
"64px",
|
||||
{
|
||||
"lineHeight": "1.1",
|
||||
"letterSpacing": "-0.02em",
|
||||
"fontWeight": "700"
|
||||
}
|
||||
],
|
||||
"headline-md": [
|
||||
"28px",
|
||||
{
|
||||
"lineHeight": "1.3",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"body-lg": [
|
||||
"18px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"headline-lg": [
|
||||
"40px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"letterSpacing": "-0.01em",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"label-sm": [
|
||||
"12px",
|
||||
{
|
||||
"lineHeight": "1.0",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontWeight": "500"
|
||||
}
|
||||
],
|
||||
"headline-lg-mobile": [
|
||||
"32px",
|
||||
{
|
||||
"lineHeight": "1.2",
|
||||
"fontWeight": "600"
|
||||
}
|
||||
],
|
||||
"body-md": [
|
||||
"16px",
|
||||
{
|
||||
"lineHeight": "1.6",
|
||||
"fontWeight": "400"
|
||||
}
|
||||
],
|
||||
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
|
||||
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface h-screen w-full overflow-hidden flex flex-col font-body-md select-none">
|
||||
<!-- Top 55%: Hero Image Area -->
|
||||
<div class="h-[440px] md:h-[480px] w-full relative shrink-0">
|
||||
<!-- Hero Background Image -->
|
||||
<div class="absolute inset-0 bg-cover bg-center" data-alt="A stunning, high-resolution botanical photograph of a lush indoor jungle featuring large Monstera deliciosa and Calathea plants. The composition is shot from a slightly elevated angle, capturing the vibrant green, textured leaves bathed in soft, natural morning light pouring through a nearby window. The background features a subtle, warm cream-colored wall that perfectly aligns with a premium minimalist light-mode app aesthetic. The depth of field is shallow, keeping the foreground leaves razor-sharp while gently blurring the background, creating a calm, focused, and high-end lifestyle magazine mood." style="background-image: url('../../../assets/welcome_hero_vertical.png')"></div>
|
||||
<!-- Subtle Top Gradient for Text Legibility -->
|
||||
<div class="absolute top-0 left-0 w-full h-32 bg-gradient-to-b from-surface/80 to-transparent"></div>
|
||||
<!-- Header: Logo & App Store Badge -->
|
||||
<div class="absolute top-12 left-0 w-full px-container-margin flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 text-white">
|
||||
<span class="material-symbols-outlined text-[28px]" style="font-variation-settings: 'FILL' 1;">spa</span>
|
||||
<span class="font-display text-display tracking-tight leading-none text-white pt-1">Green<span class="text-[#8ba885]">Lens</span></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 bg-surface-container-lowest/40 backdrop-blur-md border border-white/20 rounded-full px-3 py-1.5 shadow-sm text-white">
|
||||
<span class="material-symbols-outlined text-[14px] text-white" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="font-label-md text-label-md text-white">4.8</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Floating Testimonial Card -->
|
||||
<div class="absolute bottom-12 left-container-margin right-container-margin bg-surface-container-lowest/75 backdrop-blur-md rounded-[16px] p-4 shadow-[0px_4px_20px_rgba(0,0,0,0.5)] border border-surface-variant/50">
|
||||
<p class="font-body-md text-body-md text-on-surface mb-3 italic leading-relaxed">"Finally my plants stay alive! Highly recommend."</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<img class="w-8 h-8 rounded-full object-cover shadow-sm" data-alt="A professional, high-quality portrait photograph of a young woman with a warm, friendly smile, designed to be used as a circular avatar in a premium app interface. She has natural, effortless styling and is softly lit by diffused natural daylight. The background is a very soft, out-of-focus hint of an indoor plant shop or greenhouse, featuring muted greens and warm cream tones to perfectly match a sophisticated, minimalist light-mode aesthetic. The image exudes trust and approachability." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCqKHPbHjvPxBBSroxRXwyNuYGln0ydKqum11jR-kIoUoBeWzrSqByhgdt-maLp0b9HbdyDabUsv2PBnqZy8oIBAGSjHmtG9PhPiWARa9AcQrlRAtKWOAVYdD3y03G9Y0ilbQMz4MvFDBv57IqGO7xcp4cEAddUgjS1sgPskTA6Rl-9vXibnYMo2ScPYD6r0ch02eNjaCfxNq1sP0OGSBIksgW1o1CUuw6Brzl0WQbmoVaRaflVdiDp25qFXfG1woIYJw0qF6LZ3_E"/>
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">Anna M.</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5 text-primary">
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom 45%: Cream Sheet Area -->
|
||||
<div class="flex-1 bg-background rounded-t-[32px] -mt-6 relative z-10 px-container-margin pt-10 pb-8 flex flex-col items-center text-center shadow-[0px_-8px_30px_rgba(0,0,0,0.2)]">
|
||||
<!-- Sheet Handle (Subtle UI Detail) -->
|
||||
<div class="absolute top-4 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-3 mt-2">Welcome to GreenLens!</h1>
|
||||
<p class="font-body-lg text-body-lg text-on-surface-variant mb-auto px-4 max-w-sm">Identify, understand and care for your plants — effortlessly.</p>
|
||||
<!-- Actions -->
|
||||
<div class="w-full mt-8 mb-6">
|
||||
<button class="w-full h-[56px] bg-primary-container text-on-primary-container font-label-lg text-label-lg rounded-[14px] flex items-center justify-center transition-transform hover:scale-[0.98] active:scale-95 shadow-[0px_8px_16px_rgba(0,0,0,0.2)] mb-4">
|
||||
Let's Go
|
||||
</button>
|
||||
<button class="w-full h-[44px] font-label-lg text-label-lg text-primary-container hover:opacity-80 transition-opacity flex items-center justify-center">
|
||||
Log in
|
||||
</button>
|
||||
</div>
|
||||
<!-- Legal -->
|
||||
<p class="font-label-md text-label-md text-outline px-4 max-w-xs">
|
||||
By continuing you agree to our <a class="underline hover:text-on-surface transition-colors" href="#">Privacy Policy</a> and <a class="underline hover:text-on-surface transition-colors" href="#">Terms</a>
|
||||
</p>
|
||||
</div>
|
||||
</body></html>
|
||||
|
After Width: | Height: | Size: 645 KiB |