464 lines
17 KiB
TypeScript
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>
|
|
)
|
|
} |