Init
This commit is contained in:
13
frontend/Dockerfile
Normal file
13
frontend/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
COPY tsconfig.json vite.config.ts index.html ./
|
||||
COPY src ./src
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Simple Mail Cleaner</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "simple-mail-cleaner-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0 --port 3000",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^23.12.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^14.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.17",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
111
frontend/src/App.tsx
Normal file
111
frontend/src/App.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const languages = [
|
||||
{ code: "de", label: "Deutsch" },
|
||||
{ code: "en", label: "English" }
|
||||
];
|
||||
|
||||
export default function App() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [activeLang, setActiveLang] = useState(i18n.language);
|
||||
|
||||
const switchLanguage = (code: string) => {
|
||||
i18n.changeLanguage(code);
|
||||
setActiveLang(code);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="topbar">
|
||||
<div>
|
||||
<p className="badge">v0.1</p>
|
||||
<h1>{t("appName")}</h1>
|
||||
<p className="tagline">{t("tagline")}</p>
|
||||
</div>
|
||||
<div className="lang">
|
||||
<span>{t("language")}</span>
|
||||
<div className="lang-buttons">
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
type="button"
|
||||
className={activeLang === lang.code ? "active" : ""}
|
||||
onClick={() => switchLanguage(lang.code)}
|
||||
>
|
||||
{lang.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section className="hero">
|
||||
<div>
|
||||
<h2>{t("welcome")}</h2>
|
||||
<p className="description">{t("description")}</p>
|
||||
<div className="actions">
|
||||
<button className="primary" type="button">
|
||||
{t("start")}
|
||||
</button>
|
||||
<button className="ghost" type="button">
|
||||
{t("overview")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="status-card">
|
||||
<div className="status-header">
|
||||
<span>{t("progress")}</span>
|
||||
<strong>0%</strong>
|
||||
</div>
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: "8%" }} />
|
||||
</div>
|
||||
<div className="status-grid">
|
||||
<div>
|
||||
<p>{t("mailboxes")}</p>
|
||||
<h3>1</h3>
|
||||
</div>
|
||||
<div>
|
||||
<p>{t("jobs")}</p>
|
||||
<h3>0</h3>
|
||||
</div>
|
||||
<div>
|
||||
<p>{t("rules")}</p>
|
||||
<h3>0</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="status-note">{t("progressNote")}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid">
|
||||
<article className="card">
|
||||
<h3>{t("featureOne")}</h3>
|
||||
<p>{t("featureOneText")}</p>
|
||||
</article>
|
||||
<article className="card">
|
||||
<h3>{t("featureTwo")}</h3>
|
||||
<p>{t("featureTwoText")}</p>
|
||||
</article>
|
||||
<article className="card">
|
||||
<h3>{t("featureThree")}</h3>
|
||||
<p>{t("featureThreeText")}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="split">
|
||||
<div>
|
||||
<h3>{t("queue")}</h3>
|
||||
<p>{t("queueText")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t("security")}</h3>
|
||||
<p>{t("securityText")}</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
frontend/src/i18n.ts
Normal file
16
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en from "./locales/en/translation.json";
|
||||
import de from "./locales/de/translation.json";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
de: { translation: de }
|
||||
},
|
||||
lng: "de",
|
||||
fallbackLng: "en",
|
||||
interpolation: { escapeValue: false }
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
25
frontend/src/locales/de/translation.json
Normal file
25
frontend/src/locales/de/translation.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"appName": "Simple Mail Cleaner",
|
||||
"tagline": "Postfächer sicher und skalierbar bereinigen.",
|
||||
"start": "Bereinigung starten",
|
||||
"progress": "Fortschritt",
|
||||
"mailboxes": "Postfächer",
|
||||
"jobs": "Jobs",
|
||||
"rules": "Regeln",
|
||||
"status": "Status",
|
||||
"welcome": "Willkommen zurück",
|
||||
"description": "Verbinde GMX-, Gmail- und web.de-Accounts, deabonniere Newsletter, sortiere Mails und verfolge jeden Schritt.",
|
||||
"language": "Sprache",
|
||||
"overview": "Überblick",
|
||||
"queue": "Queue",
|
||||
"security": "Sicherheit",
|
||||
"securityText": "Tokens verschlüsselt, audit-fähige Logs, DSGVO-first Design.",
|
||||
"featureOne": "Automatisches Deabonnieren",
|
||||
"featureTwo": "Konfigurierbares Routing",
|
||||
"featureThree": "Multi-Tenant bereit",
|
||||
"featureOneText": "List-Unsubscribe one-click und Weblinks vorbereitet.",
|
||||
"featureTwoText": "Flexible Regeln für Ordner, Labels und Löschungen.",
|
||||
"featureThreeText": "Mandantenfähig mit separaten Konten, Jobs und Regeln.",
|
||||
"queueText": "Jobs, Fortschritt und Logs in Echtzeit verfolgen.",
|
||||
"progressNote": "Security-Preview – Verschlüsselung und Audit-Trails sind geplant."
|
||||
}
|
||||
25
frontend/src/locales/en/translation.json
Normal file
25
frontend/src/locales/en/translation.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"appName": "Simple Mail Cleaner",
|
||||
"tagline": "Clean inboxes at scale, safely.",
|
||||
"start": "Start cleanup",
|
||||
"progress": "Progress",
|
||||
"mailboxes": "Mailboxes",
|
||||
"jobs": "Jobs",
|
||||
"rules": "Rules",
|
||||
"status": "Status",
|
||||
"welcome": "Welcome back",
|
||||
"description": "Connect GMX, Gmail, and web.de accounts to unsubscribe newsletters, sort mail, and track every step.",
|
||||
"language": "Language",
|
||||
"overview": "Overview",
|
||||
"queue": "Queue",
|
||||
"security": "Security",
|
||||
"securityText": "Tokens encrypted, audit-ready logs, GDPR-first design.",
|
||||
"featureOne": "Automated unsubscribe",
|
||||
"featureTwo": "Configurable routing",
|
||||
"featureThree": "Multi-tenant ready",
|
||||
"featureOneText": "List-Unsubscribe one-click plus web-link handling.",
|
||||
"featureTwoText": "Flexible rules for folders, labels, and deletions.",
|
||||
"featureThreeText": "Tenant isolation with accounts, jobs, and rules per user.",
|
||||
"queueText": "Track jobs, progress, and logs in real time.",
|
||||
"progressNote": "Security posture preview – encryption and audit trails are planned."
|
||||
}
|
||||
11
frontend/src/main.tsx
Normal file
11
frontend/src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./i18n";
|
||||
import "./styles.css";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
238
frontend/src/styles.css
Normal file
238
frontend/src/styles.css
Normal file
@@ -0,0 +1,238 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Space+Grotesk:wght@400;500;600;700&display=swap");
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f7f2ea;
|
||||
--bg-accent: #fbe9d2;
|
||||
--ink: #111010;
|
||||
--muted: #5b5b57;
|
||||
--primary: #e8702a;
|
||||
--secondary: #0b6e6b;
|
||||
--card: #ffffff;
|
||||
--border: rgba(17, 16, 16, 0.12);
|
||||
--shadow: 0 20px 60px rgba(17, 16, 16, 0.12);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
||||
background: radial-gradient(circle at top, var(--bg-accent), var(--bg));
|
||||
color: var(--ink);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px 80px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "DM Serif Display", "Georgia", serif;
|
||||
font-size: clamp(32px, 4vw, 54px);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: var(--muted);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.lang {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.lang span {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lang-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lang button {
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lang button.active {
|
||||
background: var(--secondary);
|
||||
color: #fff;
|
||||
border-color: var(--secondary);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 32px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: clamp(26px, 3vw, 40px);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--muted);
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 30px rgba(232, 112, 42, 0.35);
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: rgba(11, 110, 107, 0.12);
|
||||
border-radius: 999px;
|
||||
height: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--secondary), var(--primary));
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-grid p {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.status-grid h3 {
|
||||
font-size: 24px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status-note {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 12px 30px rgba(17, 16, 16, 0.08);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 24px;
|
||||
background: #111010;
|
||||
color: #f8f1e8;
|
||||
padding: 24px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.split h3 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
13
frontend/tsconfig.json
Normal file
13
frontend/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
10
frontend/vite.config.ts
Normal file
10
frontend/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user