Styling updates en sign-in component

- Shadcn UI design tokens toevoegen aan globals.css
- Dark mode CSS variabelen voor consistente theming
- AnimatedSignIn component met collage layout
- Theme toggle en Google OAuth placeholder

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
colinislit
2025-11-17 15:26:50 +01:00
parent 99804656d7
commit 1592fdfac6
3 changed files with 392 additions and 1 deletions

View File

@@ -232,4 +232,74 @@ body {
body { body {
@apply bg-background text-foreground antialiased; @apply bg-background text-foreground antialiased;
} }
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
} }

321
components/ui/sign-in.tsx Normal file
View File

@@ -0,0 +1,321 @@
import React, { useState, useEffect } from 'react';
import { Eye, EyeOff, LogIn } from 'lucide-react';
const AnimatedSignIn: React.FC = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [showPassword, setShowPassword] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [mounted, setMounted] = useState(false);
// Animation states
const [formVisible, setFormVisible] = useState(false);
useEffect(() => {
setMounted(true);
setTimeout(() => setFormVisible(true), 300);
}, []);
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
const handleSignIn = (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
// Simulate API call
setTimeout(() => {
console.log('Sign in attempt with:', { email, password });
setIsLoading(false);
}, 1500);
};
// Only show the component once mounted to avoid hydration issues
if (!mounted) return null;
return (
<div className={`min-h-screen w-full transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-900' : 'bg-[#e8f4ef]'}`}>
<div className="flex min-h-screen items-center justify-center p-4 md:p-0">
<div className={`w-full max-w-6xl overflow-hidden rounded-2xl transition-all duration-500 ${
theme === 'dark' ? 'bg-slate-800 shadow-xl shadow-slate-700/20' : 'bg-white shadow-xl shadow-gray-200'
} ${formVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'}`}>
{/* Theme toggle */}
<button
onClick={toggleTheme}
className={`absolute right-4 top-4 rounded-full p-2 transition-colors z-10 ${
theme === 'dark'
? 'bg-slate-700 text-yellow-400 hover:bg-slate-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
aria-label="Toggle theme"
>
{theme === 'dark' ? (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="animate-pulse"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></svg>
)}
</button>
<div className="flex flex-col md:flex-row">
{/* Left side - Statistics and Images Collage */}
<div className="hidden md:block w-full md:w-3/5 bg-gray-100 p-6 animate-fade-in">
<div className="grid grid-cols-2 grid-rows-3 gap-4 h-full overflow-hidden">
{/* Top left - Person working */}
<div className="overflow-hidden rounded-xl">
<img
src="https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d"
alt="Person working"
className="w-full h-full object-cover"
style={{ opacity: 0.9 }}
/>
</div>
{/* Top right - Orange stat */}
<div
className={`rounded-xl flex flex-col justify-center items-center p-6 text-white ${
theme === 'dark' ? 'bg-orange-600' : 'bg-orange-500'
}`}
style={{
transform: formVisible ? 'translateY(0)' : 'translateY(20px)',
opacity: formVisible ? 1 : 0,
transition: 'transform 0.6s ease-out, opacity 0.6s ease-out',
transitionDelay: '0.2s',
}}
>
<h2 className="text-5xl font-bold mb-2">41%</h2>
<p className="text-center text-sm">of recruiters say entry-level positions are the hardest to fill.</p>
</div>
{/* Middle left - Person at computer */}
<div className="overflow-hidden rounded-xl">
<img
src="https://images.unsplash.com/photo-1581091226825-a6a2a5aee158"
alt="Person at computer"
className="w-full h-full object-cover"
style={{ opacity: 0.9 }}
/>
</div>
{/* Middle right - Office space */}
<div className="overflow-hidden rounded-xl">
<img
src="https://images.unsplash.com/photo-1605810230434-7631ac76ec81"
alt="Office space"
className="w-full h-full object-cover"
style={{ opacity: 0.9 }}
/>
</div>
{/* Bottom left - Green stat */}
<div
className={`rounded-xl flex flex-col justify-center items-center p-6 text-white ${
theme === 'dark' ? 'bg-green-600' : 'bg-green-500'
}`}
style={{
transform: formVisible ? 'translateY(0)' : 'translateY(20px)',
opacity: formVisible ? 1 : 0,
transition: 'transform 0.6s ease-out, opacity 0.6s ease-out',
transitionDelay: '0.4s',
}}
>
<h2 className="text-5xl font-bold mb-2">76%</h2>
<p className="text-center text-sm">of hiring managers admit attracting the right job candidates is their greatest challenge.</p>
</div>
{/* Bottom right - Library */}
<div className="overflow-hidden rounded-xl">
<img
src="https://images.unsplash.com/photo-1498050108023-c5249f4df085"
alt="Desk setup"
className="w-full h-full object-cover"
style={{ opacity: 0.9 }}
/>
</div>
</div>
</div>
{/* Right side - Sign in form */}
<div
className={`w-full md:w-2/5 p-8 md:p-12 ${
theme === 'dark' ? 'bg-slate-800 text-white' : 'bg-white text-gray-900'
}`}
style={{
transform: formVisible ? 'translateX(0)' : 'translateX(20px)',
opacity: formVisible ? 1 : 0,
transition: 'transform 0.6s ease-out, opacity 0.6s ease-out'
}}
>
<div className="flex justify-end mb-6">
<p className={`text-sm ${theme === 'dark' ? 'text-gray-300' : 'text-gray-600'}`}>
Don't have an account?
<a
href="#"
className={`ml-1 font-medium ${
theme === 'dark' ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-500'
}`}
>
Sign up
</a>
</p>
</div>
<div className="mb-8">
<h1 className={`text-2xl font-bold mb-1 ${theme === 'dark' ? 'text-white' : 'text-gray-900'}`}>
Sign in to <span className="text-blue-500">Jobsly</span>
</h1>
<p className={`text-sm ${theme === 'dark' ? 'text-gray-300' : 'text-gray-600'}`}>
Welcome to Jobsly, please enter your login details below to using the app.
</p>
</div>
<form onSubmit={handleSignIn} className="space-y-6">
<div className="space-y-1">
<label
htmlFor="email"
className={`block text-sm font-medium ${
theme === 'dark' ? 'text-gray-200' : 'text-gray-700'
}`}
>
Email Address
</label>
<div className={`relative rounded-md shadow-sm transition-all duration-300`}>
<input
type="email"
name="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`block w-full rounded-md border py-3 px-4 focus:outline-none focus:ring-2 sm:text-sm ${
theme === 'dark'
? 'bg-slate-700 border-slate-600 text-white placeholder:text-gray-400 focus:ring-blue-500'
: 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-400 focus:ring-blue-500'
}`}
placeholder="your.email@example.com"
required
/>
</div>
</div>
<div className="space-y-1">
<label
htmlFor="password"
className={`block text-sm font-medium ${
theme === 'dark' ? 'text-gray-200' : 'text-gray-700'
}`}
>
Password
</label>
<div className={`relative rounded-md shadow-sm transition-all duration-300`}>
<input
type={showPassword ? 'text' : 'password'}
name="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`block w-full rounded-md border py-3 px-4 pr-10 focus:outline-none focus:ring-2 sm:text-sm ${
theme === 'dark'
? 'bg-slate-700 border-slate-600 text-white placeholder:text-gray-400 focus:ring-blue-500'
: 'bg-white border-gray-300 text-gray-900 placeholder:text-gray-400 focus:ring-blue-500'
}`}
placeholder="••••••••"
required
/>
<button
type="button"
className={`absolute inset-y-0 right-0 flex items-center pr-3 ${
theme === 'dark' ? 'text-gray-300' : 'text-gray-500'
}`}
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff size={18} className="hover:text-gray-700 transition-colors" />
) : (
<Eye size={18} className="hover:text-gray-700 transition-colors" />
)}
</button>
</div>
</div>
<div className="flex justify-end">
<a
href="#"
className={`text-sm font-medium ${
theme === 'dark' ? 'text-blue-400 hover:text-blue-300' : 'text-blue-500 hover:text-blue-600'
}`}
>
Forgot the password?
</a>
</div>
<button
type="submit"
disabled={isLoading}
className={`flex w-full justify-center rounded-md py-3 px-4 text-sm font-semibold text-white shadow-sm transition-all duration-300 ${
theme === 'dark'
? 'bg-blue-600 hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500'
: 'bg-blue-600 hover:bg-blue-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600'
} ${isLoading ? 'cursor-not-allowed opacity-70' : ''}`}
>
{isLoading ? (
<span className="flex items-center">
<svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Signing in...
</span>
) : (
<span className="flex items-center justify-center">
Login
</span>
)}
</button>
<div className="relative flex items-center py-2">
<div className={`flex-grow border-t ${theme === 'dark' ? 'border-gray-700' : 'border-gray-300'}`}></div>
<span className={`flex-shrink mx-4 text-sm ${theme === 'dark' ? 'text-gray-400' : 'text-gray-500'}`}>OR</span>
<div className={`flex-grow border-t ${theme === 'dark' ? 'border-gray-700' : 'border-gray-300'}`}></div>
</div>
<button
type="button"
className={`flex w-full items-center justify-center gap-2 rounded-md py-3 px-4 text-sm font-medium transition-colors ${
theme === 'dark'
? 'border border-gray-700 bg-gray-800 text-white hover:bg-gray-700'
: 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
}`}
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
<path d="M1 1h22v22H1z" fill="none" />
</svg>
Sign in with Google
</button>
</form>
</div>
</div>
</div>
</div>
</div>
);
};
export { AnimatedSignIn };

View File

@@ -1,7 +1,7 @@
import type { Config } from 'tailwindcss' import type { Config } from 'tailwindcss'
const config: Config = { const config: Config = {
darkMode: 'class', darkMode: ['class', 'class'],
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}', './pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}',