Initial commit for Greenlens
This commit is contained in:
BIN
greenlns-landing/app/favicon.ico
Normal file
BIN
greenlns-landing/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1322
greenlns-landing/app/globals.css
Normal file
1322
greenlns-landing/app/globals.css
Normal file
File diff suppressed because it is too large
Load Diff
71
greenlns-landing/app/imprint/page.tsx
Normal file
71
greenlns-landing/app/imprint/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client'
|
||||
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { siteConfig } from '@/lib/site'
|
||||
|
||||
const CONTENT = {
|
||||
de: {
|
||||
title: 'Impressum',
|
||||
companyLabel: 'Unternehmen',
|
||||
addressLabel: 'Adresse',
|
||||
representativeLabel: 'Vertretungsberechtigt',
|
||||
contactLabel: 'Kontakt',
|
||||
registryLabel: 'Register',
|
||||
vatLabel: 'USt-ID',
|
||||
note: 'Vor der Veroeffentlichung muessen alle rechtlichen Angaben mit den echten Firmendaten ersetzt werden.',
|
||||
},
|
||||
en: {
|
||||
title: 'Imprint',
|
||||
companyLabel: 'Company',
|
||||
addressLabel: 'Address',
|
||||
representativeLabel: 'Represented by',
|
||||
contactLabel: 'Contact',
|
||||
registryLabel: 'Registry',
|
||||
vatLabel: 'VAT ID',
|
||||
note: 'Replace all legal placeholders with your real company details before publishing the site.',
|
||||
},
|
||||
es: {
|
||||
title: 'Aviso Legal',
|
||||
companyLabel: 'Empresa',
|
||||
addressLabel: 'Direccion',
|
||||
representativeLabel: 'Representante',
|
||||
contactLabel: 'Contacto',
|
||||
registryLabel: 'Registro',
|
||||
vatLabel: 'IVA',
|
||||
note: 'Sustituye todos los marcadores legales por tus datos reales antes de publicar el sitio.',
|
||||
},
|
||||
}
|
||||
|
||||
export default function ImprintPage() {
|
||||
const { lang } = useLang()
|
||||
const c = CONTENT[lang]
|
||||
|
||||
return (
|
||||
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
|
||||
<h1>{c.title}</h1>
|
||||
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
|
||||
<p>
|
||||
<strong>{c.companyLabel}:</strong> {siteConfig.company.legalName}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{c.addressLabel}:</strong> {siteConfig.company.addressLine1}
|
||||
</p>
|
||||
{siteConfig.company.addressLine2 ? <p>{siteConfig.company.addressLine2}</p> : null}
|
||||
<p>{siteConfig.company.country}</p>
|
||||
<p>
|
||||
<strong>{c.representativeLabel}:</strong> {siteConfig.company.representative}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{c.contactLabel}:</strong> <a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{c.registryLabel}:</strong> {siteConfig.company.registry}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{c.vatLabel}:</strong> {siteConfig.company.vatId}
|
||||
</p>
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.95rem', opacity: 0.8 }}>{c.note}</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
76
greenlns-landing/app/layout.tsx
Normal file
76
greenlns-landing/app/layout.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import { LangProvider } from '@/context/LangContext'
|
||||
import { siteConfig } from '@/lib/site'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteConfig.domain),
|
||||
title: {
|
||||
default: 'GreenLens - Plant Identifier and Care Planner',
|
||||
template: '%s | GreenLens',
|
||||
},
|
||||
description:
|
||||
'GreenLens helps you identify plants, organize your collection, and keep up with care routines in one app.',
|
||||
keywords: [
|
||||
'plant identifier by picture',
|
||||
'plant care app',
|
||||
'watering reminders',
|
||||
'houseplant tracker',
|
||||
'plant identification',
|
||||
'plant health check',
|
||||
'Pflanzen App',
|
||||
'GreenLens',
|
||||
],
|
||||
authors: [{ name: siteConfig.name }],
|
||||
openGraph: {
|
||||
title: 'GreenLens - Plant Identifier and Care Planner',
|
||||
description: 'Identify plants, get care guidance, and manage your collection with GreenLens.',
|
||||
type: 'website',
|
||||
url: siteConfig.domain,
|
||||
},
|
||||
alternates: {
|
||||
canonical: '/',
|
||||
languages: {
|
||||
de: '/',
|
||||
en: '/',
|
||||
es: '/',
|
||||
'x-default': '/',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="alternate" hrefLang="de" href="/" />
|
||||
<link rel="alternate" hrefLang="en" href="/" />
|
||||
<link rel="alternate" hrefLang="es" href="/" />
|
||||
<link rel="alternate" hrefLang="x-default" href="/" />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: siteConfig.name,
|
||||
operatingSystem: 'iOS, Android',
|
||||
applicationCategory: 'LifestyleApplication',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'EUR',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<LangProvider>{children}</LangProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
141
greenlns-landing/app/page.module.css
Normal file
141
greenlns-landing/app/page.module.css
Normal file
@@ -0,0 +1,141 @@
|
||||
.page {
|
||||
--background: #fafafa;
|
||||
--foreground: #fff;
|
||||
|
||||
--text-primary: #000;
|
||||
--text-secondary: #666;
|
||||
|
||||
--button-primary-hover: #383838;
|
||||
--button-secondary-hover: #f2f2f2;
|
||||
--button-secondary-border: #ebebeb;
|
||||
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-geist-sans);
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
background-color: var(--foreground);
|
||||
padding: 120px 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
max-width: 320px;
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
line-height: 48px;
|
||||
letter-spacing: -2.4px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.intro p {
|
||||
max-width: 440px;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
text-wrap: balance;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.intro a {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.ctas {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ctas a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 128px;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.2s;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a.primary {
|
||||
background: var(--text-primary);
|
||||
color: var(--background);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a.secondary {
|
||||
border-color: var(--button-secondary-border);
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a.primary:hover {
|
||||
background: var(--button-primary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
a.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.main {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: -1.92px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.page {
|
||||
--background: #000;
|
||||
--foreground: #000;
|
||||
|
||||
--text-primary: #ededed;
|
||||
--text-secondary: #999;
|
||||
|
||||
--button-primary-hover: #ccc;
|
||||
--button-secondary-hover: #1a1a1a;
|
||||
--button-secondary-border: #1a1a1a;
|
||||
}
|
||||
}
|
||||
29
greenlns-landing/app/page.tsx
Normal file
29
greenlns-landing/app/page.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Hero from '@/components/Hero'
|
||||
import Ticker from '@/components/Ticker'
|
||||
import Features from '@/components/Features'
|
||||
import BrownLeaf from '@/components/BrownLeaf'
|
||||
import Intelligence from '@/components/Intelligence'
|
||||
import HowItWorks from '@/components/HowItWorks'
|
||||
import FAQ from '@/components/FAQ'
|
||||
import CTA from '@/components/CTA'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main>
|
||||
<Hero />
|
||||
<Ticker />
|
||||
<Features />
|
||||
<BrownLeaf />
|
||||
<Intelligence />
|
||||
<HowItWorks />
|
||||
<FAQ />
|
||||
<CTA />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
80
greenlns-landing/app/privacy/page.tsx
Normal file
80
greenlns-landing/app/privacy/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { useLang } from '@/context/LangContext'
|
||||
import { siteConfig } from '@/lib/site'
|
||||
|
||||
const CONTENT = {
|
||||
de: {
|
||||
title: 'Datenschutzerklaerung',
|
||||
intro:
|
||||
'Der Schutz personenbezogener Daten ist uns wichtig. Diese Seite beschreibt in knapper Form, welche Daten GreenLens verarbeiten kann und warum.',
|
||||
section1: '1. Verarbeitete Daten',
|
||||
text1:
|
||||
'Je nach Nutzung der App koennen Pflanzenfotos, Kontodaten, technische Geraeteinformationen, In-App-Kaufdaten sowie freiwillig uebermittelte Support-Anfragen verarbeitet werden.',
|
||||
section2: '2. Zweck der Verarbeitung',
|
||||
text2:
|
||||
'Diese Daten werden verwendet, um Pflanzenscans auszufuehren, deine Sammlung zu speichern, Erinnerungen bereitzustellen, Abos und Credits zu verwalten sowie Support-Anfragen zu beantworten.',
|
||||
section3: '3. Drittanbieter',
|
||||
text3:
|
||||
'GreenLens nutzt technische Dienstleister fuer App-Betrieb, Analyse, Authentifizierung und In-App-Kaeufe. Dazu koennen je nach Plattform Apple, RevenueCat, PostHog oder Hosting-Anbieter gehoeren.',
|
||||
section4: '4. Kontakt',
|
||||
text4: 'Bei Fragen zum Datenschutz oder zu deinen Datenrechten kannst du uns per E-Mail kontaktieren.',
|
||||
},
|
||||
en: {
|
||||
title: 'Privacy Policy',
|
||||
intro:
|
||||
'Protecting personal data matters to us. This page summarizes what GreenLens may process and why.',
|
||||
section1: '1. Data we may process',
|
||||
text1:
|
||||
'Depending on how you use the app, GreenLens may process plant photos, account details, technical device information, in-app purchase data, and support messages you send to us.',
|
||||
section2: '2. Why we process it',
|
||||
text2:
|
||||
'We use this information to run plant scans, store your collection, provide reminders, manage subscriptions and credits, and respond to support requests.',
|
||||
section3: '3. Third-party services',
|
||||
text3:
|
||||
'GreenLens uses service providers for app delivery, analytics, authentication, and in-app purchases. Depending on platform and setup, this can include Apple, RevenueCat, PostHog, or hosting providers.',
|
||||
section4: '4. Contact',
|
||||
text4: 'If you have privacy questions or want to exercise your data rights, contact us by email.',
|
||||
},
|
||||
es: {
|
||||
title: 'Politica de Privacidad',
|
||||
intro:
|
||||
'La proteccion de los datos personales es importante para nosotros. Esta pagina resume que datos puede procesar GreenLens y por que.',
|
||||
section1: '1. Datos que podemos procesar',
|
||||
text1:
|
||||
'Segun el uso de la app, GreenLens puede procesar fotos de plantas, datos de cuenta, informacion tecnica del dispositivo, datos de compras dentro de la app y mensajes de soporte.',
|
||||
section2: '2. Para que los usamos',
|
||||
text2:
|
||||
'Usamos estos datos para ejecutar escaneos, guardar tu coleccion, ofrecer recordatorios, gestionar suscripciones y creditos, y responder solicitudes de soporte.',
|
||||
section3: '3. Servicios de terceros',
|
||||
text3:
|
||||
'GreenLens utiliza proveedores para la operacion de la app, analitica, autenticacion y compras integradas. Segun la plataforma, esto puede incluir Apple, RevenueCat, PostHog o proveedores de hosting.',
|
||||
section4: '4. Contacto',
|
||||
text4: 'Si tienes preguntas sobre privacidad o tus derechos de datos, contactanos por correo electronico.',
|
||||
},
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
const { lang } = useLang()
|
||||
const c = CONTENT[lang]
|
||||
|
||||
return (
|
||||
<main className="container" style={{ paddingTop: '8rem', paddingBottom: '8rem', maxWidth: '800px' }}>
|
||||
<h1>{c.title}</h1>
|
||||
<div style={{ marginTop: '2rem', lineHeight: '1.8', opacity: 0.9 }}>
|
||||
<p>{c.intro}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section1}</h2>
|
||||
<p>{c.text1}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section2}</h2>
|
||||
<p>{c.text2}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section3}</h2>
|
||||
<p>{c.text3}</p>
|
||||
<h2 style={{ marginTop: '1.5rem', fontSize: '1.25rem' }}>{c.section4}</h2>
|
||||
<p>{c.text4}</p>
|
||||
<p style={{ marginTop: '0.75rem' }}>
|
||||
<a href={`mailto:${siteConfig.legalEmail}`}>{siteConfig.legalEmail}</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
26
greenlns-landing/app/sitemap.ts
Normal file
26
greenlns-landing/app/sitemap.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = 'https://greenlns.ai'
|
||||
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/imprint`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/privacy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
]
|
||||
}
|
||||
104
greenlns-landing/app/support/page.tsx
Normal file
104
greenlns-landing/app/support/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { siteConfig } from '@/lib/site'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Support',
|
||||
description: 'Get support for GreenLens, including contact details, onboarding help, billing guidance, and privacy links.',
|
||||
}
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: 'How do I identify a plant with GreenLens?',
|
||||
answer:
|
||||
'Open the scanner, point your camera at the plant, and start a scan. GreenLens returns an identification result and care guidance.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need an account to use GreenLens?',
|
||||
answer:
|
||||
'Some features can be explored first, but an account is recommended if you want to save scans, manage your collection, and access billing features.',
|
||||
},
|
||||
{
|
||||
question: 'How do subscriptions and credits work?',
|
||||
answer:
|
||||
'GreenLens offers Pro subscriptions and credit top-ups for AI-powered features. Billing and plan management are available inside the app.',
|
||||
},
|
||||
{
|
||||
question: 'How can I contact support?',
|
||||
answer:
|
||||
'Send a message to our support email with your platform, app version, and a short description of the issue. Screenshots help.',
|
||||
},
|
||||
]
|
||||
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<main className="support-page">
|
||||
<section className="support-hero">
|
||||
<div className="container support-hero-inner">
|
||||
<p className="tag">Support</p>
|
||||
<h1>Help for scans, care plans, billing, and account questions.</h1>
|
||||
<p className="support-lead">
|
||||
GreenLens helps users identify plants, understand their condition, and keep a collection organized.
|
||||
If something breaks or feels unclear, this is the fastest place to start.
|
||||
</p>
|
||||
<div className="support-actions">
|
||||
<a className="btn-primary" href={`mailto:${siteConfig.supportEmail}`}>
|
||||
Email Support
|
||||
</a>
|
||||
<Link className="btn-outline support-outline" href="/privacy">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="support-grid-wrap">
|
||||
<div className="container support-grid">
|
||||
<div className="support-card">
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
Email us at <a href={`mailto:${siteConfig.supportEmail}`}>{siteConfig.supportEmail}</a>
|
||||
</p>
|
||||
<p>Include your device type, app version, and what happened right before the issue.</p>
|
||||
</div>
|
||||
|
||||
<div className="support-card">
|
||||
<h2>Common topics</h2>
|
||||
<ul className="support-list">
|
||||
<li>Plant identification issues</li>
|
||||
<li>Care reminder questions</li>
|
||||
<li>Subscriptions and credit purchases</li>
|
||||
<li>Account access and saved data</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="support-card">
|
||||
<h2>Legal</h2>
|
||||
<p>
|
||||
Review our <Link href="/privacy">Privacy Policy</Link> and <Link href="/imprint">Imprint</Link>.
|
||||
</p>
|
||||
<p>These links should be used in App Store Connect before submission.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="support-faq">
|
||||
<div className="container">
|
||||
<div className="support-section-head">
|
||||
<p className="tag">FAQ</p>
|
||||
<h2>Quick answers before you write support.</h2>
|
||||
</div>
|
||||
|
||||
<div className="support-faq-list">
|
||||
{faqs.map((item) => (
|
||||
<article key={item.question} className="support-faq-item">
|
||||
<h3>{item.question}</h3>
|
||||
<p>{item.answer}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user