Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
literal 0
HcmV?d00001
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
new file mode 100644
index 0000000..a2dc41e
--- /dev/null
+++ b/frontend/src/app/globals.css
@@ -0,0 +1,26 @@
+@import "tailwindcss";
+
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: Arial, Helvetica, sans-serif;
+}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
new file mode 100644
index 0000000..2667f15
--- /dev/null
+++ b/frontend/src/app/layout.tsx
@@ -0,0 +1,37 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+import { QueryProvider } from "@/components/QueryProvider";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "POP3 Forwarder - Automatic Email Forwarding to Gmail",
+ description: "Forward your POP3 emails to Gmail automatically with our secure and reliable service",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx
new file mode 100644
index 0000000..da4598c
--- /dev/null
+++ b/frontend/src/app/login/page.tsx
@@ -0,0 +1,156 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { authApi } from '@/lib/api';
+import { useAuthStore } from '@/store/authStore';
+
+export default function LoginPage() {
+ const router = useRouter();
+ const setUser = useAuthStore((state) => state.setUser);
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ const response = await authApi.login({ username: email, password });
+ localStorage.setItem('access_token', response.access_token);
+
+ // Redirect to dashboard
+ router.push('/dashboard');
+ } catch (err: any) {
+ setError(err.response?.data?.detail || 'Login failed. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleGoogleLogin = async () => {
+ try {
+ const redirectUri = `${window.location.origin}/auth/callback`;
+ const authUrl = await authApi.getGoogleAuthUrl(redirectUri);
+ window.location.href = authUrl;
+ } catch (err: any) {
+ setError('Failed to initialize Google login');
+ }
+ };
+
+ return (
+
+
+
+
+ POP3 to Gmail Forwarder
+
+
+ Sign in to your account
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
new file mode 100644
index 0000000..55558cb
--- /dev/null
+++ b/frontend/src/app/page.tsx
@@ -0,0 +1,181 @@
+'use client';
+
+import { useEffect } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useAuthStore } from '@/store/authStore';
+import { Mail, ArrowRight, Shield, Zap, Clock } from 'lucide-react';
+
+export default function Home() {
+ const router = useRouter();
+ const { user, isLoading } = useAuthStore();
+
+ useEffect(() => {
+ if (!isLoading && user) {
+ router.push('/dashboard');
+ }
+ }, [user, isLoading, router]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ POP3 Forwarder
+
+
+
+ Sign In
+
+
+ Sign Up
+
+
+
+
+
+
+ {/* Hero Section */}
+
+
+
+ Forward Your POP3 Emails to Gmail
+
+ Automatically
+
+
+ Connect your POP3 email accounts and automatically forward all messages to Gmail.
+ Simple, secure, and reliable email forwarding service.
+
+
+
+ Get Started Free
+
+
+
+ Sign In
+
+
+
+
+ {/* Features */}
+
+
+
+
+
+
+ Auto-Detection
+
+
+ Automatically detect POP3 server settings from your email address.
+ Quick and easy setup in minutes.
+
+
+
+
+
+
+
+
+ Scheduled Checks
+
+
+ Set custom check intervals for each account. From every minute to once a day,
+ you control the frequency.
+
+
+
+
+
+
+
+
+ Secure & Private
+
+
+ Your credentials are encrypted and secure. We use SSL/TLS for all connections
+ and OAuth2 for Gmail.
+
+
+
+
+ {/* How It Works */}
+
+
+ How It Works
+
+
+
+
+ 1
+
+
+ Connect Accounts
+
+
+ Add your POP3 email accounts with auto-detected settings
+
+
+
+
+
+ 2
+
+
+ Authorize Gmail
+
+
+ Sign in with Google to allow forwarding to your Gmail
+
+
+
+
+
+ 3
+
+
+ Relax & Enjoy
+
+
+ Emails are automatically forwarded. Monitor activity from your dashboard
+
+
+
+
+
+
+ {/* Footer */}
+
+
+ );
+}
diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx
new file mode 100644
index 0000000..669fc15
--- /dev/null
+++ b/frontend/src/app/register/page.tsx
@@ -0,0 +1,162 @@
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { authApi } from '@/lib/api';
+
+export default function RegisterPage() {
+ const router = useRouter();
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ confirmPassword: '',
+ full_name: '',
+ });
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+
+ if (formData.password !== formData.confirmPassword) {
+ setError('Passwords do not match');
+ return;
+ }
+
+ if (formData.password.length < 8) {
+ setError('Password must be at least 8 characters long');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ await authApi.register({
+ email: formData.email,
+ password: formData.password,
+ full_name: formData.full_name,
+ });
+
+ // Auto-login after registration
+ const loginResponse = await authApi.login({
+ username: formData.email,
+ password: formData.password,
+ });
+ localStorage.setItem('access_token', loginResponse.access_token);
+
+ router.push('/dashboard');
+ } catch (err: any) {
+ setError(err.response?.data?.detail || 'Registration failed. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Create your account
+
+
+ Start forwarding your emails
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx
new file mode 100644
index 0000000..67d5f05
--- /dev/null
+++ b/frontend/src/app/settings/page.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { AuthGuard } from '@/components/AuthGuard';
+import { DashboardLayout } from '@/components/DashboardLayout';
+
+export default function SettingsPage() {
+ return (
+
+
+
+ Settings
+
+ Settings page coming soon...
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/AddMailAccountModal.tsx b/frontend/src/components/AddMailAccountModal.tsx
new file mode 100644
index 0000000..6f77a54
--- /dev/null
+++ b/frontend/src/components/AddMailAccountModal.tsx
@@ -0,0 +1,341 @@
+'use client';
+
+import { useState } from 'react';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { mailAccountsApi, MailAccount, MailAccountCreate } from '@/lib/api';
+import { X, Loader2, CheckCircle, XCircle } from 'lucide-react';
+
+interface AddMailAccountModalProps {
+ account?: MailAccount | null;
+ onClose: () => void;
+}
+
+export function AddMailAccountModal({ account, onClose }: AddMailAccountModalProps) {
+ const queryClient = useQueryClient();
+ const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
+ const [testMessage, setTestMessage] = useState('');
+ const [autoDetecting, setAutoDetecting] = useState(false);
+
+ const [formData, setFormData] = useState({
+ name: account?.name || '',
+ protocol: account?.protocol || 'pop3',
+ host: account?.host || '',
+ port: account?.port || 995,
+ username: account?.username || '',
+ password: '',
+ use_ssl: account?.use_ssl ?? true,
+ check_interval_minutes: account?.check_interval_minutes || 5,
+ max_emails_per_check: account?.max_emails_per_check || 100,
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (data: MailAccountCreate) =>
+ account ? mailAccountsApi.update(account.id, data) : mailAccountsApi.create(data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['mail-accounts'] });
+ onClose();
+ },
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value, type } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked :
+ type === 'number' ? Number(value) : value,
+ }));
+ };
+
+ const handleAutoDetect = async () => {
+ if (!formData.username) {
+ alert('Please enter an email address first');
+ return;
+ }
+
+ setAutoDetecting(true);
+ try {
+ const settings = await mailAccountsApi.autoDetect(formData.username);
+ setFormData((prev) => ({
+ ...prev,
+ protocol: settings.protocol || prev.protocol,
+ host: settings.host || prev.host,
+ port: settings.port || prev.port,
+ use_ssl: settings.use_ssl ?? prev.use_ssl,
+ }));
+ alert('Settings auto-detected successfully!');
+ } catch {
+ alert('Failed to auto-detect settings. Please enter manually.');
+ } finally {
+ setAutoDetecting(false);
+ }
+ };
+
+ const handleTestConnection = async () => {
+ if (!formData.username || !formData.password || !formData.host) {
+ alert('Please fill in username, password, and host');
+ return;
+ }
+
+ setTestStatus('testing');
+ setTestMessage('');
+ try {
+ await mailAccountsApi.test({
+ protocol: formData.protocol,
+ host: formData.host,
+ port: formData.port,
+ username: formData.username,
+ password: formData.password,
+ use_ssl: formData.use_ssl,
+ });
+ setTestStatus('success');
+ setTestMessage('Connection successful!');
+ } catch (error) {
+ setTestStatus('error');
+ const errorMessage = error instanceof Error && 'response' in error
+ ? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
+ : null;
+ setTestMessage(errorMessage || 'Connection failed');
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ await createMutation.mutateAsync(formData);
+ } catch (error) {
+ const errorMessage = error instanceof Error && 'response' in error
+ ? (error as { response?: { data?: { detail?: string } } }).response?.data?.detail
+ : null;
+ alert(errorMessage || 'Failed to save account');
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/AuthGuard.tsx b/frontend/src/components/AuthGuard.tsx
new file mode 100644
index 0000000..c89c1c7
--- /dev/null
+++ b/frontend/src/components/AuthGuard.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { useAuthStore } from '@/store/authStore';
+import { userApi } from '@/lib/api';
+
+export function AuthGuard({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const { user, setUser, setLoading, isLoading } = useAuthStore();
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ const token = localStorage.getItem('access_token');
+
+ if (!token) {
+ setLoading(false);
+ router.push('/login');
+ return;
+ }
+
+ try {
+ const userData = await userApi.getCurrentUser();
+ setUser(userData);
+ } catch (error) {
+ console.error('Auth check failed:', error);
+ setUser(null);
+ router.push('/login');
+ }
+ };
+
+ checkAuth();
+ }, [router, setUser, setLoading]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return null;
+ }
+
+ return <>{children}>;
+}
diff --git a/frontend/src/components/DashboardLayout.tsx b/frontend/src/components/DashboardLayout.tsx
new file mode 100644
index 0000000..afd6e36
--- /dev/null
+++ b/frontend/src/components/DashboardLayout.tsx
@@ -0,0 +1,163 @@
+'use client';
+
+import { useState } from 'react';
+import Link from 'next/link';
+import { usePathname, useRouter } from 'next/navigation';
+import { useAuthStore } from '@/store/authStore';
+import {
+ LayoutDashboard,
+ Mail,
+ Settings,
+ LogOut,
+ Menu,
+ X,
+ User
+} from 'lucide-react';
+
+interface DashboardLayoutProps {
+ children: React.ReactNode;
+}
+
+export function DashboardLayout({ children }: DashboardLayoutProps) {
+ const [sidebarOpen, setSidebarOpen] = useState(false);
+ const pathname = usePathname();
+ const router = useRouter();
+ const { user, logout } = useAuthStore();
+
+ const handleLogout = () => {
+ logout();
+ router.push('/login');
+ };
+
+ const navigation = [
+ { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
+ { name: 'Mail Accounts', href: '/accounts', icon: Mail },
+ { name: 'Settings', href: '/settings', icon: Settings },
+ ];
+
+ return (
+
+ {/* Sidebar for desktop */}
+
+
+
+ POP3 Forwarder
+
+
+
+
+
+
+
+
+ {/* Mobile sidebar */}
+ {sidebarOpen && (
+
+ setSidebarOpen(false)} />
+
+
+ POP3 Forwarder
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Main content */}
+
+ {/* Top bar */}
+
+
+
+
+
+ {navigation.find((item) => item.href === pathname)?.name || 'Dashboard'}
+
+
+
+
+
+
+
+
+ {user?.full_name}
+ {user?.email}
+
+
+
+
+
+
+ {/* Page content */}
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/QueryProvider.tsx b/frontend/src/components/QueryProvider.tsx
new file mode 100644
index 0000000..1a0fe97
--- /dev/null
+++ b/frontend/src/components/QueryProvider.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { useState } from 'react';
+
+export function QueryProvider({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000,
+ refetchOnWindowFocus: false,
+ },
+ },
+ })
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts
new file mode 100644
index 0000000..f41119f
--- /dev/null
+++ b/frontend/src/store/authStore.ts
@@ -0,0 +1,31 @@
+import { create } from 'zustand';
+import { User } from '@/lib/api';
+
+interface AuthState {
+ user: User | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ setUser: (user: User | null) => void;
+ setLoading: (loading: boolean) => void;
+ logout: () => void;
+}
+
+export const useAuthStore = create ((set) => ({
+ user: null,
+ isAuthenticated: false,
+ isLoading: true,
+
+ setUser: (user) => set({
+ user,
+ isAuthenticated: !!user,
+ isLoading: false,
+ }),
+
+ setLoading: (loading) => set({ isLoading: loading }),
+
+ logout: () => {
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('user');
+ set({ user: null, isAuthenticated: false });
+ },
+}));
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}
|