Align frontend with InboxConverge brand
This commit is contained in:
@@ -76,7 +76,7 @@ function ProviderLogoBanner({ providerName, email }: { providerName?: string | n
|
||||
if (!icon) return null;
|
||||
const label = providerName ?? email?.split('@')[1] ?? 'provider';
|
||||
return (
|
||||
<div className="relative h-16 w-full bg-gray-50 border-b border-gray-100 overflow-hidden">
|
||||
<div className="relative h-16 w-full overflow-hidden border-b border-[#edf2f8] bg-[#f7faff]">
|
||||
<Image
|
||||
src={`/providers/${icon}.svg`}
|
||||
alt={`${label} logo`}
|
||||
@@ -177,11 +177,14 @@ export default function AccountsPage() {
|
||||
<AuthGuard>
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Mail Accounts</h1>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-extrabold text-slate-950">Mail Accounts</h1>
|
||||
<p className="mt-1 text-sm text-slate-500">Connect and monitor the external inboxes feeding Gmail.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
className="ic-button-primary"
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-2" />
|
||||
Add Account
|
||||
@@ -190,15 +193,15 @@ export default function AccountsPage() {
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-[#0b63f6]"></div>
|
||||
</div>
|
||||
) : accounts && accounts.length > 0 ? (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{accounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className={`bg-white rounded-lg shadow-md border overflow-hidden transition-opacity ${
|
||||
account.is_enabled ? 'border-gray-200' : 'border-gray-200 opacity-60'
|
||||
className={`ic-card overflow-hidden transition-opacity ${
|
||||
account.is_enabled ? '' : 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
{/* Provider logo banner – full-width strip that accommodates any aspect ratio */}
|
||||
@@ -207,19 +210,19 @@ export default function AccountsPage() {
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1 truncate">
|
||||
<h3 className="text-lg font-bold text-slate-950 mb-1 truncate">
|
||||
{account.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">{account.email_address}</p>
|
||||
<p className="text-sm text-slate-500 truncate">{account.email_address}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-2 flex-shrink-0">
|
||||
{account.is_enabled ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-[#e8f7ef] px-2 py-0.5 text-xs font-bold text-[#11834d]">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
Enabled
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 text-xs font-bold text-slate-500">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Disabled
|
||||
</span>
|
||||
@@ -229,31 +232,31 @@ export default function AccountsPage() {
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-20">Protocol:</span>
|
||||
<span className="text-gray-900 font-medium">
|
||||
<span className="w-20 text-slate-500">Protocol:</span>
|
||||
<span className="font-semibold text-slate-950">
|
||||
{account.protocol.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-20">Host:</span>
|
||||
<span className="text-gray-900">{account.host}:{account.port}</span>
|
||||
<span className="w-20 text-slate-500">Host:</span>
|
||||
<span className="text-slate-950">{account.host}:{account.port}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-20">SSL:</span>
|
||||
<span className="text-gray-900">
|
||||
<span className="w-20 text-slate-500">SSL:</span>
|
||||
<span className="text-slate-950">
|
||||
{account.use_ssl ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<span className="text-gray-500 w-20">Interval:</span>
|
||||
<span className="text-gray-900">
|
||||
<span className="w-20 text-slate-500">Interval:</span>
|
||||
<span className="text-slate-950">
|
||||
Every {account.check_interval_minutes} min
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{account.last_check_at && (
|
||||
<div className="mb-4 text-xs text-gray-500">
|
||||
<div className="mb-4 text-xs text-slate-500">
|
||||
Last checked: {new Date(account.last_check_at).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
@@ -278,15 +281,15 @@ export default function AccountsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 pt-4 border-t border-gray-200">
|
||||
<div className="flex items-center gap-2 border-t border-[#edf2f8] pt-4">
|
||||
<button
|
||||
onClick={() => handleToggle(account.id)}
|
||||
disabled={toggleMutation.isPending}
|
||||
title={account.is_enabled ? 'Disable account' : 'Enable account'}
|
||||
className={`flex items-center justify-center px-3 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 ${
|
||||
account.is_enabled
|
||||
? 'text-yellow-600 bg-yellow-50 hover:bg-yellow-100'
|
||||
: 'text-green-600 bg-green-50 hover:bg-green-100'
|
||||
? 'text-yellow-700 bg-yellow-50 hover:bg-yellow-100'
|
||||
: 'text-[#11834d] bg-[#e8f7ef] hover:bg-green-100'
|
||||
}`}
|
||||
>
|
||||
<Power className="h-4 w-4" />
|
||||
@@ -299,7 +302,7 @@ export default function AccountsPage() {
|
||||
className={`flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 ${
|
||||
successIds.has(account.id)
|
||||
? 'text-green-600 bg-green-50 hover:bg-green-100'
|
||||
: 'text-indigo-600 bg-indigo-50 hover:bg-indigo-100'
|
||||
: 'text-[#0b63f6] bg-[#e7f0ff] hover:bg-blue-100'
|
||||
}`}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${pullingIds.has(account.id) ? 'animate-spin' : ''}`} />
|
||||
@@ -313,7 +316,7 @@ export default function AccountsPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(account)}
|
||||
className="flex-1 flex items-center justify-center px-3 py-2 text-sm font-medium text-blue-600 bg-blue-50 rounded-md hover:bg-blue-100 transition-colors"
|
||||
className="flex-1 flex items-center justify-center px-3 py-2 text-sm font-bold text-[#0b63f6] bg-[#e7f0ff] rounded-md hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<Edit2 className="h-4 w-4 mr-1" />
|
||||
Edit
|
||||
@@ -332,11 +335,11 @@ export default function AccountsPage() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 bg-white rounded-lg shadow">
|
||||
<p className="text-gray-500 mb-4">No mail accounts configured yet</p>
|
||||
<div className="ic-card py-12 text-center">
|
||||
<p className="text-slate-500 mb-4">No mail accounts configured yet</p>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
className="ic-button-primary"
|
||||
>
|
||||
<Plus className="h-5 w-5 mr-2" />
|
||||
Add Your First Account
|
||||
|
||||
@@ -27,13 +27,13 @@ interface StatCardProps {
|
||||
|
||||
function StatCard({ title, value, icon: Icon, iconColor }: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="ic-card p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">{title}</p>
|
||||
<p className="mt-2 text-3xl font-semibold text-gray-900">{value}</p>
|
||||
<p className="text-sm font-semibold text-slate-500">{title}</p>
|
||||
<p className="mt-2 text-3xl font-bold text-slate-950">{value}</p>
|
||||
</div>
|
||||
<div className={`p-3 rounded-full ${iconColor}`}>
|
||||
<div className={`rounded-lg p-3 ${iconColor}`}>
|
||||
<Icon className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,36 +54,36 @@ function AccountStatusRow({ account }: { account: MailAccount }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="px-5 py-4 border-b border-gray-100 last:border-b-0">
|
||||
<div className="px-5 py-4 border-b border-[#edf2f8] last:border-b-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
{/* Left: name + email */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Inbox className="h-4 w-4 text-blue-400 shrink-0" />
|
||||
<Inbox className="h-4 w-4 text-blue-500 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-gray-900 truncate">{account.name}</p>
|
||||
<p className="text-xs text-gray-400 truncate">{account.email_address}</p>
|
||||
<p className="text-sm font-semibold text-slate-950 truncate">{account.name}</p>
|
||||
<p className="text-xs text-slate-400 truncate">{account.email_address}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: status badge + last check */}
|
||||
<div className="text-right shrink-0">
|
||||
{hasError ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-red-600">
|
||||
<span className="inline-flex items-center gap-1 text-xs font-bold text-red-600">
|
||||
<XCircle className="h-3.5 w-3.5" />
|
||||
Error
|
||||
</span>
|
||||
) : lastChecked ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-green-600">
|
||||
<span className="inline-flex items-center gap-1 text-xs font-bold text-[#11834d]">
|
||||
<CheckCircle className="h-3.5 w-3.5" />
|
||||
OK
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-xs font-medium text-gray-400">
|
||||
<span className="inline-flex items-center gap-1 text-xs font-bold text-slate-400">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Pending
|
||||
</span>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
{formatRelative(lastChecked)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ function AccountStatusRow({ account }: { account: MailAccount }) {
|
||||
|
||||
{/* Error message */}
|
||||
{hasError && (
|
||||
<div className="mt-2 flex items-start gap-1.5 p-2 bg-red-50 border border-red-200 rounded">
|
||||
<div className="mt-2 flex items-start gap-1.5 rounded-md border border-red-200 bg-red-50 p-2">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-red-700 line-clamp-2 flex-1">{account.last_error_message}</p>
|
||||
<button
|
||||
@@ -108,7 +108,7 @@ function AccountStatusRow({ account }: { account: MailAccount }) {
|
||||
|
||||
{/* Lifetime counters (only when there's activity) */}
|
||||
{(account.total_emails_processed > 0 || account.total_emails_failed > 0) && (
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-gray-500">
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-slate-500">
|
||||
<span>{account.total_emails_processed.toLocaleString()} processed</span>
|
||||
{account.total_emails_failed > 0 && (
|
||||
<span className="text-red-500">{account.total_emails_failed.toLocaleString()} failed</span>
|
||||
@@ -142,40 +142,40 @@ export default function DashboardPage() {
|
||||
title="Total Accounts"
|
||||
value={stats.totalAccounts}
|
||||
icon={Mail}
|
||||
iconColor="bg-blue-500"
|
||||
iconColor="bg-[#0b63f6]"
|
||||
/>
|
||||
<StatCard
|
||||
title="Emails Processed"
|
||||
value={stats.totalProcessed.toLocaleString()}
|
||||
icon={Send}
|
||||
iconColor="bg-green-500"
|
||||
iconColor="bg-[#11834d]"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active Accounts"
|
||||
value={stats.activeAccounts}
|
||||
icon={CheckCircle}
|
||||
iconColor="bg-purple-500"
|
||||
iconColor="bg-[#10213f]"
|
||||
/>
|
||||
<StatCard
|
||||
title="Accounts with Errors"
|
||||
value={stats.accountsWithErrors}
|
||||
icon={AlertCircle}
|
||||
iconColor={stats.accountsWithErrors > 0 ? 'bg-red-500' : 'bg-gray-400'}
|
||||
iconColor={stats.accountsWithErrors > 0 ? 'bg-red-500' : 'bg-slate-400'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mailbox Status Overview */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Mailbox Status</h3>
|
||||
<Link href="/logs" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
||||
<div className="ic-card overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b border-[#d9e3f2] bg-[#fbfdff] px-6 py-4">
|
||||
<h3 className="text-lg font-bold text-slate-950">Mailbox Status</h3>
|
||||
<Link href="/logs" className="text-sm font-bold text-[#0b63f6] hover:text-[#0649bf]">
|
||||
View activity & history →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{accountsLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-[#0b63f6]" />
|
||||
</div>
|
||||
) : accounts && accounts.length > 0 ? (
|
||||
<div>
|
||||
@@ -185,11 +185,11 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<Clock className="mx-auto h-10 w-10 text-gray-300 mb-3" />
|
||||
<p className="text-sm text-gray-500">No mail accounts configured yet.</p>
|
||||
<Clock className="mx-auto h-10 w-10 text-slate-300 mb-3" />
|
||||
<p className="text-sm text-slate-500">No mail accounts configured yet.</p>
|
||||
<Link
|
||||
href="/accounts"
|
||||
className="mt-3 inline-block text-sm text-blue-600 hover:text-blue-800 font-medium"
|
||||
className="mt-3 inline-block text-sm font-bold text-[#0b63f6] hover:text-[#0649bf]"
|
||||
>
|
||||
Add your first account →
|
||||
</Link>
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--foreground: #10213f;
|
||||
--ic-bg-soft: #f7faff;
|
||||
--ic-bg-blue: #eef5ff;
|
||||
--ic-ink: #10213f;
|
||||
--ic-ink-strong: #07162f;
|
||||
--ic-muted: #52617a;
|
||||
--ic-line: #d9e3f2;
|
||||
--ic-line-strong: #b8c8df;
|
||||
--ic-blue: #0b63f6;
|
||||
--ic-blue-deep: #0649bf;
|
||||
--ic-green: #11834d;
|
||||
--ic-green-soft: #e8f7ef;
|
||||
--ic-shadow: 0 24px 70px rgba(16, 33, 63, 0.11);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -12,15 +24,89 @@
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
body {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(238, 245, 255, 0.72), rgba(255, 255, 255, 0) 420px),
|
||||
var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-geist-sans), Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(11, 99, 246, 0.16);
|
||||
}
|
||||
|
||||
.ic-panel {
|
||||
border: 1px solid var(--ic-line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: var(--ic-shadow);
|
||||
}
|
||||
|
||||
.ic-card {
|
||||
border: 1px solid var(--ic-line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 14px 40px rgba(16, 33, 63, 0.07);
|
||||
}
|
||||
|
||||
.ic-button-primary {
|
||||
display: inline-flex;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 8px;
|
||||
background: var(--ic-blue);
|
||||
padding: 0.625rem 1rem;
|
||||
color: #ffffff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 750;
|
||||
transition: background 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.ic-button-primary:hover {
|
||||
background: var(--ic-blue-deep);
|
||||
box-shadow: 0 14px 28px rgba(11, 99, 246, 0.18);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ic-button-secondary {
|
||||
display: inline-flex;
|
||||
min-height: 42px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid var(--ic-line-strong);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 0.625rem 1rem;
|
||||
color: var(--ic-ink);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 750;
|
||||
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.ic-button-secondary:hover {
|
||||
border-color: #8aa5ca;
|
||||
background: #f9fbff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ic-focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.ic-focus:focus-visible {
|
||||
outline-color: var(--ic-blue);
|
||||
box-shadow: 0 0 0 4px rgba(11, 99, 246, 0.14);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { authApi } from '@/lib/api';
|
||||
import { BrandMark } from '@/components/BrandMark';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
@@ -42,25 +43,31 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#f7faff] px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 flex justify-center">
|
||||
<Link href="/">
|
||||
<BrandMark />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="ic-panel p-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
InboxConverge
|
||||
<h2 className="text-center text-3xl font-extrabold text-slate-950">
|
||||
Sign in
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Sign in to your account
|
||||
<p className="mt-2 text-center text-sm text-slate-500">
|
||||
Continue to your mailbox delivery dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="sr-only">
|
||||
<label htmlFor="email" className="block text-sm font-bold text-slate-700">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
@@ -71,12 +78,12 @@ export default function LoginPage() {
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="ic-focus mt-1 relative block w-full rounded-md border border-[#b8c8df] px-3 py-2.5 text-slate-950 placeholder-slate-400 focus:border-[#0b63f6] sm:text-sm"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
<label htmlFor="password" className="block text-sm font-bold text-slate-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
@@ -87,7 +94,7 @@ export default function LoginPage() {
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
className="ic-focus mt-1 relative block w-full rounded-md border border-[#b8c8df] px-3 py-2.5 text-slate-950 placeholder-slate-400 focus:border-[#0b63f6] sm:text-sm"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
@@ -97,7 +104,7 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
className="ic-button-primary w-full disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
@@ -105,10 +112,10 @@ export default function LoginPage() {
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
<div className="w-full border-t border-[#d9e3f2]" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-gray-50 text-gray-500">Or continue with</span>
|
||||
<span className="bg-white px-2 text-slate-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +123,7 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleLogin}
|
||||
className="w-full flex justify-center items-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
className="ic-button-secondary w-full"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -141,26 +148,27 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-slate-600">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
<Link href="/register" className="font-bold text-[#0b63f6] hover:text-[#0649bf]">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-center gap-4 text-xs text-gray-400">
|
||||
<Link href="/privacy" className="hover:text-gray-600 transition-colors">
|
||||
<div className="mt-6 flex justify-center gap-4 text-xs text-slate-400">
|
||||
<Link href="/privacy" className="hover:text-slate-600 transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/terms" className="hover:text-slate-600 transition-colors">
|
||||
Terms
|
||||
</Link>
|
||||
<Link href="/impressum" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/impressum" className="hover:text-slate-600 transition-colors">
|
||||
Impressum
|
||||
</Link>
|
||||
<Link href="/datenschutz" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/datenschutz" className="hover:text-slate-600 transition-colors">
|
||||
Datenschutz
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+58
-61
@@ -7,7 +7,8 @@ import { useAuthStore } from '@/store/authStore';
|
||||
import { userApi, SubscriptionPlan } from '@/lib/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { Mail, ArrowRight, Shield, Zap, Clock, Check } from 'lucide-react';
|
||||
import { ArrowRight, Shield, Zap, Clock, Check } from 'lucide-react';
|
||||
import { BrandMark } from '@/components/BrandMark';
|
||||
|
||||
async function fetchPublicPlans(): Promise<SubscriptionPlan[]> {
|
||||
const res = await api.get<SubscriptionPlan[]>('/subscriptions/plans');
|
||||
@@ -20,39 +21,39 @@ function PricingCard({ plan }: { plan: SubscriptionPlan }) {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-md p-8 flex flex-col border border-gray-100 hover:shadow-lg transition-shadow">
|
||||
<h3 className="text-xl font-bold text-gray-900">{plan.name}</h3>
|
||||
<p className="mt-2 text-sm text-gray-500 flex-1">{plan.description}</p>
|
||||
<div className="ic-card flex flex-col p-8 transition-transform hover:-translate-y-0.5">
|
||||
<h3 className="text-xl font-bold text-slate-950">{plan.name}</h3>
|
||||
<p className="mt-2 text-sm text-slate-500 flex-1">{plan.description}</p>
|
||||
<div className="mt-6">
|
||||
<span className="text-4xl font-extrabold text-gray-900">
|
||||
<span className="text-4xl font-extrabold text-slate-950">
|
||||
€{plan.price_monthly.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-gray-500">/month</span>
|
||||
<span className="text-slate-500">/month</span>
|
||||
{yearlyMonthly && (
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
<p className="text-xs text-[#11834d] mt-1">
|
||||
or €{yearlyMonthly}/mo billed yearly (€{plan.price_yearly?.toFixed(2)})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ul className="mt-6 space-y-2 text-sm text-gray-600">
|
||||
<ul className="mt-6 space-y-2 text-sm text-slate-600">
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<Check className="h-4 w-4 text-[#11834d] shrink-0" />
|
||||
{plan.max_mail_accounts === 1
|
||||
? '1 mailbox'
|
||||
: `Up to ${plan.max_mail_accounts} mailboxes`}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<Check className="h-4 w-4 text-[#11834d] shrink-0" />
|
||||
Checked every {plan.check_interval_minutes} minute{plan.check_interval_minutes > 1 ? 's' : ''}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<Check className="h-4 w-4 text-[#11834d] shrink-0" />
|
||||
Up to {plan.max_emails_per_day.toLocaleString()} emails/day
|
||||
</li>
|
||||
</ul>
|
||||
<Link
|
||||
href="/register"
|
||||
className="mt-8 block text-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
|
||||
className="ic-button-primary mt-8 w-full"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
@@ -99,25 +100,22 @@ export default function Home() {
|
||||
const hasPaidPlans = plans && plans.length > 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header */}
|
||||
<header className="border-b border-gray-200 bg-white">
|
||||
<header className="border-b border-[#d9e3f2] bg-white/90 backdrop-blur-xl">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-4">
|
||||
<div className="flex items-center">
|
||||
<Mail className="h-8 w-8 text-blue-600 mr-2" />
|
||||
<h1 className="text-2xl font-bold text-gray-900">InboxConverge</h1>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<BrandMark />
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-gray-700 hover:text-gray-900 font-medium"
|
||||
className="font-semibold text-slate-700 hover:text-slate-950"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
className="ic-button-primary"
|
||||
>
|
||||
Sign Up Free
|
||||
</Link>
|
||||
@@ -129,32 +127,31 @@ export default function Home() {
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
|
||||
{/* Hero */}
|
||||
<div className="text-center">
|
||||
<h2 className="text-4xl sm:text-5xl font-bold text-gray-900 mb-6">
|
||||
Your old inboxes,{' '}
|
||||
<span className="text-blue-600">delivered to Gmail.</span>
|
||||
<div className="mx-auto max-w-4xl text-center">
|
||||
<h2 className="mb-6 text-4xl font-extrabold leading-tight text-slate-950 sm:text-6xl">
|
||||
Your old inboxes, delivered to Gmail.
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 mb-4 max-w-2xl mx-auto">
|
||||
<p className="mx-auto mb-4 max-w-2xl text-xl leading-8 text-slate-600">
|
||||
You know the ones — that GMX account from 2009, the old ISP address your
|
||||
bank still sends to, the Hotmail you gave out in school. InboxConverge
|
||||
quietly polls them all and drops everything into your Gmail. Set it once,
|
||||
forget it exists.
|
||||
</p>
|
||||
<p className="text-base text-gray-500 mb-8 max-w-xl mx-auto">
|
||||
<p className="text-base text-slate-500 mb-8 max-w-xl mx-auto">
|
||||
No forwarding rules to configure. No email clients to keep open.
|
||||
Just your mail, where you actually read it.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4 flex-wrap">
|
||||
<Link
|
||||
href="/register"
|
||||
className="flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-lg font-medium"
|
||||
className="ic-button-primary min-h-12 px-6 text-base"
|
||||
>
|
||||
Get Started — it's free
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className="px-6 py-3 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-lg font-medium"
|
||||
className="ic-button-secondary min-h-12 px-6 text-base"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
@@ -163,40 +160,40 @@ export default function Home() {
|
||||
|
||||
{/* Features */}
|
||||
<div className="mt-20 grid md:grid-cols-3 gap-8">
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-md bg-blue-500 text-white mb-4">
|
||||
<div className="ic-card p-6">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-md bg-[#0b63f6] text-white mb-4">
|
||||
<Zap className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-xl font-bold text-slate-950 mb-2">
|
||||
Auto-detects everything
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-slate-600">
|
||||
Type your old email address and InboxConverge figures out the server
|
||||
settings. No Googling port numbers required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-md bg-green-500 text-white mb-4">
|
||||
<div className="ic-card p-6">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-md bg-[#11834d] text-white mb-4">
|
||||
<Clock className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-xl font-bold text-slate-950 mb-2">
|
||||
Runs in the background
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-slate-600">
|
||||
Checks your old inboxes on a schedule you choose — from every minute
|
||||
to once a day. New mail appears in Gmail as if it was always there.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-md">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-md bg-purple-500 text-white mb-4">
|
||||
<div className="ic-card p-6">
|
||||
<div className="flex items-center justify-center h-12 w-12 rounded-md bg-[#10213f] text-white mb-4">
|
||||
<Shield className="h-6 w-6" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
<h3 className="text-xl font-bold text-slate-950 mb-2">
|
||||
Your passwords stay yours
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-slate-600">
|
||||
Credentials are encrypted at rest and never shared. All connections
|
||||
use SSL/TLS and Gmail delivery uses OAuth2 — no app passwords needed.
|
||||
</p>
|
||||
@@ -205,36 +202,36 @@ export default function Home() {
|
||||
|
||||
{/* How It Works */}
|
||||
<div className="mt-20">
|
||||
<h3 className="text-3xl font-bold text-center text-gray-900 mb-12">
|
||||
<h3 className="text-3xl font-extrabold text-center text-slate-950 mb-12">
|
||||
Three steps and you're done
|
||||
</h3>
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-blue-100 text-blue-600 text-2xl font-bold mx-auto mb-4">
|
||||
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-[#e7f0ff] text-[#0b63f6] text-2xl font-bold mx-auto mb-4">
|
||||
1
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold text-gray-900 mb-2">Add your old inbox</h4>
|
||||
<p className="text-gray-600">
|
||||
<h4 className="text-xl font-bold text-slate-950 mb-2">Add your old inbox</h4>
|
||||
<p className="text-slate-600">
|
||||
Paste the email address — InboxConverge auto-detects the POP3/IMAP
|
||||
settings in seconds.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-blue-100 text-blue-600 text-2xl font-bold mx-auto mb-4">
|
||||
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-[#e7f0ff] text-[#0b63f6] text-2xl font-bold mx-auto mb-4">
|
||||
2
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold text-gray-900 mb-2">Connect Gmail</h4>
|
||||
<p className="text-gray-600">
|
||||
<h4 className="text-xl font-bold text-slate-950 mb-2">Connect Gmail</h4>
|
||||
<p className="text-slate-600">
|
||||
Sign in with Google once. InboxConverge delivers mail directly into
|
||||
your inbox using the Gmail API — no SMTP relay needed.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-blue-100 text-blue-600 text-2xl font-bold mx-auto mb-4">
|
||||
<div className="flex items-center justify-center h-16 w-16 rounded-full bg-[#e7f0ff] text-[#0b63f6] text-2xl font-bold mx-auto mb-4">
|
||||
3
|
||||
</div>
|
||||
<h4 className="text-xl font-semibold text-gray-900 mb-2">Close the tab</h4>
|
||||
<p className="text-gray-600">
|
||||
<h4 className="text-xl font-bold text-slate-950 mb-2">Close the tab</h4>
|
||||
<p className="text-slate-600">
|
||||
Seriously, that's it. Your mail arrives automatically from now on.
|
||||
Check the dashboard whenever you like, but you won't need to.
|
||||
</p>
|
||||
@@ -245,10 +242,10 @@ export default function Home() {
|
||||
{/* Pricing — only rendered when paid plans exist (hidden in enterprise/all-free mode) */}
|
||||
{hasPaidPlans && (
|
||||
<div className="mt-24">
|
||||
<h3 className="text-3xl font-bold text-center text-gray-900 mb-4">
|
||||
<h3 className="text-3xl font-extrabold text-center text-slate-950 mb-4">
|
||||
Pricing
|
||||
</h3>
|
||||
<p className="text-center text-gray-500 mb-12 max-w-xl mx-auto">
|
||||
<p className="text-center text-slate-500 mb-12 max-w-xl mx-auto">
|
||||
Start free. Upgrade if you need more inboxes or faster checks.
|
||||
Cancel any time — no questions, no fuss.
|
||||
</p>
|
||||
@@ -257,7 +254,7 @@ export default function Home() {
|
||||
<PricingCard key={plan.id} plan={plan} />
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-8 text-center text-sm text-gray-500">
|
||||
<p className="mt-8 text-center text-sm text-slate-500">
|
||||
All paid plans include a{' '}
|
||||
<span className="font-medium">free tier</span> when you first sign up
|
||||
— no credit card required.
|
||||
@@ -267,22 +264,22 @@ export default function Home() {
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-20 border-t border-gray-200 bg-white">
|
||||
<footer className="mt-20 border-t border-[#d9e3f2] bg-[#fbfdff]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<p className="text-center text-gray-600 text-sm mb-3">
|
||||
<p className="text-center text-slate-600 text-sm mb-3">
|
||||
© {new Date().getFullYear()} InboxConverge — made for people, not enterprises.
|
||||
</p>
|
||||
<div className="flex justify-center gap-4 text-xs text-gray-400">
|
||||
<Link href="/privacy" className="hover:text-gray-600 transition-colors">
|
||||
<div className="flex justify-center gap-4 text-xs text-slate-400">
|
||||
<Link href="/privacy" className="hover:text-slate-600 transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/terms" className="hover:text-slate-600 transition-colors">
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Link href="/impressum" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/impressum" className="hover:text-slate-600 transition-colors">
|
||||
Impressum
|
||||
</Link>
|
||||
<Link href="/datenschutz" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/datenschutz" className="hover:text-slate-600 transition-colors">
|
||||
Datenschutz
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { authApi } from '@/lib/api';
|
||||
import { BrandMark } from '@/components/BrandMark';
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
@@ -56,25 +57,31 @@ export default function RegisterPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#f7faff] px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 flex justify-center">
|
||||
<Link href="/">
|
||||
<BrandMark />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="ic-panel p-8">
|
||||
<div>
|
||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
<h2 className="text-center text-3xl font-extrabold text-slate-950">
|
||||
Create your account
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Start forwarding your emails
|
||||
<p className="mt-2 text-center text-sm text-slate-500">
|
||||
Start consolidating external mailboxes into Gmail.
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<p className="text-sm text-red-800">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-md shadow-sm space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="full_name" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="full_name" className="block text-sm font-bold text-slate-700">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
@@ -84,12 +91,12 @@ export default function RegisterPage() {
|
||||
required
|
||||
value={formData.full_name}
|
||||
onChange={(e) => setFormData({ ...formData, full_name: e.target.value })}
|
||||
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
className="ic-focus mt-1 relative block w-full rounded-md border border-[#b8c8df] px-3 py-2.5 text-slate-950 placeholder-slate-400 focus:border-[#0b63f6] sm:text-sm"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="email" className="block text-sm font-bold text-slate-700">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
@@ -100,12 +107,12 @@ export default function RegisterPage() {
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
className="ic-focus mt-1 relative block w-full rounded-md border border-[#b8c8df] px-3 py-2.5 text-slate-950 placeholder-slate-400 focus:border-[#0b63f6] sm:text-sm"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="password" className="block text-sm font-bold text-slate-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
@@ -116,12 +123,12 @@ export default function RegisterPage() {
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
className="ic-focus mt-1 relative block w-full rounded-md border border-[#b8c8df] px-3 py-2.5 text-slate-950 placeholder-slate-400 focus:border-[#0b63f6] sm:text-sm"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-bold text-slate-700">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
@@ -132,7 +139,7 @@ export default function RegisterPage() {
|
||||
required
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
className="ic-focus mt-1 relative block w-full rounded-md border border-[#b8c8df] px-3 py-2.5 text-slate-950 placeholder-slate-400 focus:border-[#0b63f6] sm:text-sm"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
@@ -142,45 +149,46 @@ export default function RegisterPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
className="ic-button-primary w-full disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Sign up'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-slate-600">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="font-medium text-blue-600 hover:text-blue-500">
|
||||
<Link href="/login" className="font-bold text-[#0b63f6] hover:text-[#0649bf]">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-center text-gray-400">
|
||||
<p className="text-xs text-center text-slate-400">
|
||||
By creating an account you agree to our{' '}
|
||||
<Link href="/terms" className="underline hover:text-gray-600">
|
||||
<Link href="/terms" className="underline hover:text-slate-600">
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link href="/privacy" className="underline hover:text-gray-600">
|
||||
<Link href="/privacy" className="underline hover:text-slate-600">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center gap-4 text-xs text-gray-400">
|
||||
<Link href="/privacy" className="hover:text-gray-600 transition-colors">
|
||||
<div className="mt-4 flex justify-center gap-4 text-xs text-slate-400">
|
||||
<Link href="/privacy" className="hover:text-slate-600 transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/terms" className="hover:text-slate-600 transition-colors">
|
||||
Terms
|
||||
</Link>
|
||||
<Link href="/impressum" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/impressum" className="hover:text-slate-600 transition-colors">
|
||||
Impressum
|
||||
</Link>
|
||||
<Link href="/datenschutz" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/datenschutz" className="hover:text-slate-600 transition-colors">
|
||||
Datenschutz
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Mail } from 'lucide-react';
|
||||
|
||||
interface BrandMarkProps {
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function BrandMark({ compact = false }: BrandMarkProps) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5 text-slate-950">
|
||||
<span className="inline-grid h-8 w-8 place-items-center rounded-md border-2 border-blue-600 bg-white text-blue-600">
|
||||
<Mail className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
{!compact && <span className="text-xl font-extrabold tracking-normal">InboxConverge</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { versionApi } from '@/lib/api';
|
||||
import { BrandMark } from '@/components/BrandMark';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Mail,
|
||||
@@ -63,35 +64,35 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
const allNavItems = [...navigation, ...adminNavigation];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen bg-[#f7faff] text-slate-900">
|
||||
{/* Sidebar for desktop */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
|
||||
<div className="flex flex-col flex-grow bg-white border-r border-gray-200">
|
||||
<div className="flex items-center h-16 flex-shrink-0 px-4 border-b border-gray-200">
|
||||
<h1 className="text-xl font-bold text-gray-900">InboxConverge</h1>
|
||||
<div className="flex flex-col flex-grow border-r border-[#d9e3f2] bg-white/95 shadow-[14px_0_42px_rgba(16,33,63,0.04)]">
|
||||
<div className="flex h-18 flex-shrink-0 items-center px-5 border-b border-[#d9e3f2]">
|
||||
<BrandMark />
|
||||
</div>
|
||||
<nav className="flex-1 px-2 py-4 space-y-1">
|
||||
<nav className="flex-1 px-3 py-5 space-y-1">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
className={`flex items-center px-3.5 py-2.5 text-sm font-semibold rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-50 text-blue-600'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-[#e7f0ff] text-[#0649bf]'
|
||||
: 'text-slate-600 hover:bg-[#f7faff] hover:text-slate-950'
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-blue-600' : 'text-gray-400'}`} />
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-[#0b63f6]' : 'text-slate-400'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{adminNavigation.length > 0 && (
|
||||
<>
|
||||
<div className="pt-4 pb-1 px-4">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Admin</p>
|
||||
<div className="pt-5 pb-1 px-3.5">
|
||||
<p className="text-xs font-bold uppercase text-slate-400">Admin</p>
|
||||
</div>
|
||||
{adminNavigation.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
|
||||
@@ -99,13 +100,13 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
className={`flex items-center px-3.5 py-2.5 text-sm font-semibold rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-purple-50 text-purple-600'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-[#e8f7ef] text-[#11834d]'
|
||||
: 'text-slate-600 hover:bg-[#f7faff] hover:text-slate-950'
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-purple-600' : 'text-gray-400'}`} />
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-[#11834d]' : 'text-slate-400'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
@@ -113,12 +114,12 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<div className="flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<div className="flex-shrink-0 border-t border-[#d9e3f2] p-4">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-50 hover:text-gray-900 transition-colors"
|
||||
className="flex items-center w-full px-3.5 py-2.5 text-sm font-semibold text-slate-600 rounded-lg hover:bg-[#f7faff] hover:text-slate-950 transition-colors"
|
||||
>
|
||||
<LogOut className="mr-3 h-5 w-5 text-gray-400" />
|
||||
<LogOut className="mr-3 h-5 w-5 text-slate-400" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -128,15 +129,15 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
{/* Mobile sidebar */}
|
||||
{sidebarOpen && (
|
||||
<div className="fixed inset-0 z-40 lg:hidden">
|
||||
<div className="fixed inset-0 bg-gray-600/75" onClick={() => setSidebarOpen(false)} />
|
||||
<div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-white">
|
||||
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
|
||||
<h1 className="text-xl font-bold text-gray-900">InboxConverge</h1>
|
||||
<button onClick={() => setSidebarOpen(false)} className="text-gray-500 hover:text-gray-700">
|
||||
<div className="fixed inset-0 bg-slate-900/50" onClick={() => setSidebarOpen(false)} />
|
||||
<div className="fixed inset-y-0 left-0 flex w-72 flex-col bg-white">
|
||||
<div className="flex items-center justify-between h-18 px-5 border-b border-[#d9e3f2]">
|
||||
<BrandMark />
|
||||
<button onClick={() => setSidebarOpen(false)} className="text-slate-500 hover:text-slate-700">
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 px-2 py-4 space-y-1">
|
||||
<nav className="flex-1 px-3 py-5 space-y-1">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
@@ -144,21 +145,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
className={`flex items-center px-3.5 py-2.5 text-sm font-semibold rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-blue-50 text-blue-600'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-[#e7f0ff] text-[#0649bf]'
|
||||
: 'text-slate-600 hover:bg-[#f7faff] hover:text-slate-950'
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-blue-600' : 'text-gray-400'}`} />
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-[#0b63f6]' : 'text-slate-400'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{adminNavigation.length > 0 && (
|
||||
<>
|
||||
<div className="pt-4 pb-1 px-4">
|
||||
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider">Admin</p>
|
||||
<div className="pt-5 pb-1 px-3.5">
|
||||
<p className="text-xs font-bold uppercase text-slate-400">Admin</p>
|
||||
</div>
|
||||
{adminNavigation.map((item) => {
|
||||
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
|
||||
@@ -167,13 +168,13 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
className={`flex items-center px-3.5 py-2.5 text-sm font-semibold rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-purple-50 text-purple-600'
|
||||
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
|
||||
? 'bg-[#e8f7ef] text-[#11834d]'
|
||||
: 'text-slate-600 hover:bg-[#f7faff] hover:text-slate-950'
|
||||
}`}
|
||||
>
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-purple-600' : 'text-gray-400'}`} />
|
||||
<item.icon className={`mr-3 h-5 w-5 ${isActive ? 'text-[#11834d]' : 'text-slate-400'}`} />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
@@ -181,12 +182,12 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<div className="flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<div className="flex-shrink-0 border-t border-[#d9e3f2] p-4">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center w-full px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-50 hover:text-gray-900 transition-colors"
|
||||
className="flex items-center w-full px-3.5 py-2.5 text-sm font-semibold text-slate-600 rounded-lg hover:bg-[#f7faff] hover:text-slate-950 transition-colors"
|
||||
>
|
||||
<LogOut className="mr-3 h-5 w-5 text-gray-400" />
|
||||
<LogOut className="mr-3 h-5 w-5 text-slate-400" />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
@@ -197,28 +198,28 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
{/* Main content */}
|
||||
<div className="lg:pl-64 flex flex-col flex-1">
|
||||
{/* Top bar */}
|
||||
<div className="sticky top-0 z-10 flex h-16 flex-shrink-0 bg-white border-b border-gray-200">
|
||||
<div className="sticky top-0 z-10 flex h-16 flex-shrink-0 border-b border-[#d9e3f2] bg-white/90 backdrop-blur-xl">
|
||||
<button
|
||||
type="button"
|
||||
className="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 lg:hidden"
|
||||
className="border-r border-[#d9e3f2] px-4 text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 lg:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<Menu className="h-6 w-6" />
|
||||
</button>
|
||||
<div className="flex flex-1 justify-between px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-1 items-center">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
<h2 className="text-lg font-bold text-slate-950">
|
||||
{allNavItems.find((item) => item.href === pathname)?.name || 'Dashboard'}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`flex h-8 w-8 items-center justify-center rounded-full text-white ${user?.is_superuser ? 'bg-purple-600' : 'bg-blue-600'}`}>
|
||||
<div className={`flex h-8 w-8 items-center justify-center rounded-md text-white ${user?.is_superuser ? 'bg-[#11834d]' : 'bg-[#0b63f6]'}`}>
|
||||
{user?.is_superuser ? <Shield className="h-4 w-4" /> : <User className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-sm font-medium text-gray-900">{user?.full_name}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-sm font-semibold text-slate-950">{user?.full_name}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{user?.email}
|
||||
{user?.is_superuser && (
|
||||
<span className="ml-1 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-700">
|
||||
@@ -234,7 +235,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1">
|
||||
<div className="py-6">
|
||||
<div className="py-7">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</div>
|
||||
@@ -242,13 +243,13 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-gray-200 bg-white py-4 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl flex flex-wrap items-center justify-between gap-2 text-xs text-gray-400">
|
||||
<footer className="border-t border-[#d9e3f2] bg-white/80 py-4 px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-7xl flex flex-wrap items-center justify-between gap-2 text-xs text-slate-400">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link href="/impressum" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/impressum" className="hover:text-slate-600 transition-colors">
|
||||
Impressum
|
||||
</Link>
|
||||
<Link href="/datenschutz" className="hover:text-gray-600 transition-colors">
|
||||
<Link href="/datenschutz" className="hover:text-slate-600 transition-colors">
|
||||
Datenschutz
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user