feat: landing page + logo, dev-mode banner + sync cap, non-blocking sync with progress splash
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import GridLayout from 'react-grid-layout';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { AnalyticsApi, LayoutApi, ExportApi } from '../api/client.js';
|
||||
import { AnalyticsApi, LayoutApi, ExportApi, SyncApi } from '../api/client.js';
|
||||
import SyncSplash from '../components/SyncSplash.jsx';
|
||||
import {
|
||||
HealthWidget, StatCard, TopSendersWidget, VolumeWidget,
|
||||
HeatmapWidget, AttachmentsWidget, StorageWidget
|
||||
@@ -26,16 +27,37 @@ export default function Dashboard() {
|
||||
const [data, setData] = useState(null);
|
||||
const [layout, setLayout] = useState(DEFAULT_LAYOUT);
|
||||
const [hidden, setHidden] = useState([]);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
AnalyticsApi.dashboard().then(setData).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
AnalyticsApi.dashboard().then(setData).catch(() => {});
|
||||
LayoutApi.get().then((saved) => {
|
||||
if (saved?.length) {
|
||||
setLayout(saved.map((w) => ({ i: w.widgetKey, x: w.x, y: w.y, w: w.w, h: w.h })));
|
||||
setHidden(saved.filter((w) => !w.visible).map((w) => w.widgetKey));
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Decide whether to show the loading splash: if a sync is running, watch it;
|
||||
// if the inbox has never been synced, kick one off automatically.
|
||||
SyncApi.status().then((s) => {
|
||||
if (s.isRunning) {
|
||||
setSyncing(true);
|
||||
} else if (!s.lastSuccessfulSyncUtc) {
|
||||
SyncApi.incremental().then(() => setSyncing(true)).catch(loadData);
|
||||
} else {
|
||||
loadData();
|
||||
}
|
||||
}).catch(loadData);
|
||||
}, [loadData]);
|
||||
|
||||
const onSyncDone = useCallback(() => {
|
||||
setSyncing(false);
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const persist = (nextLayout, nextHidden) => {
|
||||
const dto = nextLayout.map((l, idx) => ({
|
||||
@@ -69,6 +91,7 @@ export default function Dashboard() {
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
{syncing && <SyncSplash onDone={onSyncDone} />}
|
||||
<div className="toolbar">
|
||||
<div className="widget-toggles">
|
||||
{ALL_WIDGETS.map((k) => (
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Logo from '../components/Logo.jsx';
|
||||
import { AuthApi, LOGIN_URL } from '../api/client.js';
|
||||
|
||||
const FEATURES = [
|
||||
{ icon: '🔌', title: 'Gmail, synced locally', text: 'Secure Google sign-in, then a full + incremental sync of your inbox into your own PostgreSQL — with retry, backoff, and resume.' },
|
||||
{ icon: '📊', title: 'Analytics dashboard', text: 'Inbox health score, top senders, volume over time, an activity heatmap, and attachment breakdowns — on draggable, resizable widgets.' },
|
||||
{ icon: '🧹', title: 'Safe bulk cleanup', text: 'Archive, label, or delete in bulk — always with a preview and explicit confirmation before anything destructive happens.' },
|
||||
{ icon: '✉️', title: 'Unsubscribe manager', text: 'Detects List-Unsubscribe headers, groups by sender, ranks what is safe to drop, and runs a confirmed unsubscribe queue.' },
|
||||
{ icon: '🔎', title: 'Advanced search', text: 'PostgreSQL full-text search with Gmail-like syntax: from:, domain:, after:, is:unread, has:attachment.' },
|
||||
{ icon: '🤖', title: 'Optional AI', text: 'Toggle a local (Ollama) or cloud (OpenAI) model to classify mail, summarise your inbox, and suggest cleanups. Never destructive.' }
|
||||
];
|
||||
|
||||
export default function Landing() {
|
||||
const [user, setUser] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// Quietly check if already signed in to swap the CTA.
|
||||
AuthApi.me().then(setUser).catch(() => setUser(null));
|
||||
}, []);
|
||||
|
||||
const primaryCta = user
|
||||
? <button className="cta" onClick={() => navigate('/app')}>Open your dashboard →</button>
|
||||
: <a className="cta" href={LOGIN_URL}>Sign in with Google</a>;
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<header className="landing-bar">
|
||||
<Logo size={34} withWordmark />
|
||||
<div className="spacer" />
|
||||
{user
|
||||
? <button className="ghost" onClick={() => navigate('/app')}>Dashboard</button>
|
||||
: <a className="ghost" href={LOGIN_URL}>Log in</a>}
|
||||
</header>
|
||||
|
||||
<section className="hero">
|
||||
<Logo size={72} />
|
||||
<h1>Take back control of your inbox.</h1>
|
||||
<p className="sub">
|
||||
InboxIntel syncs your Gmail into your own database, shows you what is really going on,
|
||||
and helps you clean it up safely — analytics, bulk actions, and one-click unsubscribe.
|
||||
</p>
|
||||
<div className="hero-cta">{primaryCta}</div>
|
||||
<p className="fineprint">Google sign-in only. Your tokens are encrypted at rest and never logged.</p>
|
||||
</section>
|
||||
|
||||
<section className="features">
|
||||
{FEATURES.map((f) => (
|
||||
<div className="feature" key={f.title}>
|
||||
<div className="feature-icon">{f.icon}</div>
|
||||
<h3>{f.title}</h3>
|
||||
<p>{f.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="closing">
|
||||
<h2>Ready to dig in?</h2>
|
||||
{primaryCta}
|
||||
</section>
|
||||
|
||||
<footer className="landing-footer">
|
||||
<Logo size={20} withWordmark />
|
||||
<span className="muted">Gmail analytics, cleanup & automation.</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user