UI overhault
This commit is contained in:
62
components/layout/AppHeader.tsx
Normal file
62
components/layout/AppHeader.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function AppHeader() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 z-50">
|
||||
<div className="flex items-center justify-between h-full px-6">
|
||||
{/* Logo & Title */}
|
||||
<Link href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<h1 className="text-xl font-semibold text-gray-800">Mail S3 Admin</h1>
|
||||
</Link>
|
||||
|
||||
{/* Search Bar (global) */}
|
||||
<div className="flex-1 max-w-2xl mx-8">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="E-Mails durchsuchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-gray-100 rounded-lg focus:bg-white focus:ring-2 focus:ring-blue-500 focus:outline-none transition-all"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Actions */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||
title="Settings"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||
title="Help"
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-medium text-sm">
|
||||
A
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
49
components/layout/Breadcrumb.tsx
Normal file
49
components/layout/Breadcrumb.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export function Breadcrumb({ items }: BreadcrumbProps) {
|
||||
return (
|
||||
<nav className="px-6 py-2 bg-white border-b border-gray-200" aria-label="Breadcrumb">
|
||||
<ol className="flex flex-wrap items-center space-x-2 text-xs text-gray-500">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 && (
|
||||
<li aria-hidden="true" className="mx-1 text-gray-400">
|
||||
/
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
{isLast ? (
|
||||
<span className="font-semibold text-gray-700" aria-current="page">
|
||||
{item.label}
|
||||
</span>
|
||||
) : item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="hover:text-blue-600 transition-colors focus-ring rounded"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{item.label}</span>
|
||||
)}
|
||||
</li>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
86
components/layout/Sidebar.tsx
Normal file
86
components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface Domain {
|
||||
bucket: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const [domains, setDomains] = useState<Domain[]>([]);
|
||||
const [expandedDomains, setExpandedDomains] = useState<Set<string>>(new Set());
|
||||
const [totalEmails, setTotalEmails] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch domains from API
|
||||
const auth = localStorage.getItem('auth');
|
||||
if (!auth) return;
|
||||
|
||||
fetch('/api/domains', {
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setDomains(data);
|
||||
})
|
||||
.catch(err => console.error('Failed to fetch domains:', err));
|
||||
}, []);
|
||||
|
||||
const toggleDomain = (bucket: string) => {
|
||||
setExpandedDomains(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(bucket)) {
|
||||
next.delete(bucket);
|
||||
} else {
|
||||
next.add(bucket);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="fixed left-0 top-16 w-64 h-[calc(100vh-4rem)] bg-white border-r border-gray-200 overflow-y-auto">
|
||||
{/* Navigation */}
|
||||
<nav className="px-2 pt-4">
|
||||
{/* Inbox Overview */}
|
||||
<div className="mb-4">
|
||||
<Link
|
||||
href="/domains"
|
||||
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors ${
|
||||
pathname === '/domains' ? 'bg-blue-50 text-blue-700' : 'hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<span className="font-medium">All Mail</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Domains Section */}
|
||||
<div className="mb-2 px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||
Domains
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{domains.map((domain, index) => (
|
||||
<div key={`${domain.bucket}-${index}`}>
|
||||
<Link
|
||||
href={`/mailboxes?bucket=${domain.bucket}`}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 text-left transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<span className="text-sm flex-1 truncate">{domain.domain}</span>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user