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 refreshAuthToken = useAuthStore(s => s.refreshAuthToken)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const isHome = pathname === '/'
|
const isParallaxPage = pathname === '/' || pathname === '/login'
|
||||||
|
|
||||||
const [hasReferralPerm, setHasReferralPerm] = useState(false)
|
const [hasReferralPerm, setHasReferralPerm] = useState(false)
|
||||||
const [adminMgmtOpen, setAdminMgmtOpen] = useState(false)
|
const [adminMgmtOpen, setAdminMgmtOpen] = useState(false)
|
||||||
@ -119,12 +119,12 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
setAnimateIn(true)
|
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(() => {
|
useEffect(() => {
|
||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
|
|
||||||
if (!isHome) {
|
if (!isParallaxPage) {
|
||||||
// non-home: header always visible, no scroll listeners
|
// non-parallax pages: header always visible, no scroll listeners
|
||||||
setScrollY(100)
|
setScrollY(100)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -149,7 +149,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
window.removeEventListener('scroll', handleScroll)
|
window.removeEventListener('scroll', handleScroll)
|
||||||
window.removeEventListener('wheel', handleWheel)
|
window.removeEventListener('wheel', handleWheel)
|
||||||
}
|
}
|
||||||
}, [mounted, isHome])
|
}, [mounted, isParallaxPage])
|
||||||
|
|
||||||
// Fetch user permissions and set hasReferralPerm
|
// Fetch user permissions and set hasReferralPerm
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -310,13 +310,13 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
const isAdmin = mounted && rawIsAdmin
|
const isAdmin = mounted && rawIsAdmin
|
||||||
|
|
||||||
// Only gate visibility by scroll on home; elsewhere just use animateIn
|
// Only gate visibility by scroll on home; elsewhere just use animateIn
|
||||||
const headerVisible = isHome ? animateIn && scrollY > 24 : animateIn
|
const headerVisible = isParallaxPage ? animateIn && scrollY > 24 : animateIn
|
||||||
const parallaxOffset = isHome ? Math.max(-16, -scrollY * 0.15) : 0
|
const parallaxOffset = isParallaxPage ? Math.max(-16, -scrollY * 0.15) : 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={`${
|
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%)] ${
|
} 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'
|
isAdmin ? '' : 'border-b border-white/10'
|
||||||
} ${
|
} ${
|
||||||
@ -324,7 +324,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
} transition-all duration-500 ease-out`}
|
} transition-all duration-500 ease-out`}
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
|
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
|
||||||
...(isHome ? { transform: `translateY(${parallaxOffset}px)` } : {}),
|
...(isParallaxPage ? { transform: `translateY(${parallaxOffset}px)` } : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<nav
|
<nav
|
||||||
|
|||||||
@ -8,30 +8,19 @@ import { useToast } from '../../components/toast/toastComponent'
|
|||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [showBall, setShowBall] = useState(true)
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
rememberMe: false
|
rememberMe: false
|
||||||
})
|
})
|
||||||
// FIX: use a static initial width so SSR and first client render match
|
|
||||||
const [viewportWidth, setViewportWidth] = useState<number>(1200)
|
const [viewportWidth, setViewportWidth] = useState<number>(1200)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { login, error, setError, loading } = useLogin()
|
const { login, error, setError, loading } = useLogin()
|
||||||
const { showToast } = useToast()
|
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(() => {
|
useEffect(() => {
|
||||||
const handleResize = () => setViewportWidth(window.innerWidth)
|
const handleResize = () => setViewportWidth(window.innerWidth)
|
||||||
handleResize() // initialize on mount (runs only on client)
|
handleResize()
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
return () => window.removeEventListener('resize', handleResize)
|
return () => window.removeEventListener('resize', handleResize)
|
||||||
}, [])
|
}, [])
|
||||||
@ -125,135 +114,49 @@ export default function LoginForm() {
|
|||||||
<div
|
<div
|
||||||
className="w-full relative"
|
className="w-full relative"
|
||||||
style={{
|
style={{
|
||||||
// CHANGED: full-height flex box for perfect vertical centering
|
// removed full-height so curved loop is visible right under the form
|
||||||
minHeight: '100vh',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
// REMOVE marble image so Waves shows through
|
// REMOVE marble image so Waves shows through
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
// Subtle padding to breathe on mobile
|
// Reduced top padding so the curved loop is closer to the form
|
||||||
padding: isMobile ? '0.75rem' : '1.5rem',
|
padding: isMobile ? '0.5rem 0.75rem 0.75rem' : '0.2rem 1.5rem 1.5rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<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={{
|
style={{
|
||||||
width: formWidth,
|
width: formWidth,
|
||||||
maxWidth: formMaxWidth,
|
maxWidth: formMaxWidth,
|
||||||
minWidth: isMobile ? '0' : '420px',
|
minWidth: isMobile ? '0' : '420px',
|
||||||
// CHANGED: tighter padding; removed transform scaling
|
// CHANGED: tighter padding; removed transform scaling
|
||||||
padding: isMobile ? '1rem' : '2rem',
|
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 */}
|
{/* Content (title + earth removed) */}
|
||||||
{showBall && !isMobile && (
|
<div
|
||||||
<div className="absolute -top-16 left-1/2 -translate-x-1/2 w-28 z-20">
|
style={{
|
||||||
<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">
|
// CHANGED: smaller margins; the card is centered now
|
||||||
{/* Inner small circle with cartoony Earth */}
|
marginTop: isMobile ? '0.25rem' : isTablet ? '0.5rem' : '0.75rem',
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
marginBottom: isMobile ? '1rem' : isTablet ? '1.25rem' : '1.5rem',
|
||||||
<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">
|
width: '100%',
|
||||||
<svg
|
}}
|
||||||
viewBox="0 0 64 64"
|
>
|
||||||
className="w-14 h-14"
|
{/* Title + Subtitle (restored) */}
|
||||||
role="img"
|
<div className="mb-6 text-center">
|
||||||
aria-label="Cartoon Earth"
|
<h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm">
|
||||||
>
|
PROFIT PLANET
|
||||||
<defs>
|
</h1>
|
||||||
<radialGradient id="earth-ocean" cx="50%" cy="40%" r="65%">
|
<p className="mt-1 text-sm md:text-base text-slate-700/90">
|
||||||
<stop offset="0%" stopColor="#3fa9f5" />
|
Welcome back! Login to continue.
|
||||||
<stop offset="100%" stopColor="#1d5fae" />
|
</p>
|
||||||
</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>
|
</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
|
<form
|
||||||
className="space-y-6 w-full"
|
className="space-y-6 w-full"
|
||||||
@ -263,7 +166,7 @@ export default function LoginForm() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{/* Email Field */}
|
{/* Email Field */}
|
||||||
<div>
|
<div className="field-animated">
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="block text-base font-semibold text-[#0F172A] mb-1"
|
className="block text-base font-semibold text-[#0F172A] mb-1"
|
||||||
@ -281,7 +184,7 @@ export default function LoginForm() {
|
|||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleInputChange}
|
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={{
|
style={{
|
||||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
||||||
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
|
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
|
||||||
@ -292,7 +195,7 @@ export default function LoginForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
<div>
|
<div className="field-animated">
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
className="block text-base font-semibold text-[#0F172A] mb-1"
|
className="block text-base font-semibold text-[#0F172A] mb-1"
|
||||||
@ -307,14 +210,18 @@ export default function LoginForm() {
|
|||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? 'text' : 'password'}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={handleInputChange}
|
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={{
|
style={{
|
||||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
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"
|
placeholder="Dein Passwort"
|
||||||
required
|
required
|
||||||
@ -331,7 +238,7 @@ export default function LoginForm() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Remember Me & Show Password */}
|
{/* Remember Me & Show Password */}
|
||||||
<div className="mt-2 flex items-center justify-between">
|
<div className="mt-2 flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -374,10 +281,10 @@ export default function LoginForm() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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
|
loading
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
? 'border-white/30 bg-white/20 text-slate-300 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/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={{
|
style={{
|
||||||
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
|
||||||
@ -385,8 +292,8 @@ export default function LoginForm() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
||||||
Anmeldung läuft...
|
Anmeldung läuft...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -406,70 +313,46 @@ export default function LoginForm() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* CSS Animations */}
|
{/* Input animations */}
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
@keyframes orbit-1 {
|
@keyframes field-fade-in {
|
||||||
0% { transform: rotate(0deg); }
|
from {
|
||||||
100% { transform: rotate(360deg); }
|
opacity: 0;
|
||||||
|
transform: translateY(8px) scale(0.99);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes orbit-2 {
|
|
||||||
0% { transform: rotate(0deg); }
|
@keyframes input-focus-pulse {
|
||||||
100% { transform: rotate(-360deg); }
|
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;
|
.field-animated {
|
||||||
transform-origin: 0 0;
|
animation: field-fade-in 0.45s ease-out both;
|
||||||
}
|
}
|
||||||
.animate-orbit-2 {
|
|
||||||
animation: orbit-2 4s linear infinite;
|
.input-animated {
|
||||||
transform-origin: 0 0;
|
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>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import useAuthStore from '../store/authStore'
|
|||||||
import { ToastProvider } from '../components/toast/toastComponent'
|
import { ToastProvider } from '../components/toast/toastComponent'
|
||||||
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
|
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
|
||||||
import Waves from '../components/waves'
|
import Waves from '../components/waves'
|
||||||
|
import CurvedLoop from '../components/curvedLoop'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [hasHydrated, setHasHydrated] = useState(false)
|
const [hasHydrated, setHasHydrated] = useState(false)
|
||||||
@ -67,8 +68,16 @@ export default function LoginPage() {
|
|||||||
xGap={12}
|
xGap={12}
|
||||||
yGap={36}
|
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">
|
<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 />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user