profit-planet-frontend/src/app/login/components/LoginForm.tsx
2025-11-17 22:11:53 +01:00

464 lines
17 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useLogin } from '../hooks/useLogin'
export default function LoginForm() {
const [showPassword, setShowPassword] = useState(false)
const [showBall, setShowBall] = useState(true)
const [formData, setFormData] = useState({
email: '',
password: '',
rememberMe: false
})
const [viewportWidth, setViewportWidth] = useState<number>(
typeof window !== 'undefined' ? window.innerWidth : 1200
)
const router = useRouter()
const { login, error, setError, loading } = useLogin()
// Responsive ball visibility
useEffect(() => {
const handleResizeBall = () => setShowBall(window.innerWidth >= 768)
handleResizeBall()
window.addEventListener('resize', handleResizeBall)
return () => window.removeEventListener('resize', handleResizeBall)
}, [])
// Track viewport width for dynamic scaling
useEffect(() => {
const handleResize = () => setViewportWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
setError('') // Clear error when user starts typing
}
const validateForm = (): boolean => {
if (!formData.email.trim()) {
setError('E-Mail-Adresse ist erforderlich')
return false
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
setError('Bitte gib eine gültige E-Mail-Adresse ein')
return false
}
if (!formData.password.trim()) {
setError('Passwort ist erforderlich')
return false
}
if (formData.password.length < 6) {
setError('Passwort muss mindestens 6 Zeichen lang sein')
return false
}
return true
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) return
await login({
email: formData.email,
password: formData.password,
rememberMe: formData.rememberMe
})
}
// Dynamic breakpoints
const isMobile = viewportWidth < 640
const isTablet = viewportWidth >= 640 && viewportWidth < 1024
const isSmallLaptop = viewportWidth >= 1024 && viewportWidth < 1366
// Dynamic width & scale
const formWidth = isMobile
? '98vw'
: isTablet
? '85vw'
: isSmallLaptop
? '50vw'
: '40vw'
const formMaxWidth = isMobile
? 'none'
: isTablet
? '620px'
: isSmallLaptop
? '660px'
: '720px'
const formScale = (() => {
if (isMobile) return 1
if (isTablet) return 0.95
if (isSmallLaptop) return 0.9
if (viewportWidth >= 1366 && viewportWidth < 1680) return 0.85
return 0.82
})()
return (
<div
className="w-full flex justify-center items-start relative"
style={{
// Ensure the background fills the viewport height
minHeight: '100vh',
// Reduce bottom padding to avoid extra white space
paddingTop: isMobile ? '0.75rem' : '4rem',
paddingBottom: isMobile ? '1rem' : '1.5rem',
backgroundImage: 'url(/images/misc/marble_bluegoldwhite_BG.jpg)',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div
className="bg-white rounded-2xl shadow-2xl flex flex-col items-center relative border-t-4 border-[#8D6B1D]"
style={{
width: formWidth,
maxWidth: formMaxWidth,
minWidth: isMobile ? '0' : '400px',
padding: isMobile ? '0.75rem' : '2rem',
marginTop: isMobile ? '0.5rem' : undefined,
transform: formScale !== 1 ? `scale(${formScale})` : undefined,
transformOrigin: 'top center'
}}
>
{/* Animated Ball - Desktop Only */}
{showBall && !isMobile && (
<div className="absolute -top-16 left-1/2 -translate-x-1/2 w-28 z-20">
<div className="w-28 h-28 rounded-full bg-gradient-to-br from-[#8D6B1D] via-[#A67C20] to-[#C49225] shadow-xl border-4 border-white relative">
{/* Inner small circle with cartoony Earth */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-16 h-16 rounded-full bg-white/15 backdrop-blur-sm border border-white/25 flex items-center justify-center shadow-inner relative overflow-hidden">
<svg
viewBox="0 0 64 64"
className="w-14 h-14"
role="img"
aria-label="Cartoon Earth"
>
<defs>
<radialGradient id="earth-ocean" cx="50%" cy="40%" r="65%">
<stop offset="0%" stopColor="#3fa9f5" />
<stop offset="100%" stopColor="#1d5fae" />
</radialGradient>
<linearGradient id="earth-glow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="rgba(255,255,255,0.55)" />
<stop offset="60%" stopColor="rgba(255,255,255,0)" />
</linearGradient>
</defs>
<circle cx="32" cy="32" r="30" fill="url(#earth-ocean)" />
{/* Land masses (stylized) */}
<path
fill="#4caf50"
d="M18 30c4-6 10-9 16-9 3 0 5 1 7 3 2 2 1 4-1 5-4 2-8 2-11 5-2 2-3 4-6 4-5 0-8-5-5-8Z"
/>
<path
fill="#66bb6a"
d="M40 18c3 1 6 3 7 6 1 3 0 5-2 6-2 1-3 0-5-2-3-3-6-5-6-7 0-3 3-4 6-3Z"
opacity=".9"
/>
<path
fill="#43a047"
d="M26 44c2-2 5-3 8-2 2 1 3 3 1 5-2 3-6 5-9 4-3-1-3-5 0-7Z"
opacity=".85"
/>
{/* Atmospheric rim */}
<circle
cx="32"
cy="32"
r="30"
fill="none"
stroke="rgba(255,255,255,0.35)"
strokeWidth="1.5"
/>
{/* Light sheen */}
<ellipse
cx="26"
cy="22"
rx="11"
ry="7"
fill="url(#earth-glow)"
opacity=".6"
/>
</svg>
{/* Subtle gloss overlay */}
<span className="pointer-events-none absolute inset-0 before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_35%_30%,rgba(255,255,255,0.45),transparent_70%)]" />
</div>
</div>
{/* Orbiting balls (unchanged) */}
<span className="absolute left-1/2 top-1/2 w-0 h-0">
<span className="block absolute animate-orbit-1" style={{ width: 0, height: 0 }}>
<span
className="block w-3 h-3 bg-[#8D6B1D] rounded-full shadow-lg"
style={{ transform: 'translateX(44px)' }}
/>
</span>
<span className="block absolute animate-orbit-2" style={{ width: 0, height: 0 }}>
<span
className="block w-2.5 h-2.5 bg-[#A67C20] rounded-full shadow-md"
style={{ transform: 'translateX(-36px)' }}
/>
</span>
</span>
</div>
</div>
)}
{/* Content */}
<div style={{
marginTop: isMobile ? '0.5rem' : isTablet ? '1rem' : '1.5rem',
marginBottom: isMobile ? '1.5rem' : isTablet ? '1.75rem' : '2rem',
width: '100%',
}}>
<h1
className="mb-2 text-center text-4xl font-extrabold text-[#0F172A] tracking-tight drop-shadow-lg"
style={{
fontSize: isMobile ? '2rem' : isTablet ? '2.25rem' : undefined,
marginTop: isMobile ? '0.5rem' : undefined,
}}
>
Profit Planet
</h1>
<p
className="mb-8 text-center text-lg text-[#8D6B1D] font-medium"
style={{
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : undefined,
marginBottom: isMobile ? '1rem' : isTablet ? '1.5rem' : undefined,
}}
>
Welcome back! Login to continue.
</p>
<form
className="space-y-7 w-full"
style={{
gap: isMobile ? '0.75rem' : isTablet ? '1rem' : undefined,
}}
onSubmit={handleSubmit}
>
{/* Email Field */}
<div>
<label
htmlFor="email"
className="block text-base font-semibold text-[#0F172A] mb-1"
style={{
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
marginBottom: isMobile ? '0.25rem' : undefined,
}}
>
E-Mail-Adresse
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
value={formData.email}
onChange={handleInputChange}
className="appearance-none block w-full px-4 py-3 border border-gray-300 rounded-lg placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D] text-base bg-white text-[#0F172A] transition"
style={{
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
padding: isMobile ? '0.4rem 0.75rem' : isTablet ? '0.5rem 0.875rem' : undefined,
}}
placeholder="deine@email.com"
required
/>
</div>
{/* Password Field */}
<div>
<label
htmlFor="password"
className="block text-base font-semibold text-[#0F172A] mb-1"
style={{
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
marginBottom: isMobile ? '0.25rem' : undefined,
}}
>
Passwort
</label>
<div className="relative">
<input
id="password"
name="password"
type={showPassword ? "text" : "password"}
autoComplete="current-password"
value={formData.password}
onChange={handleInputChange}
className="appearance-none block w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D] text-base bg-white text-[#0F172A] transition"
style={{
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
padding: isMobile ? '0.4rem 2.5rem 0.4rem 0.75rem' : isTablet ? '0.5rem 2.75rem 0.5rem 0.875rem' : '0.75rem 3rem 0.75rem 1rem',
}}
placeholder="Dein Passwort"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-slate-500 hover:text-[#8D6B1D] transition-colors" />
) : (
<EyeIcon className="h-5 w-5 text-slate-500 hover:text-[#8D6B1D] transition-colors" />
)}
</button>
</div>
{/* Remember Me & Show Password */}
<div className="mt-2 flex items-center justify-between">
<div className="flex items-center">
<input
id="rememberMe"
name="rememberMe"
type="checkbox"
checked={formData.rememberMe}
onChange={handleInputChange}
className="h-4 w-4 text-[#8D6B1D] border-2 border-gray-300 rounded focus:ring-[#8D6B1D] focus:ring-2"
/>
<label htmlFor="rememberMe" className="ml-2 text-sm text-slate-700">
Angemeldet bleiben
</label>
</div>
<div className="flex items-center">
<input
id="show-password"
type="checkbox"
className="h-4 w-4 border-2 border-gray-300 rounded focus:ring-[#8D6B1D] focus:ring-2"
checked={showPassword}
onChange={(e) => setShowPassword(e.target.checked)}
/>
<label htmlFor="show-password" className="ml-2 text-sm text-slate-700">
Passwort anzeigen
</label>
</div>
</div>
</div>
{/* Error Message */}
{error && (
<div className="text-red-500 text-sm bg-red-50 border border-red-200 rounded-lg p-3">
{error}
</div>
)}
{/* Submit Button */}
<div>
<button
type="submit"
disabled={loading}
className={`w-full py-3 px-6 rounded-lg shadow-md text-base font-bold text-white transition-all duration-200 transform hover:-translate-y-0.5 ${
loading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-gradient-to-r from-[#8D6B1D] via-[#A67C20] to-[#C49225] hover:from-[#7A5E1A] hover:to-[#B8851F] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2'
}`}
style={{
fontSize: isMobile ? '0.9rem' : isTablet ? '0.95rem' : undefined,
padding: isMobile ? '0.6rem 1rem' : isTablet ? '0.7rem 1.25rem' : undefined,
}}
>
{loading ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Anmeldung läuft...
</div>
) : (
'Anmelden'
)}
</button>
</div>
{/* Forgot Password */}
<div className="mt-4 flex justify-end">
<button
type="button"
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors"
onClick={() => router.push("/password-reset")}
>
Passwort vergessen?
</button>
</div>
</form>
{/* Registration Section */}
<div
className="mt-10 w-full"
style={{
marginTop: isMobile ? '1rem' : isTablet ? '1.5rem' : undefined,
}}
>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200"></div>
</div>
<div
className="relative flex justify-center text-base"
style={{
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
}}
>
<a href="/register" className="px-3 bg-white text-[#8D6B1D]">Noch kein Account?</a>
</div>
</div>
<div
className="mt-7 text-center"
style={{
marginTop: isMobile ? '0.75rem' : isTablet ? '1rem' : undefined,
}}
>
<p
className="text-base text-slate-700"
style={{
fontSize: isMobile ? '0.8rem' : isTablet ? '0.85rem' : undefined,
}}
>
Profit Planet is available by invitation only.
</p>
<p
className="text-base text-[#8D6B1D] mt-2"
style={{
fontSize: isMobile ? '0.8rem' : isTablet ? '0.85rem' : undefined,
}}
>
Contact us for an invitation!
</p>
</div>
</div>
</div>
{/* CSS Animations */}
<style jsx>{`
@keyframes orbit-1 {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes orbit-2 {
0% { transform: rotate(0deg); }
100% { transform: rotate(-360deg); }
}
.animate-orbit-1 {
animation: orbit-1 3s linear infinite;
transform-origin: 0 0;
}
.animate-orbit-2 {
animation: orbit-2 4s linear infinite;
transform-origin: 0 0;
}
`}</style>
</div>
</div>
)
}