Initial commit for Greenlens
7
greenlns-landing/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
*.md
|
||||
nginx
|
||||
docker-compose.yml
|
||||
41
greenlns-landing/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
33
greenlns-landing/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
# ── Dependencies ────────────────────────────────────────────────────────────
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# ── Builder ─────────────────────────────────────────────────────────────────
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# ── Runner ──────────────────────────────────────────────────────────────────
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
36
greenlns-landing/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
BIN
greenlns-landing/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
1322
greenlns-landing/app/globals.css
Normal file
71
greenlns-landing/app/imprint/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
76
greenlns-landing/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
141
greenlns-landing/app/page.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
29
greenlns-landing/app/page.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
80
greenlns-landing/app/privacy/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
26
greenlns-landing/app/sitemap.ts
Normal 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,
|
||||
},
|
||||
]
|
||||
}
|
||||
104
greenlns-landing/app/support/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
349
greenlns-landing/components/BrownLeaf.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
74
greenlns-landing/components/CTA.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
212
greenlns-landing/components/FAQ.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
165
greenlns-landing/components/Features.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
50
greenlns-landing/components/Footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
189
greenlns-landing/components/Hero.tsx
Normal 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>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
82
greenlns-landing/components/HowItWorks.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
100
greenlns-landing/components/Intelligence.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
123
greenlns-landing/components/Navbar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
56
greenlns-landing/components/Testimonials.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
greenlns-landing/components/Ticker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
27
greenlns-landing/context/LangContext.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, ReactNode } from 'react'
|
||||
import { Lang, translations } from '@/lib/i18n'
|
||||
|
||||
interface LangCtx {
|
||||
lang: Lang
|
||||
setLang: (l: Lang) => void
|
||||
t: typeof translations.de
|
||||
}
|
||||
|
||||
const LangContext = createContext<LangCtx>({
|
||||
lang: 'de',
|
||||
setLang: () => {},
|
||||
t: translations.de,
|
||||
})
|
||||
|
||||
export function LangProvider({ children }: { children: ReactNode }) {
|
||||
const [lang, setLang] = useState<Lang>('de')
|
||||
return (
|
||||
<LangContext.Provider value={{ lang, setLang, t: translations[lang] }}>
|
||||
{children}
|
||||
</LangContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLang = () => useContext(LangContext)
|
||||
66
greenlns-landing/docker-compose.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-GreenLens}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-GreenLens}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-GreenLens}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
# Expose to Railway/external — set firewall rules on your server!
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY is required}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?MINIO_SECRET_KEY is required}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./nginx/certs:/etc/nginx/certs:ro
|
||||
depends_on:
|
||||
- app
|
||||
- minio
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
204
greenlns-landing/lib/i18n.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
export type Lang = 'de' | 'en' | 'es'
|
||||
|
||||
export const translations = {
|
||||
de: {
|
||||
nav: {
|
||||
features: 'Features',
|
||||
tech: 'Technologie',
|
||||
how: 'So funktioniert es',
|
||||
download: 'App laden',
|
||||
cta: 'Jetzt starten',
|
||||
},
|
||||
hero: {
|
||||
eyebrow: 'GreenLens App - Botanische Intelligenz',
|
||||
h1a: 'Dein Urban',
|
||||
h1b: 'Jungle,',
|
||||
h1em: 'besser gepflegt.',
|
||||
desc: 'Scanne Pflanzen, verstehe ihre Beduerfnisse und organisiere Pflege, Erinnerungen und Sammlung in einer App.',
|
||||
primary: 'App entdecken',
|
||||
secondary: 'Mehr erfahren',
|
||||
badge: 'Plant care with AI',
|
||||
segTitle: 'Wobei hilft dir GreenLens?',
|
||||
segOpt1: 'Ich will die Pflanze erkennen.',
|
||||
segOpt2: 'Ich will Pflegeprobleme verstehen.',
|
||||
},
|
||||
brownLeaf: {
|
||||
tag: 'Pflanzenhilfe',
|
||||
headline: 'Weniger raten.',
|
||||
sub: 'Schneller verstehen, was deiner Pflanze fehlt.',
|
||||
desc: 'GreenLens hilft dir dabei, Symptome einzuordnen, Pflanzen zu identifizieren und naechste Pflegeschritte klarer zu machen.',
|
||||
before: 'Vorher',
|
||||
after: 'Nachher',
|
||||
sliderLabel: 'Vergleichs-Slider',
|
||||
proof1title: 'Braune Spitzen einordnen',
|
||||
proof1desc: 'Schneller von Unsicherheit zu einer klareren Ursache',
|
||||
proof2title: 'Pflegeplan strukturieren',
|
||||
proof2desc: 'Weniger Chaos bei Licht, Wasser und Intervallen',
|
||||
proof3title: 'Hinweise an einem Ort',
|
||||
proof3desc: 'Scan, Sammlung und Care Notes zusammengefuehrt',
|
||||
},
|
||||
features: {
|
||||
tag: 'Features',
|
||||
h2a: 'Alles, was dein',
|
||||
h2b: 'Urban Jungle braucht.',
|
||||
desc: 'Von der ersten Identifikation bis zur laufenden Pflege hilft GreenLens dir, Pflanzen besser zu verstehen und besser zu organisieren.',
|
||||
},
|
||||
cta: {
|
||||
tag: 'Download',
|
||||
h2a: 'Bereit fuer bessere',
|
||||
h2em: 'Pflanzenpflege?',
|
||||
desc: 'GreenLens hilft dir beim Erkennen, Verstehen und Pflegen deiner Pflanzen. Wenn der Store-Link noch nicht live ist, erreichst du uns ueber die Support-Seite.',
|
||||
apple: 'Laden im',
|
||||
google: 'Jetzt bei',
|
||||
support: 'Zur',
|
||||
supportLabel: 'Support-Seite',
|
||||
contact: 'Direkt',
|
||||
email: 'E-Mail senden',
|
||||
comingSoon: 'Die App ist bald verfuegbar.',
|
||||
liveNote: 'Die App ist bereits in den Stores verfuegbar.',
|
||||
},
|
||||
footer: {
|
||||
brand: 'Die App fuer Pflanzenfans, die Sammlung, Identifikation und Pflege an einem Ort wollen.',
|
||||
copy: '© 2026 GreenLens. Alle Rechte vorbehalten.',
|
||||
cols: [
|
||||
{ title: 'Produkt', links: ['Features', 'Technologie', 'App laden', 'Support'] },
|
||||
{ title: 'Ressourcen', links: ['So funktioniert es', 'FAQ', 'Support'] },
|
||||
{ title: 'Rechtliches', links: ['Impressum', 'Datenschutz'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
en: {
|
||||
nav: {
|
||||
features: 'Features',
|
||||
tech: 'Technology',
|
||||
how: 'How it works',
|
||||
download: 'Get the App',
|
||||
cta: 'Get Started',
|
||||
},
|
||||
hero: {
|
||||
eyebrow: 'GreenLens App - Botanical intelligence',
|
||||
h1a: 'Your Urban',
|
||||
h1b: 'Jungle,',
|
||||
h1em: 'better cared for.',
|
||||
desc: 'Scan plants, understand what they need, and keep care, reminders, and your collection in one app.',
|
||||
primary: 'Explore the App',
|
||||
secondary: 'Learn more',
|
||||
badge: 'Plant care with AI',
|
||||
segTitle: 'What do you need help with?',
|
||||
segOpt1: 'I want to identify the plant.',
|
||||
segOpt2: 'I want to understand care problems.',
|
||||
},
|
||||
brownLeaf: {
|
||||
tag: 'Plant help',
|
||||
headline: 'Less guessing.',
|
||||
sub: 'Understand faster what your plant may need.',
|
||||
desc: 'GreenLens helps you interpret symptoms, identify plants, and get clearer next-step care guidance.',
|
||||
before: 'Before',
|
||||
after: 'After',
|
||||
sliderLabel: 'Before and after slider',
|
||||
proof1title: 'Assess brown leaf tips',
|
||||
proof1desc: 'Move from uncertainty to a clearer diagnosis path',
|
||||
proof2title: 'Structure the care routine',
|
||||
proof2desc: 'Reduce the chaos around light, water, and intervals',
|
||||
proof3title: 'Keep guidance in one place',
|
||||
proof3desc: 'Scan, collection, and care notes together',
|
||||
},
|
||||
features: {
|
||||
tag: 'Features',
|
||||
h2a: 'Everything your',
|
||||
h2b: 'Urban Jungle needs.',
|
||||
desc: 'From first identification to ongoing care, GreenLens helps you understand plants better and stay organized.',
|
||||
},
|
||||
cta: {
|
||||
tag: 'Download',
|
||||
h2a: 'Ready for better',
|
||||
h2em: 'plant care?',
|
||||
desc: '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.',
|
||||
apple: 'Download on the',
|
||||
google: 'Get it on',
|
||||
support: 'Open',
|
||||
supportLabel: 'Support Page',
|
||||
contact: 'Direct',
|
||||
email: 'Send Email',
|
||||
comingSoon: 'The app is coming soon.',
|
||||
liveNote: 'The app is already available in app stores.',
|
||||
},
|
||||
footer: {
|
||||
brand: 'The app for plant lovers who want identification, care, and collection management in one place.',
|
||||
copy: '© 2026 GreenLens. All rights reserved.',
|
||||
cols: [
|
||||
{ title: 'Product', links: ['Features', 'Technology', 'Get the App', 'Support'] },
|
||||
{ title: 'Resources', links: ['How it works', 'FAQ', 'Support'] },
|
||||
{ title: 'Legal', links: ['Imprint', 'Privacy Policy'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
es: {
|
||||
nav: {
|
||||
features: 'Funciones',
|
||||
tech: 'Tecnologia',
|
||||
how: 'Como funciona',
|
||||
download: 'Descargar',
|
||||
cta: 'Empezar',
|
||||
},
|
||||
hero: {
|
||||
eyebrow: 'GreenLens App - Inteligencia botanica',
|
||||
h1a: 'Tu jardin',
|
||||
h1b: 'urbano,',
|
||||
h1em: 'mejor cuidado.',
|
||||
desc: 'Escanea plantas, entiende lo que necesitan y organiza cuidado, recordatorios y coleccion en una sola app.',
|
||||
primary: 'Explorar la app',
|
||||
secondary: 'Saber mas',
|
||||
badge: 'Plant care with AI',
|
||||
segTitle: 'En que te ayuda GreenLens?',
|
||||
segOpt1: 'Quiero identificar la planta.',
|
||||
segOpt2: 'Quiero entender problemas de cuidado.',
|
||||
},
|
||||
brownLeaf: {
|
||||
tag: 'Ayuda para plantas',
|
||||
headline: 'Menos adivinanzas.',
|
||||
sub: 'Entiende mas rapido que puede necesitar tu planta.',
|
||||
desc: 'GreenLens te ayuda a interpretar sintomas, identificar plantas y obtener pasos de cuidado mas claros.',
|
||||
before: 'Antes',
|
||||
after: 'Despues',
|
||||
sliderLabel: 'Comparador antes y despues',
|
||||
proof1title: 'Evaluar puntas marrones',
|
||||
proof1desc: 'Pasa de la duda a una causa mas clara',
|
||||
proof2title: 'Ordenar la rutina',
|
||||
proof2desc: 'Menos caos con luz, riego e intervalos',
|
||||
proof3title: 'Guardar ayuda en un lugar',
|
||||
proof3desc: 'Escaneo, coleccion y notas juntos',
|
||||
},
|
||||
features: {
|
||||
tag: 'Funciones',
|
||||
h2a: 'Todo lo que tu',
|
||||
h2b: 'jardin urbano necesita.',
|
||||
desc: 'Desde la primera identificacion hasta el cuidado continuo, GreenLens te ayuda a entender mejor tus plantas y a organizarte.',
|
||||
},
|
||||
cta: {
|
||||
tag: 'Descarga',
|
||||
h2a: 'Listo para un mejor',
|
||||
h2em: 'cuidado de plantas?',
|
||||
desc: '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.',
|
||||
apple: 'Descargar en',
|
||||
google: 'Disponible en',
|
||||
support: 'Abrir',
|
||||
supportLabel: 'Soporte',
|
||||
contact: 'Contacto',
|
||||
email: 'Enviar correo',
|
||||
comingSoon: 'La app estara disponible pronto.',
|
||||
liveNote: 'La app ya esta disponible en las tiendas.',
|
||||
},
|
||||
footer: {
|
||||
brand: 'La app para quienes quieren identificacion, cuidado y coleccion de plantas en un solo lugar.',
|
||||
copy: '© 2026 GreenLens. Todos los derechos reservados.',
|
||||
cols: [
|
||||
{ title: 'Producto', links: ['Funciones', 'Tecnologia', 'Descargar', 'Support'] },
|
||||
{ title: 'Recursos', links: ['Como funciona', 'FAQ', 'Support'] },
|
||||
{ title: 'Legal', links: ['Aviso legal', 'Privacidad'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, unknown>
|
||||
|
||||
export type Translations = typeof translations.de
|
||||
20
greenlns-landing/lib/site.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const siteConfig = {
|
||||
name: 'GreenLens',
|
||||
domain: 'https://greenlns.ai',
|
||||
supportEmail: 'support@greenlns.ai',
|
||||
legalEmail: 'contact@greenlns.ai',
|
||||
iosAppStoreUrl: '',
|
||||
androidPlayStoreUrl: '',
|
||||
company: {
|
||||
legalName: 'GreenLens',
|
||||
representative: 'Tim Knuth',
|
||||
addressLine1: 'Replace with your legal business address',
|
||||
addressLine2: '',
|
||||
country: 'Germany',
|
||||
registry: 'Replace with your company registry details',
|
||||
vatId: 'Replace with your VAT ID or remove this line',
|
||||
},
|
||||
} as const
|
||||
|
||||
export const hasIosStoreUrl = siteConfig.iosAppStoreUrl.trim().length > 0
|
||||
export const hasAndroidStoreUrl = siteConfig.androidPlayStoreUrl.trim().length > 0
|
||||
10
greenlns-landing/next.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
62
greenlns-landing/nginx/default.conf
Normal file
@@ -0,0 +1,62 @@
|
||||
# ── HTTP → HTTPS Redirect ───────────────────────────────────────────────────
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# ── Landing Page (Next.js) ──────────────────────────────────────────────────
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name greenlns.ai;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://app:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
|
||||
# ── MinIO API (public image URLs) ──────────────────────────────────────────
|
||||
server {
|
||||
listen 9000 ssl;
|
||||
server_name greenlns.ai;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
# Allow large image uploads
|
||||
client_max_body_size 20m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://minio:9000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
# ── MinIO Console (Admin UI) ────────────────────────────────────────────────
|
||||
server {
|
||||
listen 9001 ssl;
|
||||
server_name greenlns.ai;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://minio:9001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
}
|
||||
1019
greenlns-landing/package-lock.json
generated
Normal file
22
greenlns-landing/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "GreenLens-landing",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.34.3",
|
||||
"next": "16.1.6",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
BIN
greenlns-landing/public/GreenLensHype.mp4
Normal file
BIN
greenlns-landing/public/ai-analysis.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
4
greenlns-landing/public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 4C11 10 6 14 4 18C2 22 4 28 10 28C14 28 16 26 16 26C16 26 18 28 22 28C28 28 30 22 28 18C26 14 21 10 16 4Z" fill="#2A5C3F"/>
|
||||
<path d="M16 4C14 8 13 12 14 16C15 20 18 22 16 26" stroke="#F4F1E8" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
1
greenlns-landing/public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
greenlns-landing/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
greenlns-landing/public/greenlens.mp4
Normal file
BIN
greenlns-landing/public/hero-plant.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
1
greenlns-landing/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
greenlns-landing/public/plant-collection.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
4
greenlns-landing/public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://greenlns.ai/sitemap.xml
|
||||
BIN
greenlns-landing/public/scan-feature.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
greenlns-landing/public/track-feature.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
greenlns-landing/public/unhealthy-plant.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
1
greenlns-landing/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
greenlns-landing/public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
43
greenlns-landing/tsconfig.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||