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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
'use client'
import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
const CONTENT = {
de: {
title: 'Impressum',
companyLabel: 'Unternehmen',
addressLabel: 'Adresse',
representativeLabel: 'Vertretungsberechtigt',
contactLabel: 'Kontakt',
registryLabel: 'Register',
vatLabel: 'USt-ID',
note: 'Vor der Veroeffentlichung muessen alle rechtlichen Angaben mit den echten Firmendaten ersetzt werden.',
},
en: {
title: 'Imprint',
companyLabel: 'Company',
addressLabel: 'Address',
representativeLabel: 'Represented by',
contactLabel: 'Contact',
registryLabel: 'Registry',
vatLabel: 'VAT ID',
note: 'Replace all legal placeholders with your real company details before publishing the site.',
},
es: {
title: 'Aviso Legal',
companyLabel: 'Empresa',
addressLabel: 'Direccion',
representativeLabel: 'Representante',
contactLabel: 'Contacto',
registryLabel: 'Registro',
vatLabel: 'IVA',
note: 'Sustituye todos los marcadores legales por tus datos reales antes de publicar el sitio.',
},
}
export default function ImprintPage() {
const { lang } = useLang()
const c = CONTENT[lang]
return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
<h1>{c.title}</h1>
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
<p>
<strong>{c.companyLabel}:</strong> {siteConfig.company.legalName}
</p>
<p>
<strong>{c.addressLabel}:</strong> {siteConfig.company.addressLine1}
</p>
{siteConfig.company.addressLine2 ? <p>{siteConfig.company.addressLine2}</p> : null}
<p>{siteConfig.company.country}</p>
<p>
<strong>{c.representativeLabel}:</strong> {siteConfig.company.representative}
</p>
<p>
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
<p>
<strong>{c.registryLabel}:</strong> {siteConfig.company.registry}
</p>
<p>
<strong>{c.vatLabel}:</strong> {siteConfig.company.vatId}
</p>
<p style={{ marginTop: '1rem', fontSize: '0.95rem', opacity: 0.8 }}>{c.note}</p>
</div>
</main>
)
}

View File

@@ -0,0 +1,76 @@
import type { Metadata } from 'next'
import './globals.css'
import { LangProvider } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
export const metadata: Metadata = {
metadataBase: new URL(siteConfig.domain),
title: {
default: 'GreenLens - Plant Identifier and Care Planner',
template: '%s | GreenLens',
},
description:
'GreenLens helps you identify plants, organize your collection, and keep up with care routines in one app.',
keywords: [
'plant identifier by picture',
'plant care app',
'watering reminders',
'houseplant tracker',
'plant identification',
'plant health check',
'Pflanzen App',
'GreenLens',
],
authors: [{ name: siteConfig.name }],
openGraph: {
title: 'GreenLens - Plant Identifier and Care Planner',
description: 'Identify plants, get care guidance, and manage your collection with GreenLens.',
type: 'website',
url: siteConfig.domain,
},
alternates: {
canonical: '/',
languages: {
de: '/',
en: '/',
es: '/',
'x-default': '/',
},
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="de">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="alternate" hrefLang="de" href="/" />
<link rel="alternate" hrefLang="en" href="/" />
<link rel="alternate" hrefLang="es" href="/" />
<link rel="alternate" hrefLang="x-default" href="/" />
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: siteConfig.name,
operatingSystem: 'iOS, Android',
applicationCategory: 'LifestyleApplication',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'EUR',
},
}),
}}
/>
</head>
<body>
<LangProvider>{children}</LangProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,141 @@
.page {
--background: #fafafa;
--foreground: #fff;
--text-primary: #000;
--text-secondary: #666;
--button-primary-hover: #383838;
--button-secondary-hover: #f2f2f2;
--button-secondary-border: #ebebeb;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
font-family: var(--font-geist-sans);
background-color: var(--background);
}
.main {
display: flex;
min-height: 100vh;
width: 100%;
max-width: 800px;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
background-color: var(--foreground);
padding: 120px 60px;
}
.intro {
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
gap: 24px;
}
.intro h1 {
max-width: 320px;
font-size: 40px;
font-weight: 600;
line-height: 48px;
letter-spacing: -2.4px;
text-wrap: balance;
color: var(--text-primary);
}
.intro p {
max-width: 440px;
font-size: 18px;
line-height: 32px;
text-wrap: balance;
color: var(--text-secondary);
}
.intro a {
font-weight: 500;
color: var(--text-primary);
}
.ctas {
display: flex;
flex-direction: row;
width: 100%;
max-width: 440px;
gap: 16px;
font-size: 14px;
}
.ctas a {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
padding: 0 16px;
border-radius: 128px;
border: 1px solid transparent;
transition: 0.2s;
cursor: pointer;
width: fit-content;
font-weight: 500;
}
a.primary {
background: var(--text-primary);
color: var(--background);
gap: 8px;
}
a.secondary {
border-color: var(--button-secondary-border);
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
a.primary:hover {
background: var(--button-primary-hover);
border-color: transparent;
}
a.secondary:hover {
background: var(--button-secondary-hover);
border-color: transparent;
}
}
@media (max-width: 600px) {
.main {
padding: 48px 24px;
}
.intro {
gap: 16px;
}
.intro h1 {
font-size: 32px;
line-height: 40px;
letter-spacing: -1.92px;
}
}
@media (prefers-color-scheme: dark) {
.logo {
filter: invert();
}
.page {
--background: #000;
--foreground: #000;
--text-primary: #ededed;
--text-secondary: #999;
--button-primary-hover: #ccc;
--button-secondary-hover: #1a1a1a;
--button-secondary-border: #1a1a1a;
}
}

View File

@@ -0,0 +1,29 @@
import Navbar from '@/components/Navbar'
import Hero from '@/components/Hero'
import Ticker from '@/components/Ticker'
import Features from '@/components/Features'
import BrownLeaf from '@/components/BrownLeaf'
import Intelligence from '@/components/Intelligence'
import HowItWorks from '@/components/HowItWorks'
import FAQ from '@/components/FAQ'
import CTA from '@/components/CTA'
import Footer from '@/components/Footer'
export default function Home() {
return (
<>
<Navbar />
<main>
<Hero />
<Ticker />
<Features />
<BrownLeaf />
<Intelligence />
<HowItWorks />
<FAQ />
<CTA />
</main>
<Footer />
</>
)
}

View File

@@ -0,0 +1,80 @@
'use client'
import { useLang } from '@/context/LangContext'
import { siteConfig } from '@/lib/site'
const CONTENT = {
de: {
title: 'Datenschutzerklaerung',
intro:
'Der Schutz personenbezogener Daten ist uns wichtig. Diese Seite beschreibt in knapper Form, welche Daten GreenLens verarbeiten kann und warum.',
section1: '1. Verarbeitete Daten',
text1:
'Je nach Nutzung der App koennen Pflanzenfotos, Kontodaten, technische Geraeteinformationen, In-App-Kaufdaten sowie freiwillig uebermittelte Support-Anfragen verarbeitet werden.',
section2: '2. Zweck der Verarbeitung',
text2:
'Diese Daten werden verwendet, um Pflanzenscans auszufuehren, deine Sammlung zu speichern, Erinnerungen bereitzustellen, Abos und Credits zu verwalten sowie Support-Anfragen zu beantworten.',
section3: '3. Drittanbieter',
text3:
'GreenLens nutzt technische Dienstleister fuer App-Betrieb, Analyse, Authentifizierung und In-App-Kaeufe. Dazu koennen je nach Plattform Apple, RevenueCat, PostHog oder Hosting-Anbieter gehoeren.',
section4: '4. Kontakt',
text4: 'Bei Fragen zum Datenschutz oder zu deinen Datenrechten kannst du uns per E-Mail kontaktieren.',
},
en: {
title: 'Privacy Policy',
intro:
'Protecting personal data matters to us. This page summarizes what GreenLens may process and why.',
section1: '1. Data we may process',
text1:
'Depending on how you use the app, GreenLens may process plant photos, account details, technical device information, in-app purchase data, and support messages you send to us.',
section2: '2. Why we process it',
text2:
'We use this information to run plant scans, store your collection, provide reminders, manage subscriptions and credits, and respond to support requests.',
section3: '3. Third-party services',
text3:
'GreenLens uses service providers for app delivery, analytics, authentication, and in-app purchases. Depending on platform and setup, this can include Apple, RevenueCat, PostHog, or hosting providers.',
section4: '4. Contact',
text4: 'If you have privacy questions or want to exercise your data rights, contact us by email.',
},
es: {
title: 'Politica de Privacidad',
intro:
'La proteccion de los datos personales es importante para nosotros. Esta pagina resume que datos puede procesar GreenLens y por que.',
section1: '1. Datos que podemos procesar',
text1:
'Segun el uso de la app, GreenLens puede procesar fotos de plantas, datos de cuenta, informacion tecnica del dispositivo, datos de compras dentro de la app y mensajes de soporte.',
section2: '2. Para que los usamos',
text2:
'Usamos estos datos para ejecutar escaneos, guardar tu coleccion, ofrecer recordatorios, gestionar suscripciones y creditos, y responder solicitudes de soporte.',
section3: '3. Servicios de terceros',
text3:
'GreenLens utiliza proveedores para la operacion de la app, analitica, autenticacion y compras integradas. Segun la plataforma, esto puede incluir Apple, RevenueCat, PostHog o proveedores de hosting.',
section4: '4. Contacto',
text4: 'Si tienes preguntas sobre privacidad o tus derechos de datos, contactanos por correo electronico.',
},
}
export default function PrivacyPage() {
const { lang } = useLang()
const c = CONTENT[lang]
return (
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
<h1>{c.title}</h1>
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
<p>{c.intro}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section1}</h2>
<p>{c.text1}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section2}</h2>
<p>{c.text2}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section3}</h2>
<p>{c.text3}</p>
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section4}</h2>
<p>{c.text4}</p>
<p style={{ marginTop: '0.75rem' }}>
<a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
</p>
</div>
</main>
)
}

View File

@@ -0,0 +1,26 @@
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://greenlns.ai'
return [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1,
},
{
url: `${baseUrl}/imprint`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.3,
},
{
url: `${baseUrl}/privacy`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.3,
},
]
}

View File

@@ -0,0 +1,104 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { siteConfig } from '@/lib/site'
export const metadata: Metadata = {
title: 'Support',
description: 'Get support for GreenLens, including contact details, onboarding help, billing guidance, and privacy links.',
}
const faqs = [
{
question: 'How do I identify a plant with GreenLens?',
answer:
'Open the scanner, point your camera at the plant, and start a scan. GreenLens returns an identification result and care guidance.',
},
{
question: 'Do I need an account to use GreenLens?',
answer:
'Some features can be explored first, but an account is recommended if you want to save scans, manage your collection, and access billing features.',
},
{
question: 'How do subscriptions and credits work?',
answer:
'GreenLens offers Pro subscriptions and credit top-ups for AI-powered features. Billing and plan management are available inside the app.',
},
{
question: 'How can I contact support?',
answer:
'Send a message to our support email with your platform, app version, and a short description of the issue. Screenshots help.',
},
]
export default function SupportPage() {
return (
<main className="support-page">
<section className="support-hero">
<div className="container support-hero-inner">
<p className="tag">Support</p>
<h1>Help for scans, care plans, billing, and account questions.</h1>
<p className="support-lead">
GreenLens helps users identify plants, understand their condition, and keep a collection organized.
If something breaks or feels unclear, this is the fastest place to start.
</p>
<div className="support-actions">
<a className="btn-primary" href={`mailto:${siteConfig.supportEmail}`}>
Email Support
</a>
<Link className="btn-outline support-outline" href="/privacy">
Privacy Policy
</Link>
</div>
</div>
</section>
<section className="support-grid-wrap">
<div className="container support-grid">
<div className="support-card">
<h2>Contact</h2>
<p>
Email us at <a href={`mailto:${siteConfig.supportEmail}`}>{siteConfig.supportEmail}</a>
</p>
<p>Include your device type, app version, and what happened right before the issue.</p>
</div>
<div className="support-card">
<h2>Common topics</h2>
<ul className="support-list">
<li>Plant identification issues</li>
<li>Care reminder questions</li>
<li>Subscriptions and credit purchases</li>
<li>Account access and saved data</li>
</ul>
</div>
<div className="support-card">
<h2>Legal</h2>
<p>
Review our <Link href="/privacy">Privacy Policy</Link> and <Link href="/imprint">Imprint</Link>.
</p>
<p>These links should be used in App Store Connect before submission.</p>
</div>
</div>
</section>
<section className="support-faq">
<div className="container">
<div className="support-section-head">
<p className="tag">FAQ</p>
<h2>Quick answers before you write support.</h2>
</div>
<div className="support-faq-list">
{faqs.map((item) => (
<article key={item.question} className="support-faq-item">
<h3>{item.question}</h3>
<p>{item.answer}</p>
</article>
))}
</div>
</div>
</section>
</main>
)
}