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 { toast } from 'sonner';
import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
export default function LoginPage() {
// console.log('[LoginPage] Rendering...');
@@ -114,28 +115,31 @@ export default function LoginPage() {
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
<button
onClick={() => navigate('/')}
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center mb-6"
>
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
Back to Homepage
</button>
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
<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">
<div className="flex items-center justify-between mb-6">
<button
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
</button>
<ThemeToggle />
</div>
<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
</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
</p>
</div>
{error && (
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">{error}</p>
<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 dark:text-red-400 text-sm">{error}</p>
</div>
)}
@@ -143,7 +147,7 @@ export default function LoginPage() {
<div>
<label
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
</label>
@@ -154,7 +158,7 @@ export default function LoginPage() {
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
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
/>
</div>
@@ -163,13 +167,13 @@ export default function LoginPage() {
<div className="flex items-center justify-between mb-1">
<label
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
</label>
<Link
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?
</Link>
@@ -181,7 +185,7 @@ export default function LoginPage() {
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
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
/>
</div>
@@ -189,23 +193,23 @@ export default function LoginPage() {
<button
type="submit"
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'}
</button>
</form>
<div className="my-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div>
<span className="px-4 text-sm text-gray-500">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>
<span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
</div>
<button
onClick={handleGithubLogin}
disabled={isGithubLoading}
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" />
<span>
@@ -214,11 +218,11 @@ export default function LoginPage() {
</button>
<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?{' '}
<a
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
</a>
@@ -226,7 +230,7 @@ export default function LoginPage() {
</div>
<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
</p>
</div>
+33 -29
View File
@@ -9,6 +9,7 @@ import { GithubOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
export default function SignupPage() {
const navigate = useNavigate();
@@ -137,26 +138,29 @@ export default function SignupPage() {
};
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
<button
onClick={() => navigate('/')}
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center mb-6"
>
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
Back to Homepage
</button>
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
<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">
<div className="flex items-center justify-between mb-6">
<button
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
</button>
<ThemeToggle />
</div>
<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
</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>
{error && (
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">{error}</p>
<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 dark:text-red-400 text-sm">{error}</p>
</div>
)}
@@ -164,7 +168,7 @@ export default function SignupPage() {
<div>
<label
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
</label>
@@ -175,7 +179,7 @@ export default function SignupPage() {
onChange={(e) => setFullname(e.target.value)}
placeholder="John Doe"
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
/>
</div>
@@ -183,7 +187,7 @@ export default function SignupPage() {
<div>
<label
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
</label>
@@ -194,7 +198,7 @@ export default function SignupPage() {
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
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
/>
</div>
@@ -202,7 +206,7 @@ export default function SignupPage() {
<div>
<label
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
</label>
@@ -213,7 +217,7 @@ export default function SignupPage() {
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
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
/>
</div>
@@ -221,7 +225,7 @@ export default function SignupPage() {
<div>
<label
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
</label>
@@ -232,7 +236,7 @@ export default function SignupPage() {
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
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
/>
</div>
@@ -240,23 +244,23 @@ export default function SignupPage() {
<button
type="submit"
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'}
</button>
</form>
<div className="my-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div>
<span className="px-4 text-sm text-gray-500">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>
<span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
</div>
<button
onClick={handleGithubLogin}
disabled={isGithubLoading}
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" />
<span>
@@ -265,11 +269,11 @@ export default function SignupPage() {
</button>
<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?{' '}
<a
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
</a>
@@ -277,7 +281,7 @@ export default function SignupPage() {
</div>
<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
</p>
</div>
+5 -5
View File
@@ -6,21 +6,21 @@ const DashboardLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false);
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)} />
<div className="flex-1 flex flex-col overflow-auto">
{/* 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
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" />
</svg>
</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>
<main className="flex-1 overflow-auto">
+31 -31
View File
@@ -42,25 +42,25 @@ const DashboardPage: FC = (): ReactElement => {
return (
<div className="p-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'}!
</h1>
{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>
{invitations.length > 0 && (
<div className="mb-8 bg-blue-50 border border-blue-200 rounded-lg p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4">
<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 dark:text-white mb-4">
Team Invitations ({invitations.length})
</h2>
<div className="space-y-3">
{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>
<p className="font-medium text-gray-900">{invitation.team.name}</p>
<p className="text-sm text-gray-600">Invited by {invitation.inviter.fullname}</p>
<p className="font-medium text-gray-900 dark:text-white">{invitation.team.name}</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">Invited by {invitation.inviter.fullname}</p>
</div>
<div className="flex space-x-2">
<Button size="sm" onClick={() => handleAcceptInvitation(invitation.id)}>Accept</Button>
@@ -74,53 +74,53 @@ const DashboardPage: FC = (): ReactElement => {
{myTeams.length > 0 ? (
<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">
{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" />}
<div className="p-4 md:p-6">
<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" />}
<div>
<h3 className="text-base md:text-lg font-bold text-gray-900">{team.name}</h3>
<p className="text-xs md:text-sm text-gray-600">City: {team.city}</p>
<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 dark:text-neutral-400">City: {team.city}</p>
</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>
</Link>
))}
</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-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>
<p className="text-sm md:text-base text-gray-600">Use the sidebar to browse teams or create your own</p>
<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 dark:text-neutral-400">Use the sidebar to browse teams or create your own</p>
</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="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 items-start">
{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">
<span className="text-gray-400 text-3xl md:text-4xl">User</span>
<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 dark:text-neutral-500 text-3xl md:text-4xl">User</span>
</div>
)}
<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 ? (
<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>
@@ -130,20 +130,20 @@ const DashboardPage: FC = (): ReactElement => {
</div>
<div className="space-y-4 md:space-y-6">
{user?.bio && (
<div className="bg-white rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2">About</h3>
<p className="text-gray-800 leading-relaxed">{user.bio}</p>
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<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 dark:text-neutral-200 leading-relaxed">{user.bio}</p>
</div>
)}
<div className="bg-white rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Contact</h3>
<div className="flex items-center text-gray-700">
<span className="text-gray-900">{user?.email}</span>
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<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 dark:text-neutral-300">
<span className="text-gray-900 dark:text-white">{user?.email}</span>
</div>
</div>
{user?.skills && user.skills.length > 0 && (
<div className="bg-white rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Skills</h3>
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<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">
{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>
+2 -2
View File
@@ -107,10 +107,10 @@ export default function RootLayout() {
// Show loading state while checking auth
if (isChecking) {
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="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>
);
+110 -108
View File
@@ -2,6 +2,7 @@ import { useNavigate } from 'react-router';
import { useState } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Icon } from '@iconify/react';
import { ThemeToggle } from '../components/theme-toggle';
export default function HomePage() {
const navigate = useNavigate();
@@ -62,13 +63,13 @@ export default function HomePage() {
];
return (
<div className="min-h-screen bg-white">
<div className="min-h-screen bg-white dark:bg-neutral-950">
{/* Navigation */}
<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 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
href="#top"
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">
<a
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
</a>
<a
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
</a>
<a
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
</a>
<ThemeToggle />
<Button
onClick={() => navigate('/auth/login')}
size="sm"
@@ -113,47 +115,50 @@ export default function HomePage() {
</Button>
</div>
{/* Mobile Menu Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2 cursor-pointer"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div className="flex items-center gap-2 md:hidden">
<ThemeToggle />
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 cursor-pointer text-gray-900 dark:text-white"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</nav>
{/* Mobile Menu */}
{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">
<a
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
</a>
<a
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
</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
</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
</a>
<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"
style={{ transform: 'translate(0px, 0px)', opacity: 1 }}
></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 className="mx-auto container px-4 relative flex flex-col items-center">
{/* Logos */}
@@ -189,19 +194,19 @@ export default function HomePage() {
className="h-12 md:h-16"
/>
</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>
<div className="flex items-center">
<img
src="images/sponsors/kolosal-logo_rlxbck.svg"
alt="Kolosal.ai"
className="h-8 md:h-12"
className="h-8 md:h-12 dark:invert"
/>
</div>
</div>
{/* 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
</h1>
{/* Subtitle */}
@@ -209,12 +214,12 @@ export default function HomePage() {
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
</p>
{/* 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
yang menghadirkan dampak nyata
</p>
{/* 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">
<Icon icon="streamline-plump:web" width="16" height="16" />
<span>Online</span>
@@ -236,7 +241,7 @@ export default function HomePage() {
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
target="_blank"
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
</a>
@@ -244,7 +249,7 @@ export default function HomePage() {
{/* Scroll indicator */}
<div className="mt-12 md:mt-20">
<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"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -261,17 +266,17 @@ export default function HomePage() {
</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">
<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>
</h2>
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 mb-6">
<span className="font-semibold text-gray-900">Hackathon</span>{' '}
<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 dark:text-white">Hackathon</span>{' '}
adalah kompetisi pengembangan teknologi yang mengajak talenta muda
untuk berkolaborasi dalam menciptakan solusi inovatif.
</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{' '}
<span className="font-semibold text-primary-500">
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
@@ -282,10 +287,10 @@ export default function HomePage() {
</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="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>
</h2>
<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">
{/* 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="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="ic:round-star"
className="h-8 w-8 text-primary-500"
/>
</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">
Rp6.000.000
</p>
</div>
{/* 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="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="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>
<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">
Rp4.000.000
</p>
</div>
{/* 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="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="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>
<h3 className="text-xl font-bold text-center mb-2">Juara 3</h3>
<p className="text-2xl font-bold text-orange-600 text-center font-sans">
<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 dark:text-orange-500 text-center font-sans">
Rp2.500.000
</p>
</div>
{/* 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="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="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>
<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
</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
</p>
</div>
@@ -364,13 +369,13 @@ export default function HomePage() {
</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="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>
</h2>
<p className="text-lg text-gray-600">
<p className="text-lg text-gray-600 dark:text-neutral-400">
Jadwal lengkap pelaksanaan hackathon
</p>
</div>
@@ -380,14 +385,14 @@ export default function HomePage() {
<div className="flex gap-6">
<div className="flex flex-col items-center">
<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 className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2">
30 November 2025
</p>
<h3 className="text-xl font-bold mb-2">Penutupan Registrasi</h3>
<p className="text-gray-600">
<h3 className="text-xl font-bold mb-2 dark:text-white">Penutupan Registrasi</h3>
<p className="text-gray-600 dark:text-neutral-400">
Batas akhir pendaftaran peserta hackathon.
</p>
</div>
@@ -397,14 +402,14 @@ export default function HomePage() {
<div className="flex gap-6">
<div className="flex flex-col items-center">
<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 className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2">
30 November 2025
</p>
<h3 className="text-xl font-bold mb-2">Technical Meeting</h3>
<p className="text-gray-600">
<h3 className="text-xl font-bold mb-2 dark:text-white">Technical Meeting</h3>
<p className="text-gray-600 dark:text-neutral-400">
Akan diadakan technical meeting terkait lomba melalui Google
Meet. Stay tune di grup WA Hackathon.
</p>
@@ -415,14 +420,14 @@ export default function HomePage() {
<div className="flex gap-6">
<div className="flex flex-col items-center">
<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 className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2">
1 - 7 Desember 2025
</p>
<h3 className="text-xl font-bold mb-2">Tahap Penyisihan</h3>
<p className="text-gray-600">
<h3 className="text-xl font-bold mb-2 dark:text-white">Tahap Penyisihan</h3>
<p className="text-gray-600 dark:text-neutral-400">
Peserta mengerjakan tantangan yang diberikan.
</p>
</div>
@@ -432,14 +437,14 @@ export default function HomePage() {
<div className="flex gap-6">
<div className="flex flex-col items-center">
<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 className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2">
8 - 14 Desember 2025
</p>
<h3 className="text-xl font-bold mb-2">Penilaian & Webinar</h3>
<p className="text-gray-600">
<h3 className="text-xl font-bold mb-2 dark:text-white">Penilaian & Webinar</h3>
<p className="text-gray-600 dark:text-neutral-400">
Proses penilaian oleh juri dan sesi webinar.
</p>
</div>
@@ -454,8 +459,8 @@ export default function HomePage() {
<p className="text-primary-500 font-semibold mb-2">
15 Desember 2025
</p>
<h3 className="text-xl font-bold mb-2">Pengumuman & Final</h3>
<p className="text-gray-600">
<h3 className="text-xl font-bold mb-2 dark:text-white">Pengumuman & Final</h3>
<p className="text-gray-600 dark:text-neutral-400">
Presentasi final dan pengumuman pemenang.
</p>
</div>
@@ -465,47 +470,44 @@ export default function HomePage() {
</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="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>
</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
</p>
</div>
<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 */}
<div className="flex flex-col justify-between bg-white 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">
<div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
<h3 className="text-p3 font-bold mb-1 dark:text-white">
Alifais Farrel Ramdhani
</h3>
<div>
<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>
{/* Judge 2 */}
<div className="flex flex-col justify-between bg-white 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">Anka Tama</h3>
<div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
<h3 className="text-p3 font-bold mb-1 dark:text-white">Anka Tama</h3>
<div>
<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>
{/* Judge 3 */}
<div className="flex flex-col justify-between bg-white 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">Hafid Nur</h3>
<div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
<h3 className="text-p3 font-bold mb-1 dark:text-white">Hafid Nur</h3>
<div>
<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>
@@ -513,25 +515,25 @@ export default function HomePage() {
</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="text-center mb-12">
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500">
FAQ
</h2>
<p className="text-lg text-gray-600">
<p className="text-lg text-gray-600 dark:text-neutral-400">
Pertanyaan yang Sering Diajukan
</p>
</div>
<div className="space-y-4 font-sans">
{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
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}
</span>
<svg
@@ -551,7 +553,7 @@ export default function HomePage() {
</svg>
</button>
{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>
))}
@@ -560,12 +562,12 @@ export default function HomePage() {
</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">
<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>
</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
</p>
@@ -574,12 +576,12 @@ export default function HomePage() {
href="https://kolosal.ai"
target="_blank"
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
src="images/sponsors/kolosal-logo_rlxbck.svg"
alt="Kolosal.ai"
className="h-12 md:h-16"
className="h-12 md:h-16 dark:invert"
/>
</a>
</div>
@@ -589,13 +591,13 @@ export default function HomePage() {
{/* CTA Section */}
<section
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">
<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>
</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
terbaik,
<br className="hidden md:block" /> belajar dari para ahli, dan
@@ -613,13 +615,13 @@ export default function HomePage() {
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
target="_blank"
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
</a>
</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{' '}
<span className="text-primary-500 font-semibold">
30 November 2025
+15 -15
View File
@@ -128,14 +128,14 @@ const ProfilePage: FC = (): ReactElement => {
const isLoading = isUpdating || isUploading;
return (
<div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 px-4 py-8">
<div className="bg-white w-full max-w-md p-8 rounded-xl shadow-lg">
<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 dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-lg">
<div className="mb-6">
<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
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
className="w-6 h-6"
@@ -152,7 +152,7 @@ const ProfilePage: FC = (): ReactElement => {
</svg>
</Link>
</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>
<form onSubmit={onSubmit} className="space-y-6">
@@ -163,11 +163,11 @@ const ProfilePage: FC = (): ReactElement => {
<img
src={avatarPreview}
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">
<span className="text-gray-400 text-4xl">👤</span>
<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 dark:text-neutral-500 text-4xl">👤</span>
</div>
)}
{/* Camera overlay */}
@@ -204,7 +204,7 @@ const ProfilePage: FC = (): ReactElement => {
/>
</label>
</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
<br />
Format: JPG, PNG. Max 5MB
@@ -221,7 +221,7 @@ const ProfilePage: FC = (): ReactElement => {
{/* City */}
<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
</label>
<Controller
@@ -240,7 +240,7 @@ const ProfilePage: FC = (): ReactElement => {
{/* Role/Skills */}
<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
</label>
<Controller
@@ -263,9 +263,9 @@ const ProfilePage: FC = (): ReactElement => {
: (field.value || []).filter((v) => v !== role);
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>
))}
</div>
@@ -276,8 +276,8 @@ const ProfilePage: FC = (): ReactElement => {
{/* Bio */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Bio <span className="text-gray-400">(Optional)</span>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Bio <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
</label>
<Controller
control={form.control}
@@ -6,21 +6,21 @@ const TeamDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false);
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)} />
<div className="flex-1 flex flex-col overflow-auto">
{/* 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
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" />
</svg>
</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>
<main className="flex-1 overflow-auto">
+58 -58
View File
@@ -60,7 +60,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (isLoadingTeam || isLoadingMembers) {
return (
<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>
);
}
@@ -68,16 +68,16 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (!team) {
return (
<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>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* 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 && (
<div className="w-full h-32 md:h-48 overflow-hidden">
<img
@@ -98,13 +98,13 @@ const TeamDashboardPage: FC = (): ReactElement => {
/>
)}
<div>
<h1 className="text-xl md:text-3xl font-bold text-gray-900 line-clamp-2">{team.name}</h1>
<p className="text-sm md:text-base text-gray-600 mt-1 line-clamp-1">📍 {team.city}</p>
<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 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">
<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'}
</span>
<span className="text-sm text-gray-500">
<span className="text-sm text-gray-500 dark:text-neutral-400">
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
</span>
{isLeader && (
@@ -124,15 +124,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Main Content */}
<div className="lg:col-span-2 space-y-4 md:space-y-6">
{/* Team Description */}
<div className="bg-white 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>
<p className="text-gray-700 whitespace-pre-wrap">{team.description}</p>
<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 dark:text-white mb-3 md:mb-4">About Team</h2>
<p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">{team.description}</p>
</div>
{/* Team Actions - Only for Leader */}
{isLeader && (
<div className="bg-white 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>
<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 dark:text-white mb-3 md:mb-4">Team Management</h2>
<div className="grid gap-3 sm:grid-cols-2">
<Button
className="w-full"
@@ -176,7 +176,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Link>
</div>
{!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)
</p>
)}
@@ -185,8 +185,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Quick Actions for Members (non-leaders) */}
{!isLeader && isMember && (
<div className="bg-white 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>
<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 dark:text-white mb-3 md:mb-4">Quick Actions</h2>
<div className="grid gap-3 sm:grid-cols-2">
<Link to={`/teams/${teamId}/chat`}>
<Button className="w-full" variant="secondary">
@@ -206,12 +206,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Submission Status - Only show to members */}
{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">
<span className="text-3xl"></span>
<div>
<h3 className="font-bold text-green-900">Project Submitted</h3>
<p className="text-green-700 text-sm">
<h3 className="font-bold text-green-900 dark:text-green-400">Project Submitted</h3>
<p className="text-green-700 dark:text-green-300 text-sm">
Your team has successfully submitted a project
</p>
</div>
@@ -228,12 +228,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Sidebar */}
<div className="space-y-4 md:space-y-6">
{/* Team Leader */}
<div className="bg-white 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>
<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 dark:text-white mb-3 md:mb-4">Team Leader</h3>
{team.leader && (
<button
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 ? (
<img
@@ -242,21 +242,21 @@ const TeamDashboardPage: FC = (): ReactElement => {
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">
<span className="text-gray-500">👤</span>
<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 dark:text-neutral-400">👤</span>
</div>
)}
<div>
<p className="font-medium text-gray-900">{team.leader.fullname}</p>
<p className="text-sm text-gray-600">{team.leader.email}</p>
<p className="font-medium text-gray-900 dark:text-white">{team.leader.fullname}</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">{team.leader.email}</p>
</div>
</button>
)}
</div>
{/* Team Members */}
<div className="bg-white 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">
<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 dark:text-white mb-3 md:mb-4">
Members ({members.length})
</h3>
<div className="space-y-3">
@@ -264,7 +264,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<button
key={member.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 ? (
<img
@@ -273,15 +273,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
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">
<span className="text-gray-500 text-sm">👤</span>
<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 dark:text-neutral-400 text-sm">👤</span>
</div>
)}
<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}
</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'}
</p>
</div>
@@ -295,22 +295,22 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Invite Member Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black/20 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">
<h2 className="text-2xl font-bold text-gray-900 mb-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 dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Invite Team Member
</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.
</p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-6">
<p className="text-sm text-yellow-800">
<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 dark:text-yellow-300">
<strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in.
</p>
</div>
<form onSubmit={handleInviteMember} className="space-y-4">
<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
</label>
<input
@@ -319,10 +319,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
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
/>
<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}
</p>
</div>
@@ -353,15 +353,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Join Requests Modal */}
{showJoinRequestsModal && (
<div className="fixed inset-0 bg-black/30 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="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 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">
<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})
</h2>
<button
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>
@@ -369,15 +369,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{pendingJoinRequests.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600 text-lg">No pending join requests</p>
<p className="text-gray-500 text-sm mt-2">
<p className="text-gray-600 dark:text-neutral-400 text-lg">No pending join requests</p>
<p className="text-gray-500 dark:text-neutral-500 text-sm mt-2">
When users request to join your team, they'll appear here
</p>
</div>
) : (
<div className="space-y-4">
{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 space-x-3 flex-1">
{request.user?.avatar ? (
@@ -387,25 +387,25 @@ const TeamDashboardPage: FC = (): ReactElement => {
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">
<span className="text-gray-500 text-xl">👤</span>
<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 dark:text-neutral-400 text-xl">👤</span>
</div>
)}
<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'}
</p>
<p className="text-sm text-gray-600">
<p className="text-sm text-gray-600 dark:text-neutral-400">
{request.user?.email}
</p>
{request.message && (
<div className="mt-2 bg-gray-50 rounded-lg p-3">
<p className="text-sm text-gray-700">
<div className="mt-2 bg-gray-50 dark:bg-neutral-800 rounded-lg p-3">
<p className="text-sm text-gray-700 dark:text-neutral-300">
<strong>Message:</strong> {request.message}
</p>
</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()}
</p>
</div>
@@ -429,8 +429,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div>
</div>
{members.length >= MAX_TEAM_MEMBERS && (
<div className="mt-3 bg-yellow-50 border border-yellow-200 rounded-lg p-2">
<p className="text-xs text-yellow-800">
<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 dark:text-yellow-300">
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS} members). Remove a member before accepting new requests.
</p>
</div>
+24 -24
View File
@@ -63,14 +63,14 @@ const BrowseTeamsPage: FC = (): ReactElement => {
});
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* 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="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Browse Teams</h1>
<p className="text-gray-600 mt-1">Find and join teams looking for members</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Browse Teams</h1>
<p className="text-gray-600 dark:text-neutral-400 mt-1">Find and join teams looking for members</p>
</div>
<Link to="/dashboard" className="hidden md:block">
<Button variant="secondary">Back to Dashboard</Button>
@@ -81,10 +81,10 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Filters */}
<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>
<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
</label>
<input
@@ -92,11 +92,11 @@ const BrowseTeamsPage: FC = (): ReactElement => {
placeholder="Search by team name..."
value={searchTerm}
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>
<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
</label>
<CitySelect
@@ -111,17 +111,17 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Teams List */}
{isLoading ? (
<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>
) : teams.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<p className="text-gray-600 text-lg">No teams found</p>
<p className="text-gray-500 mt-2">Try adjusting your filters</p>
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center">
<p className="text-gray-600 dark:text-neutral-400 text-lg">No teams found</p>
<p className="text-gray-500 dark:text-neutral-500 mt-2">Try adjusting your filters</p>
</div>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{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
src={team.banner || '/images/banner-imphnen.png'}
alt={team.name}
@@ -136,19 +136,19 @@ const BrowseTeamsPage: FC = (): ReactElement => {
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">
<span className="text-gray-500 text-xl">👥</span>
<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 dark:text-neutral-400 text-xl">👥</span>
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-gray-900 line-clamp-2">{team.name}</h3>
<div className="text-sm text-gray-600 flex gap-2">
<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 dark:text-neutral-400 flex gap-2">
<p className="truncate flex-1 min-w-0">📍 {team.city}</p>
<p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p>
</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}
</p>
<div className="space-y-3 mt-auto">
@@ -189,23 +189,23 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Join Request Modal */}
{showJoinModal && (
<div className="fixed inset-0 bg-black/30 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">
<h2 className="text-2xl font-bold text-gray-900 mb-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 dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Request to Join Team
</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
</p>
<form onSubmit={onSubmit} className="space-y-4">
<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
</label>
<textarea
{...form.register('message')}
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..."
/>
{form.formState.errors.message && (
+5 -5
View File
@@ -6,21 +6,21 @@ const TeamsLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false);
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)} />
<div className="flex-1 flex flex-col overflow-auto">
{/* 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
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" />
</svg>
</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>
<main className="flex-1 overflow-auto">
@@ -6,21 +6,21 @@ const UserDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false);
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)} />
<div className="flex-1 flex flex-col overflow-auto">
{/* 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
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" />
</svg>
</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>
<main className="flex-1 overflow-auto">
@@ -70,15 +70,15 @@ export const CitySelect: FC<CitySelectProps> = ({
}}
onClick={handleInputClick}
placeholder={placeholder}
className={`w-full h-[42px] px-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
error ? 'border-red-500' : 'border-gray-300'
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 dark:border-neutral-600'
}`}
/>
{value && !isOpen && (
<button
type="button"
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>
@@ -88,15 +88,15 @@ export const CitySelect: FC<CitySelectProps> = ({
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
{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 ? (
<ul className="py-1">
{filteredCities.map((city) => (
<li
key={city}
onClick={() => handleSelectCity(city)}
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 ${
value === city ? 'bg-blue-100 text-blue-700' : ''
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${
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}
@@ -104,7 +104,7 @@ export const CitySelect: FC<CitySelectProps> = ({
))}
</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
</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 { useMyTeams, useAuthStore, supabase } from '@imphnen-frontend-service/service';
import { useNavigate } from 'react-router';
import { toast } from 'sonner';
import { useTheme } from './theme-provider';
interface NavItem {
name: string;
@@ -21,6 +22,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
const navigate = useNavigate();
const { session, clearSession } = useAuthStore();
const { data: teamsData } = useMyTeams();
const { theme, setTheme, resolvedTheme } = useTheme();
const user = session?.user;
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 = (
<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 */}
<div className="p-6 border-b flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-900">🏆 Hackathon</h1>
<div className="p-6 border-b dark:border-neutral-700 flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Hackathon</h1>
{onClose && (
<button
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" />
</svg>
</button>
@@ -109,7 +145,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</div>
{/* 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">
{user?.avatar ? (
<img
@@ -118,15 +154,15 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
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">
<span className="text-gray-500 text-lg">👤</span>
<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 dark:text-neutral-400 text-lg">👤</span>
</div>
)}
<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'}
</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>
@@ -144,8 +180,8 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
to={item.path}
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
isActive
? 'bg-blue-50 text-blue-700 font-medium'
: 'text-gray-700 hover:bg-gray-100'
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-medium'
: 'text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800'
}`}
>
{item.icon}
@@ -157,11 +193,18 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</ul>
</nav>
{/* Logout Button */}
<div className="p-4 border-t">
{/* Theme Toggle & Logout */}
<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
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">
<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';
@source "../../../libs/ui/**/*.{ts,tsx}";
/* Enable class-based dark mode (using .dark class on html element) */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd;
@@ -108,6 +111,14 @@
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 */
::-webkit-scrollbar {
width: 6px;
@@ -118,19 +129,41 @@
background: var(--color-neutral-50);
}
.dark ::-webkit-scrollbar-track {
background: var(--color-neutral-900);
}
::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 3px;
}
.dark ::-webkit-scrollbar-thumb {
background: var(--color-neutral-600);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-500);
}
html {
font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400;
font-size: 16px;
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,
} from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner';
import { ThemeProvider } from './components/theme-provider';
import './index.css';
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(
<StrictMode>
<QueryProvider>
<ModalLoginProvider>
<Toaster position="top-right" />
<RouterProvider router={router} />
</ModalLoginProvider>
</QueryProvider>
<ThemeProvider defaultTheme="system">
<QueryProvider>
<ModalLoginProvider>
<Toaster position="top-right" richColors />
<RouterProvider router={router} />
</ModalLoginProvider>
</QueryProvider>
</ThemeProvider>
</StrictMode>
);