feat(hackathon): add dark mode support with theme toggle

- Add ThemeProvider with system/light/dark mode support and localStorage persistence
- Add ThemeToggle component for pages without sidebar (landing, login, signup)
- Enable class-based dark mode in Tailwind CSS v4 via @custom-variant
- Add dark mode classes to landing page, login, signup, and all dashboard pages
- Keep IMPHNEN logo blue in dark mode, invert Kolosal logo

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Maulana Sodiqin
2025-11-26 15:25:23 +07:00
co-authored by Claude
parent c9f5b9e4a5
commit 1839f4cab0
18 changed files with 594 additions and 342 deletions
+30 -26
View File
@@ -9,6 +9,7 @@ import { GithubOutlined } from '@ant-design/icons';
import { useNavigate, Link } from 'react-router'; import { useNavigate, Link } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
export default function LoginPage() { export default function LoginPage() {
// console.log('[LoginPage] Rendering...'); // console.log('[LoginPage] Rendering...');
@@ -114,28 +115,31 @@ export default function LoginPage() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200"> <div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700">
<button <div className="flex items-center justify-between mb-6">
onClick={() => navigate('/')} <button
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center mb-6" onClick={() => navigate('/')}
> className="cursor-pointer text-primary-500 hover:text-primary-600 dark:text-primary-400 dark:hover:text-primary-300 text-base font-sans flex items-center"
<Icon icon="ic:baseline-chevron-left" width="24" height="24" /> >
Back to Homepage <Icon icon="ic:baseline-chevron-left" width="24" height="24" />
</button> Back to Homepage
</button>
<ThemeToggle />
</div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2"> <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Welcome Back Welcome Back
</h2> </h2>
<p className="text-gray-600 font-sans"> <p className="text-gray-600 dark:text-neutral-400 font-sans">
Sign in to join or create your hackathon team Sign in to join or create your hackathon team
</p> </p>
</div> </div>
{error && ( {error && (
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="mb-6 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-600 text-sm">{error}</p> <p className="text-red-600 dark:text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
@@ -143,7 +147,7 @@ export default function LoginPage() {
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Email Email
</label> </label>
@@ -154,7 +158,7 @@ export default function LoginPage() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com" placeholder="your@email.com"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -163,13 +167,13 @@ export default function LoginPage() {
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<label <label
htmlFor="password" htmlFor="password"
className="block text-sm font-medium text-gray-700" className="block text-sm font-medium text-gray-700 dark:text-neutral-300"
> >
Password Password
</label> </label>
<Link <Link
to="/auth/forgot-password" to="/auth/forgot-password"
className="text-sm text-primary-600 hover:text-primary-700" className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
> >
Forgot password? Forgot password?
</Link> </Link>
@@ -181,7 +185,7 @@ export default function LoginPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -189,23 +193,23 @@ export default function LoginPage() {
<button <button
type="submit" type="submit"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-400 dark:disabled:bg-neutral-600 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
{isEmailLoading ? 'Signing in...' : 'Sign in with Email'} {isEmailLoading ? 'Signing in...' : 'Sign in with Email'}
</button> </button>
</form> </form>
<div className="my-6 flex items-center"> <div className="my-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
<span className="px-4 text-sm text-gray-500">OR</span> <span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
</div> </div>
<button <button
onClick={handleGithubLogin} onClick={handleGithubLogin}
disabled={isGithubLoading} disabled={isGithubLoading}
type="button" type="button"
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 border border-gray-300 rounded-lg font-semibold hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg font-semibold text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-100 dark:disabled:bg-neutral-800 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
<GithubOutlined className="text-xl" /> <GithubOutlined className="text-xl" />
<span> <span>
@@ -214,11 +218,11 @@ export default function LoginPage() {
</button> </button>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600 text-sm"> <p className="text-gray-600 dark:text-neutral-400 text-sm">
Don't have an account?{' '} Don't have an account?{' '}
<a <a
href="/auth/signup" href="/auth/signup"
className="text-primary-600 hover:text-primary-700 font-semibold" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-semibold"
> >
Sign up Sign up
</a> </a>
@@ -226,7 +230,7 @@ export default function LoginPage() {
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-500 text-xs"> <p className="text-gray-500 dark:text-neutral-500 text-xs">
By signing in, you agree to our Terms of Service and Privacy Policy By signing in, you agree to our Terms of Service and Privacy Policy
</p> </p>
</div> </div>
+33 -29
View File
@@ -9,6 +9,7 @@ import { GithubOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
export default function SignupPage() { export default function SignupPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -137,26 +138,29 @@ export default function SignupPage() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200"> <div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700">
<button <div className="flex items-center justify-between mb-6">
onClick={() => navigate('/')} <button
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center mb-6" onClick={() => navigate('/')}
> className="cursor-pointer text-primary-500 hover:text-primary-600 dark:text-primary-400 dark:hover:text-primary-300 text-base font-sans flex items-center"
<Icon icon="ic:baseline-chevron-left" width="24" height="24" /> >
Back to Homepage <Icon icon="ic:baseline-chevron-left" width="24" height="24" />
</button> Back to Homepage
</button>
<ThemeToggle />
</div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2"> <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Create Account Create Account
</h2> </h2>
<p className="text-gray-600">Join the hackathon community</p> <p className="text-gray-600 dark:text-neutral-400">Join the hackathon community</p>
</div> </div>
{error && ( {error && (
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="mb-6 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-600 text-sm">{error}</p> <p className="text-red-600 dark:text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
@@ -164,7 +168,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="fullname" htmlFor="fullname"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Full Name Full Name
</label> </label>
@@ -175,7 +179,7 @@ export default function SignupPage() {
onChange={(e) => setFullname(e.target.value)} onChange={(e) => setFullname(e.target.value)}
placeholder="John Doe" placeholder="John Doe"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -183,7 +187,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Email Email
</label> </label>
@@ -194,7 +198,7 @@ export default function SignupPage() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com" placeholder="your@email.com"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -202,7 +206,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="password" htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Password Password
</label> </label>
@@ -213,7 +217,7 @@ export default function SignupPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -221,7 +225,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="confirmPassword" htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Confirm Password Confirm Password
</label> </label>
@@ -232,7 +236,7 @@ export default function SignupPage() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -240,23 +244,23 @@ export default function SignupPage() {
<button <button
type="submit" type="submit"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-400 dark:disabled:bg-neutral-600 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
{isEmailLoading ? 'Creating account...' : 'Create Account'} {isEmailLoading ? 'Creating account...' : 'Create Account'}
</button> </button>
</form> </form>
<div className="my-6 flex items-center"> <div className="my-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
<span className="px-4 text-sm text-gray-500">OR</span> <span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
</div> </div>
<button <button
onClick={handleGithubLogin} onClick={handleGithubLogin}
disabled={isGithubLoading} disabled={isGithubLoading}
type="button" type="button"
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 border border-gray-300 rounded-lg font-semibold hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg font-semibold text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-100 dark:disabled:bg-neutral-800 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
<GithubOutlined className="text-xl" /> <GithubOutlined className="text-xl" />
<span> <span>
@@ -265,11 +269,11 @@ export default function SignupPage() {
</button> </button>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600 text-sm"> <p className="text-gray-600 dark:text-neutral-400 text-sm">
Already have an account?{' '} Already have an account?{' '}
<a <a
href="/auth/login" href="/auth/login"
className="text-primary-600 hover:text-primary-700 font-semibold" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-semibold"
> >
Sign in Sign in
</a> </a>
@@ -277,7 +281,7 @@ export default function SignupPage() {
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-500 text-xs"> <p className="text-gray-500 dark:text-neutral-500 text-xs">
By signing up, you agree to our Terms of Service and Privacy Policy By signing up, you agree to our Terms of Service and Privacy Policy
</p> </p>
</div> </div>
+5 -5
View File
@@ -6,21 +6,21 @@ const DashboardLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto"> <div className="flex-1 flex flex-col overflow-auto">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
+31 -31
View File
@@ -42,25 +42,25 @@ const DashboardPage: FC = (): ReactElement => {
return ( return (
<div className="p-8"> <div className="p-8">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}! Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
</h1> </h1>
{user?.location && ( {user?.location && (
<p className="text-gray-600 mt-1">Location: {user.location}</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">Location: {user.location}</p>
)} )}
</div> </div>
{invitations.length > 0 && ( {invitations.length > 0 && (
<div className="mb-8 bg-blue-50 border border-blue-200 rounded-lg p-6"> <div className="mb-8 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4"> <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Team Invitations ({invitations.length}) Team Invitations ({invitations.length})
</h2> </h2>
<div className="space-y-3"> <div className="space-y-3">
{invitations.map((invitation) => ( {invitations.map((invitation) => (
<div key={invitation.id} className="bg-white p-4 rounded-lg shadow-sm flex items-center justify-between"> <div key={invitation.id} className="bg-white dark:bg-neutral-800 p-4 rounded-lg shadow-sm flex items-center justify-between">
<div> <div>
<p className="font-medium text-gray-900">{invitation.team.name}</p> <p className="font-medium text-gray-900 dark:text-white">{invitation.team.name}</p>
<p className="text-sm text-gray-600">Invited by {invitation.inviter.fullname}</p> <p className="text-sm text-gray-600 dark:text-neutral-400">Invited by {invitation.inviter.fullname}</p>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button size="sm" onClick={() => handleAcceptInvitation(invitation.id)}>Accept</Button> <Button size="sm" onClick={() => handleAcceptInvitation(invitation.id)}>Accept</Button>
@@ -74,53 +74,53 @@ const DashboardPage: FC = (): ReactElement => {
{myTeams.length > 0 ? ( {myTeams.length > 0 ? (
<div className="mb-6 md:mb-8"> <div className="mb-6 md:mb-8">
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mb-3 md:mb-4">My Team</h2> <h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">My Team</h2>
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
{myTeams.map((team) => ( {myTeams.map((team) => (
<Link key={team.id} to={'/teams/' + team.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"> <Link key={team.id} to={'/teams/' + team.id} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
{team.banner && <img src={team.banner} alt={team.name} className="w-full h-32 object-cover" />} {team.banner && <img src={team.banner} alt={team.name} className="w-full h-32 object-cover" />}
<div className="p-4 md:p-6"> <div className="p-4 md:p-6">
<div className="flex items-center space-x-3 mb-2 md:mb-3"> <div className="flex items-center space-x-3 mb-2 md:mb-3">
{team.logo && <img src={team.logo} alt={team.name} className="w-10 h-10 md:w-12 md:h-12 rounded-full object-cover" />} {team.logo && <img src={team.logo} alt={team.name} className="w-10 h-10 md:w-12 md:h-12 rounded-full object-cover" />}
<div> <div>
<h3 className="text-base md:text-lg font-bold text-gray-900">{team.name}</h3> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white">{team.name}</h3>
<p className="text-xs md:text-sm text-gray-600">City: {team.city}</p> <p className="text-xs md:text-sm text-gray-600 dark:text-neutral-400">City: {team.city}</p>
</div> </div>
</div> </div>
<p className="text-gray-600 text-xs md:text-sm line-clamp-2">{team.description}</p> <p className="text-gray-600 dark:text-neutral-400 text-xs md:text-sm line-clamp-2">{team.description}</p>
</div> </div>
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
) : ( ) : (
<div className="mb-6 md:mb-8 bg-white rounded-lg shadow-md p-6 md:p-8"> <div className="mb-6 md:mb-8 bg-white dark:bg-neutral-800 rounded-lg shadow-md p-6 md:p-8">
<div className="text-center"> <div className="text-center">
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div> <div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mb-1 md:mb-2">You are not in a team yet</h2> <h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-1 md:mb-2">You are not in a team yet</h2>
<p className="text-sm md:text-base text-gray-600">Use the sidebar to browse teams or create your own</p> <p className="text-sm md:text-base text-gray-600 dark:text-neutral-400">Use the sidebar to browse teams or create your own</p>
</div> </div>
</div> </div>
)} )}
<div className="mt-8 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl shadow-lg"> <div className="mt-8 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-neutral-800 dark:to-neutral-900 rounded-xl shadow-lg">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 h-24 rounded-t-xl"></div> <div className="bg-gradient-to-r from-blue-600 to-indigo-600 h-24 rounded-t-xl"></div>
<div className="px-4 md:px-8 pb-4 md:pb-8"> <div className="px-4 md:px-8 pb-4 md:pb-8">
<div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6"> <div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6">
<div className="flex flex-col md:flex-row items-start"> <div className="flex flex-col md:flex-row items-start">
{user?.avatar ? ( {user?.avatar ? (
<img src={user.avatar} alt={user.fullname || 'User'} className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white shadow-lg object-cover" /> <img src={user.avatar} alt={user.fullname || 'User'} className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg object-cover" />
) : ( ) : (
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white shadow-lg bg-gray-200 flex items-center justify-center"> <div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-400 text-3xl md:text-4xl">User</span> <span className="text-gray-400 dark:text-neutral-500 text-3xl md:text-4xl">User</span>
</div> </div>
)} )}
<div className="md:ml-6 mt-4 md:mt-14"> <div className="md:ml-6 mt-4 md:mt-14">
<h2 className="text-xl md:text-2xl font-bold text-gray-900">{user?.fullname || user?.email?.split('@')[0] || 'Unnamed User'}</h2> <h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">{user?.fullname || user?.email?.split('@')[0] || 'Unnamed User'}</h2>
{user?.location ? ( {user?.location ? (
<p className="text-gray-600 flex items-center mt-1 text-sm md:text-base">{user.location}</p> <p className="text-gray-600 dark:text-neutral-400 flex items-center mt-1 text-sm md:text-base">{user.location}</p>
) : ( ) : (
<Link to="/onboarding/user" className="text-blue-600 hover:text-blue-700 text-xs md:text-sm mt-1 inline-block">Complete your profile</Link> <Link to="/onboarding/user" className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-xs md:text-sm mt-1 inline-block">Complete your profile</Link>
)} )}
</div> </div>
</div> </div>
@@ -130,20 +130,20 @@ const DashboardPage: FC = (): ReactElement => {
</div> </div>
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{user?.bio && ( {user?.bio && (
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2">About</h3> <h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-2">About</h3>
<p className="text-gray-800 leading-relaxed">{user.bio}</p> <p className="text-gray-800 dark:text-neutral-200 leading-relaxed">{user.bio}</p>
</div> </div>
)} )}
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Contact</h3> <h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-3">Contact</h3>
<div className="flex items-center text-gray-700"> <div className="flex items-center text-gray-700 dark:text-neutral-300">
<span className="text-gray-900">{user?.email}</span> <span className="text-gray-900 dark:text-white">{user?.email}</span>
</div> </div>
</div> </div>
{user?.skills && user.skills.length > 0 && ( {user?.skills && user.skills.length > 0 && (
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Skills</h3> <h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-3">Skills</h3>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{user.skills.map((skill: string) => ( {user.skills.map((skill: string) => (
<span key={skill} className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium shadow-sm hover:bg-blue-700 transition-colors">{skill}</span> <span key={skill} className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium shadow-sm hover:bg-blue-700 transition-colors">{skill}</span>
+2 -2
View File
@@ -107,10 +107,10 @@ export default function RootLayout() {
// Show loading state while checking auth // Show loading state while checking auth
if (isChecking) { if (isChecking) {
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-center"> <div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div> <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
<p className="text-gray-600">Loading...</p> <p className="text-gray-600 dark:text-neutral-400">Loading...</p>
</div> </div>
</div> </div>
); );
+110 -108
View File
@@ -2,6 +2,7 @@ import { useNavigate } from 'react-router';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ThemeToggle } from '../components/theme-toggle';
export default function HomePage() { export default function HomePage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -62,13 +63,13 @@ export default function HomePage() {
]; ];
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white dark:bg-neutral-950">
{/* Navigation */} {/* Navigation */}
<div id="#top" className="hidden"></div> <div id="#top" className="hidden"></div>
<nav className="border-b border-gray-200 bg-white sticky top-0 z-50"> <nav className="border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 sticky top-0 z-50">
<div className="flex items-center justify-between max-w-7xl mx-auto px-4 md:px-8 py-4"> <div className="flex items-center justify-between max-w-7xl mx-auto px-4 md:px-8 py-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg md:text-xl font-bold">IMPHNEN</span> <span className="text-lg md:text-xl font-bold dark:text-white">IMPHNEN</span>
<a <a
href="#top" href="#top"
className="text-lg md:text-xl font-bold text-primary-500 hover:cursor-pointer" className="text-lg md:text-xl font-bold text-primary-500 hover:cursor-pointer"
@@ -80,22 +81,23 @@ export default function HomePage() {
<div className="hidden md:flex text-label1 items-center gap-4 lg:gap-8"> <div className="hidden md:flex text-label1 items-center gap-4 lg:gap-8">
<a <a
href="#timeline" href="#timeline"
className="text-gray-600 hover:text-gray-900 transition-colors" className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
Timeline Timeline
</a> </a>
<a <a
href="#hadiah" href="#hadiah"
className="text-gray-600 hover:text-gray-900 transition-colors" className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
Hadiah Hadiah
</a> </a>
<a <a
href="#faq" href="#faq"
className="text-gray-600 hover:text-gray-900 transition-colors" className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
FAQ FAQ
</a> </a>
<ThemeToggle />
<Button <Button
onClick={() => navigate('/auth/login')} onClick={() => navigate('/auth/login')}
size="sm" size="sm"
@@ -113,47 +115,50 @@ export default function HomePage() {
</Button> </Button>
</div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <div className="flex items-center gap-2 md:hidden">
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} <ThemeToggle />
className="md:hidden p-2 cursor-pointer" <button
> onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
<svg className="p-2 cursor-pointer text-gray-900 dark:text-white"
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-6 h-6"
strokeLinejoin="round" fill="none"
strokeWidth={2} stroke="currentColor"
d="M4 6h16M4 12h16M4 18h16" viewBox="0 0 24 24"
/> >
</svg> <path
</button> strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div> </div>
</nav> </nav>
{/* Mobile Menu */} {/* Mobile Menu */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden border-b border-gray-200 bg-white"> <div className="md:hidden border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
<div className="flex flex-col items-start gap-4 px-4 py-4"> <div className="flex flex-col items-start gap-4 px-4 py-4">
<a <a
href="#timeline" href="#timeline"
className="ms-3 text-gray-600 hover:text-gray-900" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
> >
Timeline Timeline
</a> </a>
<a <a
href="#hadiah" href="#hadiah"
className="ms-3 text-gray-600 hover:text-gray-900" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
> >
Hadiah Hadiah
</a> </a>
<a href="#faq" className="ms-3 text-gray-600 hover:text-gray-900"> <a href="#faq" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white">
FAQ FAQ
</a> </a>
<a href="#masuk" className="ms-3 text-gray-600 hover:text-gray-900"> <a href="#masuk" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white">
Masuk Masuk
</a> </a>
<button <button
@@ -177,7 +182,7 @@ export default function HomePage() {
className="absolute bottom-1/3 -right-20 w-100 h-100 rounded-full bg-linear-to-r from-blue-400/20 to-primary/20 blur-3xl" className="absolute bottom-1/3 -right-20 w-100 h-100 rounded-full bg-linear-to-r from-blue-400/20 to-primary/20 blur-3xl"
style={{ transform: 'translate(0px, 0px)', opacity: 1 }} style={{ transform: 'translate(0px, 0px)', opacity: 1 }}
></div> ></div>
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-size-[40px_40px]"></div> <div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] dark:bg-[linear-gradient(rgba(59,130,246,0.1)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.1)_1px,transparent_1px)] bg-size-[40px_40px]"></div>
</div> </div>
<div className="mx-auto container px-4 relative flex flex-col items-center"> <div className="mx-auto container px-4 relative flex flex-col items-center">
{/* Logos */} {/* Logos */}
@@ -189,19 +194,19 @@ export default function HomePage() {
className="h-12 md:h-16" className="h-12 md:h-16"
/> />
</div> </div>
<span className="text-3xl md:text-5xl font-bold text-gray-400"> <span className="text-3xl md:text-5xl font-bold text-gray-400 dark:text-neutral-500">
× ×
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<img <img
src="images/sponsors/kolosal-logo_rlxbck.svg" src="images/sponsors/kolosal-logo_rlxbck.svg"
alt="Kolosal.ai" alt="Kolosal.ai"
className="h-8 md:h-12" className="h-8 md:h-12 dark:invert"
/> />
</div> </div>
</div> </div>
{/* Title */} {/* Title */}
<h1 className="text-h1 font-bold text-gray-900 mb-4 text-center"> <h1 className="text-h1 font-bold text-gray-900 dark:text-white mb-4 text-center">
Hackathon Hackathon
</h1> </h1>
{/* Subtitle */} {/* Subtitle */}
@@ -209,12 +214,12 @@ export default function HomePage() {
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif" "Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
</p> </p>
{/* Description */} {/* Description */}
<p className="text-p3 text-gray-600 max-w-lg md:max-w-xl text-center mb-8 md:mb-12 px-4 font-sans"> <p className="text-p3 text-gray-600 dark:text-neutral-400 max-w-lg md:max-w-xl text-center mb-8 md:mb-12 px-4 font-sans">
Kompetisi pengembangan teknologi untuk menciptakan solusi inovatif Kompetisi pengembangan teknologi untuk menciptakan solusi inovatif
yang menghadirkan dampak nyata yang menghadirkan dampak nyata
</p> </p>
{/* Status */} {/* Status */}
<div className="flex items-center gap-4 md:gap-8 mb-10 md:mb-16 text-base text-gray-600"> <div className="flex items-center gap-4 md:gap-8 mb-10 md:mb-16 text-base text-gray-600 dark:text-neutral-400">
<div className="flex items-center gap-2 md:gap-3"> <div className="flex items-center gap-2 md:gap-3">
<Icon icon="streamline-plump:web" width="16" height="16" /> <Icon icon="streamline-plump:web" width="16" height="16" />
<span>Online</span> <span>Online</span>
@@ -236,7 +241,7 @@ export default function HomePage() {
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL" href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center justify-center px-4 py-2.5 border-2 border-gray-300 hover:border-gray-400 transition-colors text-center bg-transparent hover:text-gray-900 hover:bg-gray-50 text-base text-gray-600 rounded-lg font-bai-jamjuree font-semibold" className="inline-flex items-center justify-center px-4 py-2.5 border-2 border-gray-300 dark:border-neutral-600 hover:border-gray-400 dark:hover:border-neutral-500 transition-colors text-center bg-transparent hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 text-base text-gray-600 dark:text-neutral-400 rounded-lg font-bai-jamjuree font-semibold"
> >
Gabung Grup WA Hackathon Gabung Grup WA Hackathon
</a> </a>
@@ -244,7 +249,7 @@ export default function HomePage() {
{/* Scroll indicator */} {/* Scroll indicator */}
<div className="mt-12 md:mt-20"> <div className="mt-12 md:mt-20">
<svg <svg
className="w-6 h-6 text-gray-400 animate-bounce" className="w-6 h-6 text-gray-400 dark:text-neutral-500 animate-bounce"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -261,17 +266,17 @@ export default function HomePage() {
</section> </section>
{/* About Section */} {/* About Section */}
<section className="py-16 md:py-24 px-4 md:px-8 bg-white"> <section className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950">
<div className="max-w-4xl mx-auto text-center"> <div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-5xl font-bold mb-10"> <h2 className="text-3xl md:text-5xl font-bold mb-10 dark:text-white">
Tentang <span className="text-primary-500">Hackathon</span> Tentang <span className="text-primary-500">Hackathon</span>
</h2> </h2>
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 mb-6"> <p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400 mb-6">
<span className="font-semibold text-gray-900">Hackathon</span>{' '} <span className="font-semibold text-gray-900 dark:text-white">Hackathon</span>{' '}
adalah kompetisi pengembangan teknologi yang mengajak talenta muda adalah kompetisi pengembangan teknologi yang mengajak talenta muda
untuk berkolaborasi dalam menciptakan solusi inovatif. untuk berkolaborasi dalam menciptakan solusi inovatif.
</p> </p>
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600"> <p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400">
IMPHNEN bersama Kolosal.ai mengadakan Hackathon dengan tema lomba{' '} IMPHNEN bersama Kolosal.ai mengadakan Hackathon dengan tema lomba{' '}
<span className="font-semibold text-primary-500"> <span className="font-semibold text-primary-500">
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif" "Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
@@ -282,10 +287,10 @@ export default function HomePage() {
</section> </section>
{/* Prizes Section */} {/* Prizes Section */}
<section id="hadiah" className="py-16 md:py-24 px-4 md:px-8 bg-gray-50"> <section id="hadiah" className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900">
<div className="max-w-lg md:max-w-6xl mx-auto"> <div className="max-w-lg md:max-w-6xl mx-auto">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl md:text-5xl font-bold mb-4 font-bai-jamjuree"> <h2 className="text-3xl md:text-5xl font-bold mb-4 font-bai-jamjuree dark:text-white">
Hadiah <span className="text-primary-500">Menarik</span> Hadiah <span className="text-primary-500">Menarik</span>
</h2> </h2>
<p className="text-xl md:text-2xl text-primary-500 font-semibold font-sans"> <p className="text-xl md:text-2xl text-primary-500 font-semibold font-sans">
@@ -295,67 +300,67 @@ export default function HomePage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Prize 1 */} {/* Prize 1 */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-primary-500 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-primary-500 dark:hover:border-primary-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-primary-500" className="h-8 w-8 text-primary-500"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2">Juara 1</h3> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">Juara 1</h3>
<p className="text-2xl font-bold text-primary-500 text-center font-sans"> <p className="text-2xl font-bold text-primary-500 text-center font-sans">
Rp6.000.000 Rp6.000.000
</p> </p>
</div> </div>
{/* Prize 2 */} {/* Prize 2 */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-gray-500 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-gray-500 dark:hover:border-neutral-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-gray-100 dark:bg-neutral-700 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-gray-600" className="h-8 w-8 text-gray-600 dark:text-neutral-400"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2">Juara 2</h3> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">Juara 2</h3>
<p className="text-2xl font-bold text-primary-500 text-center font-sans"> <p className="text-2xl font-bold text-primary-500 text-center font-sans">
Rp4.000.000 Rp4.000.000
</p> </p>
</div> </div>
{/* Prize 3 */} {/* Prize 3 */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-orange-300 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-orange-300 dark:hover:border-orange-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-orange-600" className="h-8 w-8 text-orange-600 dark:text-orange-500"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2">Juara 3</h3> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">Juara 3</h3>
<p className="text-2xl font-bold text-orange-600 text-center font-sans"> <p className="text-2xl font-bold text-orange-600 dark:text-orange-500 text-center font-sans">
Rp2.500.000 Rp2.500.000
</p> </p>
</div> </div>
{/* Special Prize */} {/* Special Prize */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-purple-300 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-purple-300 dark:hover:border-purple-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-purple-600" className="h-8 w-8 text-purple-600 dark:text-purple-500"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2"> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">
Juara Kategori Lainnya Juara Kategori Lainnya
</h3> </h3>
<p className="text-2xl font-bold text-purple-600 text-center font-sans"> <p className="text-2xl font-bold text-purple-600 dark:text-purple-500 text-center font-sans">
Rp2.000.000 Rp2.000.000
</p> </p>
</div> </div>
@@ -364,13 +369,13 @@ export default function HomePage() {
</section> </section>
{/* Timeline Section */} {/* Timeline Section */}
<section id="timeline" className="py-16 md:py-24 px-4 md:px-8 bg-white"> <section id="timeline" className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950">
<div className="max-w-4xl mx-auto font-sans"> <div className="max-w-4xl mx-auto font-sans">
<div className="text-center mb-12 font-bai-jamjuree"> <div className="text-center mb-12 font-bai-jamjuree">
<h2 className="text-3xl md:text-5xl font-bold mb-4"> <h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
Timeline <span className="text-primary-500">Acara</span> Timeline <span className="text-primary-500">Acara</span>
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600 dark:text-neutral-400">
Jadwal lengkap pelaksanaan hackathon Jadwal lengkap pelaksanaan hackathon
</p> </p>
</div> </div>
@@ -380,14 +385,14 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
30 November 2025 30 November 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Penutupan Registrasi</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">Penutupan Registrasi</h3>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-neutral-400">
Batas akhir pendaftaran peserta hackathon. Batas akhir pendaftaran peserta hackathon.
</p> </p>
</div> </div>
@@ -397,14 +402,14 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
30 November 2025 30 November 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Technical Meeting</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">Technical Meeting</h3>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-neutral-400">
Akan diadakan technical meeting terkait lomba melalui Google Akan diadakan technical meeting terkait lomba melalui Google
Meet. Stay tune di grup WA Hackathon. Meet. Stay tune di grup WA Hackathon.
</p> </p>
@@ -415,14 +420,14 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
1 - 7 Desember 2025 1 - 7 Desember 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Tahap Penyisihan</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">Tahap Penyisihan</h3>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-neutral-400">
Peserta mengerjakan tantangan yang diberikan. Peserta mengerjakan tantangan yang diberikan.
</p> </p>
</div> </div>
@@ -432,14 +437,14 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
8 - 14 Desember 2025 8 - 14 Desember 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Penilaian & Webinar</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">Penilaian & Webinar</h3>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-neutral-400">
Proses penilaian oleh juri dan sesi webinar. Proses penilaian oleh juri dan sesi webinar.
</p> </p>
</div> </div>
@@ -454,8 +459,8 @@ export default function HomePage() {
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
15 Desember 2025 15 Desember 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Pengumuman & Final</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">Pengumuman & Final</h3>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-neutral-400">
Presentasi final dan pengumuman pemenang. Presentasi final dan pengumuman pemenang.
</p> </p>
</div> </div>
@@ -465,47 +470,44 @@ export default function HomePage() {
</section> </section>
{/* Judges Section */} {/* Judges Section */}
<section className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 font-sans"> <section className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900 font-sans">
<div className="md:max-w-6xl mx-auto"> <div className="md:max-w-6xl mx-auto">
<div className="text-center mb-12 font-bai-jamjuree"> <div className="text-center mb-12 font-bai-jamjuree">
<h2 className="text-3xl md:text-5xl font-bold mb-4"> <h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
Dewan <span className="text-primary-500">Juri</span> Dewan <span className="text-primary-500">Juri</span>
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600 dark:text-neutral-400">
Perwakilan dari IMPHNEN dan Kolosal.ai yang akan menilai karya Perwakilan dari IMPHNEN dan Kolosal.ai yang akan menilai karya
</p> </p>
</div> </div>
<div className="w-full max-w-md md:max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="w-full max-w-md md:max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Judge 1 */} {/* Judge 1 */}
<div className="flex flex-col justify-between bg-white rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
{/* <div className="w-24 h-24 bg-gray-200 rounded-full mx-auto mb-4"></div> */} <h3 className="text-p3 font-bold mb-1 dark:text-white">
<h3 className="text-p3 font-bold mb-1">
Alifais Farrel Ramdhani Alifais Farrel Ramdhani
</h3> </h3>
<div> <div>
<p className="text-primary-500 font-semibold mb-1">CTO</p> <p className="text-primary-500 font-semibold mb-1">CTO</p>
<p className="text-gray-600">Kolosal.ai</p> <p className="text-gray-600 dark:text-neutral-400">Kolosal.ai</p>
</div> </div>
</div> </div>
{/* Judge 2 */} {/* Judge 2 */}
<div className="flex flex-col justify-between bg-white rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
{/* <div className="w-24 h-24 bg-gray-200 rounded-full mx-auto mb-4"></div> */} <h3 className="text-p3 font-bold mb-1 dark:text-white">Anka Tama</h3>
<h3 className="text-p3 font-bold mb-1">Anka Tama</h3>
<div> <div>
<p className="text-primary-500 font-semibold mb-1">Admin</p> <p className="text-primary-500 font-semibold mb-1">Admin</p>
<p className="text-gray-600">IMPHNEN</p> <p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
</div> </div>
</div> </div>
{/* Judge 3 */} {/* Judge 3 */}
<div className="flex flex-col justify-between bg-white rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
{/* <div className="w-24 h-24 bg-gray-200 rounded-full mx-auto mb-4"></div> */} <h3 className="text-p3 font-bold mb-1 dark:text-white">Hafid Nur</h3>
<h3 className="text-p3 font-bold mb-1">Hafid Nur</h3>
<div> <div>
<p className="text-primary-500 font-semibold mb-1">Moderator</p> <p className="text-primary-500 font-semibold mb-1">Moderator</p>
<p className="text-gray-600">IMPHNEN</p> <p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
</div> </div>
</div> </div>
</div> </div>
@@ -513,25 +515,25 @@ export default function HomePage() {
</section> </section>
{/* FAQ Section */} {/* FAQ Section */}
<section id="faq" className="py-16 md:py-24 px-4 md:px-8 bg-white"> <section id="faq" className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500"> <h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500">
FAQ FAQ
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600 dark:text-neutral-400">
Pertanyaan yang Sering Diajukan Pertanyaan yang Sering Diajukan
</p> </p>
</div> </div>
<div className="space-y-4 font-sans"> <div className="space-y-4 font-sans">
{faqs.map((faq, index) => ( {faqs.map((faq, index) => (
<div key={index} className="border border-gray-200 rounded-lg"> <div key={index} className="border border-gray-200 dark:border-neutral-700 rounded-lg">
<button <button
onClick={() => setOpenFaq(openFaq === index ? null : index)} onClick={() => setOpenFaq(openFaq === index ? null : index)}
className="w-full px-6 py-4 flex items-center justify-between text-left hover:bg-gray-50 transition-colors cursor-pointer" className="w-full px-6 py-4 flex items-center justify-between text-left hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors cursor-pointer"
> >
<span className="font-semibold text-gray-900"> <span className="font-semibold text-gray-900 dark:text-white">
{faq.question} {faq.question}
</span> </span>
<svg <svg
@@ -551,7 +553,7 @@ export default function HomePage() {
</svg> </svg>
</button> </button>
{openFaq === index && ( {openFaq === index && (
<div className="px-6 pb-4 text-gray-600">{faq.answer}</div> <div className="px-6 pb-4 text-gray-600 dark:text-neutral-400">{faq.answer}</div>
)} )}
</div> </div>
))} ))}
@@ -560,12 +562,12 @@ export default function HomePage() {
</section> </section>
{/* Sponsors Section */} {/* Sponsors Section */}
<section className="py-20 md:py-28 px-4 md:px-8 bg-gray-50"> <section className="py-20 md:py-28 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900">
<div className="max-w-6xl mx-auto text-center"> <div className="max-w-6xl mx-auto text-center">
<h2 className="text-3xl md:text-5xl font-bold mb-4"> <h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
Sponsor & <span className="text-primary-500">Partner</span> Sponsor & <span className="text-primary-500">Partner</span>
</h2> </h2>
<p className="text-lg text-gray-600 mb-8"> <p className="text-lg text-gray-600 dark:text-neutral-400 mb-8">
Acara ini sepenuhnya disponsori oleh Acara ini sepenuhnya disponsori oleh
</p> </p>
@@ -574,12 +576,12 @@ export default function HomePage() {
href="https://kolosal.ai" href="https://kolosal.ai"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="bg-white rounded-xl p-8 shadow-lg inline-block border-2 border-transparent hover:border-gray-500 transition-colors" className="bg-white dark:bg-neutral-800 rounded-xl p-8 shadow-lg inline-block border-2 border-transparent hover:border-gray-500 dark:hover:border-neutral-500 transition-colors"
> >
<img <img
src="images/sponsors/kolosal-logo_rlxbck.svg" src="images/sponsors/kolosal-logo_rlxbck.svg"
alt="Kolosal.ai" alt="Kolosal.ai"
className="h-12 md:h-16" className="h-12 md:h-16 dark:invert"
/> />
</a> </a>
</div> </div>
@@ -589,13 +591,13 @@ export default function HomePage() {
{/* CTA Section */} {/* CTA Section */}
<section <section
id="masuk" id="masuk"
className="py-20 md:py-28 px-4 md:px-8 bg-linear-to-b from-white to-blue-50" className="py-20 md:py-28 px-4 md:px-8 bg-linear-to-b from-white to-blue-50 dark:from-neutral-950 dark:to-neutral-900"
> >
<div className="max-w-4xl mx-auto text-center font-sans"> <div className="max-w-4xl mx-auto text-center font-sans">
<h2 className="text-3xl md:text-5xl font-bold mb-6 font-bai-jamjuree"> <h2 className="text-3xl md:text-5xl font-bold mb-6 font-bai-jamjuree dark:text-white">
Segera Daftarkan <span className="text-primary-500">Timmu!</span> Segera Daftarkan <span className="text-primary-500">Timmu!</span>
</h2> </h2>
<p className="text-lg md:text-xl text-left md:text-center text-gray-600 mb-8"> <p className="text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400 mb-8">
Jangan lewatkan kesempatan emas untuk bersaing dengan developer Jangan lewatkan kesempatan emas untuk bersaing dengan developer
terbaik, terbaik,
<br className="hidden md:block" /> belajar dari para ahli, dan <br className="hidden md:block" /> belajar dari para ahli, dan
@@ -613,13 +615,13 @@ export default function HomePage() {
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL" href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="px-8 py-3 bg-white text-gray-900 text-lg rounded-lg border-2 border-gray-300 hover:border-gray-400 transition-colors font-semibold" className="px-8 py-3 bg-white dark:bg-neutral-800 text-gray-900 dark:text-white text-lg rounded-lg border-2 border-gray-300 dark:border-neutral-600 hover:border-gray-400 dark:hover:border-neutral-500 transition-colors font-semibold"
> >
Gabung Grup WA Hackathon Gabung Grup WA Hackathon
</a> </a>
</div> </div>
<p className="text-sm text-gray-500 mt-6"> <p className="text-sm text-gray-500 dark:text-neutral-500 mt-6">
Pendaftaran ditutup pada{' '} Pendaftaran ditutup pada{' '}
<span className="text-primary-500 font-semibold"> <span className="text-primary-500 font-semibold">
30 November 2025 30 November 2025
+15 -15
View File
@@ -128,14 +128,14 @@ const ProfilePage: FC = (): ReactElement => {
const isLoading = isUpdating || isUploading; const isLoading = isUpdating || isUploading;
return ( return (
<div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 px-4 py-8"> <div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 px-4 py-8">
<div className="bg-white w-full max-w-md p-8 rounded-xl shadow-lg"> <div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-lg">
<div className="mb-6"> <div className="mb-6">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h1 className="text-2xl font-bold text-gray-900">Edit Profile</h1> <h1 className="text-2xl font-bold text-gray-900 dark:text-white">Edit Profile</h1>
<Link <Link
to="/dashboard" to="/dashboard"
className="text-gray-500 hover:text-gray-700" className="text-gray-500 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200"
> >
<svg <svg
className="w-6 h-6" className="w-6 h-6"
@@ -152,7 +152,7 @@ const ProfilePage: FC = (): ReactElement => {
</svg> </svg>
</Link> </Link>
</div> </div>
<p className="text-gray-600">Update your photo and name</p> <p className="text-gray-600 dark:text-neutral-400">Update your photo and name</p>
</div> </div>
<form onSubmit={onSubmit} className="space-y-6"> <form onSubmit={onSubmit} className="space-y-6">
@@ -163,11 +163,11 @@ const ProfilePage: FC = (): ReactElement => {
<img <img
src={avatarPreview} src={avatarPreview}
alt="Avatar preview" alt="Avatar preview"
className="w-32 h-32 rounded-full object-cover border-4 border-gray-200 group-hover:border-blue-400 transition-colors" className="w-32 h-32 rounded-full object-cover border-4 border-gray-200 dark:border-neutral-700 group-hover:border-blue-400 dark:group-hover:border-blue-500 transition-colors"
/> />
) : ( ) : (
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors"> <div className="w-32 h-32 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center group-hover:bg-gray-300 dark:group-hover:bg-neutral-600 transition-colors">
<span className="text-gray-400 text-4xl">👤</span> <span className="text-gray-400 dark:text-neutral-500 text-4xl">👤</span>
</div> </div>
)} )}
{/* Camera overlay */} {/* Camera overlay */}
@@ -204,7 +204,7 @@ const ProfilePage: FC = (): ReactElement => {
/> />
</label> </label>
</div> </div>
<p className="text-xs text-gray-500 text-center"> <p className="text-xs text-gray-500 dark:text-neutral-400 text-center">
Click the camera icon to change your photo Click the camera icon to change your photo
<br /> <br />
Format: JPG, PNG. Max 5MB Format: JPG, PNG. Max 5MB
@@ -221,7 +221,7 @@ const ProfilePage: FC = (): ReactElement => {
{/* City */} {/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
City City
</label> </label>
<Controller <Controller
@@ -240,7 +240,7 @@ const ProfilePage: FC = (): ReactElement => {
{/* Role/Skills */} {/* Role/Skills */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Role / Skills Role / Skills
</label> </label>
<Controller <Controller
@@ -263,9 +263,9 @@ const ProfilePage: FC = (): ReactElement => {
: (field.value || []).filter((v) => v !== role); : (field.value || []).filter((v) => v !== role);
field.onChange(newValue); field.onChange(newValue);
}} }}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" className="rounded border-gray-300 dark:border-neutral-600 text-blue-600 focus:ring-blue-500 dark:bg-neutral-800"
/> />
<span className="text-sm">{role}</span> <span className="text-sm dark:text-neutral-300">{role}</span>
</label> </label>
))} ))}
</div> </div>
@@ -276,8 +276,8 @@ const ProfilePage: FC = (): ReactElement => {
{/* Bio */} {/* Bio */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Bio <span className="text-gray-400">(Optional)</span> Bio <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
</label> </label>
<Controller <Controller
control={form.control} control={form.control}
@@ -6,21 +6,21 @@ const TeamDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto"> <div className="flex-1 flex flex-col overflow-auto">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
+58 -58
View File
@@ -60,7 +60,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (isLoadingTeam || isLoadingMembers) { if (isLoadingTeam || isLoadingMembers) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-gray-600">Loading team...</div> <div className="text-gray-600 dark:text-neutral-400">Loading team...</div>
</div> </div>
); );
} }
@@ -68,16 +68,16 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (!team) { if (!team) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Team not found</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Team not found</h2>
<Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button> <Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header with Banner */} {/* Header with Banner */}
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
{team.banner && ( {team.banner && (
<div className="w-full h-32 md:h-48 overflow-hidden"> <div className="w-full h-32 md:h-48 overflow-hidden">
<img <img
@@ -98,13 +98,13 @@ const TeamDashboardPage: FC = (): ReactElement => {
/> />
)} )}
<div> <div>
<h1 className="text-xl md:text-3xl font-bold text-gray-900 line-clamp-2">{team.name}</h1> <h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h1>
<p className="text-sm md:text-base text-gray-600 mt-1 line-clamp-1">📍 {team.city}</p> <p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1 line-clamp-1">📍 {team.city}</p>
<div className="flex flex-wrap items-center gap-2 md:space-x-4 mt-2"> <div className="flex flex-wrap items-center gap-2 md:space-x-4 mt-2">
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500 dark:text-neutral-400">
{members.length} {members.length === 1 ? 'Member' : 'Members'} {members.length} {members.length === 1 ? 'Member' : 'Members'}
</span> </span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500 dark:text-neutral-400">
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'} {team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
</span> </span>
{isLeader && ( {isLeader && (
@@ -124,15 +124,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Main Content */} {/* Main Content */}
<div className="lg:col-span-2 space-y-4 md:space-y-6"> <div className="lg:col-span-2 space-y-4 md:space-y-6">
{/* Team Description */} {/* Team Description */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">About Team</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">About Team</h2>
<p className="text-gray-700 whitespace-pre-wrap">{team.description}</p> <p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">{team.description}</p>
</div> </div>
{/* Team Actions - Only for Leader */} {/* Team Actions - Only for Leader */}
{isLeader && ( {isLeader && (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">Team Management</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Team Management</h2>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<Button <Button
className="w-full" className="w-full"
@@ -176,7 +176,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Link> </Link>
</div> </div>
{!canInvite && members.length >= MAX_TEAM_MEMBERS && ( {!canInvite && members.length >= MAX_TEAM_MEMBERS && (
<p className="text-sm text-gray-600 mt-3 text-center"> <p className="text-sm text-gray-600 dark:text-neutral-400 mt-3 text-center">
Maximum team size reached ({MAX_TEAM_MEMBERS} members) Maximum team size reached ({MAX_TEAM_MEMBERS} members)
</p> </p>
)} )}
@@ -185,8 +185,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Quick Actions for Members (non-leaders) */} {/* Quick Actions for Members (non-leaders) */}
{!isLeader && isMember && ( {!isLeader && isMember && (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">Quick Actions</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Quick Actions</h2>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<Link to={`/teams/${teamId}/chat`}> <Link to={`/teams/${teamId}/chat`}>
<Button className="w-full" variant="secondary"> <Button className="w-full" variant="secondary">
@@ -206,12 +206,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Submission Status - Only show to members */} {/* Submission Status - Only show to members */}
{team.has_submission && isMember && ( {team.has_submission && isMember && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 md:p-6"> <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 md:p-6">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<span className="text-3xl"></span> <span className="text-3xl"></span>
<div> <div>
<h3 className="font-bold text-green-900">Project Submitted</h3> <h3 className="font-bold text-green-900 dark:text-green-400">Project Submitted</h3>
<p className="text-green-700 text-sm"> <p className="text-green-700 dark:text-green-300 text-sm">
Your team has successfully submitted a project Your team has successfully submitted a project
</p> </p>
</div> </div>
@@ -228,12 +228,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Sidebar */} {/* Sidebar */}
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{/* Team Leader */} {/* Team Leader */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 mb-3 md:mb-4">Team Leader</h3> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Team Leader</h3>
{team.leader && ( {team.leader && (
<button <button
onClick={() => navigate(`/users/${team.leader.id}`)} onClick={() => navigate(`/users/${team.leader.id}`)}
className="w-full flex items-center space-x-3 hover:bg-gray-100 rounded-lg p-2 transition-colors text-left cursor-pointer" className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
> >
{team.leader.avatar ? ( {team.leader.avatar ? (
<img <img
@@ -242,21 +242,21 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500">👤</span> <span className="text-gray-500 dark:text-neutral-400">👤</span>
</div> </div>
)} )}
<div> <div>
<p className="font-medium text-gray-900">{team.leader.fullname}</p> <p className="font-medium text-gray-900 dark:text-white">{team.leader.fullname}</p>
<p className="text-sm text-gray-600">{team.leader.email}</p> <p className="text-sm text-gray-600 dark:text-neutral-400">{team.leader.email}</p>
</div> </div>
</button> </button>
)} )}
</div> </div>
{/* Team Members */} {/* Team Members */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 mb-3 md:mb-4"> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Members ({members.length}) Members ({members.length})
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
@@ -264,7 +264,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<button <button
key={member.id} key={member.id}
onClick={() => navigate(`/users/${member.user.id}`)} onClick={() => navigate(`/users/${member.user.id}`)}
className="w-full flex items-center space-x-3 hover:bg-gray-100 rounded-lg p-2 transition-colors text-left cursor-pointer" className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
> >
{member.user.avatar ? ( {member.user.avatar ? (
<img <img
@@ -273,15 +273,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-10 h-10 rounded-full object-cover" className="w-10 h-10 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 text-sm">👤</span> <span className="text-gray-500 dark:text-neutral-400 text-sm">👤</span>
</div> </div>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 truncate"> <p className="font-medium text-gray-900 dark:text-white truncate">
{member.user.fullname} {member.user.fullname}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500 dark:text-neutral-400">
{member.role === ETeamMemberRole.LEADER ? 'Leader' : 'Member'} {member.role === ETeamMemberRole.LEADER ? 'Leader' : 'Member'}
</p> </p>
</div> </div>
@@ -295,22 +295,22 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Invite Member Modal */} {/* Invite Member Modal */}
{showInviteModal && ( {showInviteModal && (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Invite Team Member Invite Team Member
</h2> </h2>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 dark:text-neutral-400 mb-4">
Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in. Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in.
</p> </p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-6"> <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800 dark:text-yellow-300">
<strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in. <strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in.
</p> </p>
</div> </div>
<form onSubmit={handleInviteMember} className="space-y-4"> <form onSubmit={handleInviteMember} className="space-y-4">
<div> <div>
<label htmlFor="invite-email" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="invite-email" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Email Address Email Address
</label> </label>
<input <input
@@ -319,10 +319,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Enter email address..." placeholder="Enter email address..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
required required
/> />
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 dark:text-neutral-400 mt-1">
Current members: {members.length}/{MAX_TEAM_MEMBERS} Current members: {members.length}/{MAX_TEAM_MEMBERS}
</p> </p>
</div> </div>
@@ -353,15 +353,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Join Requests Modal */} {/* Join Requests Modal */}
{showJoinRequestsModal && ( {showJoinRequestsModal && (
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-900"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Join Requests ({pendingJoinRequests.length}) Join Requests ({pendingJoinRequests.length})
</h2> </h2>
<button <button
onClick={() => setShowJoinRequestsModal(false)} onClick={() => setShowJoinRequestsModal(false)}
className="text-gray-400 hover:text-gray-600 text-2xl" className="text-gray-400 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300 text-2xl"
> >
</button> </button>
@@ -369,15 +369,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{pendingJoinRequests.length === 0 ? ( {pendingJoinRequests.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-600 text-lg">No pending join requests</p> <p className="text-gray-600 dark:text-neutral-400 text-lg">No pending join requests</p>
<p className="text-gray-500 text-sm mt-2"> <p className="text-gray-500 dark:text-neutral-500 text-sm mt-2">
When users request to join your team, they'll appear here When users request to join your team, they'll appear here
</p> </p>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{pendingJoinRequests.map((request: any) => ( {pendingJoinRequests.map((request: any) => (
<div key={request.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow"> <div key={request.id} className="border border-gray-200 dark:border-neutral-700 rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1"> <div className="flex items-start space-x-3 flex-1">
{request.user?.avatar ? ( {request.user?.avatar ? (
@@ -387,25 +387,25 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center shrink-0"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center shrink-0">
<span className="text-gray-500 text-xl">👤</span> <span className="text-gray-500 dark:text-neutral-400 text-xl">👤</span>
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-gray-900"> <p className="font-semibold text-gray-900 dark:text-white">
{request.user?.fullname || 'Unknown User'} {request.user?.fullname || 'Unknown User'}
</p> </p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
{request.user?.email} {request.user?.email}
</p> </p>
{request.message && ( {request.message && (
<div className="mt-2 bg-gray-50 rounded-lg p-3"> <div className="mt-2 bg-gray-50 dark:bg-neutral-800 rounded-lg p-3">
<p className="text-sm text-gray-700"> <p className="text-sm text-gray-700 dark:text-neutral-300">
<strong>Message:</strong> {request.message} <strong>Message:</strong> {request.message}
</p> </p>
</div> </div>
)} )}
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 dark:text-neutral-500 mt-2">
Requested {new Date(request.created_at).toLocaleDateString()} Requested {new Date(request.created_at).toLocaleDateString()}
</p> </p>
</div> </div>
@@ -429,8 +429,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
</div> </div>
{members.length >= MAX_TEAM_MEMBERS && ( {members.length >= MAX_TEAM_MEMBERS && (
<div className="mt-3 bg-yellow-50 border border-yellow-200 rounded-lg p-2"> <div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-2">
<p className="text-xs text-yellow-800"> <p className="text-xs text-yellow-800 dark:text-yellow-300">
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS} members). Remove a member before accepting new requests. Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS} members). Remove a member before accepting new requests.
</p> </p>
</div> </div>
+24 -24
View File
@@ -63,14 +63,14 @@ const BrowseTeamsPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header */} {/* Header */}
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Browse Teams</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Browse Teams</h1>
<p className="text-gray-600 mt-1">Find and join teams looking for members</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">Find and join teams looking for members</p>
</div> </div>
<Link to="/dashboard" className="hidden md:block"> <Link to="/dashboard" className="hidden md:block">
<Button variant="secondary">Back to Dashboard</Button> <Button variant="secondary">Back to Dashboard</Button>
@@ -81,10 +81,10 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Filters */} {/* Filters */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="bg-white p-6 rounded-lg shadow-sm mb-6"> <div className="bg-white dark:bg-neutral-900 p-6 rounded-lg shadow-sm mb-6">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Search Teams Search Teams
</label> </label>
<input <input
@@ -92,11 +92,11 @@ const BrowseTeamsPage: FC = (): ReactElement => {
placeholder="Search by team name..." placeholder="Search by team name..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-[42px] px-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full h-[42px] px-3 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Filter by City Filter by City
</label> </label>
<CitySelect <CitySelect
@@ -111,17 +111,17 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Teams List */} {/* Teams List */}
{isLoading ? ( {isLoading ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-600">Loading teams...</p> <p className="text-gray-600 dark:text-neutral-400">Loading teams...</p>
</div> </div>
) : teams.length === 0 ? ( ) : teams.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm p-12 text-center"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center">
<p className="text-gray-600 text-lg">No teams found</p> <p className="text-gray-600 dark:text-neutral-400 text-lg">No teams found</p>
<p className="text-gray-500 mt-2">Try adjusting your filters</p> <p className="text-gray-500 dark:text-neutral-500 mt-2">Try adjusting your filters</p>
</div> </div>
) : ( ) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{teams.map((team) => ( {teams.map((team) => (
<div key={team.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col"> <div key={team.id} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col">
<img <img
src={team.banner || '/images/banner-imphnen.png'} src={team.banner || '/images/banner-imphnen.png'}
alt={team.name} alt={team.name}
@@ -136,19 +136,19 @@ const BrowseTeamsPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 text-xl">👥</span> <span className="text-gray-500 dark:text-neutral-400 text-xl">👥</span>
</div> </div>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-gray-900 line-clamp-2">{team.name}</h3> <h3 className="text-lg font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h3>
<div className="text-sm text-gray-600 flex gap-2"> <div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
<p className="truncate flex-1 min-w-0">📍 {team.city}</p> <p className="truncate flex-1 min-w-0">📍 {team.city}</p>
<p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p> <p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p>
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 text-sm mb-4 line-clamp-3"> <p className="text-gray-600 dark:text-neutral-400 text-sm mb-4 line-clamp-3">
{team.description} {team.description}
</p> </p>
<div className="space-y-3 mt-auto"> <div className="space-y-3 mt-auto">
@@ -189,23 +189,23 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Join Request Modal */} {/* Join Request Modal */}
{showJoinModal && ( {showJoinModal && (
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Request to Join Team Request to Join Team
</h2> </h2>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 dark:text-neutral-400 mb-6">
Send a message to the team leader explaining why you want to join Send a message to the team leader explaining why you want to join
</p> </p>
<form onSubmit={onSubmit} className="space-y-4"> <form onSubmit={onSubmit} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Your Message Your Message
</label> </label>
<textarea <textarea
{...form.register('message')} {...form.register('message')}
rows={4} rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
placeholder="Tell the team leader why you want to join their team..." placeholder="Tell the team leader why you want to join their team..."
/> />
{form.formState.errors.message && ( {form.formState.errors.message && (
+5 -5
View File
@@ -6,21 +6,21 @@ const TeamsLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto"> <div className="flex-1 flex flex-col overflow-auto">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
@@ -6,21 +6,21 @@ const UserDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto"> <div className="flex-1 flex flex-col overflow-auto">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
@@ -70,15 +70,15 @@ export const CitySelect: FC<CitySelectProps> = ({
}} }}
onClick={handleInputClick} onClick={handleInputClick}
placeholder={placeholder} placeholder={placeholder}
className={`w-full h-[42px] px-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full h-[42px] px-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 ${
error ? 'border-red-500' : 'border-gray-300' error ? 'border-red-500' : 'border-gray-300 dark:border-neutral-600'
}`} }`}
/> />
{value && !isOpen && ( {value && !isOpen && (
<button <button
type="button" type="button"
onClick={handleClearSelection} onClick={handleClearSelection}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300"
> >
</button> </button>
@@ -88,15 +88,15 @@ export const CitySelect: FC<CitySelectProps> = ({
{error && <p className="text-sm text-red-500 mt-1">{error}</p>} {error && <p className="text-sm text-red-500 mt-1">{error}</p>}
{isOpen && ( {isOpen && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto"> <div className="absolute z-50 w-full mt-1 bg-white dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg shadow-lg max-h-60 overflow-y-auto">
{filteredCities.length > 0 ? ( {filteredCities.length > 0 ? (
<ul className="py-1"> <ul className="py-1">
{filteredCities.map((city) => ( {filteredCities.map((city) => (
<li <li
key={city} key={city}
onClick={() => handleSelectCity(city)} onClick={() => handleSelectCity(city)}
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 ${ className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${
value === city ? 'bg-blue-100 text-blue-700' : '' value === city ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400' : 'text-gray-900 dark:text-neutral-200'
}`} }`}
> >
{city} {city}
@@ -104,7 +104,7 @@ export const CitySelect: FC<CitySelectProps> = ({
))} ))}
</ul> </ul>
) : ( ) : (
<div className="px-3 py-2 text-gray-500 text-sm"> <div className="px-3 py-2 text-gray-500 dark:text-neutral-400 text-sm">
No cities found No cities found
</div> </div>
)} )}
+59 -16
View File
@@ -1,8 +1,9 @@
import { FC, useState, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { Link, useLocation } from 'react-router'; import { Link, useLocation } from 'react-router';
import { useMyTeams, useAuthStore, supabase } from '@imphnen-frontend-service/service'; import { useMyTeams, useAuthStore, supabase } from '@imphnen-frontend-service/service';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTheme } from './theme-provider';
interface NavItem { interface NavItem {
name: string; name: string;
@@ -21,6 +22,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { session, clearSession } = useAuthStore(); const { session, clearSession } = useAuthStore();
const { data: teamsData } = useMyTeams(); const { data: teamsData } = useMyTeams();
const { theme, setTheme, resolvedTheme } = useTheme();
const user = session?.user; const user = session?.user;
const myTeams = teamsData?.data || []; const myTeams = teamsData?.data || [];
@@ -91,17 +93,51 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
}, },
]; ];
const cycleTheme = () => {
if (theme === 'light') setTheme('dark');
else if (theme === 'dark') setTheme('system');
else setTheme('light');
};
const getThemeIcon = () => {
if (theme === 'system') {
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
);
}
if (resolvedTheme === 'dark') {
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
);
}
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
);
};
const getThemeLabel = () => {
if (theme === 'system') return 'System';
if (theme === 'dark') return 'Dark';
return 'Light';
};
const sidebarContent = ( const sidebarContent = (
<div className="w-64 bg-white border-r min-h-screen flex flex-col"> <div className="w-64 bg-white dark:bg-neutral-900 border-r dark:border-neutral-700 min-h-screen flex flex-col">
{/* Logo / Brand with Close Button */} {/* Logo / Brand with Close Button */}
<div className="p-6 border-b flex items-center justify-between"> <div className="p-6 border-b dark:border-neutral-700 flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="text-xl font-bold text-gray-900 dark:text-white">Hackathon</h1>
{onClose && ( {onClose && (
<button <button
onClick={onClose} onClick={onClose}
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors" className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-gray-500 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
@@ -109,7 +145,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</div> </div>
{/* User Info */} {/* User Info */}
<div className="p-4 border-b"> <div className="p-4 border-b dark:border-neutral-700">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{user?.avatar ? ( {user?.avatar ? (
<img <img
@@ -118,15 +154,15 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
className="w-10 h-10 rounded-full object-cover" className="w-10 h-10 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 text-lg">👤</span> <span className="text-gray-500 dark:text-neutral-400 text-lg">👤</span>
</div> </div>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate"> <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{user?.fullname || user?.email?.split('@')[0] || 'User'} {user?.fullname || user?.email?.split('@')[0] || 'User'}
</p> </p>
<p className="text-xs text-gray-500 truncate">{user?.email}</p> <p className="text-xs text-gray-500 dark:text-neutral-400 truncate">{user?.email}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -144,8 +180,8 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
to={item.path} to={item.path}
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${ className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
isActive isActive
? 'bg-blue-50 text-blue-700 font-medium' ? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-medium'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800'
}`} }`}
> >
{item.icon} {item.icon}
@@ -157,11 +193,18 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</ul> </ul>
</nav> </nav>
{/* Logout Button */} {/* Theme Toggle & Logout */}
<div className="p-4 border-t"> <div className="p-4 border-t dark:border-neutral-700 space-y-2">
<button
onClick={cycleTheme}
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
>
{getThemeIcon()}
<span>{getThemeLabel()}</span>
</button>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 hover:bg-red-50 transition-colors" className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
@@ -0,0 +1,100 @@
import { createContext, useContext, useEffect, useState, FC, ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const STORAGE_KEY = 'hackathon-theme';
function getSystemTheme(): 'light' | 'dark' {
if (typeof window !== 'undefined') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light';
}
interface ThemeProviderProps {
children: ReactNode;
defaultTheme?: Theme;
}
export const ThemeProvider: FC<ThemeProviderProps> = ({ children, defaultTheme = 'system' }) => {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
return stored || defaultTheme;
}
return defaultTheme;
});
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(() => {
if (theme === 'system') {
return getSystemTheme();
}
return theme;
});
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
};
useEffect(() => {
const root = document.documentElement;
// Calculate the resolved theme
const resolved = theme === 'system' ? getSystemTheme() : theme;
setResolvedTheme(resolved);
// Apply dark class to root
if (resolved === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [theme]);
// Listen for system theme changes
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(newTheme);
const root = document.documentElement;
if (newTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{children}
</ThemeContext.Provider>
);
};
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
export default ThemeProvider;
@@ -0,0 +1,63 @@
import { FC } from 'react';
import { useTheme } from './theme-provider';
interface ThemeToggleProps {
className?: string;
}
// Sun icon for light mode
const SunIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
);
// Moon icon for dark mode
const MoonIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
);
// System icon (monitor)
const SystemIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
);
export const ThemeToggle: FC<ThemeToggleProps> = ({ className = '' }) => {
const { theme, setTheme, resolvedTheme } = useTheme();
const cycleTheme = () => {
if (theme === 'light') setTheme('dark');
else if (theme === 'dark') setTheme('system');
else setTheme('light');
};
const getThemeLabel = () => {
if (theme === 'system') return 'System';
return theme === 'dark' ? 'Dark' : 'Light';
};
const renderIcon = () => {
if (theme === 'system') {
return <SystemIcon />;
}
return resolvedTheme === 'dark' ? <MoonIcon /> : <SunIcon />;
};
return (
<button
type="button"
onClick={cycleTheme}
className={`p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors cursor-pointer text-gray-600 dark:text-neutral-400 ${className}`}
title={`Theme: ${getThemeLabel()}`}
aria-label={`Current theme: ${getThemeLabel()}. Click to change.`}
>
{renderIcon()}
</button>
);
};
export default ThemeToggle;
+33
View File
@@ -2,6 +2,9 @@
@import 'tailwindcss'; @import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}"; @source "../../../libs/ui/**/*.{ts,tsx}";
/* Enable class-based dark mode (using .dark class on html element) */
@custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--color-primary-50: #f0f8ff; --color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd; --color-primary-100: #e1f0fd;
@@ -108,6 +111,14 @@
border-color: var(--color-neutral-200, currentColor); border-color: var(--color-neutral-200, currentColor);
} }
.dark *,
.dark ::after,
.dark ::before,
.dark ::backdrop,
.dark ::file-selector-button {
border-color: var(--color-neutral-700, currentColor);
}
/* Custom scrollbar */ /* Custom scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
@@ -118,19 +129,41 @@
background: var(--color-neutral-50); background: var(--color-neutral-50);
} }
.dark ::-webkit-scrollbar-track {
background: var(--color-neutral-900);
}
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #cccccc; background: #cccccc;
border-radius: 3px; border-radius: 3px;
} }
.dark ::-webkit-scrollbar-thumb {
background: var(--color-neutral-600);
}
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300); background: var(--color-neutral-300);
} }
.dark ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-500);
}
html { html {
font-family: 'Bai Jamjuree', sans-serif; font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
line-height: 1.2; line-height: 1.2;
} }
/* Dark mode base styles */
.dark {
color-scheme: dark;
}
.dark body {
background-color: var(--color-neutral-950);
color: var(--color-neutral-100);
}
} }
+9 -6
View File
@@ -9,6 +9,7 @@ import {
QueryProvider, QueryProvider,
} from '@imphnen-frontend-service/utils'; } from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import { ThemeProvider } from './components/theme-provider';
import './index.css'; import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const files = import.meta.glob('./app/**/*(page|layout).tsx');
@@ -36,11 +37,13 @@ if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <ThemeProvider defaultTheme="system">
<ModalLoginProvider> <QueryProvider>
<Toaster position="top-right" /> <ModalLoginProvider>
<RouterProvider router={router} /> <Toaster position="top-right" richColors />
</ModalLoginProvider> <RouterProvider router={router} />
</QueryProvider> </ModalLoginProvider>
</QueryProvider>
</ThemeProvider>
</StrictMode> </StrictMode>
); );