Align frontend with InboxConverge brand

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