beautify: homepage and login page
This commit is contained in:
parent
a88efc3e9f
commit
94fbd080d3
153
src/app/components/curvedLoop.tsx
Normal file
153
src/app/components/curvedLoop.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useEffect, useState, useMemo, useId, FC, PointerEvent } from 'react';
|
||||
|
||||
interface CurvedLoopProps {
|
||||
marqueeText?: string;
|
||||
speed?: number;
|
||||
className?: string;
|
||||
curveAmount?: number;
|
||||
direction?: 'left' | 'right';
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
const CurvedLoop: FC<CurvedLoopProps> = ({
|
||||
marqueeText = '',
|
||||
speed = 2,
|
||||
className,
|
||||
curveAmount = -50,
|
||||
direction = 'left',
|
||||
interactive = true
|
||||
}) => {
|
||||
const text = useMemo(() => {
|
||||
const hasTrailing = /\s|\u00A0$/.test(marqueeText);
|
||||
return (hasTrailing ? marqueeText.replace(/\s+$/, '') : marqueeText) + '\u00A0';
|
||||
}, [marqueeText]);
|
||||
|
||||
const measureRef = useRef<SVGTextElement | null>(null);
|
||||
const textPathRef = useRef<SVGTextPathElement | null>(null);
|
||||
const pathRef = useRef<SVGPathElement | null>(null);
|
||||
const [spacing, setSpacing] = useState(0);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const uid = useId();
|
||||
const pathId = `curve-${uid}`;
|
||||
const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;
|
||||
|
||||
const dragRef = useRef(false);
|
||||
const lastXRef = useRef(0);
|
||||
const dirRef = useRef<'left' | 'right'>(direction);
|
||||
const velRef = useRef(0);
|
||||
|
||||
const textLength = spacing;
|
||||
const totalText = textLength
|
||||
? Array(Math.ceil(1800 / textLength) + 2)
|
||||
.fill(text)
|
||||
.join('')
|
||||
: text;
|
||||
const ready = spacing > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());
|
||||
}, [text, className]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!spacing) return;
|
||||
if (textPathRef.current) {
|
||||
const initial = -spacing;
|
||||
textPathRef.current.setAttribute('startOffset', initial + 'px');
|
||||
setOffset(initial);
|
||||
}
|
||||
}, [spacing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!spacing || !ready) return;
|
||||
let frame = 0;
|
||||
const step = () => {
|
||||
if (!dragRef.current && textPathRef.current) {
|
||||
const delta = dirRef.current === 'right' ? speed : -speed;
|
||||
const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');
|
||||
let newOffset = currentOffset + delta;
|
||||
const wrapPoint = spacing;
|
||||
if (newOffset <= -wrapPoint) newOffset += wrapPoint;
|
||||
if (newOffset > 0) newOffset -= wrapPoint;
|
||||
textPathRef.current.setAttribute('startOffset', newOffset + 'px');
|
||||
setOffset(newOffset);
|
||||
}
|
||||
frame = requestAnimationFrame(step);
|
||||
};
|
||||
frame = requestAnimationFrame(step);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [spacing, speed, ready]);
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
if (!interactive) return;
|
||||
dragRef.current = true;
|
||||
lastXRef.current = e.clientX;
|
||||
velRef.current = 0;
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (!interactive || !dragRef.current || !textPathRef.current) return;
|
||||
const dx = e.clientX - lastXRef.current;
|
||||
lastXRef.current = e.clientX;
|
||||
velRef.current = dx;
|
||||
const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');
|
||||
let newOffset = currentOffset + dx;
|
||||
const wrapPoint = spacing;
|
||||
if (newOffset <= -wrapPoint) newOffset += wrapPoint;
|
||||
if (newOffset > 0) newOffset -= wrapPoint;
|
||||
textPathRef.current.setAttribute('startOffset', newOffset + 'px');
|
||||
setOffset(newOffset);
|
||||
};
|
||||
|
||||
const endDrag = () => {
|
||||
if (!interactive) return;
|
||||
dragRef.current = false;
|
||||
dirRef.current = velRef.current > 0 ? 'right' : 'left';
|
||||
};
|
||||
|
||||
const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full flex items-center justify-center"
|
||||
style={{ visibility: ready ? 'visible' : 'hidden', cursor: cursorStyle }}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={endDrag}
|
||||
onPointerLeave={endDrag}
|
||||
>
|
||||
<svg
|
||||
className="select-none w-full overflow-visible block aspect-[100/12] text-[2.25rem] md:text-[2.75rem] lg:text-[3rem] font-bold uppercase leading-none"
|
||||
viewBox="0 0 1440 120"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<text
|
||||
ref={measureRef}
|
||||
xmlSpace="preserve"
|
||||
style={{ visibility: 'hidden', opacity: 0, pointerEvents: 'none' }}
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
<defs>
|
||||
<path ref={pathRef} id={pathId} d={pathD} fill="none" stroke="transparent" />
|
||||
</defs>
|
||||
{ready && (
|
||||
<text xmlSpace="preserve" className={`fill-[#0F172A] ${className ?? ''}`}>
|
||||
<textPath
|
||||
ref={textPathRef}
|
||||
href={`#${pathId}`}
|
||||
startOffset={offset + 'px'}
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
{totalText}
|
||||
</textPath>
|
||||
</text>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurvedLoop;
|
||||
@ -68,7 +68,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
const refreshAuthToken = useAuthStore(s => s.refreshAuthToken)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const isHome = pathname === '/'
|
||||
const isParallaxPage = pathname === '/' || pathname === '/login'
|
||||
|
||||
const [hasReferralPerm, setHasReferralPerm] = useState(false)
|
||||
const [adminMgmtOpen, setAdminMgmtOpen] = useState(false)
|
||||
@ -119,12 +119,12 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
setAnimateIn(true)
|
||||
}, [])
|
||||
|
||||
// Home-page scroll listener: reveal header after first scroll with slight parallax
|
||||
// Home + login scroll listener: reveal header after first scroll with slight parallax
|
||||
useEffect(() => {
|
||||
if (!mounted) return
|
||||
|
||||
if (!isHome) {
|
||||
// non-home: header always visible, no scroll listeners
|
||||
if (!isParallaxPage) {
|
||||
// non-parallax pages: header always visible, no scroll listeners
|
||||
setScrollY(100)
|
||||
return
|
||||
}
|
||||
@ -149,7 +149,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [mounted, isHome])
|
||||
}, [mounted, isParallaxPage])
|
||||
|
||||
// Fetch user permissions and set hasReferralPerm
|
||||
useEffect(() => {
|
||||
@ -310,13 +310,13 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
const isAdmin = mounted && rawIsAdmin
|
||||
|
||||
// Only gate visibility by scroll on home; elsewhere just use animateIn
|
||||
const headerVisible = isHome ? animateIn && scrollY > 24 : animateIn
|
||||
const parallaxOffset = isHome ? Math.max(-16, -scrollY * 0.15) : 0
|
||||
const headerVisible = isParallaxPage ? animateIn && scrollY > 24 : animateIn
|
||||
const parallaxOffset = isParallaxPage ? Math.max(-16, -scrollY * 0.15) : 0
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`${
|
||||
isHome ? 'fixed top-0 left-0 w-full' : 'relative'
|
||||
isParallaxPage ? 'fixed top-0 left-0 w-full' : 'relative'
|
||||
} isolate z-10 shadow-lg shadow-black/30 after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:bg-[radial-gradient(circle_at_20%_20%,rgba(56,124,255,0.18),transparent_55%),radial-gradient(circle_at_80%_35%,rgba(139,92,246,0.16),transparent_60%)] ${
|
||||
isAdmin ? '' : 'border-b border-white/10'
|
||||
} ${
|
||||
@ -324,7 +324,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
} transition-all duration-500 ease-out`}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
|
||||
...(isHome ? { transform: `translateY(${parallaxOffset}px)` } : {}),
|
||||
...(isParallaxPage ? { transform: `translateY(${parallaxOffset}px)` } : {}),
|
||||
}}
|
||||
>
|
||||
<nav
|
||||
|
||||
@ -8,30 +8,19 @@ import { useToast } from '../../components/toast/toastComponent'
|
||||
|
||||
export default function LoginForm() {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showBall, setShowBall] = useState(true)
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
// FIX: use a static initial width so SSR and first client render match
|
||||
const [viewportWidth, setViewportWidth] = useState<number>(1200)
|
||||
const router = useRouter()
|
||||
const { login, error, setError, loading } = useLogin()
|
||||
const { showToast } = useToast()
|
||||
|
||||
// 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)
|
||||
handleResize() // initialize on mount (runs only on client)
|
||||
handleResize()
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
@ -125,135 +114,49 @@ export default function LoginForm() {
|
||||
<div
|
||||
className="w-full relative"
|
||||
style={{
|
||||
// CHANGED: full-height flex box for perfect vertical centering
|
||||
minHeight: '100vh',
|
||||
// removed full-height so curved loop is visible right under the form
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
// REMOVE marble image so Waves shows through
|
||||
background: 'transparent',
|
||||
// Subtle padding to breathe on mobile
|
||||
padding: isMobile ? '0.75rem' : '1.5rem',
|
||||
// Reduced top padding so the curved loop is closer to the form
|
||||
padding: isMobile ? '0.5rem 0.75rem 0.75rem' : '0.2rem 1.5rem 1.5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl shadow-2xl flex flex-col items-center relative border-t-4 border-[#8D6B1D]"
|
||||
className="rounded-3xl shadow-2xl flex flex-col items-center relative border border-white/35"
|
||||
style={{
|
||||
width: formWidth,
|
||||
maxWidth: formMaxWidth,
|
||||
minWidth: isMobile ? '0' : '420px',
|
||||
// CHANGED: tighter padding; removed transform scaling
|
||||
padding: isMobile ? '1rem' : '2rem',
|
||||
// more translucent, glassy background
|
||||
backgroundColor: 'rgba(255,255,255,0.55)',
|
||||
backdropFilter: 'blur(18px)',
|
||||
WebkitBackdropFilter: 'blur(18px)',
|
||||
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
|
||||
}}
|
||||
>
|
||||
{/* 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>
|
||||
{/* Content (title + earth removed) */}
|
||||
<div
|
||||
style={{
|
||||
// CHANGED: smaller margins; the card is centered now
|
||||
marginTop: isMobile ? '0.25rem' : isTablet ? '0.5rem' : '0.75rem',
|
||||
marginBottom: isMobile ? '1rem' : isTablet ? '1.25rem' : '1.5rem',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{/* Title + Subtitle (restored) */}
|
||||
<div className="mb-6 text-center">
|
||||
<h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm">
|
||||
PROFIT PLANET
|
||||
</h1>
|
||||
<p className="mt-1 text-sm md:text-base text-slate-700/90">
|
||||
Welcome back! Login to continue.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div style={{
|
||||
// CHANGED: smaller margins; the card is centered now
|
||||
marginTop: isMobile ? '0.25rem' : isTablet ? '0.5rem' : '0.75rem',
|
||||
marginBottom: isMobile ? '1rem' : isTablet ? '1.25rem' : '1.5rem',
|
||||
width: '100%',
|
||||
}}>
|
||||
<h1
|
||||
className="mb-2 text-center font-extrabold text-[#0F172A] tracking-tight drop-shadow-lg"
|
||||
style={{
|
||||
// CHANGED: slightly smaller headline on mobile to reduce vertical space
|
||||
fontSize: isMobile ? '1.75rem' : isTablet ? '2rem' : '2.25rem',
|
||||
marginTop: isMobile ? '0.25rem' : undefined,
|
||||
}}
|
||||
>
|
||||
Profit Planet
|
||||
</h1>
|
||||
<p
|
||||
className="mb-6 text-center text-[#8D6B1D] font-medium"
|
||||
style={{
|
||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1.05rem',
|
||||
// CHANGED: reduce bottom margin
|
||||
marginBottom: isMobile ? '0.75rem' : isTablet ? '1rem' : '1rem',
|
||||
}}
|
||||
>
|
||||
Welcome back! Login to continue.
|
||||
</p>
|
||||
|
||||
<form
|
||||
className="space-y-6 w-full"
|
||||
@ -263,7 +166,7 @@ export default function LoginForm() {
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<div className="field-animated">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-base font-semibold text-[#0F172A] mb-1"
|
||||
@ -281,7 +184,7 @@ export default function LoginForm() {
|
||||
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"
|
||||
className="input-animated appearance-none block w-full px-4 py-3 border border-white/40 rounded-xl placeholder-slate-600/80 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] text-base bg-white/60 text-[#0F172A] shadow-sm transition"
|
||||
style={{
|
||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
||||
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
|
||||
@ -292,7 +195,7 @@ export default function LoginForm() {
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div>
|
||||
<div className="field-animated">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-base font-semibold text-[#0F172A] mb-1"
|
||||
@ -307,14 +210,18 @@ export default function LoginForm() {
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "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"
|
||||
className="input-animated appearance-none block w-full px-4 py-3 pr-12 border border-white/40 rounded-xl placeholder-slate-600/80 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] text-base bg-white/60 text-[#0F172A] shadow-sm transition"
|
||||
style={{
|
||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
||||
padding: isMobile ? '0.5rem 2.5rem 0.5rem 0.75rem' : isTablet ? '0.6rem 2.75rem 0.6rem 0.875rem' : '0.7rem 3rem 0.7rem 1rem',
|
||||
padding: isMobile
|
||||
? '0.5rem 2.5rem 0.5rem 0.75rem'
|
||||
: isTablet
|
||||
? '0.6rem 2.75rem 0.6rem 0.875rem'
|
||||
: '0.7rem 3rem 0.7rem 1rem',
|
||||
}}
|
||||
placeholder="Dein Passwort"
|
||||
required
|
||||
@ -331,7 +238,7 @@ export default function LoginForm() {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Remember Me & Show Password */}
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
@ -374,10 +281,10 @@ export default function LoginForm() {
|
||||
<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 ${
|
||||
className={`w-full py-3 px-6 rounded-xl text-base font-semibold transition-all duration-200 transform hover:-translate-y-0.5 border ${
|
||||
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'
|
||||
? 'border-white/30 bg-white/20 text-slate-300 cursor-not-allowed'
|
||||
: 'border-white/55 bg-white/30 text-[#0F172A] shadow-[0_10px_30px_rgba(15,23,42,0.45)] hover:bg-white/40 hover:shadow-[0_16px_40px_rgba(15,23,42,0.6)] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80'
|
||||
}`}
|
||||
style={{
|
||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
||||
@ -385,8 +292,8 @@ export default function LoginForm() {
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
||||
Anmeldung läuft...
|
||||
</div>
|
||||
) : (
|
||||
@ -406,70 +313,46 @@ export default function LoginForm() {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Registration Section */}
|
||||
<div
|
||||
className="mt-8 w-full"
|
||||
style={{
|
||||
marginTop: isMobile ? '0.75rem' : isTablet ? '1rem' : '1rem',
|
||||
}}
|
||||
>
|
||||
<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 */}
|
||||
{/* Input animations */}
|
||||
<style jsx>{`
|
||||
@keyframes orbit-1 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
@keyframes field-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.99);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes orbit-2 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(-360deg); }
|
||||
|
||||
@keyframes input-focus-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(141, 107, 29, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 3px rgba(141, 107, 29, 0.35);
|
||||
}
|
||||
}
|
||||
.animate-orbit-1 {
|
||||
animation: orbit-1 3s linear infinite;
|
||||
transform-origin: 0 0;
|
||||
|
||||
.field-animated {
|
||||
animation: field-fade-in 0.45s ease-out both;
|
||||
}
|
||||
.animate-orbit-2 {
|
||||
animation: orbit-2 4s linear infinite;
|
||||
transform-origin: 0 0;
|
||||
|
||||
.input-animated {
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
transform 0.12s ease;
|
||||
}
|
||||
|
||||
.input-animated:focus {
|
||||
animation: input-focus-pulse 0.22s ease-out;
|
||||
background-color: rgba(255, 255, 255, 0.96);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,7 @@ import useAuthStore from '../store/authStore'
|
||||
import { ToastProvider } from '../components/toast/toastComponent'
|
||||
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
|
||||
import Waves from '../components/waves'
|
||||
import CurvedLoop from '../components/curvedLoop'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [hasHydrated, setHasHydrated] = useState(false)
|
||||
@ -67,8 +68,16 @@ export default function LoginPage() {
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
<div className="relative z-10 flex-1 flex items-center justify-center">
|
||||
<div className="relative z-10 flex-1 flex flex-col justify-start space-y-4 pt-10 pb-10">
|
||||
<div className="w-full">
|
||||
<CurvedLoop
|
||||
marqueeText="Welcome to profit planet ✦"
|
||||
speed={1}
|
||||
interactive={false}
|
||||
className="tracking-[0.2em]"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex items-center justify-center">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user