kenso war das

This commit is contained in:
2026-02-03 23:27:25 +01:00
parent 6bf3c603d8
commit ef2faa21fd
73 changed files with 8416 additions and 257 deletions

View File

@@ -1,10 +1,10 @@
# EmailSorter
# MailFlow
KI-gestützte E-Mail-Sortierung für mehr Produktivität und weniger Stress.
## Überblick
EmailSorter ist eine SaaS-Anwendung, die automatisch E-Mails kategorisiert und sortiert. Die Anwendung nutzt:
MailFlow ist eine SaaS-Anwendung, die automatisch E-Mails kategorisiert und sortiert. Die Anwendung nutzt:
- **React + Vite** Frontend mit Tailwind CSS
- **Node.js + Express** Backend
@@ -50,7 +50,7 @@ EmailSorter ist eine SaaS-Anwendung, die automatisch E-Mails kategorisiert und s
```bash
git clone <repo-url>
cd emailsorter
cd mailflow
```
### 2. Dependencies installieren

View File

@@ -3,18 +3,18 @@
<head>
<meta charset="UTF-8" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes" />
<meta name="description" content="E-Mail-Sorter - AI-powered email sorting for maximum productivity. Automatically organize your inbox." />
<meta name="description" content="MailFlow - AI-powered email sorting for maximum productivity. Automatically organize your inbox." />
<meta name="theme-color" content="#22c55e" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title>EmailSorter - Your inbox, finally organized</title>
<title>MailFlow - Your inbox, finally organized</title>
<!-- Prevent FOUC for dark mode - Enhanced Dark Reader detection -->
<script>
(function() {

View File

@@ -1,5 +1,5 @@
{
"name": "emailsorter-client",
"name": "mailflow-client",
"private": true,
"version": "0.0.0",
"type": "module",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
client/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
client/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -1,17 +1,22 @@
{
"name": "EmailSorter",
"short_name": "EmailSorter",
"name": "MailFlow",
"short_name": "MailFlow",
"description": "AI-powered email sorting for maximum productivity",
"icons": [
{
"src": "/favicon.svg",
"src": "/favicon.png",
"sizes": "any",
"type": "image/svg+xml"
"type": "image/png"
},
{
"src": "/apple-touch-icon.svg",
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/svg+xml"
"type": "image/png"
},
{
"src": "/logo.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#22c55e",

View File

@@ -13,7 +13,7 @@ export function ShareResults({ sortedCount, referralCode }: ShareResultsProps) {
const [copied, setCopied] = useState(false)
const { user } = useAuth()
const shareText = `I cleaned up ${sortedCount} emails with EmailSorter${referralCode ? `! Use code ${referralCode} for a bonus.` : '!'}`
const shareText = `I cleaned up ${sortedCount} emails with MailFlow${referralCode ? `! Use code ${referralCode} for a bonus.` : '!'}`
const shareUrl = referralCode
? `${window.location.origin}?ref=${referralCode}`
: window.location.origin
@@ -33,7 +33,7 @@ export function ShareResults({ sortedCount, referralCode }: ShareResultsProps) {
if (navigator.share) {
try {
await navigator.share({
title: 'EmailSorter - Clean Inbox',
title: 'MailFlow - Clean Inbox',
text: shareText,
url: shareUrl,
})

View File

@@ -53,7 +53,7 @@ export function FAQ() {
<p className="mt-10 text-center text-sm text-slate-600 dark:text-slate-400">
Still unsure?{' '}
<a
href="mailto:support@emailsorter.webklar.com"
href="mailto:support@mailflow.webklar.com"
className="text-slate-700 dark:text-slate-300 hover:underline"
>
Email us we reply fast

View File

@@ -1,107 +1,174 @@
import {
Brain,
Zap,
Shield,
Clock,
Settings,
Inbox,
Filter
import {
FolderTree,
MousePointerClick,
CalendarClock,
ScanSearch,
ShieldCheck,
Sparkles,
Filter,
} from 'lucide-react'
const features = [
const FEATURES = [
{
icon: Inbox,
icon: FolderTree,
title: "Categories, not chaos",
description: "Leads, clients, invoices, newsletters — sorted into folders. Your inbox shows what pays first.",
color: "from-violet-500 to-purple-600",
highlight: true,
desc: "Leads, clients, invoices, newsletters — sorted into folders. Your inbox shows what pays first.",
glowClass: "from-orange-500/20 to-amber-500/20",
delay: 0,
},
{
icon: Zap,
icon: MousePointerClick,
title: "One click to sort",
description: "Connect your inbox, click Sort Now. No rules to write. We read and categorize; you review.",
color: "from-amber-500 to-orange-600",
highlight: true,
desc: "Connect your inbox, click Sort Now. No rules to write. We read and categorize; you review.",
glowClass: "from-emerald-500/20 to-teal-500/20",
delay: 50,
},
{
icon: Settings,
icon: CalendarClock,
title: "Runs when you want",
description: "Sort on demand or set a schedule. Your inbox stays organized without you touching it.",
color: "from-blue-500 to-cyan-600",
highlight: true,
desc: "Sort on demand or set a schedule. Your inbox stays organized without you touching it.",
glowClass: "from-blue-500/20 to-indigo-500/20",
delay: 100,
},
{
icon: Brain,
icon: ScanSearch,
title: "Content-aware sorting",
description: "We look at sender, subject, and a short snippet to decide the category. No keyword lists.",
color: "from-green-500 to-emerald-600"
desc: "We look at sender, subject, and a short snippet to decide the category. No keyword lists.",
glowClass: "from-violet-500/20 to-purple-500/20",
delay: 150,
},
{
icon: Shield,
icon: ShieldCheck,
title: "Minimal data",
description: "We only read what we need to categorize. No storing email body or attachments. GDPR compliant.",
color: "from-pink-500 to-rose-600"
desc: "We only read what we need to categorize. No storing email body or attachments. GDPR compliant.",
glowClass: "from-green-500/20 to-emerald-500/20",
delay: 200,
},
{
icon: Clock,
icon: Sparkles,
title: "Less time on triage",
description: "Spend less time deciding what's important. Inbox shows clients and leads first.",
color: "from-indigo-500 to-blue-600"
desc: "Spend less time deciding what's important. Inbox shows clients and leads first.",
glowClass: "from-cyan-500/20 to-blue-500/20",
delay: 250,
},
]
function FeatureItem({
icon: Icon,
title,
desc,
glowClass,
delay,
}: {
icon: React.ElementType
title: string
desc: string
glowClass: string
delay: number
}) {
return (
<div
className="group relative"
style={{ animationDelay: `${delay}ms` }}
>
<div
aria-hidden="true"
className={`absolute inset-0 bg-gradient-to-br ${glowClass} rounded-xl opacity-0 group-hover:opacity-100 blur-xl transition-opacity duration-300 -z-10 scale-110`}
/>
<div
className="relative p-4 rounded-xl border border-slate-200 dark:border-slate-700 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm h-full
transform-gpu transition-all duration-300 ease-out
hover:border-primary-400/40 dark:hover:border-primary-500/40 hover:shadow-lg hover:shadow-primary-500/5
hover:-translate-y-1 hover:scale-[1.02]
group-hover:bg-white dark:group-hover:bg-slate-800/95"
>
<div
aria-hidden="true"
className="absolute inset-0 rounded-xl overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div className="absolute inset-0 -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-out bg-gradient-to-r from-transparent via-white/10 dark:via-white/5 to-transparent" />
</div>
<div className="relative z-10">
<div className="flex items-start gap-3 mb-3">
<div
className="p-2.5 bg-primary-500/10 rounded-xl text-primary-600 dark:text-primary-400 shrink-0
transition-all duration-300 ease-out
group-hover:bg-primary-500 group-hover:text-white
group-hover:scale-110 group-hover:rotate-3 group-hover:shadow-lg group-hover:shadow-primary-500/25"
>
<Icon className="h-6 w-6" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden />
</div>
<h4 className="text-base font-semibold text-slate-900 dark:text-slate-100 transition-colors duration-300 group-hover:text-primary-600 dark:group-hover:text-primary-400 pt-0.5">
{title}
</h4>
</div>
<p
className="text-sm text-slate-500 dark:text-slate-400 leading-relaxed transition-all duration-300
opacity-90 group-hover:opacity-100 group-hover:text-slate-700 dark:group-hover:text-slate-300"
>
{desc}
</p>
</div>
</div>
</div>
)
}
export function Features() {
return (
<section id="features" className="py-24 bg-slate-50 dark:bg-slate-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
What it does
</h2>
<p className="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
Sort incoming mail into categories so your inbox shows what matters first. No rules to write.
</p>
</div>
{/* Single card in engineering-card style */}
<div
data-slot="card"
className="relative bg-white/80 dark:bg-slate-800/60 text-slate-900 dark:text-slate-100 flex flex-col gap-6 rounded-2xl border border-slate-200 dark:border-slate-700 backdrop-blur-md shadow-lg shadow-black/5 dark:shadow-black/20 transform-gpu transition-all duration-300 ease-out hover:shadow-2xl hover:shadow-primary-500/10 hover:border-primary-400/30 dark:hover:border-primary-500/20 overflow-hidden
before:absolute before:inset-0 before:rounded-2xl before:p-[1.5px] before:bg-gradient-to-br before:from-primary-500/50 before:via-accent-500/30 before:to-primary-500/50 before:-z-10 before:opacity-0 before:transition-opacity before:duration-500 hover:before:opacity-100
after:absolute after:inset-0 after:rounded-2xl after:-z-20 after:bg-gradient-to-br after:from-primary-500/5 after:via-transparent after:to-accent-500/5 after:opacity-0 after:transition-opacity after:duration-500 hover:after:opacity-100
p-6 md:p-8"
>
<div className="mb-8 text-center">
<h3 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">What it does</h3>
<p className="text-slate-600 dark:text-slate-400 max-w-xl mx-auto">
Sort incoming mail into categories so your inbox shows what matters first. No rules to write.
</p>
</div>
{/* Features grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((feature, index) => (
<FeatureCard key={index} {...feature} index={index} />
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{FEATURES.map((feature, i) => (
<FeatureItem key={i} {...feature} />
))}
</div>
{/* Bottom illustration */}
<div className="mt-20 relative">
<div className="bg-white dark:bg-slate-800 rounded-3xl border border-slate-200 dark:border-slate-700 shadow-xl p-8 max-w-4xl mx-auto">
<div className="grid md:grid-cols-3 gap-8 items-center">
{/* Before */}
<div className="text-center">
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
<Inbox className="w-10 h-10 text-red-500 dark:text-red-400" />
</div>
<h4 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">Before</h4>
<p className="text-sm text-slate-500 dark:text-slate-400">Inbox chaos</p>
<div className="mt-3 text-3xl font-bold text-red-500 dark:text-red-400">847</div>
<p className="text-xs text-slate-400 dark:text-slate-500">unread emails</p>
</div>
{/* Arrow */}
<div className="hidden md:flex justify-center">
<div className="w-24 h-24 rounded-full bg-gradient-to-r from-primary-500 to-accent-500 flex items-center justify-center shadow-lg">
<Filter className="w-10 h-10 text-white" />
{/* Before → After strip (engineering-card style) */}
<div
className="mt-8 p-5 bg-gradient-to-r from-primary-500/10 via-accent-500/10 to-primary-500/10 rounded-xl border border-primary-500/20
relative overflow-hidden group/featured transition-all duration-300 hover:border-primary-500/40 hover:shadow-lg hover:shadow-primary-500/10"
>
<div
aria-hidden="true"
className="absolute inset-0 rounded-xl opacity-0 group-hover/featured:opacity-100 transition-opacity duration-500"
>
<div className="absolute inset-[-1px] rounded-xl bg-gradient-to-r from-primary-500 via-accent-500 to-primary-500 bg-[length:200%_100%] animate-gradient-x opacity-30" />
</div>
<div className="relative flex flex-col md:flex-row items-stretch md:items-center gap-6 md:gap-8">
<div className="flex-1 flex flex-col md:flex-row items-center gap-4 md:gap-6 p-4 rounded-xl bg-slate-50/50 dark:bg-slate-900/50 border border-slate-200/50 dark:border-slate-700/50">
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Before</span>
<div className="text-center md:text-left">
<p className="font-bold text-slate-900 dark:text-slate-100 text-lg">Inbox chaos</p>
<p className="text-3xl md:text-4xl font-bold text-primary-600 dark:text-primary-400 mt-1">847</p>
<p className="text-sm text-slate-500 dark:text-slate-400">unread emails</p>
</div>
</div>
{/* After */}
<div className="text-center">
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<Inbox className="w-10 h-10 text-green-500 dark:text-green-400" />
<div className="flex items-center justify-center shrink-0 text-slate-400 dark:text-slate-500 group-hover/featured:text-primary-500 transition-colors duration-300">
<Filter className="w-7 h-7" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden />
</div>
<div className="flex-1 flex flex-col md:flex-row items-center gap-4 md:gap-6 p-4 rounded-xl bg-primary-500/10 border border-primary-500/20">
<span className="text-xs font-semibold uppercase tracking-wider text-primary-600 dark:text-primary-400">After</span>
<div className="text-center md:text-left">
<p className="font-bold text-slate-900 dark:text-slate-100 text-lg">All sorted</p>
<p className="text-3xl md:text-4xl font-bold text-primary-600 dark:text-primary-400 mt-1">12</p>
<p className="text-sm text-slate-500 dark:text-slate-400">important emails</p>
</div>
<h4 className="font-semibold text-slate-900 dark:text-slate-100 mb-1">After</h4>
<p className="text-sm text-slate-500 dark:text-slate-400">All sorted</p>
<div className="mt-3 text-3xl font-bold text-green-500 dark:text-green-400">12</div>
<p className="text-xs text-slate-400 dark:text-slate-500">important emails</p>
</div>
</div>
</div>
@@ -110,31 +177,3 @@ export function Features() {
</section>
)
}
interface FeatureCardProps {
icon: React.ElementType
title: string
description: string
color: string
index: number
highlight?: boolean
}
function FeatureCard({ icon: Icon, title, description, color, index, highlight }: FeatureCardProps) {
return (
<div
className={`group rounded-2xl p-6 border transition-all duration-300 ${
highlight
? 'bg-gradient-to-br from-white dark:from-slate-800 to-slate-50 dark:to-slate-800/50 border-primary-200 dark:border-primary-800 hover:border-primary-300 dark:hover:border-primary-700 hover:shadow-xl'
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 hover:border-primary-200 dark:hover:border-primary-800 hover:shadow-lg'
}`}
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className={`w-14 h-14 rounded-xl bg-gradient-to-br ${color} flex items-center justify-center mb-5 group-hover:scale-110 transition-transform duration-300 shadow-lg`}>
<Icon className="w-7 h-7 text-white" />
</div>
<h3 className={`${highlight ? 'text-2xl' : 'text-xl'} font-semibold text-slate-900 dark:text-slate-100 mb-2`}>{title}</h3>
<p className="text-slate-600 dark:text-slate-400">{description}</p>
</div>
)
}

View File

@@ -8,12 +8,15 @@ export function Footer() {
<div className="grid md:grid-cols-4 gap-12">
{/* Brand */}
<div className="md:col-span-1">
<Link to="/" className="flex items-center gap-2 mb-4">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-white">
E-Mail-<span className="text-primary-400">Sorter</span>
<Link to="/" className="flex items-center mb-4 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-20 h-20 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-white ml-[5px]">
Mail<span className="text-primary-400">Flow</span>
</span>
</Link>
<p className="text-sm text-slate-400 mb-6">
@@ -79,10 +82,10 @@ export function Footer() {
<ul className="space-y-3">
<li>
<a
href="mailto:support@emailsorter.webklar.com"
href="mailto:support@mailflow.webklar.com"
className="hover:text-white transition-colors"
>
support@emailsorter.webklar.com
support@mailflow.webklar.com
</a>
</li>
<li>
@@ -125,7 +128,7 @@ export function Footer() {
<div className="mt-12 pt-8 border-t border-slate-800">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 mb-4">
<p className="text-sm text-slate-500">
© {new Date().getFullYear()} EmailSorter
© {new Date().getFullYear()} MailFlow
</p>
</div>
{/* webklar.com Verweis */}

View File

@@ -1,3 +1,4 @@
import { useState, useRef, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { captureUTMParams } from '@/lib/analytics'
import { cn } from '@/lib/utils'
@@ -5,8 +6,106 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ArrowRight, Sparkles, Check } from 'lucide-react'
const HERO_TESTIMONIALS = [
{ name: 'Sarah M.', quote: 'Inbox finally under control.', position: 'top-20 -left-10', rotate: '-20deg', side: 'left' as const },
{ name: 'Tom K.', quote: 'Newsletters sorted automatically.', position: 'top-1/2 -left-10 -translate-y-1/2', rotate: '-10deg', side: 'left' as const },
{ name: 'Lisa R.', quote: 'Leads never get lost again.', position: 'top-20 -right-10', rotate: '20deg', side: 'right' as const },
{ name: 'Jan P.', quote: 'Game changer for freelancers.', position: 'bottom-20 -left-10', rotate: '-10deg', side: 'left' as const },
{ name: 'Anna L.', quote: 'Gmail & Outlook in one place.', position: 'bottom-1/2 -right-10 -translate-y-1/2', rotate: '10deg', side: 'right' as const },
{ name: 'Max B.', quote: 'Spam stays out of my inbox.', position: 'bottom-20 -right-10', rotate: '20deg', side: 'right' as const },
]
function useScrollBow(heroRef: React.RefObject<HTMLElement | null>) {
const [progress, setProgress] = useState(0)
useEffect(() => {
const hero = heroRef?.current
// #region agent log
fetch('http://127.0.0.1:7245/ingest/e4d1df4e-a6e3-4cf2-a51c-bd8134c263cd',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Hero.tsx:useScrollBow effect',message:'effect run',data:{hasHero:!!hero,height:hero?.getBoundingClientRect?.()?.height,rectTop:hero?.getBoundingClientRect?.()?.top},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H4'})}).catch(()=>{});
// #endregion
if (!hero) return
const onScroll = () => {
const rect = hero.getBoundingClientRect()
const h = rect.height
if (h <= 0) return
const p = Math.max(0, Math.min(1, -rect.top / h))
setProgress((prev) => {
if (Math.abs(prev - p) > 0.05) {
// #region agent log
fetch('http://127.0.0.1:7245/ingest/e4d1df4e-a6e3-4cf2-a51c-bd8134c263cd',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Hero.tsx:onScroll',message:'progress update',data:{p,rectTop:rect.top,h},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H1'})}).catch(()=>{});
// #endregion
}
return p
})
}
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [heroRef])
return progress
}
type TestimonialItem = (typeof HERO_TESTIMONIALS)[number]
function HeroEdgeCard({ name, quote, position, rotate, side, scrollProgress }: TestimonialItem & { scrollProgress: number }) {
const dropY = scrollProgress * 80
const flyOutX = scrollProgress * 600
const moveX = side === 'left' ? -flyOutX : flyOutX
const transform = `translate(${moveX}px, ${dropY}px) rotate(${rotate})`
const opacity = Math.max(0, 1 - scrollProgress * 1.2)
const visibility = opacity <= 0 ? 'hidden' : 'visible'
// When scrollProgress === 0, do NOT set opacity so CSS hero-edge-in can run (staggered fade-in).
// Once user scrolls, we drive opacity from scroll so cards bow out.
const styleOpacity = scrollProgress > 0 ? opacity : undefined
// #region agent log
if (name === HERO_TESTIMONIALS[0].name) {
fetch('http://127.0.0.1:7245/ingest/e4d1df4e-a6e3-4cf2-a51c-bd8134c263cd',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Hero.tsx:HeroEdgeCard',message:'card style',data:{scrollProgress,transform,opacity,styleOpacity,visibility},timestamp:Date.now(),sessionId:'debug-session',runId:'post-fix',hypothesisId:'H1,H2'})}).catch(()=>{});
}
// #endregion
return (
<div
className={cn(
'hero-edge-card absolute z-20 flex items-center gap-1 rounded-md bg-white p-1 shadow-md dark:bg-slate-800 border border-slate-200 dark:border-slate-700',
position,
'hidden md:flex transition-all duration-200 ease-out'
)}
style={{ transform, ...(styleOpacity !== undefined ? { opacity: styleOpacity } : {}), visibility }}
>
<img
alt="E-Mail"
width={128}
height={128}
className="h-auto max-h-32 w-32 shrink-0 object-contain object-center block m-0"
src="/logo.png"
onLoad={(e) => {
const img = e.currentTarget
if (name === HERO_TESTIMONIALS[0].name) {
// #region agent log
fetch('http://127.0.0.1:7245/ingest/e4d1df4e-a6e3-4cf2-a51c-bd8134c263cd',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Hero.tsx:img onLoad',message:'image size',data:{naturalWidth:img.naturalWidth,naturalHeight:img.naturalHeight,width:img.width,height:img.height},timestamp:Date.now(),sessionId:'debug-session',runId:'post-fix',hypothesisId:'H5'})}).catch(()=>{});
// #endregion
}
}}
/>
<div className="max-w-[160px]">
<h3 className="text-sm font-medium text-slate-800 dark:text-slate-200">{name}</h3>
<p className="text-xs text-slate-600 dark:text-slate-400">{quote}</p>
</div>
</div>
)
}
export function Hero() {
const navigate = useNavigate()
const heroRef = useRef<HTMLElement>(null)
const scrollProgress = useScrollBow(heroRef)
// #region agent log
useEffect(() => {
const t = setTimeout(() => {
const el = heroRef.current
fetch('http://127.0.0.1:7245/ingest/e4d1df4e-a6e3-4cf2-a51c-bd8134c263cd',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'Hero.tsx:mount',message:'after mount',data:{hasHero:!!el,height:el?.getBoundingClientRect?.()?.height,innerWidth:typeof window!=='undefined'?window.innerWidth:0},timestamp:Date.now(),sessionId:'debug-session',hypothesisId:'H3,H4'})}).catch(()=>{});
}, 100)
return () => clearTimeout(t)
}, [])
// #endregion
const handleCTAClick = () => {
// Capture UTM parameters before navigation
@@ -15,8 +114,13 @@ export function Hero() {
}
return (
<section className="relative min-h-screen flex items-center overflow-hidden">
{/* Background */}
<section ref={heroRef} className="relative min-h-screen flex items-center overflow-hidden">
{/* Edge cards stick to sides, animate on scroll (bow out) */}
{HERO_TESTIMONIALS.map((t) => (
<HeroEdgeCard key={t.name} {...t} scrollProgress={scrollProgress} />
))}
{/* Background unchanged */}
<div className="absolute inset-0 gradient-hero" />
<div className="absolute inset-0 gradient-mesh opacity-30" />

View File

@@ -5,6 +5,7 @@ import {
PartyPopper,
ArrowDown
} from 'lucide-react'
import SpotlightCard from '@/components/ui/SpotlightCard'
const steps = [
{
@@ -91,9 +92,12 @@ function StepCard({ icon: Icon, step, title, description }: StepCardProps) {
return (
<div className="relative">
{/* Card */}
<div className="bg-slate-50 dark:bg-slate-800 rounded-2xl p-6 text-center hover:bg-white dark:hover:bg-slate-700 hover:shadow-xl transition-all duration-300 border border-transparent hover:border-slate-200 dark:hover:border-slate-600">
<SpotlightCard
spotlightColor="rgba(34, 197, 94, 0.2)"
className="bg-slate-50 dark:bg-slate-800 rounded-2xl p-6 text-center hover:bg-white dark:hover:bg-slate-700 hover:shadow-xl transition-all duration-300 border border-transparent hover:border-slate-200 dark:hover:border-slate-600"
>
{/* Step number */}
<div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-gradient-to-r from-primary-500 to-primary-600 dark:from-primary-600 dark:to-primary-700 text-white text-sm font-bold px-4 py-1 rounded-full shadow-md">
<div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-gradient-to-r from-primary-500 to-primary-600 dark:from-primary-600 dark:to-primary-700 text-white text-sm font-bold px-4 py-1 rounded-full shadow-md z-10">
{step}
</div>
@@ -105,7 +109,7 @@ function StepCard({ icon: Icon, step, title, description }: StepCardProps) {
{/* Content */}
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">{title}</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm">{description}</p>
</div>
</SpotlightCard>
</div>
)
}

View File

@@ -32,12 +32,15 @@ export function Navbar() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
E-Mail-<span className="text-primary-600 dark:text-primary-400">Sorter</span>
<Link to="/" className="flex items-center leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-20 h-20 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100 ml-[5px]">
Mail<span className="text-primary-600 dark:text-primary-400">Flow</span>
</span>
</Link>

View File

@@ -1,4 +1,5 @@
import { Code2, Users, Zap } from 'lucide-react'
import SpotlightCard from '@/components/ui/SpotlightCard'
const items = [
{
@@ -33,8 +34,9 @@ export function Testimonials() {
<div className="grid md:grid-cols-3 gap-6">
{items.map((item, index) => (
<div
<SpotlightCard
key={index}
spotlightColor="rgba(34, 197, 94, 0.25)"
className="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10"
>
<div className="w-10 h-10 rounded-lg bg-primary-500/20 flex items-center justify-center mb-4">
@@ -42,7 +44,7 @@ export function Testimonials() {
</div>
<h3 className="text-base font-semibold text-white mb-1">{item.title}</h3>
<p className="text-slate-400 text-sm">{item.description}</p>
</div>
</SpotlightCard>
))}
</div>
</div>

View File

@@ -0,0 +1,44 @@
.card-spotlight {
position: relative;
overflow: hidden;
--mouse-x: 50%;
--mouse-y: 50%;
--spotlight-color: rgba(34, 197, 94, 0.2);
transition: border-color 0.3s ease;
}
.card-spotlight::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(
circle 400px at var(--mouse-x) var(--mouse-y),
var(--spotlight-color),
transparent 80%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
z-index: 0;
}
.card-spotlight:hover::before,
.card-spotlight:focus-within::before {
opacity: 0.6;
}
.card-spotlight:hover {
border-color: rgba(34, 197, 94, 0.4);
}
.dark .card-spotlight:hover {
border-color: rgba(34, 197, 94, 0.3);
}
.card-spotlight > * {
position: relative;
z-index: 1;
}

View File

@@ -0,0 +1,36 @@
import { useRef } from 'react'
import './SpotlightCard.css'
interface SpotlightCardProps {
children: React.ReactNode
className?: string
spotlightColor?: string
}
const SpotlightCard = ({ children, className = '', spotlightColor = 'rgba(34, 197, 94, 0.2)' }: SpotlightCardProps) => {
const divRef = useRef<HTMLDivElement>(null)
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!divRef.current) return
const rect = divRef.current.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
divRef.current.style.setProperty('--mouse-x', `${x}px`)
divRef.current.style.setProperty('--mouse-y', `${y}px`)
divRef.current.style.setProperty('--spotlight-color', spotlightColor)
}
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
className={`card-spotlight ${className}`}
>
{children}
</div>
)
}
export default SpotlightCard

View File

@@ -210,6 +210,40 @@ body {
opacity: 0.8;
}
/* Hero edge cards scroll animation (stick to side, bow out on scroll) */
@keyframes hero-edge-in {
from { opacity: 0; }
to { opacity: 1; }
}
.hero-edge-card {
animation: hero-edge-in 0.6s ease-out forwards;
}
/* Bild nur so groß wie der sichtbare Inhalt, keine unsichtbare Box */
.hero-edge-card img {
display: block;
max-width: 8rem;
max-height: 8rem;
width: auto;
height: auto;
object-fit: contain;
flex-shrink: 0;
line-height: 0;
vertical-align: middle;
}
.hero-edge-card:nth-child(1) { animation-delay: 0.2s; opacity: 0; }
.hero-edge-card:nth-child(2) { animation-delay: 0.35s; opacity: 0; }
.hero-edge-card:nth-child(3) { animation-delay: 0.5s; opacity: 0; }
.hero-edge-card:nth-child(4) { animation-delay: 0.4s; opacity: 0; }
.hero-edge-card:nth-child(5) { animation-delay: 0.55s; opacity: 0; }
.hero-edge-card:nth-child(6) { animation-delay: 0.7s; opacity: 0; }
@media (max-width: 767px) {
.hero-edge-card { opacity: 0.2 !important; }
}
/* Animation classes */
@keyframes float {
0%, 100% { transform: translateY(0px); }
@@ -226,6 +260,16 @@ body {
to { opacity: 1; transform: translateY(0); }
}
/* Gradient animation for feature card strip (engineering-card style) */
@keyframes gradient-x {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.animate-gradient-x {
animation: gradient-x 3s ease infinite;
}
.animate-float {
animation: float 6s ease-in-out infinite;
}

View File

@@ -236,12 +236,15 @@ export function Dashboard() {
<header className="bg-white/90 dark:bg-slate-900/90 backdrop-blur-md border-b border-slate-200 dark:border-slate-700 sticky top-0 z-50 shadow-sm">
<div className="w-full px-3 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14 sm:h-16">
<Link to="/" className="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
<div className="w-8 h-8 sm:w-9 sm:h-9 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-lg shadow-primary-500/20">
<Mail className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<span className="text-base sm:text-lg font-bold text-slate-900 dark:text-slate-100 whitespace-nowrap">
E-Mail-<span className="text-primary-600 dark:text-primary-400">Sorter</span>
<Link to="/" className="flex items-center flex-shrink-0 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-20 h-20 sm:w-20 sm:h-20 rounded-lg shadow-lg object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-base sm:text-lg font-bold text-slate-900 dark:text-slate-100 whitespace-nowrap ml-[5px]">
Mail<span className="text-primary-600 dark:text-primary-400">Flow</span>
</span>
</Link>

View File

@@ -32,12 +32,15 @@ export function ForgotPassword() {
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo */}
<Link to="/" className="flex items-center justify-center gap-2 mb-8">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
Email<span className="text-primary-600 dark:text-primary-400">Sorter</span>
<Link to="/" className="flex items-center justify-center mb-8 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-24 h-24 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100 ml-[5px]">
Mail<span className="text-primary-600 dark:text-primary-400">Flow</span>
</span>
</Link>

View File

@@ -43,7 +43,7 @@ export function Imprint() {
<div className="space-y-6 text-slate-700 dark:text-slate-300">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">Operator</h3>
<p className="mb-2">EmailSorter is operated by:</p>
<p className="mb-2">MailFlow is operated by:</p>
<p className="mb-4">
<strong>webklar.com</strong><br />
Kenso Grimm, Justin Klein
@@ -90,12 +90,12 @@ export function Imprint() {
</a>
</p>
<p className="mt-4 text-sm text-slate-600 dark:text-slate-400">
For questions regarding EmailSorter specifically:{' '}
For questions regarding MailFlow specifically:{' '}
<a
href="mailto:support@emailsorter.com"
href="mailto:support@mailflow.com"
className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 underline"
>
support@emailsorter.com
support@mailflow.com
</a>
</p>
</div>

View File

@@ -36,12 +36,15 @@ export function Login() {
<div className="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8 bg-slate-900">
<div className="w-full max-w-md">
{/* Logo */}
<Link to="/" className="flex items-center gap-2 mb-8">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-white">
E-Mail-<span className="text-primary-400">Sorter</span>
<Link to="/" className="flex items-center mb-8 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-24 h-24 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-white ml-[5px]">
Mail<span className="text-primary-400">Flow</span>
</span>
</Link>
@@ -133,7 +136,7 @@ export function Login() {
Your inbox under control
</h2>
<p className="text-primary-100">
Thousands of users already trust EmailSorter for more productive email communication.
Thousands of users already trust MailFlow for more productive email communication.
</p>
</div>
</div>

View File

@@ -40,7 +40,7 @@ export function Privacy() {
<div className="bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-lg p-6 mb-8">
<h2 className="text-xl font-semibold text-slate-900 dark:text-slate-100 mb-4">Data Protection Information</h2>
<p className="text-slate-700 dark:text-slate-300 mb-4">
EmailSorter is operated by webklar.com. The following privacy policy applies to the use of this website and our services.
MailFlow is operated by webklar.com. The following privacy policy applies to the use of this website and our services.
</p>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mt-6 mb-3">1. Responsible Party</h3>
@@ -64,7 +64,7 @@ export function Privacy() {
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mt-6 mb-3">2. Data Collection and Processing</h3>
<p className="text-slate-700 dark:text-slate-300 mb-4">
When you use EmailSorter, we collect and process the following data:
When you use MailFlow, we collect and process the following data:
</p>
<ul className="list-disc list-inside text-slate-700 dark:text-slate-300 mb-4 space-y-2 ml-4">
<li>Account information (email address, name)</li>
@@ -78,7 +78,7 @@ export function Privacy() {
We process your data exclusively for the following purposes:
</p>
<ul className="list-disc list-inside text-slate-700 dark:text-slate-300 mb-4 space-y-2 ml-4">
<li>Providing and improving the EmailSorter service</li>
<li>Providing and improving the MailFlow service</li>
<li>Automated email sorting and categorization</li>
<li>Processing payments and subscriptions</li>
<li>Customer support and communication</li>

View File

@@ -86,7 +86,7 @@ export function Register() {
</Badge>
<h2 className="text-4xl font-bold text-white mb-6">
Start with EmailSorter today
Start with MailFlow today
</h2>
<ul className="space-y-4 mb-8">
@@ -117,12 +117,15 @@ export function Register() {
<div className="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8 bg-white dark:bg-slate-900">
<div className="w-full max-w-md">
{/* Logo */}
<Link to="/" className="flex items-center gap-2 mb-8">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
E-Mail-<span className="text-primary-600 dark:text-primary-400">Sorter</span>
<Link to="/" className="flex items-center mb-8 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-24 h-24 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100 ml-[5px]">
Mail<span className="text-primary-600 dark:text-primary-400">Flow</span>
</span>
</Link>

View File

@@ -86,12 +86,15 @@ export function ResetPassword() {
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo */}
<Link to="/" className="flex items-center justify-center gap-2 mb-8">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
Email<span className="text-primary-600 dark:text-primary-400">Sorter</span>
<Link to="/" className="flex items-center justify-center mb-8 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-24 h-24 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100 ml-[5px]">
Mail<span className="text-primary-600 dark:text-primary-400">Flow</span>
</span>
</Link>

View File

@@ -2413,7 +2413,7 @@ export function Settings() {
<CardHeader>
<CardTitle>Referrals</CardTitle>
<CardDescription>
Share EmailSorter and earn rewards
Share MailFlow and earn rewards
</CardDescription>
</CardHeader>
<CardContent>
@@ -2500,7 +2500,7 @@ export function Settings() {
<Card>
<CardHeader>
<CardTitle>Current Subscription</CardTitle>
<CardDescription>Manage your EmailSorter subscription</CardDescription>
<CardDescription>Manage your MailFlow subscription</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-primary-50 to-accent-50 dark:from-primary-900/30 dark:to-accent-900/30 rounded-xl border border-primary-100 dark:border-primary-800">

View File

@@ -54,12 +54,15 @@ export function VerifyEmail() {
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo */}
<Link to="/" className="flex items-center justify-center gap-2 mb-8">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Mail className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
Email<span className="text-primary-600 dark:text-primary-400">Sorter</span>
<Link to="/" className="flex items-center justify-center mb-8 leading-none">
<img
src="/logo.png"
alt="MailFlow Logo"
className="w-24 h-24 rounded-xl object-contain pr-[5px] block"
style={{ display: 'block', margin: 0, padding: 0 }}
/>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100 ml-[5px]">
Mail<span className="text-primary-600 dark:text-primary-400">Flow</span>
</span>
</Link>
@@ -98,7 +101,7 @@ export function VerifyEmail() {
</div>
<p className="text-slate-600 dark:text-slate-400">
Du kannst jetzt alle Features von EmailSorter nutzen.
Du kannst jetzt alle Features von MailFlow nutzen.
</p>
<Button onClick={() => navigate('/dashboard')} className="w-full">
@@ -144,8 +147,8 @@ export function VerifyEmail() {
{/* Help text */}
<p className="text-center text-sm text-slate-500 dark:text-slate-400 mt-6">
Probleme? Kontaktiere uns unter{' '}
<a href="mailto:support@emailsorter.de" className="text-primary-600 dark:text-primary-400 hover:underline">
support@emailsorter.de
<a href="mailto:support@mailflow.de" className="text-primary-600 dark:text-primary-400 hover:underline">
support@mailflow.de
</a>
</p>
</div>

View File

@@ -1,6 +1,6 @@
# EmailSorter - Einrichtungsanleitung
# MailFlow - Einrichtungsanleitung
Diese Anleitung führt dich durch die komplette Einrichtung von EmailSorter.
Diese Anleitung führt dich durch die komplette Einrichtung von MailFlow.
---
@@ -31,13 +31,13 @@ Diese Anleitung führt dich durch die komplette Einrichtung von EmailSorter.
1. Gehe zu [cloud.appwrite.io](https://cloud.appwrite.io)
2. Erstelle einen kostenlosen Account
3. Erstelle ein neues Projekt (z.B. "EmailSorter")
3. Erstelle ein neues Projekt (z.B. "MailFlow")
### 1.2 API Key erstellen
1. Gehe zu **Settings****API Credentials**
2. Klicke auf **Create API Key**
3. Name: `EmailSorter Backend`
3. Name: `MailFlow Backend`
4. Wähle **alle Berechtigungen** aus (Full Access)
5. Kopiere den API Key
@@ -45,7 +45,7 @@ Diese Anleitung führt dich durch die komplette Einrichtung von EmailSorter.
1. Gehe zu **Databases**
2. Klicke auf **Create Database**
3. Name: `email_sorter_db`
3. Name: `mailflow_db`
4. Kopiere die **Database ID**
### 1.4 Bootstrap ausführen
@@ -73,7 +73,7 @@ Dies erstellt automatisch alle benötigten Collections:
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=deine_projekt_id
APPWRITE_API_KEY=dein_api_key
APPWRITE_DATABASE_ID=email_sorter_db
APPWRITE_DATABASE_ID=mailflow_db
```
```env
@@ -102,17 +102,17 @@ VITE_APPWRITE_PROJECT_ID=deine_projekt_id
Gehe zu **Products****Add product**:
#### Basic Plan
- Name: `EmailSorter Basic`
- Name: `MailFlow Basic`
- Preis: `9.00 EUR` / Monat
- Kopiere die **Price ID** (beginnt mit `price_`)
#### Pro Plan
- Name: `EmailSorter Pro`
- Name: `MailFlow Pro`
- Preis: `19.00 EUR` / Monat
- Kopiere die **Price ID**
#### Business Plan
- Name: `EmailSorter Business`
- Name: `MailFlow Business`
- Preis: `49.00 EUR` / Monat
- Kopiere die **Price ID**
@@ -175,7 +175,7 @@ STRIPE_PRICE_BUSINESS=price_...
1. Gehe zu **APIs & Services****OAuth consent screen**
2. Wähle **External**
3. Fülle aus:
- App name: `EmailSorter`
- App name: `MailFlow`
- User support email: Deine E-Mail
- Developer contact: Deine E-Mail
4. **Scopes** hinzufügen:
@@ -189,7 +189,7 @@ STRIPE_PRICE_BUSINESS=price_...
1. Gehe zu **APIs & Services****Credentials**
2. Klicke auf **Create Credentials****OAuth client ID**
3. Typ: **Web application**
4. Name: `EmailSorter Web`
4. Name: `MailFlow Web`
5. **Authorized redirect URIs**:
- `http://localhost:3000/api/oauth/gmail/callback` (Entwicklung)
- `https://deine-domain.de/api/oauth/gmail/callback` (Produktion)

24
engineering-card/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
(function() {
var stored = localStorage.getItem("theme");
var dark = stored === "dark" || (stored !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
if (dark) document.documentElement.classList.add("dark");
})();
</script>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Engineering Impact Card</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3543
engineering-card/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
{
"name": "engineering-card",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"tailwindcss": "^4.1.18",
"vite": "^7.2.4"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
/* App-specific overrides if needed; layout in App.jsx uses Tailwind */

View File

@@ -0,0 +1,41 @@
import { useState, useEffect } from "react";
import { Moon, Sun } from "lucide-react";
import { EngineeringImpactCard } from "./components/EngineeringImpactCard";
import "./App.css";
function App() {
const [dark, setDark] = useState(() => {
const stored = localStorage.getItem("theme");
if (stored === "dark" || stored === "light") return stored === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
});
useEffect(() => {
const root = document.documentElement;
if (dark) {
root.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
root.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}, [dark]);
return (
<main className="min-h-screen bg-background p-6 md:p-8 flex flex-col items-center justify-center relative">
<button
type="button"
onClick={() => setDark((d) => !d)}
className="fixed top-4 right-4 z-50 p-2.5 rounded-xl border border-border bg-card/80 backdrop-blur-sm text-foreground hover:bg-primary/10 hover:border-primary/30 hover:text-primary transition-all duration-300 shadow-lg"
aria-label={dark ? "Hellmodus aktivieren" : "Dark Mode aktivieren"}
>
{dark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button>
<div className="w-full max-w-5xl">
<EngineeringImpactCard />
</div>
</main>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,157 @@
import {
FolderTree,
MousePointerClick,
CalendarClock,
ScanSearch,
ShieldCheck,
Sparkles,
} from "lucide-react";
const FEATURES = [
{
icon: FolderTree,
title: "Categories, not chaos",
desc: "Leads, clients, invoices, newsletters — sorted into folders. Your inbox shows what pays first.",
glowClass: "from-orange-500/20 to-amber-500/20",
delay: 0,
},
{
icon: MousePointerClick,
title: "One click to sort",
desc: "Connect your inbox, click Sort Now. No rules to write. We read and categorize; you review.",
glowClass: "from-emerald-500/20 to-teal-500/20",
delay: 50,
},
{
icon: CalendarClock,
title: "Runs when you want",
desc: "Sort on demand or set a schedule. Your inbox stays organized without you touching it.",
glowClass: "from-blue-500/20 to-indigo-500/20",
delay: 100,
},
{
icon: ScanSearch,
title: "Content-aware sorting",
desc: "We look at sender, subject, and a short snippet to decide the category. No keyword lists.",
glowClass: "from-violet-500/20 to-purple-500/20",
delay: 150,
},
{
icon: ShieldCheck,
title: "Minimal data",
desc: "We only read what we need to categorize. No storing email body or attachments. GDPR compliant.",
glowClass: "from-green-500/20 to-emerald-500/20",
delay: 200,
},
{
icon: Sparkles,
title: "Less time on triage",
desc: "Spend less time deciding what's important. Inbox shows clients and leads first.",
glowClass: "from-cyan-500/20 to-blue-500/20",
delay: 250,
},
];
function FeatureItem({ icon: Icon, title, desc, glowClass, delay }) {
return (
<div
className="group relative"
style={{ animationDelay: `${delay}ms` }}
>
<div
aria-hidden="true"
className={`absolute inset-0 bg-gradient-to-br ${glowClass} rounded-xl opacity-0 group-hover:opacity-100 blur-xl transition-opacity duration-300 -z-10 scale-110`}
/>
<div
className="relative p-4 rounded-xl border border-border/50 bg-card/80 backdrop-blur-sm h-full
transform-gpu transition-all duration-300 ease-out
hover:border-primary/40 hover:shadow-lg hover:shadow-primary/5
hover:-translate-y-1 hover:scale-[1.02]
group-hover:bg-card/95"
>
<div
aria-hidden="true"
className="absolute inset-0 rounded-xl overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-300"
>
<div className="absolute inset-0 -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-out bg-gradient-to-r from-transparent via-white/10 to-transparent" />
</div>
<div className="relative z-10">
<div className="flex items-start gap-3 mb-3">
<div
className="p-2.5 bg-primary/10 rounded-xl text-primary shrink-0
transition-all duration-300 ease-out
group-hover:bg-primary group-hover:text-primary-foreground
group-hover:scale-110 group-hover:rotate-3 group-hover:shadow-lg group-hover:shadow-primary/25"
>
<Icon className="h-6 w-6" aria-hidden strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</div>
<h4 className="text-base font-semibold text-foreground transition-colors duration-300 group-hover:text-primary pt-0.5">
{title}
</h4>
</div>
<p
className="text-sm text-muted-foreground leading-relaxed transition-all duration-300
opacity-90 group-hover:opacity-100 group-hover:text-foreground/90"
>
{desc}
</p>
</div>
</div>
</div>
);
}
export function EngineeringImpactCard() {
return (
<div
data-slot="card"
className="bg-card/80 text-card-foreground flex flex-col gap-6 rounded-2xl border backdrop-blur-md shadow-lg shadow-black/5 dark:bg-card/60 dark:shadow-black/20 transform-gpu relative transition-all duration-300 ease-out hover:shadow-2xl hover:shadow-primary/10 hover:border-primary/30 dark:hover:shadow-primary/20 before:absolute before:inset-0 before:rounded-2xl before:p-[1.5px] before:bg-gradient-to-br before:from-primary/50 before:via-accent/30 before:to-primary/50 before:-z-10 before:opacity-0 before:transition-opacity before:duration-500 hover:before:opacity-100 after:absolute after:inset-0 after:rounded-2xl after:-z-20 after:bg-gradient-to-br after:from-primary/5 after:via-transparent after:to-accent/5 after:opacity-0 after:transition-opacity after:duration-500 hover:after:opacity-100 p-6 md:p-8 overflow-hidden"
>
<div className="mb-8 text-center">
<h3 className="text-2xl font-bold mb-2">What it does</h3>
<p className="text-muted-foreground max-w-xl mx-auto">
Sort incoming mail into categories so your inbox shows what matters first. No rules to write.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{FEATURES.map((feature, i) => (
<FeatureItem key={i} {...feature} />
))}
</div>
<div
className="mt-8 p-5 bg-gradient-to-r from-primary/10 via-accent/10 to-secondary/10 rounded-xl border border-primary/20
relative overflow-hidden group/featured transition-all duration-300 hover:border-primary/40 hover:shadow-lg hover:shadow-primary/10"
>
<div
aria-hidden="true"
className="absolute inset-0 rounded-xl opacity-0 group-hover/featured:opacity-100 transition-opacity duration-500"
>
<div className="absolute inset-[-1px] rounded-xl bg-gradient-to-r from-primary via-accent to-primary bg-[length:200%_100%] animate-gradient-x opacity-30" />
</div>
<div className="relative flex flex-col md:flex-row items-stretch md:items-center gap-6 md:gap-8">
<div className="flex-1 flex flex-col md:flex-row items-center gap-4 md:gap-6 p-4 rounded-xl bg-background/50 border border-border/50">
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Before</span>
<div className="text-center md:text-left">
<p className="font-bold text-foreground text-lg">Inbox chaos</p>
<p className="text-3xl md:text-4xl font-bold text-primary mt-1">847</p>
<p className="text-sm text-muted-foreground">unread emails</p>
</div>
</div>
<div className="flex items-center justify-center shrink-0 text-muted-foreground group-hover/featured:text-primary transition-colors duration-300">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</div>
<div className="flex-1 flex flex-col md:flex-row items-center gap-4 md:gap-6 p-4 rounded-xl bg-primary/10 border border-primary/20">
<span className="text-xs font-semibold uppercase tracking-wider text-primary">After</span>
<div className="text-center md:text-left">
<p className="font-bold text-foreground text-lg">All sorted</p>
<p className="text-3xl md:text-4xl font-bold text-primary mt-1">12</p>
<p className="text-sm text-muted-foreground">important emails</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,78 @@
@import "tailwindcss";
/* Theme semantic tokens (light) */
@theme {
--color-background: oklch(0.98 0.008 65);
--color-foreground: oklch(0.20 0.03 40);
--color-primary: oklch(0.55 0.18 40);
--color-primary-foreground: oklch(0.98 0.01 60);
--color-secondary: oklch(0.94 0.02 70);
--color-secondary-foreground: oklch(0.30 0.04 40);
--color-muted: oklch(0.95 0.015 65);
--color-muted-foreground: oklch(0.40 0.03 45);
--color-card: oklch(0.995 0.005 60);
--color-card-foreground: var(--color-foreground);
--color-border: oklch(0.88 0.02 60);
--color-ring: oklch(0.55 0.18 40);
--color-accent: oklch(0.45 0.15 15);
--color-accent-foreground: oklch(0.98 0.01 60);
--radius: 0.625rem;
}
.dark {
--color-background: oklch(0.15 0.02 40);
--color-foreground: oklch(0.92 0.02 70);
--color-primary: oklch(0.70 0.16 55);
--color-primary-foreground: oklch(0.15 0.03 40);
--color-secondary: oklch(0.25 0.025 45);
--color-secondary-foreground: oklch(0.88 0.02 70);
--color-muted: oklch(0.23 0.02 42);
--color-muted-foreground: oklch(0.60 0.03 65);
--color-card: oklch(0.19 0.025 40);
--color-border: oklch(0.30 0.025 42);
--color-ring: oklch(0.70 0.16 55);
--color-accent: oklch(0.55 0.14 20);
--color-accent-foreground: oklch(0.95 0.01 70);
}
/* Gradient animation for featured strip */
@keyframes gradient-x {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.animate-gradient-x {
animation: gradient-x 3s ease infinite;
}
/* Base */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: var(--color-background);
color: var(--color-foreground);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* Tailwind v4 uses these; ensure border uses theme */
[class*="border"] {
border-color: var(--color-border);
}
/* Optional: font preload in index.html for Inter + Space Grotesk */
h1, h2, h3, h4 {
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
font-weight: 700;
letter-spacing: -0.02em;
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

21
herosection/index.html Normal file
View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="de" style="color-scheme: dark;">
<head>
<meta charset="UTF-8" />
<script>
(function() {
var stored = localStorage.getItem("theme");
var dark = stored === "dark" || (stored !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches);
if (dark) document.documentElement.classList.add("dark");
else document.documentElement.classList.remove("dark");
})();
</script>
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hero Section Centered Image</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

BIN
herosection/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

3543
herosection/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
herosection/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "herosection",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"tailwindcss": "^4.1.18",
"vite": "^7.2.4"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

48
herosection/src/App.jsx Normal file
View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react'
import { Moon, Sun } from 'lucide-react'
import HeroSection from './components/HeroSection.jsx'
function App() {
const [dark, setDark] = useState(() => {
const stored = localStorage.getItem('theme')
if (stored === 'dark' || stored === 'light') return stored === 'dark'
return window.matchMedia('(prefers-color-scheme: dark)').matches
})
useEffect(() => {
const root = document.documentElement
if (dark) {
root.classList.add('dark')
localStorage.setItem('theme', 'dark')
} else {
root.classList.remove('dark')
localStorage.setItem('theme', 'light')
}
}, [dark])
return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-900">
<button
type="button"
onClick={() => setDark((d) => !d)}
className="fixed top-4 right-4 z-[100] rounded-lg border border-neutral-200 bg-white/90 p-2.5 text-neutral-700 shadow-lg backdrop-blur-sm transition-colors hover:bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800/90 dark:text-neutral-200 dark:hover:bg-neutral-700"
aria-label={dark ? 'Hellmodus' : 'Dark Mode'}
>
{dark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</button>
<HeroSection />
<section className="relative z-40 min-h-[60vh] px-4 py-20 md:px-8 md:py-28">
<div className="mx-auto max-w-4xl text-center">
<h2 className="text-2xl font-semibold text-neutral-800 dark:text-neutral-100 sm:text-3xl md:text-4xl">
What happens next
</h2>
<p className="mt-4 text-neutral-600 dark:text-neutral-400">
Scroll down to explore more. This section makes the page scrollable.
</p>
</div>
</section>
</div>
)
}
export default App

View File

@@ -0,0 +1,80 @@
import { useState, useRef, useEffect } from 'react'
const TESTIMONIALS = [
{ name: 'Manu Arora', quote: 'Fantastic AI, highly recommend it.', position: 'top-20 -left-10', rotate: '-20deg', side: 'left' },
{ name: 'Tyler Durden', quote: 'AI revolutionized my business model.', position: 'top-1/2 -left-10 -translate-y-1/2', rotate: '-10deg', side: 'left' },
{ name: 'Alice Johnson', quote: 'Transformed the way I work!', position: 'top-20 -right-10', rotate: '20deg', side: 'right' },
{ name: 'Bob Smith', quote: 'Absolutely revolutionary, a game-changer.', position: 'bottom-20 -left-10', rotate: '-10deg', side: 'left' },
{ name: 'Cathy Lee', quote: 'Improved my work efficiency and daily life.', position: 'bottom-1/2 -right-10 -translate-y-1/2', rotate: '10deg', side: 'right' },
{ name: 'David Wright', quote: "It's like having a superpower!", position: 'bottom-20 -right-10', rotate: '20deg', side: 'right' },
]
// Scroll-Fortschritt 0..1: wie weit die Hero-Section nach oben weggescrollt ist → Karten bewegen sich im Bogen nach unten
function useScrollBow(heroRef) {
const [progress, setProgress] = useState(0)
useEffect(() => {
const hero = heroRef?.current
if (!hero) return
const onScroll = () => {
const rect = hero.getBoundingClientRect()
const h = rect.height
if (h <= 0) return
const p = Math.max(0, Math.min(1, -rect.top / h))
setProgress(p)
}
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [heroRef])
return progress
}
const EMAIL_LOGO = '/logo.png'
function TestimonialCard({ name, quote, position, rotate, side, scrollProgress }) {
const dropY = scrollProgress * 80
const flyOutX = scrollProgress * 600
const moveX = side === 'left' ? -flyOutX : flyOutX
const transform = `translate(${moveX}px, ${dropY}px) rotate(${rotate})`
const opacity = Math.max(0, 1 - scrollProgress * 1.2)
const visibility = opacity <= 0 ? 'hidden' : 'visible'
return (
<div
className={`hero-edge-card absolute z-20 flex items-center gap-2 rounded-md bg-white p-4 shadow-lg dark:bg-neutral-800 ${position} hidden md:flex transition-all duration-200 ease-out`}
style={{ transform, opacity, visibility }}
>
<img alt="E-Mail" width={50} height={50} className="size-12 shrink-0 object-contain" src={EMAIL_LOGO} />
<div className="max-w-[180px]">
<h3 className="text-xs text-neutral-800 md:text-base dark:text-neutral-200">{name}</h3>
<p className="text-[10px] text-neutral-600 md:text-sm dark:text-neutral-400">{quote}</p>
</div>
</div>
)
}
function HeroSection() {
const heroRef = useRef(null)
const scrollProgress = useScrollBow(heroRef)
return (
<div
ref={heroRef}
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gray-50 px-4 md:px-8 dark:bg-neutral-900 animate-gradient-bg"
>
{/* Rand: Testimonial-Karten bewegen sich beim Scrollen in einem Bogen nach unten */}
{TESTIMONIALS.map((t) => (
<TestimonialCard key={t.name} {...t} scrollProgress={scrollProgress} />
))}
{/* Dezentes animiertes Grid im Hintergrund (nur auf md+) */}
<div
className="absolute inset-0 z-10 hidden md:block opacity-40 animate-grid-pulse bg-[length:24px_24px] bg-[linear-gradient(to_right,#8881_1px,transparent_1px),linear-gradient(to_bottom,#8881_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#fff1_1px,transparent_1px),linear-gradient(to_bottom,#fff1_1px,transparent_1px)]"
aria-hidden
/>
{/* Overlay: auf Mobile sichtbar, ab md ausgeblendet */}
<div className="absolute inset-0 z-30 h-full w-full bg-white opacity-80 md:opacity-0 dark:bg-neutral-900 pointer-events-none" aria-hidden />
</div>
)
}
export default HeroSection

125
herosection/src/index.css Normal file
View File

@@ -0,0 +1,125 @@
@import "tailwindcss";
/* Hero Section Hintergrund-Animation */
@keyframes gradient-shift {
0%, 100% { opacity: 1; background-position: 0% 50%; }
50% { opacity: 0.95; background-position: 100% 50%; }
}
@keyframes grid-pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.7; }
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-gradient-bg {
background: linear-gradient(135deg, rgb(250 250 250) 0%, rgb(245 245 245) 50%, rgb(250 250 250) 100%);
background-size: 200% 200%;
animation: gradient-shift 8s ease infinite;
}
.dark .animate-gradient-bg {
background: linear-gradient(135deg, rgb(23 23 23) 0%, rgb(38 38 38) 50%, rgb(23 23 23) 100%);
background-size: 200% 200%;
}
.animate-grid-pulse {
animation: grid-pulse 4s ease-in-out infinite;
}
.hero-content-in {
animation: fade-in-up 0.8s ease-out forwards;
}
.hero-content-in:nth-child(9) { animation-delay: 0.1s; opacity: 0; }
.hero-content-in:nth-child(10) { animation-delay: 0.3s; opacity: 0; }
.hero-content-in:nth-child(11) { animation-delay: 0.5s; opacity: 0; }
.hero-submit-btn {
transition: box-shadow 0.2s ease, transform 0.15s ease;
}
.hero-submit-btn:hover {
box-shadow: 0px -1px 0px 0px #FFFFFF50 inset, 0px 1px 0px 0px #FFFFFF50 inset, 0 0 20px rgba(255,255,255,0.2);
}
.dark .hero-submit-btn:hover {
box-shadow: 0px -1px 0px 0px #00000030 inset, 0px 1px 0px 0px #00000030 inset, 0 0 20px rgba(0,0,0,0.2);
}
.hero-submit-btn:active {
transform: scale(0.98);
}
/* Rand-Elemente: Scroll-Animation (Marquee) */
@keyframes marquee-scroll {
0% { transform: translateX(0%); }
100% { transform: translateX(-100%); }
}
.hero-marquee-track {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
animation: marquee-scroll 25s linear infinite;
}
.hero-marquee-track:hover {
animation-play-state: paused;
}
.hero-marquee-container {
overflow: hidden;
display: flex;
position: relative;
width: 100%;
mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
-webkit-mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
}
.dark .hero-marquee-container {
mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
-webkit-mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
}
/* Rand: Testimonial-Karten Einblend-Animation (nur opacity, Rotation bleibt per inline) */
@keyframes hero-edge-in {
from { opacity: 0; }
to { opacity: 1; }
}
.hero-edge-card {
animation: hero-edge-in 0.6s ease-out forwards;
}
.hero-edge-card:nth-child(1) { animation-delay: 0.2s; opacity: 0; }
.hero-edge-card:nth-child(2) { animation-delay: 0.35s; opacity: 0; }
.hero-edge-card:nth-child(3) { animation-delay: 0.5s; opacity: 0; }
.hero-edge-card:nth-child(4) { animation-delay: 0.4s; opacity: 0; }
.hero-edge-card:nth-child(5) { animation-delay: 0.55s; opacity: 0; }
.hero-edge-card:nth-child(6) { animation-delay: 0.7s; opacity: 0; }
@media (max-width: 1023px) {
.hero-edge-card { opacity: 0.2 !important; }
}
/* Base */
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
-webkit-font-smoothing: antialiased;
}

10
herosection/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})

View File

@@ -1,8 +1,8 @@
# Marketing & Promotion Strategie für EmailSorter
# Marketing & Promotion Strategie für MailFlow
## Übersicht
Dieses Verzeichnis enthält alle Marketing-Dokumente und Strategien für die Promotion von EmailSorter. Die Strategie fokussiert sich auf organisches Wachstum durch TikTok, YouTube, Influencer-Marketing und Product Hunt.
Dieses Verzeichnis enthält alle Marketing-Dokumente und Strategien für die Promotion von MailFlow. Die Strategie fokussiert sich auf organisches Wachstum durch TikTok, YouTube, Influencer-Marketing und Product Hunt.
---

View File

@@ -1,4 +1,4 @@
# n8n Workflows für EmailSorter
# n8n Workflows für MailFlow
Dieses Verzeichnis enthält optionale n8n Workflows zur E-Mail-Automatisierung.
@@ -11,7 +11,7 @@ Dieses Verzeichnis enthält optionale n8n Workflows zur E-Mail-Automatisierung.
2. **Credentials einrichten**
- Gmail OAuth2 Credentials
- Mistral AI API Key (https://console.mistral.ai/)
- HTTP Header Auth für EmailSorter API
- HTTP Header Auth für MailFlow API
## Workflows
@@ -23,7 +23,7 @@ Haupt-Workflow für die E-Mail-Sortierung:
2. **Gmail: E-Mail abrufen**: Holt E-Mail-Details
3. **Mistral AI: Klassifizieren**: KI kategorisiert die E-Mail
4. **Gmail: Label setzen**: Fügt entsprechendes Label hinzu
5. **Statistiken aktualisieren**: Sendet Update an EmailSorter API
5. **Statistiken aktualisieren**: Sendet Update an MailFlow API
## Setup
@@ -51,18 +51,18 @@ n8n import:workflow --input=workflows/email-sorter-workflow.json
### 3. Environment Variables
```env
EMAILSORTER_API_URL=http://localhost:3000
EMAILSORTER_API_KEY=your-api-key
MAILFLOW_API_URL=http://localhost:3000
MAILFLOW_API_KEY=your-api-key
```
### 4. Webhook URL notieren
Nach dem Aktivieren des Workflows wird eine Webhook-URL generiert:
```
https://your-n8n-instance.com/webhook/email-sorter-webhook
https://your-n8n-instance.com/webhook/mailflow-webhook
```
Diese URL im EmailSorter Backend konfigurieren.
Diese URL im MailFlow Backend konfigurieren.
## Anpassungen
@@ -89,7 +89,7 @@ Nach dem Label-Node einen "Gmail: Archive" Node hinzufügen:
- Ausführungen in n8n UI überwachen
- Fehler-Benachrichtigungen einrichten
- Statistiken im EmailSorter Dashboard prüfen
- Statistiken im MailFlow Dashboard prüfen
## Skalierung

View File

@@ -1,5 +1,5 @@
{
"name": "EmailSorter - Automatische E-Mail-Sortierung",
"name": "MailFlow - Automatische E-Mail-Sortierung",
"nodes": [
{
"parameters": {
@@ -13,7 +13,7 @@
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 300],
"webhookId": "email-sorter-webhook"
"webhookId": "mailflow-webhook"
},
{
"parameters": {
@@ -81,7 +81,7 @@
"resource": "message",
"operation": "addLabels",
"messageId": "={{ $('Gmail: E-Mail abrufen').item.json.id }}",
"labelIds": ["EmailSorter/Newsletter"]
"labelIds": ["MailFlow/Newsletter"]
},
"id": "gmail-label-newsletter",
"name": "Gmail: Newsletter Label",
@@ -100,7 +100,7 @@
"resource": "message",
"operation": "addLabels",
"messageId": "={{ $('Gmail: E-Mail abrufen').item.json.id }}",
"labelIds": ["={{ 'EmailSorter/' + $('Mistral AI: Klassifizieren').item.json.choices[0].message.content.trim() }}"]
"labelIds": ["={{ 'MailFlow/' + $('Mistral AI: Klassifizieren').item.json.choices[0].message.content.trim() }}"]
},
"id": "gmail-label-other",
"name": "Gmail: Kategorie Label",
@@ -116,7 +116,7 @@
},
{
"parameters": {
"url": "={{ $env.EMAILSORTER_API_URL }}/api/email/stats/update",
"url": "={{ $env.MAILFLOW_API_URL }}/api/email/stats/update",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,

BIN
pics/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

View File

@@ -1,5 +1,5 @@
# ═══════════════════════════════════════════════════════════════════════════
# EmailSorter - Appwrite CORS Platform Setup (Vollautomatisch via API)
# MailFlow - Appwrite CORS Platform Setup (Vollautomatisch via API)
# ═══════════════════════════════════════════════════════════════════════════
# Dieses Script versucht, die Platform automatisch über die Appwrite API
# hinzuzufügen. Falls der API Key nicht die richtigen Scopes hat, wird

View File

@@ -1,5 +1,5 @@
# ═══════════════════════════════════════════════════════════════════════════
# EmailSorter - Appwrite CORS Platform Setup (Automatisch)
# MailFlow - Appwrite CORS Platform Setup (Automatisch)
# ═══════════════════════════════════════════════════════════════════════════
# Dieses Script fügt automatisch die Production-Platform zu Appwrite hinzu,
# um CORS-Fehler zu beheben.

View File

@@ -1,5 +1,5 @@
# ═══════════════════════════════════════════════════════════════════════════
# EmailSorter - Appwrite Setup Script
# MailFlow - Appwrite Setup Script
# ═══════════════════════════════════════════════════════════════════════════
#
# ⚠️ WICHTIG: Diese Datei ist VERALTET!
@@ -33,7 +33,7 @@ if (-not (Test-Path ".\server\.env")) {
Write-Host " APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1" -ForegroundColor Gray
Write-Host " APPWRITE_PROJECT_ID=deine_projekt_id" -ForegroundColor Gray
Write-Host " APPWRITE_API_KEY=dein_api_key" -ForegroundColor Gray
Write-Host " APPWRITE_DATABASE_ID=emailsorter" -ForegroundColor Gray
Write-Host " APPWRITE_DATABASE_ID=mailflow" -ForegroundColor Gray
Write-Host "`nSiehe server/env.example für ein Template." -ForegroundColor Cyan
exit 1
}
@@ -54,7 +54,7 @@ if (-not (Test-Path "package.json")) {
exit 1
}
Write-Host "`n=== EmailSorter Bootstrap ===" -ForegroundColor Cyan
Write-Host "`n=== MailFlow Bootstrap ===" -ForegroundColor Cyan
Write-Host "Verwende bootstrap-v2.mjs (aktuelle Version)" -ForegroundColor Green
Write-Host ""

View File

@@ -1,8 +1,8 @@
# EmailSorter Production Setup Script
# MailFlow Production Setup Script
# Dieses Script hilft beim Setup für Production
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "EmailSorter Production Setup" -ForegroundColor Cyan
Write-Host "MailFlow Production Setup" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
@@ -59,18 +59,18 @@ if (Test-Path $serverPath) {
# Prüfe ob Server bereits läuft
$pm2List = pm2 list 2>&1
if ($pm2List -match "emailsorter-api") {
if ($pm2List -match "mailflow-api") {
Write-Host " Server läuft bereits. Neustart..." -ForegroundColor Yellow
pm2 restart emailsorter-api
pm2 restart mailflow-api
} else {
Write-Host " Starte Backend Server mit PM2..." -ForegroundColor Yellow
pm2 start index.mjs --name emailsorter-api
pm2 start index.mjs --name mailflow-api
pm2 save
}
Write-Host "✓ Backend Server gestartet" -ForegroundColor Green
Write-Host " Status: pm2 status" -ForegroundColor Cyan
Write-Host " Logs: pm2 logs emailsorter-api" -ForegroundColor Cyan
Write-Host " Logs: pm2 logs mailflow-api" -ForegroundColor Cyan
} else {
Write-Host "✗ Server Verzeichnis nicht gefunden!" -ForegroundColor Red
}

View File

@@ -2,8 +2,8 @@ import 'dotenv/config';
import { Client, Databases, ID, Permission, Role } from "node-appwrite";
/**
* EmailSorter Database Bootstrap Script v2
* Creates all required collections for the full EmailSorter app
* MailFlow Database Bootstrap Script v2
* Creates all required collections for the full MailFlow app
*/
const requiredEnv = [
@@ -26,8 +26,8 @@ const client = new Client()
const db = new Databases(client);
const DB_ID = process.env.APPWRITE_DATABASE_ID || 'emailsorter';
const DB_NAME = 'EmailSorter';
const DB_ID = process.env.APPWRITE_DATABASE_ID || 'mailflow';
const DB_NAME = 'MailFlow';
// Helper: create database if not exists
async function ensureDatabase() {
@@ -262,7 +262,7 @@ async function setupCollections() {
async function main() {
console.log('\n========================================');
console.log(' EmailSorter Database Bootstrap v2');
console.log(' MailFlow Database Bootstrap v2');
console.log('========================================\n');
await ensureDatabase();

View File

@@ -1,5 +1,5 @@
/**
* EmailSorter - Database Cleanup Script
* MailFlow - Database Cleanup Script
*
* ⚠️ WICHTIG: Liest Credentials aus Umgebungsvariablen (.env)
* Keine hardcoded API Keys mehr!

View File

@@ -33,11 +33,11 @@ try {
const productsResponse = await databases.listDocuments(
process.env.APPWRITE_DATABASE_ID,
'products',
[Query.equal('slug', 'email-sorter'), Query.equal('isActive', true)]
[Query.equal('slug', 'mailflow'), Query.equal('isActive', true)]
);
if (productsResponse.documents.length === 0) {
console.error('❌ No active product found with slug "email-sorter"');
console.error('❌ No active product found with slug "mailflow"');
process.exit(1);
}

View File

@@ -1,5 +1,5 @@
# ═══════════════════════════════════════════════════════════════════════════
# EmailSorter Backend - Konfiguration
# MailFlow Backend - Konfiguration
# ═══════════════════════════════════════════════════════════════════════════
# Kopiere diese Datei nach `.env` und fülle die Werte aus.
@@ -21,7 +21,7 @@ FRONTEND_URL=http://localhost:5173
APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
APPWRITE_PROJECT_ID=dein_projekt_id
APPWRITE_API_KEY=dein_api_key_mit_allen_berechtigungen
APPWRITE_DATABASE_ID=email_sorter_db
APPWRITE_DATABASE_ID=mailflow_db
# ─────────────────────────────────────────────────────────────────────────────
# Stripe (ERFORDERLICH)

View File

@@ -1,5 +1,5 @@
/**
* EmailSorter Backend Server
* MailFlow Backend Server
* Main entry point
*/

View File

@@ -1,7 +1,7 @@
{
"name": "email-sorter-server",
"name": "mailflow-server",
"version": "2.0.0",
"description": "EmailSorter Backend Server - KI-gestützte E-Mail-Sortierung",
"description": "MailFlow Backend Server - KI-gestützte E-Mail-Sortierung",
"main": "index.mjs",
"type": "module",
"engines": {

View File

@@ -164,7 +164,7 @@ router.post('/connect-demo',
}),
asyncHandler(async (req, res) => {
const { userId } = req.body
const demoEmail = `demo-${userId.slice(0, 8)}@emailsorter.demo`
const demoEmail = `demo-${userId.slice(0, 8)}@mailflow.demo`
// Check if demo account already exists
const existingAccounts = await emailAccounts.getByUser(userId)
@@ -456,7 +456,7 @@ router.post('/sort',
try {
const gmail = await getGmailService(account.accessToken, account.refreshToken)
// FIRST: Clean up old "EmailSorter/..." labels
// FIRST: Clean up old "MailFlow/..." labels
const deletedLabels = await gmail.cleanupOldLabels()
if (deletedLabels > 0) {
log.success(`${deletedLabels} old labels cleaned up`)
@@ -528,7 +528,7 @@ router.post('/sort',
for (const nl of preferences.nameLabels) {
if (!nl.enabled) continue
try {
const labelName = `EmailSorter/Team/${nl.name}`
const labelName = `MailFlow/Team/${nl.name}`
const label = await gmail.createLabel(labelName, '#4a86e8')
if (label) {
nameLabelMap[nl.id || nl.name] = label.id
@@ -1203,7 +1203,7 @@ router.post('/sort-demo', asyncHandler(async (req, res) => {
/**
* POST /api/email/cleanup
* Cleanup old EmailSorter labels from Gmail
* Cleanup old MailFlow labels from Gmail
*/
router.post('/cleanup',
validate({
@@ -1235,7 +1235,7 @@ router.post('/cleanup',
respond.success(res, {
deleted,
message: deleted > 0
? `${deleted} old "EmailSorter/..." labels were deleted`
? `${deleted} old "MailFlow/..." labels were deleted`
: 'No old labels found'
})
})
@@ -1549,8 +1549,8 @@ async function processPromotionsCleanup(account, action, deleteAfterDays, matchC
const gmail = await getGmailService(account.accessToken, account.refreshToken)
// Find emails with matching categories/labels older than deleteAfterDays
// Look for emails with EmailSorter labels matching the categories
const labelQueries = matchCategories.map(cat => `label:EmailSorter/${cat}`).join(' OR ')
// Look for emails with MailFlow labels matching the categories
const labelQueries = matchCategories.map(cat => `label:MailFlow/${cat}`).join(' OR ')
const query = `(${labelQueries}) before:${cutoffDate.getFullYear()}/${String(cutoffDate.getMonth() + 1).padStart(2, '0')}/${String(cutoffDate.getDate()).padStart(2, '0')}`
const response = await gmail.gmail.users.messages.list({

View File

@@ -107,7 +107,7 @@ export class GmailService {
/**
* Create or get a label
* @param {string} name - Label name (e.g., "EmailSorter/VIP")
* @param {string} name - Label name (e.g., "MailFlow/VIP")
* @param {string} color - Optional label color (must be from Gmail's palette)
*/
async createLabel(name, color = null) {
@@ -272,14 +272,14 @@ export class GmailService {
}
/**
* Cleanup old EmailSorter labels
* Removes all labels starting with "EmailSorter/" and old German labels
* Cleanup old MailFlow labels
* Removes all labels starting with "MailFlow/" and old German labels
*/
async cleanupOldLabels() {
// Old labels to remove (German and old format)
const OLD_LABELS = [
// Old "EmailSorter/" prefix labels
'EmailSorter/',
// Old "MailFlow/" prefix labels
'MailFlow/',
// Old German labels
'Wichtig', 'Kunden', 'Rechnungen', 'Sicherheit', 'Termine', 'Prüfen', 'Werbung',
'VIP / Wichtig', 'Kunden / Projekte', 'Rechnungen / Belege',
@@ -293,8 +293,8 @@ export class GmailService {
// Filter labels that match old patterns
const labelsToDelete = labels.filter(l => {
if (!l.name) return false
// Check for EmailSorter/ prefix
if (l.name.startsWith('EmailSorter/')) return true
// Check for MailFlow/ prefix
if (l.name.startsWith('MailFlow/')) return true
// Check for exact matches with old labels
if (OLD_LABELS.includes(l.name)) return true
return false

View File

@@ -7,7 +7,7 @@ import { ImapFlow } from 'imapflow'
import { log } from '../middleware/logger.mjs'
const INBOX = 'INBOX'
const FOLDER_PREFIX = 'EmailSorter'
const FOLDER_PREFIX = 'MailFlow'
/** Map category key to IMAP folder name */
export function getFolderNameForCategory(category) {
@@ -134,7 +134,7 @@ export class ImapService {
}
/**
* Ensure folder exists (create if not). Use subfolder under EmailSorter to avoid clutter.
* Ensure folder exists (create if not). Use subfolder under MailFlow to avoid clutter.
*/
async ensureFolder(folderName) {
const path = `${FOLDER_PREFIX}/${folderName}`
@@ -150,7 +150,7 @@ export class ImapService {
}
/**
* Move message (by UID) from INBOX to folder name (under EmailSorter/)
* Move message (by UID) from INBOX to folder name (under MailFlow/)
*/
async moveToFolder(messageId, folderName) {
const path = `${FOLDER_PREFIX}/${folderName}`

View File

@@ -302,7 +302,7 @@ export class OutlookService {
notificationUrl: webhookUrl,
resource: 'me/mailFolders(\'inbox\')/messages',
expirationDateTime,
clientState: 'email-sorter-webhook',
clientState: 'mailflow-webhook',
}),
})
}