beautify: homepage and login page

This commit is contained in:
DeathKaioken 2026-01-13 22:13:21 +01:00
parent a88efc3e9f
commit 94fbd080d3
4 changed files with 249 additions and 204 deletions

View 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;

View File

@ -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

View File

@ -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>

View File

@ -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>