Launch
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
*.md
|
||||
nginx
|
||||
docker-compose.yml
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
*.md
|
||||
nginx
|
||||
docker-compose.yml
|
||||
|
||||
82
greenlns-landing/.gitignore
vendored
82
greenlns-landing/.gitignore
vendored
@@ -1,41 +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
|
||||
# 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
|
||||
|
||||
@@ -1,33 +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"]
|
||||
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"]
|
||||
|
||||
@@ -1,36 +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.
|
||||
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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,71 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,76 +1,72 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
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" />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,141 +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;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,80 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +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,
|
||||
},
|
||||
]
|
||||
}
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = 'https://greenlns-landing.vercel.app'
|
||||
|
||||
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,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,104 +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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
91
greenlns-landing/app/terms/page.tsx
Normal file
91
greenlns-landing/app/terms/page.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { siteConfig } from '@/lib/site'
|
||||
|
||||
const CONTENT = {
|
||||
de: {
|
||||
title: 'Nutzungsbedingungen',
|
||||
intro:
|
||||
'Durch die Nutzung von GreenLens stimmst du diesen Nutzungsbedingungen zu. Bitte lies sie sorgfaeltig durch.',
|
||||
section1: '1. Leistungen',
|
||||
text1:
|
||||
'GreenLens bietet KI-gestuetzte Pflanzenidentifikation, Gesundheitsdiagnosen und Pflegeerinnerungen. Die App kann kostenlos genutzt werden; erweiterte Funktionen erfordern ein Abonnement oder Credits.',
|
||||
section2: '2. Abonnements und In-App-Kaeufe',
|
||||
text2:
|
||||
'GreenLens Pro ist ein automatisch erneuerbares Abonnement (monatlich oder jaehrlich). Es wird ueber deinen Apple-Account abgerechnet. Die Verlaengerung erfolgt automatisch, sofern du nicht mindestens 24 Stunden vor Ablauf des Abrechnungszeitraums kuendigst. Credits sind einmalige Kaeufe und nicht uebertragbar.',
|
||||
section3: '3. Kuendigung',
|
||||
text3:
|
||||
'Du kannst dein Abonnement jederzeit in den iPhone-Einstellungen unter deinem Apple-ID-Konto kuendigen. Nach der Kuendigung behast du den Zugriff bis zum Ende des bezahlten Zeitraums.',
|
||||
section4: '4. Haftungsausschluss',
|
||||
text4:
|
||||
'GreenLens stellt Informationen auf Basis von KI-Analysen bereit. Diese ersetzen keine professionelle Beratung. Wir uebernehmen keine Haftung fuer Schaeden, die durch die Nutzung der App entstehen.',
|
||||
section5: '5. Kontakt',
|
||||
text5: 'Bei Fragen zu diesen Nutzungsbedingungen erreichst du uns per E-Mail.',
|
||||
},
|
||||
en: {
|
||||
title: 'Terms of Use',
|
||||
intro:
|
||||
'By using GreenLens, you agree to these Terms of Use. Please read them carefully.',
|
||||
section1: '1. Services',
|
||||
text1:
|
||||
'GreenLens provides AI-powered plant identification, health diagnosis, and care reminders. The app is free to use; advanced features require a subscription or credits.',
|
||||
section2: '2. Subscriptions and In-App Purchases',
|
||||
text2:
|
||||
'GreenLens Pro is an auto-renewable subscription (monthly or yearly). Payment is charged to your Apple Account. Your subscription automatically renews unless cancelled at least 24 hours before the end of the current billing period. Credits are one-time purchases and are non-transferable.',
|
||||
section3: '3. Cancellation',
|
||||
text3:
|
||||
'You can cancel your subscription at any time in iPhone Settings under your Apple ID account. After cancellation, you retain access until the end of the paid period.',
|
||||
section4: '4. Disclaimer',
|
||||
text4:
|
||||
'GreenLens provides information based on AI analysis. This does not replace professional advice. We accept no liability for damages arising from use of the app.',
|
||||
section5: '5. Contact',
|
||||
text5: 'If you have questions about these Terms of Use, contact us by email.',
|
||||
},
|
||||
es: {
|
||||
title: 'Terminos de Uso',
|
||||
intro:
|
||||
'Al usar GreenLens, aceptas estos Terminos de Uso. Por favor, leelos detenidamente.',
|
||||
section1: '1. Servicios',
|
||||
text1:
|
||||
'GreenLens ofrece identificacion de plantas, diagnostico de salud y recordatorios de cuidado basados en IA. La app es gratuita; las funciones avanzadas requieren una suscripcion o creditos.',
|
||||
section2: '2. Suscripciones y Compras',
|
||||
text2:
|
||||
'GreenLens Pro es una suscripcion de renovacion automatica (mensual o anual). El pago se carga a tu cuenta de Apple. La suscripcion se renueva automaticamente salvo que la canceles al menos 24 horas antes del final del periodo actual. Los creditos son compras unicas y no son transferibles.',
|
||||
section3: '3. Cancelacion',
|
||||
text3:
|
||||
'Puedes cancelar tu suscripcion en cualquier momento en los Ajustes del iPhone bajo tu cuenta de Apple ID. Tras la cancelacion, conservas el acceso hasta el final del periodo pagado.',
|
||||
section4: '4. Exencion de responsabilidad',
|
||||
text4:
|
||||
'GreenLens proporciona informacion basada en analisis de IA. Esto no reemplaza el asesoramiento profesional. No aceptamos responsabilidad por danos derivados del uso de la app.',
|
||||
section5: '5. Contacto',
|
||||
text5: 'Si tienes preguntas sobre estos Terminos de Uso, contactanos por correo electronico.',
|
||||
},
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
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>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section5}</h2>
|
||||
<p>{c.text5}</p>
|
||||
<p style={{ marginTop: '0.75rem' }}>
|
||||
<a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,349 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,74 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,212 +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>
|
||||
);
|
||||
}
|
||||
'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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,165 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,50 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,189 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,82 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,100 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,123 +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>
|
||||
)
|
||||
}
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,56 +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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +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)
|
||||
'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)
|
||||
|
||||
@@ -1,66 +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:
|
||||
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:
|
||||
|
||||
@@ -1,204 +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
|
||||
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
|
||||
|
||||
@@ -1,20 +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
|
||||
export const siteConfig = {
|
||||
name: 'GreenLens',
|
||||
domain: 'https://greenlns-landing.vercel.app',
|
||||
supportEmail: 'knuth.timo@gmail.com',
|
||||
legalEmail: 'knuth.timo@gmail.com',
|
||||
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
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -1,62 +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;
|
||||
}
|
||||
}
|
||||
# ── 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;
|
||||
}
|
||||
}
|
||||
|
||||
2038
greenlns-landing/package-lock.json
generated
2038
greenlns-landing/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +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"
|
||||
}
|
||||
}
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +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>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 360 B |
1
greenlns-landing/public/googleccd5315437d68a49.html
Normal file
1
greenlns-landing/public/googleccd5315437d68a49.html
Normal file
@@ -0,0 +1 @@
|
||||
google-site-verification: googleccd5315437d68a49.html
|
||||
@@ -1,4 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://greenlns.ai/sitemap.xml
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://greenlns-landing.vercel.app/sitemap.xml
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
{
|
||||
"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"
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user