Initial commit for Greenlens

This commit is contained in:
Timo Knuth
2026-03-16 21:31:46 +01:00
parent 307135671f
commit 05d4f6e78b
573 changed files with 54233 additions and 1891 deletions

View File

@@ -0,0 +1,349 @@
'use client'
import { useRef, useState } from 'react'
import { useLang } from '@/context/LangContext'
export default function BrownLeaf() {
const { t } = useLang()
const bl = t.brownLeaf
const [sliderVal, setSliderVal] = useState(50)
const containerRef = useRef<HTMLDivElement>(null)
const proofs = [
{
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#e07a50" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
</svg>
),
title: bl.proof1title,
desc: bl.proof1desc,
color: '#e07a50',
},
{
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#56a074" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z" />
<path d="M12.56 6.6A10.97 10.97 0 0 0 14 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 0 1-11.91 4.97" />
</svg>
),
title: bl.proof2title,
desc: bl.proof2desc,
color: '#56a074',
},
{
icon: (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#3d7a56" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9 3h6l-1 7H10z" />
<path d="M12 10v4" />
<path d="M8 21v-4a4 4 0 0 1 8 0v4" />
<path d="M6 21h12" />
<path d="M4 10h3" />
<path d="M17 10h3" />
</svg>
),
title: bl.proof3title,
desc: bl.proof3desc,
color: '#3d7a56',
},
]
return (
<section className="brownleaf" id="brownleaf" aria-labelledby="bl-heading">
<div className="container">
{/* Header */}
<header className="bl-header reveal">
<p className="tag">{bl.tag}</p>
<h2 id="bl-heading">
{bl.headline}<br />
<em>{bl.sub}</em>
</h2>
<p className="bl-desc">{bl.desc}</p>
</header>
{/* Before / After Slider */}
<div className="bl-slider-wrap reveal delay-1" ref={containerRef}>
<div
className="bl-slider-track"
style={{ '--pos': `${sliderVal}%` } as React.CSSProperties}
aria-label={bl.sliderLabel}
>
{/* BEFORE brown/unhealthy (always visible) */}
<div className="bl-panel bl-before" aria-hidden="true">
<div className="bl-panel-img bl-before-img" />
<div className="bl-panel-overlay bl-before-overlay" />
<span className="bl-label bl-label-before">{bl.before}</span>
</div>
{/* AFTER healthy plant (clips from right) */}
<div
className="bl-panel bl-after"
style={{ clipPath: `inset(0 0 0 ${sliderVal}%)` }}
aria-hidden="true"
>
<div className="bl-panel-img bl-after-img" />
<div className="bl-panel-overlay bl-after-overlay" />
<span className="bl-label bl-label-after">{bl.after}</span>
</div>
{/* Divider line */}
<div
className="bl-divider"
style={{ left: `${sliderVal}%` }}
aria-hidden="true"
>
<div className="bl-divider-handle">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M9 18l-6-6 6-6" />
<path d="M15 6l6 6-6 6" />
</svg>
</div>
</div>
{/* Range input for accessibility & interaction */}
<input
type="range"
min={0}
max={100}
value={sliderVal}
onChange={e => setSliderVal(Number(e.target.value))}
className="bl-range"
aria-label={bl.sliderLabel}
/>
</div>
{/* Scan badge overlay */}
<div className="bl-scan-badge">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9.5 2a2.5 2.5 0 0 1 5 0v.5a2 2 0 0 0 1.5 1.94V6a2 2 0 0 0 2 2h.5a2.5 2.5 0 0 1 0 5H18a2 2 0 0 0-2 2v.56A2.5 2.5 0 0 1 14.5 22h-5A2.5 2.5 0 0 1 7 19.56V18a2 2 0 0 0-2-2h-.5a2.5 2.5 0 0 1 0-5H5a2 2 0 0 0 2-2V4.44A2 2 0 0 0 8.5 2.5z" />
<circle cx="12" cy="12" r="2" />
</svg>
Botanical Intelligence
</div>
</div>
{/* Success Story Cards */}
<div className="bl-proofs">
{proofs.map((p, i) => (
<div className={`bl-proof reveal delay-${i + 1}`} key={p.title}>
<div className="bl-proof-icon" style={{ background: `${p.color}18`, border: `1px solid ${p.color}30` }}>
{p.icon}
</div>
<div>
<h4 className="bl-proof-title">{p.title}</h4>
<p className="bl-proof-desc">{p.desc}</p>
</div>
</div>
))}
</div>
</div>
<style jsx>{`
.brownleaf {
padding: var(--s16) 0;
background: var(--dark);
position: relative;
overflow: hidden;
}
.brownleaf::before {
content: '';
position: absolute;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(224,122,80,0.08) 0%, transparent 70%);
top: -100px;
left: -200px;
pointer-events: none;
}
.bl-header {
text-align: center;
max-width: 640px;
margin: 0 auto var(--s12);
}
.bl-header h2 {
color: var(--cream);
margin-bottom: var(--s3);
}
.bl-header h2 em {
display: block;
font-style: italic;
color: var(--green-light);
}
.bl-desc {
color: var(--text-light);
font-size: 1rem;
line-height: 1.75;
}
/* Slider */
.bl-slider-wrap {
position: relative;
max-width: 800px;
margin: 0 auto var(--s12);
}
.bl-slider-track {
position: relative;
border-radius: var(--r-xl);
overflow: hidden;
aspect-ratio: 16/9;
user-select: none;
box-shadow: 0 30px 80px rgba(0,0,0,0.4), 0 0 0 1px rgba(244,241,232,0.06);
cursor: col-resize;
}
.bl-panel {
position: absolute;
inset: 0;
}
.bl-panel-img {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
}
.bl-before-img {
background-image: url(/unhealthy-plant.png);
}
.bl-after-img {
background-image: url(/plant-collection.png);
filter: saturate(1.3) brightness(1.05);
}
.bl-panel-overlay {
position: absolute;
inset: 0;
}
.bl-before-overlay {
background: linear-gradient(135deg, rgba(80,40,10,0.35) 0%, transparent 60%);
}
.bl-after-overlay {
background: linear-gradient(135deg, rgba(13,40,20,0.2) 0%, transparent 60%);
}
.bl-label {
position: absolute;
bottom: 1.2rem;
font-size: 0.7rem;
font-weight: 800;
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(244,241,232,0.9);
background: rgba(0,0,0,0.45);
backdrop-filter: blur(8px);
border-radius: 999px;
padding: 0.3rem 0.8rem;
border: 1px solid rgba(244,241,232,0.12);
}
.bl-label-before { left: 1.2rem; }
.bl-label-after { right: 1.2rem; }
/* Divider */
.bl-divider {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: rgba(244,241,232,0.7);
transform: translateX(-50%);
pointer-events: none;
z-index: 10;
}
.bl-divider-handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
color: var(--dark);
}
/* Range input */
.bl-range {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: col-resize;
z-index: 20;
margin: 0;
}
/* Scan badge */
.bl-scan-badge {
position: absolute;
top: 1.2rem;
right: 1.2rem;
display: flex;
align-items: center;
gap: 0.4rem;
background: rgba(19,31,22,0.85);
backdrop-filter: blur(12px);
border: 1px solid rgba(86,160,116,0.3);
border-radius: var(--r-md);
padding: 0.5rem 0.9rem;
color: var(--green-light);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
z-index: 5;
pointer-events: none;
}
/* Proof cards */
.bl-proofs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--s3);
max-width: 800px;
margin: 0 auto;
}
.bl-proof {
display: flex;
align-items: flex-start;
gap: var(--s2);
background: rgba(244,241,232,0.04);
border: 1px solid rgba(244,241,232,0.08);
border-radius: var(--r-lg);
padding: var(--s3);
}
.bl-proof-icon {
width: 44px;
height: 44px;
min-width: 44px;
border-radius: var(--r-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
}
.bl-proof-title {
font-family: var(--body);
font-size: 0.82rem;
font-weight: 700;
color: var(--cream);
margin-bottom: 3px;
}
.bl-proof-desc {
font-size: 0.75rem;
color: var(--text-light);
line-height: 1.5;
}
@media (max-width: 768px) {
.bl-proofs {
grid-template-columns: 1fr;
}
}
`}</style>
</section>
)
}

View File

@@ -0,0 +1,74 @@
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { useLang } from '@/context/LangContext'
import { hasAndroidStoreUrl, hasIosStoreUrl, siteConfig } from '@/lib/site'
export default function CTA() {
const { t } = useLang()
return (
<section className="cta-section" id="cta" aria-labelledby="cta-heading">
<div className="container">
<div className="cta-card">
<div className="cta-card-glow" aria-hidden="true" />
<div className="cta-content">
<p className="tag" style={{ color: 'var(--green-light)' }}>{t.cta.tag}</p>
<h2 id="cta-heading">
{t.cta.h2a}<br />
<em>{t.cta.h2em}</em>
</h2>
<p>{t.cta.desc}</p>
<div className="store-row">
<a
href={hasIosStoreUrl ? siteConfig.iosAppStoreUrl : '/support'}
className="store-btn"
id="cta-appstore"
aria-label="App Store or support"
>
<svg className="store-btn-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
</svg>
<div>
<span className="store-btn-small">{hasIosStoreUrl ? t.cta.apple : t.cta.support}</span>
<span className="store-btn-big">{hasIosStoreUrl ? 'App Store' : t.cta.supportLabel}</span>
</div>
</a>
<a
href={hasAndroidStoreUrl ? siteConfig.androidPlayStoreUrl : `mailto:${siteConfig.supportEmail}`}
className="store-btn"
id="cta-googleplay"
aria-label="Google Play or contact"
>
<svg className="store-btn-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M3.18 23.76c.33.18.7.24 1.08.17L14.84 12 11.17 8.33 3.18 23.76zm17.16-12.38-3.32-1.89-3.78 3.78 3.78 3.78 3.34-1.9c.95-.54.95-1.92-.02-2.77zM2.01 1.11C1.7 1.44 1.5 1.97 1.5 2.67v18.66c0 .7.2 1.23.51 1.56l.08.08L12.16 12v-.22L2.09 1.03l-.08.08zm9.16 10.67 2.67 2.67-10.5 5.97 7.83-8.64z" />
</svg>
<div>
<span className="store-btn-small">{hasAndroidStoreUrl ? t.cta.google : t.cta.contact}</span>
<span className="store-btn-big">{hasAndroidStoreUrl ? 'Google Play' : t.cta.email}</span>
</div>
</a>
</div>
<p className="cta-footnote">
{hasIosStoreUrl || hasAndroidStoreUrl ? t.cta.liveNote : t.cta.comingSoon} <Link href="/support">Support</Link>
</p>
</div>
<div className="cta-visual" aria-hidden="true">
<Image
src="/plant-collection.png"
alt=""
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
<div className="cta-visual-overlay" />
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,212 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useLang } from '@/context/LangContext';
const faqs = [
{
question: {
en: 'How does GreenLens identify a plant?',
de: 'Wie erkennt GreenLens eine Pflanze?',
es: 'Como identifica GreenLens una planta?'
},
answer: {
en: 'GreenLens analyzes the plant photo and combines that with app-side care guidance so you can move from scan to next steps faster.',
de: 'GreenLens analysiert das Pflanzenfoto und verbindet das Ergebnis mit Pflegehinweisen in der App, damit du schneller zu klaren naechsten Schritten kommst.',
es: 'GreenLens analiza la foto de la planta y combina el resultado con indicaciones de cuidado dentro de la app para que avances mas rapido.'
}
},
{
question: {
en: 'Is GreenLens free to use?',
de: 'Ist GreenLens kostenlos?',
es: 'Es GreenLens gratuito?'
},
answer: {
en: 'GreenLens includes free functionality plus paid options such as subscriptions and credit top-ups for advanced AI features.',
de: 'GreenLens bietet kostenlose Funktionen und zusaetzlich kostenpflichtige Optionen wie Abos und Credit-Top-ups fuer erweiterte KI-Funktionen.',
es: 'GreenLens incluye funciones gratuitas y tambien opciones de pago como suscripciones y creditos para funciones de IA mas umfangreiche.'
}
},
{
question: {
en: 'Can I use it offline?',
de: 'Kann ich die App offline nutzen?',
es: 'Puedo usarla sin conexion?'
},
answer: {
en: 'Some experiences may require a connection, especially for scan-related features. Saved information inside the app can remain available afterward.',
de: 'Einige Funktionen benoetigen eine Verbindung, besonders scanbezogene Features. Gespeicherte Informationen in der App koennen danach weiter verfuegbar bleiben.',
es: 'Algunas funciones requieren conexion, especialmente las relacionadas con escaneos. La informacion guardada puede seguir disponible despues.'
}
},
{
question: {
en: 'What kind of plants can I use it for?',
de: 'Fuer welche Pflanzen kann ich die App nutzen?',
es: 'Para que tipo de plantas puedo usar la app?'
},
answer: {
en: 'GreenLens is built for everyday plant owners who want help with houseplants, garden plants, and general care questions.',
de: 'GreenLens richtet sich an Pflanzenbesitzer, die Hilfe bei Zimmerpflanzen, Gartenpflanzen und allgemeinen Pflegefragen wollen.',
es: 'GreenLens esta pensada para personas que quieren ayuda con plantas de interior, jardin y preguntas generales de cuidado.'
}
},
{
question: {
en: 'How do I start my plant collection?',
de: 'Wie starte ich meine Pflanzensammlung?',
es: 'Como empiezo mi coleccion de plantas?'
},
answer: {
en: 'Start with a scan, review the result, and save the plant to your collection to keep notes, reminders, and follow-up care in one place.',
de: 'Starte mit einem Scan, pruefe das Ergebnis und speichere die Pflanze in deiner Sammlung, damit Notizen, Erinnerungen und Pflege an einem Ort bleiben.',
es: 'Empieza con un escaneo, revisa el resultado y guarda la planta en tu coleccion para mantener notas, recordatorios y cuidado en un solo lugar.'
}
}
];
const TEXT = {
de: { tag: 'Fragen', h2: ['Haeufig gestellte', 'Fragen'], desc: 'Alles, was du ueber GreenLens und den Einstieg wissen musst.' },
en: { tag: 'Questions', h2: ['Frequently Asked', 'Questions'], desc: 'Everything you need to know about GreenLens and getting started.' },
es: { tag: 'Preguntas', h2: ['Preguntas', 'Frecuentes'], desc: 'Todo lo que necesitas saber sobre GreenLens y el inicio.' },
}
export default function FAQ() {
const { lang } = useLang();
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const text = TEXT[lang];
return (
<section id="faq" className="section faq">
<div className="container">
<div className="section-header reveal">
<span className="tag">{text.tag}</span>
<h2>{text.h2[0]} <em>{text.h2[1]}</em></h2>
<p className="section-desc">
{text.desc}
</p>
</div>
<div className="faq-grid reveal delay-1">
{faqs.map((faq, index) => (
<div
key={index}
className={`faq-item ${activeIndex === index ? 'active' : ''}`}
onClick={() => setActiveIndex(activeIndex === index ? null : index)}
>
<div className="faq-question">
<h3>{faq.question[lang]}</h3>
<span className="faq-icon">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M5 7.5L10 12.5L15 7.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
</div>
<AnimatePresence>
{activeIndex === index && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className="faq-answer"
>
<p>{faq.answer[lang]}</p>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
</div>
<style jsx>{`
.faq {
background: var(--cream-alt);
padding: var(--s16) 0;
}
.section-header {
text-align: center;
max-width: 600px;
margin: 0 auto var(--s12);
}
.section-header h2 em {
display: block;
font-style: italic;
color: var(--green);
}
.section-desc {
font-size: 1.05rem;
color: var(--muted);
margin-top: var(--s2);
}
.faq-grid {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: var(--s3);
}
.faq-item {
background: var(--white);
border-radius: var(--r-lg);
border: 1px solid rgba(19, 31, 22, 0.04);
cursor: pointer;
overflow: hidden;
transition: border var(--t), box-shadow var(--t), transform var(--t);
}
.faq-item:hover {
border-color: rgba(42, 92, 63, 0.15);
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(19, 31, 22, 0.05);
}
.faq-item.active {
border-color: var(--green);
box-shadow: 0 15px 40px rgba(42, 92, 63, 0.1);
}
.faq-question {
padding: var(--s4) var(--s6);
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.faq-question h3 {
font-family: var(--body);
font-size: 1.05rem;
font-weight: 700;
color: var(--dark);
margin: 0;
}
.faq-icon {
color: var(--muted);
transition: transform var(--t);
}
.faq-item.active .faq-icon {
transform: rotate(180deg);
color: var(--green);
}
.faq-answer {
padding: 0 var(--s6) var(--s4);
}
.faq-answer p {
color: var(--muted);
font-size: 0.95rem;
line-height: 1.6;
margin: 0;
}
@media (max-width: 768px) {
.faq-question {
padding: var(--s3) var(--s4);
}
.faq-answer {
padding: 0 var(--s4) var(--s3);
}
}
`}</style>
</section>
);
}

View File

@@ -0,0 +1,165 @@
'use client'
import Image from 'next/image'
import { useLang } from '@/context/LangContext'
const featurePillIcons = [
<svg key="a" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
</svg>,
<svg key="b" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" />
</svg>,
<svg key="c" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
<circle cx="12" cy="10" r="3" />
</svg>,
]
const PILL_KEYS = [
{ titleKey: 'pillRemindersTitle', descKey: 'pillRemindersDesc' },
{ titleKey: 'pillDiagTitle', descKey: 'pillDiagDesc' },
{ titleKey: 'pillLocationTitle', descKey: 'pillLocationDesc' },
] as const
const PILL_TEXT = {
de: [
{ title: 'Smarte Erinnerungen', desc: 'Vergiss nie mehr das Gießen personalisiert für jede Pflanze.' },
{ title: 'Diagnose & Hilfe', desc: 'KI erkennt Krankheiten und Schädlinge sofort.' },
{ title: 'Standort-Tipps', desc: 'Pflegehinweise basierend auf deinem Klima und Licht.' },
],
en: [
{ title: 'Smart Reminders', desc: 'Never forget watering again personalized for every plant.' },
{ title: 'Diagnosis & Help', desc: 'AI detects diseases and pests instantly.' },
{ title: 'Location Tips', desc: 'Care advice based on your climate and light conditions.' },
],
es: [
{ title: 'Recordatorios inteligentes', desc: 'Nunca olvides regar personalizado para cada planta.' },
{ title: 'Diagnóstico y ayuda', desc: 'La IA detecta enfermedades y plagas al instante.' },
{ title: 'Consejos por ubicación', desc: 'Consejos basados en tu clima y condiciones de luz.' },
],
}
const CARD_TEXT = {
de: {
chip1: 'KI Scan', h3a: 'Scan it.', pa: 'Richte die Kamera auf jede Pflanze GreenLens erkennt sie in Sekundenbruchteilen und liefert alle Infos.',
chip2: 'Tracking', h3b: 'Track it.', pb: 'Gießplan, Lichtbedarf und Wachstum alles in einer Timeline.',
chip3: 'Sammlung', h3c: 'Grow it.', pc: 'Baue deine digitale Pflanzenbibliothek auf mit Fotos und Notizen.',
altA: 'Person scannt eine Pflanze mit der GreenLens App',
altB: 'Pflanzen auf einem Regal mit Pflegeplänen',
altC: 'Pflanzensammlung im Urban Jungle Stil',
},
en: {
chip1: 'AI Scan', h3a: 'Scan it.', pa: 'Point your camera at any plant GreenLens identifies it in milliseconds and delivers all the info.',
chip2: 'Tracking', h3b: 'Track it.', pb: 'Watering schedule, light needs and growth all in one timeline.',
chip3: 'Collection', h3c: 'Grow it.', pc: 'Build your digital plant library with photos and notes.',
altA: 'Person scanning a plant with the GreenLens app',
altB: 'Plants on a shelf with care plans',
altC: 'Plant collection in urban jungle style',
},
es: {
chip1: 'Escaneo IA', h3a: 'Escanéala.', pa: 'Apunta la cámara a cualquier planta GreenLens la identifica en milisegundos y entrega toda la información.',
chip2: 'Seguimiento', h3b: 'Monitoréala.', pb: 'Plan de riego, necesidades de luz y crecimiento todo en una línea de tiempo.',
chip3: 'Colección', h3c: 'Hazla crecer.', pc: 'Construye tu biblioteca digital de plantas con fotos y notas.',
altA: 'Persona escaneando una planta con la app GreenLens',
altB: 'Plantas en un estante con planes de cuidado',
altC: 'Colección de plantas estilo jungla urbana',
},
}
export default function Features() {
const { lang, t } = useLang()
const cards = CARD_TEXT[lang]
const pills = PILL_TEXT[lang]
return (
<section className="features" id="features" aria-labelledby="features-heading">
<div className="container">
{/* Header */}
<header className="features-header reveal">
<p className="tag">{t.features.tag}</p>
<h2 id="features-heading">
{t.features.h2a}<br />{t.features.h2b}
</h2>
<p>{t.features.desc}</p>
</header>
{/* Bento grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
{/* Left large card: Scan it */}
<div className="bento-card bento-large reveal">
<Image
src="/scan-feature.png"
alt={cards.altA}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="bento-card-img"
style={{ objectFit: 'cover' }}
/>
<div className="bento-card-overlay" />
<div className="bento-card-content">
<span className="bento-chip">{cards.chip1}</span>
<h3>{cards.h3a}</h3>
<p>{cards.pa}</p>
</div>
</div>
{/* Right two stacked cards */}
<div style={{ display: 'grid', gridTemplateRows: '1fr 1fr', gap: '1rem' }}>
<div className="bento-card bento-small reveal delay-1">
<Image
src="/track-feature.png"
alt={cards.altB}
fill
sizes="(max-width: 768px) 100vw, 25vw"
className="bento-card-img"
style={{ objectFit: 'cover' }}
/>
<div className="bento-card-overlay" />
<div className="bento-card-content">
<span className="bento-chip">{cards.chip2}</span>
<h3>{cards.h3b}</h3>
<p>{cards.pb}</p>
</div>
</div>
<div className="bento-card bento-small reveal delay-2">
<Image
src="/plant-collection.png"
alt={cards.altC}
fill
sizes="(max-width: 768px) 100vw, 25vw"
className="bento-card-img"
style={{ objectFit: 'cover' }}
/>
<div className="bento-card-overlay" />
<div className="bento-card-content">
<span className="bento-chip">{cards.chip3}</span>
<h3>{cards.h3c}</h3>
<p>{cards.pc}</p>
</div>
</div>
</div>
</div>
{/* Feature pills */}
<div className="features-pills">
{pills.map((f, i) => (
<div className={`feature-pill reveal delay-${i + 1}`} key={f.title}>
<div className="feature-pill-icon">{featurePillIcons[i]}</div>
<div className="feature-pill-text">
<h4>{f.title}</h4>
<p>{f.desc}</p>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,50 @@
'use client'
import Link from 'next/link'
import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
const LINK_HREFS = [
['#features', '#intelligence', '#cta', '/support'],
['/#how', '/#faq', '/support'],
['/imprint', '/privacy'],
]
export default function Footer() {
const { t } = useLang()
return (
<footer className="footer" id="footer">
<div className="container">
<div className="footer-inner">
<div>
<Link href="/" className="nav-logo" style={{ fontSize: '1.5rem' }}>
GREENLENS
</Link>
<p className="footer-brand-desc">{t.footer.brand}</p>
</div>
{t.footer.cols.map((col, ci) => (
<div className="footer-col" key={col.title}>
<div className="footer-col-title">{col.title}</div>
{col.links.map((label, li) => (
<Link key={label} href={LINK_HREFS[ci]?.[li] ?? '/support'}>
{label}
</Link>
))}
</div>
))}
</div>
<div className="footer-brand-xl" aria-hidden="true">GREENLENS</div>
<div className="footer-bottom">
<p>{t.footer.copy}</p>
<a href={`mailto:${siteConfig.supportEmail}`} className="footer-contact">
{siteConfig.supportEmail}
</a>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,189 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useLang } from '@/context/LangContext'
function useReveal() {
useEffect(() => {
const els = document.querySelectorAll('.reveal, .reveal-fade')
const obs = new IntersectionObserver(
(entries) => entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('active') }),
{ threshold: 0.12 }
)
els.forEach(el => obs.observe(el))
return () => obs.disconnect()
}, [])
}
export default function Hero() {
useReveal()
const bgRef = useRef<HTMLDivElement>(null)
const { t } = useLang()
const [segChoice, setSegChoice] = useState<0 | 1 | null>(null)
useEffect(() => {
const handle = () => {
if (bgRef.current) {
const y = window.scrollY * 0.3
bgRef.current.style.transform = `scale(1.08) translateY(${y}px)`
}
}
window.addEventListener('scroll', handle, { passive: true })
setTimeout(() => bgRef.current?.classList.add('loaded'), 100)
return () => window.removeEventListener('scroll', handle)
}, [])
const handleSeg = (choice: 0 | 1) => {
setSegChoice(choice)
const target = choice === 0 ? '#features' : '#brownleaf'
const el = document.querySelector(target)
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
return (
<section className="hero" id="hero" aria-label="Hero">
{/* Background */}
<div
ref={bgRef}
className="hero-bg-image"
style={{ backgroundImage: 'url(/hero-plant.png)' }}
aria-hidden="true"
/>
<div className="hero-bg-overlay" aria-hidden="true" />
<div className="container">
{/* Content */}
<div className="hero-content">
<div className="hero-eyebrow reveal">
<span className="hero-eyebrow-dot" />
<span className="hero-eyebrow-text">{t.hero.eyebrow}</span>
</div>
<h1 className="reveal delay-1">
{t.hero.h1a}<br />{t.hero.h1b}<br />
<em>{t.hero.h1em}</em>
</h1>
<p className="hero-desc reveal delay-2">
{t.hero.desc}
</p>
<div className="hero-actions reveal delay-3">
<a href="#cta" className="btn-primary" id="hero-cta-primary">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z" />
<path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12" />
</svg>
&nbsp;{t.hero.primary}
</a>
<a href="#features" className="btn-outline" id="hero-cta-secondary">
{t.hero.secondary}
</a>
</div>
{/* Segmentation widget */}
<div className="hero-seg reveal delay-4" role="group" aria-label={t.hero.segTitle}>
<p className="hero-seg-title">{t.hero.segTitle}</p>
<div className="hero-seg-options">
<button
className={`hero-seg-btn${segChoice === 0 ? ' hero-seg-btn--active' : ''}`}
onClick={() => handleSeg(0)}
aria-pressed={segChoice === 0}
>
<span className="hero-seg-radio" aria-hidden="true" />
{t.hero.segOpt1}
</button>
<button
className={`hero-seg-btn${segChoice === 1 ? ' hero-seg-btn--active' : ''}`}
onClick={() => handleSeg(1)}
aria-pressed={segChoice === 1}
>
<span className="hero-seg-radio" aria-hidden="true" />
{t.hero.segOpt2}
</button>
</div>
</div>
</div>
{/* Video 16:9 */}
<div className="hero-visual reveal-fade delay-2">
<div className="hero-video-card hero-video-16-9">
<video autoPlay loop muted playsInline aria-label="GreenLens App Demo">
<source src="/GreenLensHype.mp4" type="video/mp4" />
</video>
<div className="hero-video-card-overlay" />
<div className="hero-video-badge">
<span className="hero-video-badge-dot" />
{t.hero.badge}
</div>
</div>
</div>
</div>
<style jsx>{`
.hero-seg {
margin-top: 2rem;
background: rgba(244,241,232,0.06);
border: 1px solid rgba(244,241,232,0.12);
border-radius: 16px;
padding: 1.2rem 1.5rem;
max-width: 460px;
}
.hero-seg-title {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(244,241,232,0.55);
margin-bottom: 0.8rem;
font-family: var(--body);
}
.hero-seg-options {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.hero-seg-btn {
display: flex;
align-items: center;
gap: 0.75rem;
background: transparent;
border: 1.5px solid rgba(244,241,232,0.15);
border-radius: 10px;
padding: 0.65rem 1rem;
color: rgba(244,241,232,0.75);
font-size: 0.82rem;
font-weight: 500;
text-align: left;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s;
font-family: var(--body);
}
.hero-seg-btn:hover {
background: rgba(244,241,232,0.08);
border-color: rgba(244,241,232,0.3);
color: rgba(244,241,232,0.95);
}
.hero-seg-btn--active {
background: rgba(86,160,116,0.15);
border-color: rgba(86,160,116,0.5);
color: #fff;
}
.hero-seg-radio {
width: 14px;
height: 14px;
min-width: 14px;
border-radius: 50%;
border: 2px solid rgba(244,241,232,0.35);
display: inline-block;
transition: border-color 0.2s, background 0.2s;
}
.hero-seg-btn--active .hero-seg-radio {
border-color: var(--green-light);
background: var(--green-light);
box-shadow: 0 0 0 3px rgba(86,160,116,0.2);
}
`}</style>
</section>
)
}

View File

@@ -0,0 +1,82 @@
'use client'
import Image from 'next/image'
import { useLang } from '@/context/LangContext'
const STEPS = {
de: [
{ num: '01', title: 'Pflanze fotografieren', desc: 'Öffne die App, richte die Kamera auf deine Pflanze und tippe auf Scan. Das war\'s schon.' },
{ num: '02', title: 'KI identifiziert sofort', desc: 'In unter einer Sekunde erhältst du den genauen Namen, die Art und alle wichtigen Eckdaten.' },
{ num: '03', title: 'Pflegeplan erhalten', desc: 'GreenLens erstellt automatisch einen personalisierten Pflegeplan passend zu deiner Pflanze und deinem Standort.' },
{ num: '04', title: 'Wachstum verfolgen', desc: 'Dokumentiere Fotos, tracke das Gießen und lass dich an wichtige Pflegetermine erinnern.' },
],
en: [
{ num: '01', title: 'Photograph your plant', desc: 'Open the app, point the camera at your plant and tap Scan. That\'s it.' },
{ num: '02', title: 'AI identifies instantly', desc: 'In under a second you get the exact name, species and all key details.' },
{ num: '03', title: 'Receive care plan', desc: 'GreenLens automatically creates a personalized care plan for your plant and location.' },
{ num: '04', title: 'Track growth', desc: 'Document photos, track watering and get reminded of important care dates.' },
],
es: [
{ num: '01', title: 'Fotografía tu planta', desc: 'Abre la app, apunta la cámara a tu planta y toca Escanear. Eso es todo.' },
{ num: '02', title: 'La IA identifica al instante', desc: 'En menos de un segundo obtienes el nombre exacto, la especie y todos los datos clave.' },
{ num: '03', title: 'Recibe el plan de cuidado', desc: 'GreenLens crea automáticamente un plan de cuidado personalizado para tu planta y ubicación.' },
{ num: '04', title: 'Seguimiento del crecimiento', desc: 'Documenta fotos, registra el riego y recibe recordatorios de citas de cuidado importantes.' },
],
}
const TAG = { de: 'So funktionierts', en: 'How it works', es: 'Cómo funciona' }
const H2 = {
de: ['Einfacher', 'als du', 'denkst.'],
en: ['Simpler', 'than you', 'think.'],
es: ['Más fácil', 'de lo que', 'crees.'],
}
const ALT = {
de: 'GreenLens App in Verwendung Pflanze wird gescannt',
en: 'GreenLens app in use scanning a plant',
es: 'App GreenLens en uso escaneando una planta',
}
export default function HowItWorks() {
const { lang } = useLang()
const steps = STEPS[lang]
const h2 = H2[lang]
return (
<section className="how" id="how" aria-labelledby="how-heading">
<div className="container">
<div className="how-text">
<p className="tag">{TAG[lang]}</p>
<h2 id="how-heading">
{h2[0]}<br />{h2[1]}<br /><em>{h2[2]}</em>
</h2>
<div className="how-steps">
{steps.map((s, i) => (
<div className={`how-step reveal delay-${i + 1}`} key={s.num}>
<div className="how-step-num">{s.num}</div>
<div>
<h4>{s.title}</h4>
<p>{s.desc}</p>
</div>
</div>
))}
</div>
</div>
<div className="how-visual reveal-fade delay-2">
<div className="how-img-wrap" style={{ position: 'relative', height: '500px' }}>
<Image
src="/scan-feature.png"
alt={ALT[lang]}
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover', borderRadius: '24px' }}
/>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,100 @@
'use client'
import Image from 'next/image'
import { useLang } from '@/context/LangContext'
const ICONS = [
<svg key="a" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9.5 2a2.5 2.5 0 0 1 5 0v.5a2 2 0 0 0 1.5 1.94V6a2 2 0 0 0 2 2h.5a2.5 2.5 0 0 1 0 5H18a2 2 0 0 0-2 2v.56A2.5 2.5 0 0 1 14.5 22h-5A2.5 2.5 0 0 1 7 19.56V18a2 2 0 0 0-2-2h-.5a2.5 2.5 0 0 1 0-5H5a2 2 0 0 0 2-2V4.44A2 2 0 0 0 8.5 2.5z" />
<circle cx="12" cy="12" r="2" />
</svg>,
<svg key="b" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" />
</svg>,
<svg key="c" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
<circle cx="12" cy="10" r="3" />
</svg>,
<svg key="d" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--green-light)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20" />
</svg>,
]
const ITEMS = {
de: [
{ title: 'Scan-basierte Erkennung', desc: 'Vom Foto zur besseren Einordnung in wenigen Schritten.' },
{ title: 'Pflegeorientierte Hinweise', desc: 'Hilft dir, naechste Pflegeentscheidungen schneller zu treffen.' },
{ title: 'Sammlung und Verlauf', desc: 'Behalte Scans, Pflanzen und Notizen an einem Ort.' },
{ title: 'Lexikon und Suche', desc: 'Suche Pflanzen und vergleiche Informationen in einer App.' },
],
en: [
{ title: 'Scan-based identification', desc: 'Move from photo to clearer plant context in a few steps.' },
{ title: 'Care-focused guidance', desc: 'Helps you make faster next-step care decisions.' },
{ title: 'Collection and history', desc: 'Keep scans, plants, and notes in one place.' },
{ title: 'Lexicon and search', desc: 'Look up plants and compare information in one app.' },
],
es: [
{ title: 'Identificacion basada en escaneo', desc: 'Pasa de una foto a un contexto mas claro en pocos pasos.' },
{ title: 'Guias centradas en cuidado', desc: 'Te ayuda a decidir los siguientes pasos mas rapido.' },
{ title: 'Coleccion e historial', desc: 'Guarda escaneos, plantas y notas en un solo lugar.' },
{ title: 'Lexico y busqueda', desc: 'Busca plantas y compara informacion dentro de una app.' },
],
}
const TAG_TEXT = { de: 'Technologie', en: 'Technology', es: 'Tecnologia' }
const BODY_TEXT = {
de: 'GreenLens verbindet Scan-Ergebnisse, Pflegekontext und Sammlungsverwaltung in einer App. So kommst du schneller von einem Pflanzenfoto zu einer verstaendlichen Entscheidung.',
en: 'GreenLens combines scan results, care context, and collection management in one app, helping you move from plant photo to a clearer decision faster.',
es: '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.',
}
export default function Intelligence() {
const { lang } = useLang()
const items = ITEMS[lang]
return (
<section className="intelligence" id="intelligence" aria-labelledby="intel-heading">
<div className="container">
<div className="intelligence-text reveal">
<p className="tag">{TAG_TEXT[lang]}</p>
<h2 id="intel-heading">
Botanical<br />
<em>Intelligence.</em>
</h2>
<p>{BODY_TEXT[lang]}</p>
<div className="intelligence-list">
{items.map((item, i) => (
<div className="intelligence-item" key={item.title}>
<div className="intelligence-item-icon">{ICONS[i]}</div>
<div>
<h4>{item.title}</h4>
<p>{item.desc}</p>
</div>
</div>
))}
</div>
</div>
<div className="intelligence-visual reveal-fade delay-2">
<div className="intelligence-img-frame" style={{ position: 'relative', height: '600px', overflow: 'hidden' }}>
<Image
src="/ai-analysis.png"
alt="Botanical AI plant analysis visualization"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
<div className="intelligence-img-overlay" />
<div className="intelligence-overlay-text">
<h3>
Botanical<br />
<em>Intelligence.</em>
</h3>
</div>
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,123 @@
'use client'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useLang } from '@/context/LangContext'
import type { Lang } from '@/lib/i18n'
const LANGS: { code: Lang; label: string; flag: string }[] = [
{ code: 'de', label: 'DE', flag: 'DE' },
{ code: 'en', label: 'EN', flag: 'EN' },
{ code: 'es', label: 'ES', flag: 'ES' },
]
export default function Navbar() {
const [scrolled, setScrolled] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const { lang, setLang, t } = useLang()
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40)
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
return (
<nav className={`navbar${scrolled ? ' scrolled' : ''}`} id="navbar" role="navigation" aria-label="Main navigation">
<div className="container">
<Link href="/" className="nav-logo" aria-label="GreenLens Home">
GREENLENS
</Link>
<div className={`nav-links${menuOpen ? ' nav-links--open' : ''}`}>
<a href="#features" onClick={() => setMenuOpen(false)}>{t.nav.features}</a>
<a href="#intelligence" onClick={() => setMenuOpen(false)}>{t.nav.tech}</a>
<a href="#faq" onClick={() => setMenuOpen(false)}>FAQ</a>
<a href="#how" onClick={() => setMenuOpen(false)}>{t.nav.how}</a>
<Link href="/support" onClick={() => setMenuOpen(false)}>Support</Link>
<a href="#cta" onClick={() => setMenuOpen(false)}>{t.nav.download}</a>
<div className="lang-switcher" role="group" aria-label="Language selector">
{LANGS.map((l) => (
<button
key={l.code}
className={`lang-btn${lang === l.code ? ' lang-btn--active' : ''}`}
onClick={() => {
setLang(l.code)
setMenuOpen(false)
}}
aria-label={`Switch to ${l.label}`}
aria-pressed={lang === l.code}
>
<span>{l.flag}</span>
<span>{l.label}</span>
</button>
))}
</div>
<a href="#cta" className="nav-cta" onClick={() => setMenuOpen(false)}>{t.nav.cta}</a>
</div>
<button
className="nav-hamburger"
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={menuOpen}
onClick={() => setMenuOpen((o) => !o)}
>
<span />
<span />
<span />
</button>
</div>
<style jsx>{`
.nav-links--open {
display: flex !important;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: rgba(19,31,22,0.97);
backdrop-filter: blur(18px);
padding: 1.5rem 2rem;
gap: 1rem;
border-top: 1px solid rgba(244,241,232,0.08);
}
.lang-switcher {
display: flex;
align-items: center;
gap: 2px;
background: rgba(244,241,232,0.07);
border: 1px solid rgba(244,241,232,0.12);
border-radius: 999px;
padding: 3px;
}
.lang-btn {
display: flex;
align-items: center;
gap: 4px;
background: transparent;
border: none;
color: rgba(244,241,232,0.6);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
padding: 4px 10px;
border-radius: 999px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
white-space: nowrap;
}
.lang-btn:hover {
color: rgba(244,241,232,0.9);
background: rgba(244,241,232,0.1);
}
.lang-btn--active {
background: rgba(244,241,232,0.15);
color: #fff;
}
`}</style>
</nav>
)
}

View File

@@ -0,0 +1,56 @@
const testimonials = [
{
stars: '★★★★★',
text: '"Ich kann meine Monstera endlich richtig pflegen! Die App hat sofort erkannt was ihr gefehlt hat und mir einen tagesgenauen Gießplan erstellt."',
name: 'Lena M.',
role: 'Urban Jungle Enthusiastin',
emoji: '🌿',
},
{
stars: '★★★★★',
text: '"Endlich eine App die wirklich funktioniert. In 2 Sekunden wusste ich wie meine unbekannte Pflanze heißt und was sie braucht."',
name: 'Tobias K.',
role: 'Balkonpflanzen-Fan',
emoji: '🌱',
},
{
stars: '★★★★★',
text: '"Das Design ist wunderschön und die KI unglaublich präzise. Meine Pflanzensammlung wächst seitdem doppelt so schnell!"',
name: 'Sarah R.',
role: 'Pflanzen-Influencerin',
emoji: '🪴',
},
]
export default function Testimonials() {
return (
<section className="testimonials" id="testimonials" aria-labelledby="testi-heading">
<div className="container">
<header className="testimonials-header reveal">
<p className="tag">Bewertungen</p>
<h2 id="testi-heading">
Geliebt von<br />Pflanzenfreund:innen.
</h2>
</header>
<div className="testimonials-grid">
{testimonials.map((t, i) => (
<div className={`testi-card reveal delay-${i + 1}`} key={t.name}>
<div className="testi-stars">{t.stars}</div>
<p className="testi-text">{t.text}</p>
<div className="testi-author">
<div className="testi-avatar">{t.emoji}</div>
<div>
<div className="testi-name">{t.name}</div>
<div className="testi-role">{t.role}</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,29 @@
export default function Ticker() {
const items = [
'Scan it',
'Track it',
'Live Design',
'Urban Jungle',
'Botanical KI',
'Pflege-Tipps',
'Scan it',
'Track it',
'Live Design',
'Urban Jungle',
'Botanical KI',
'Pflege-Tipps',
]
return (
<div className="ticker-wrap" aria-hidden="true">
<div className="ticker-track">
{[...items, ...items].map((item, i) => (
<span key={i} className="ticker-item">
{item}
<span className="ticker-dot" />
</span>
))}
</div>
</div>
)
}