Merge branch 'develop' into hackathon/fix-ui

This commit is contained in:
Hafid Nur
2025-11-26 16:30:52 +07:00
28 changed files with 1047 additions and 617 deletions
+30 -26
View File
@@ -9,6 +9,7 @@ import { GithubOutlined } from '@ant-design/icons';
import { useNavigate, Link } from 'react-router'; import { useNavigate, Link } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
export default function LoginPage() { export default function LoginPage() {
// console.log('[LoginPage] Rendering...'); // console.log('[LoginPage] Rendering...');
@@ -114,28 +115,31 @@ export default function LoginPage() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200"> <div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700">
<button <div className="flex items-center justify-between mb-6">
onClick={() => navigate('/')} <button
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center mb-6" onClick={() => navigate('/')}
> className="cursor-pointer text-primary-500 hover:text-primary-600 dark:text-primary-400 dark:hover:text-primary-300 text-base font-sans flex items-center"
<Icon icon="ic:baseline-chevron-left" width="24" height="24" /> >
Back to Homepage <Icon icon="ic:baseline-chevron-left" width="24" height="24" />
</button> Back to Homepage
</button>
<ThemeToggle />
</div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2"> <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Welcome Back Welcome Back
</h2> </h2>
<p className="text-gray-600 font-sans"> <p className="text-gray-600 dark:text-neutral-400 font-sans">
Sign in to join or create your hackathon team Sign in to join or create your hackathon team
</p> </p>
</div> </div>
{error && ( {error && (
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="mb-6 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-600 text-sm">{error}</p> <p className="text-red-600 dark:text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
@@ -143,7 +147,7 @@ export default function LoginPage() {
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Email Email
</label> </label>
@@ -154,7 +158,7 @@ export default function LoginPage() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com" placeholder="your@email.com"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -163,13 +167,13 @@ export default function LoginPage() {
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<label <label
htmlFor="password" htmlFor="password"
className="block text-sm font-medium text-gray-700" className="block text-sm font-medium text-gray-700 dark:text-neutral-300"
> >
Password Password
</label> </label>
<Link <Link
to="/auth/forgot-password" to="/auth/forgot-password"
className="text-sm text-primary-600 hover:text-primary-700" className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
> >
Forgot password? Forgot password?
</Link> </Link>
@@ -181,7 +185,7 @@ export default function LoginPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -189,23 +193,23 @@ export default function LoginPage() {
<button <button
type="submit" type="submit"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-400 dark:disabled:bg-neutral-600 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
{isEmailLoading ? 'Signing in...' : 'Sign in with Email'} {isEmailLoading ? 'Signing in...' : 'Sign in with Email'}
</button> </button>
</form> </form>
<div className="my-6 flex items-center"> <div className="my-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
<span className="px-4 text-sm text-gray-500">OR</span> <span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
</div> </div>
<button <button
onClick={handleGithubLogin} onClick={handleGithubLogin}
disabled={isGithubLoading} disabled={isGithubLoading}
type="button" type="button"
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 border border-gray-300 rounded-lg font-semibold hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg font-semibold text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-100 dark:disabled:bg-neutral-800 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
<GithubOutlined className="text-xl" /> <GithubOutlined className="text-xl" />
<span> <span>
@@ -214,11 +218,11 @@ export default function LoginPage() {
</button> </button>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600 text-sm"> <p className="text-gray-600 dark:text-neutral-400 text-sm">
Don't have an account?{' '} Don't have an account?{' '}
<a <a
href="/auth/signup" href="/auth/signup"
className="text-primary-600 hover:text-primary-700 font-semibold" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-semibold"
> >
Sign up Sign up
</a> </a>
@@ -226,7 +230,7 @@ export default function LoginPage() {
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-500 text-xs"> <p className="text-gray-500 dark:text-neutral-500 text-xs">
By signing in, you agree to our Terms of Service and Privacy Policy By signing in, you agree to our Terms of Service and Privacy Policy
</p> </p>
</div> </div>
+33 -29
View File
@@ -9,6 +9,7 @@ import { GithubOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ThemeToggle } from '../../../components/theme-toggle';
export default function SignupPage() { export default function SignupPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -137,26 +138,29 @@ export default function SignupPage() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200"> <div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700">
<button <div className="flex items-center justify-between mb-6">
onClick={() => navigate('/')} <button
className="cursor-pointer text-primary-500 hover:text-primary-600 text-base font-sans flex items-center mb-6" onClick={() => navigate('/')}
> className="cursor-pointer text-primary-500 hover:text-primary-600 dark:text-primary-400 dark:hover:text-primary-300 text-base font-sans flex items-center"
<Icon icon="ic:baseline-chevron-left" width="24" height="24" /> >
Back to Homepage <Icon icon="ic:baseline-chevron-left" width="24" height="24" />
</button> Back to Homepage
</button>
<ThemeToggle />
</div>
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-2"> <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Create Account Create Account
</h2> </h2>
<p className="text-gray-600">Join the hackathon community</p> <p className="text-gray-600 dark:text-neutral-400">Join the hackathon community</p>
</div> </div>
{error && ( {error && (
<div className="mb-6 p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="mb-6 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-600 text-sm">{error}</p> <p className="text-red-600 dark:text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
@@ -164,7 +168,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="fullname" htmlFor="fullname"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Full Name Full Name
</label> </label>
@@ -175,7 +179,7 @@ export default function SignupPage() {
onChange={(e) => setFullname(e.target.value)} onChange={(e) => setFullname(e.target.value)}
placeholder="John Doe" placeholder="John Doe"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -183,7 +187,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Email Email
</label> </label>
@@ -194,7 +198,7 @@ export default function SignupPage() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com" placeholder="your@email.com"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -202,7 +206,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="password" htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Password Password
</label> </label>
@@ -213,7 +217,7 @@ export default function SignupPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -221,7 +225,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="confirmPassword" htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-1" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1"
> >
Confirm Password Confirm Password
</label> </label>
@@ -232,7 +236,7 @@ export default function SignupPage() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" className="w-full px-4 py-2.5 border border-gray-300 dark:border-neutral-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 disabled:bg-gray-100 dark:disabled:bg-neutral-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -240,23 +244,23 @@ export default function SignupPage() {
<button <button
type="submit" type="submit"
disabled={isEmailLoading} disabled={isEmailLoading}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-400 dark:disabled:bg-neutral-600 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
{isEmailLoading ? 'Creating account...' : 'Create Account'} {isEmailLoading ? 'Creating account...' : 'Create Account'}
</button> </button>
</form> </form>
<div className="my-6 flex items-center"> <div className="my-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
<span className="px-4 text-sm text-gray-500">OR</span> <span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
<div className="flex-1 border-t border-gray-300"></div> <div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
</div> </div>
<button <button
onClick={handleGithubLogin} onClick={handleGithubLogin}
disabled={isGithubLoading} disabled={isGithubLoading}
type="button" type="button"
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 border border-gray-300 rounded-lg font-semibold hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors cursor-pointer" className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg font-semibold text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-neutral-900 disabled:bg-gray-100 dark:disabled:bg-neutral-800 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
<GithubOutlined className="text-xl" /> <GithubOutlined className="text-xl" />
<span> <span>
@@ -265,11 +269,11 @@ export default function SignupPage() {
</button> </button>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600 text-sm"> <p className="text-gray-600 dark:text-neutral-400 text-sm">
Already have an account?{' '} Already have an account?{' '}
<a <a
href="/auth/login" href="/auth/login"
className="text-primary-600 hover:text-primary-700 font-semibold" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-semibold"
> >
Sign in Sign in
</a> </a>
@@ -277,7 +281,7 @@ export default function SignupPage() {
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-500 text-xs"> <p className="text-gray-500 dark:text-neutral-500 text-xs">
By signing up, you agree to our Terms of Service and Privacy Policy By signing up, you agree to our Terms of Service and Privacy Policy
</p> </p>
</div> </div>
+7 -5
View File
@@ -6,18 +6,18 @@ const DashboardLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden sticky top-0 bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden sticky top-0 bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg <svg
className="w-6 h-6 text-gray-600" className="w-6 h-6 text-gray-600 dark:text-neutral-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -30,7 +30,9 @@ const DashboardLayout: FC = (): ReactElement => {
/> />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">
Hackathon
</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
+35 -31
View File
@@ -43,11 +43,11 @@ const DashboardPage: FC = (): ReactElement => {
return ( return (
<div className="p-8 max-w-7xl mx-auto"> <div className="p-8 max-w-7xl mx-auto">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}! Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
</h1> </h1>
{user?.location && ( {user?.location && (
<p className="text-gray-600 mt-1 font-sans flex items-center space-x-1"> <p className="text-gray-600 dark:text-neutral-400 mt-1 font-sans flex items-center space-x-1">
<Icon icon="heroicons:map-pin-16-solid" width="24" height="24" /> <Icon icon="heroicons:map-pin-16-solid" width="24" height="24" />
<span>{user.location}</span> <span>{user.location}</span>
</p> </p>
@@ -55,21 +55,21 @@ const DashboardPage: FC = (): ReactElement => {
</div> </div>
{invitations.length > 0 && ( {invitations.length > 0 && (
<div className="mb-8 bg-primary-50 border border-primary-200 rounded-lg p-6"> <div className="mb-8 bg-primary-50 dark:bg-blue-900/20 border border-primary-200 dark:border-blue-800 rounded-lg p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4"> <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Team Invitations ({invitations.length}) Team Invitations ({invitations.length})
</h2> </h2>
<div className="space-y-3 font-sans"> <div className="space-y-3 font-sans">
{invitations.map((invitation) => ( {invitations.map((invitation) => (
<div <div
key={invitation.id} key={invitation.id}
className="bg-white p-4 rounded-lg shadow-sm flex items-center justify-between" className="bg-white dark:bg-neutral-800 p-4 rounded-lg shadow-sm flex items-center justify-between"
> >
<div> <div>
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900 dark:text-white">
{invitation.team.name} {invitation.team.name}
</p> </p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
Invited by {invitation.inviter.fullname} Invited by {invitation.inviter.fullname}
</p> </p>
</div> </div>
@@ -96,7 +96,7 @@ const DashboardPage: FC = (): ReactElement => {
{myTeams.length > 0 ? ( {myTeams.length > 0 ? (
<div className="mb-6 md:mb-8"> <div className="mb-6 md:mb-8">
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mb-3 md:mb-4"> <h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
My Team My Team
</h2> </h2>
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
@@ -104,7 +104,7 @@ const DashboardPage: FC = (): ReactElement => {
<Link <Link
key={team.id} key={team.id}
to={'/teams/' + team.id} to={'/teams/' + team.id}
className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow" className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
> >
{team.banner && ( {team.banner && (
<img <img
@@ -123,15 +123,15 @@ const DashboardPage: FC = (): ReactElement => {
/> />
)} )}
<div> <div>
<h3 className="text-base md:text-lg font-bold text-gray-900"> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white">
{team.name} {team.name}
</h3> </h3>
<p className="text-xs md:text-sm text-gray-600"> <p className="text-xs md:text-sm text-gray-600 dark:text-neutral-400">
City: {team.city} City: {team.city}
</p> </p>
</div> </div>
</div> </div>
<p className="text-gray-600 text-xs md:text-sm line-clamp-2"> <p className="text-gray-600 dark:text-neutral-400 text-xs md:text-sm line-clamp-2">
{team.description} {team.description}
</p> </p>
</div> </div>
@@ -140,20 +140,20 @@ const DashboardPage: FC = (): ReactElement => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="mb-6 md:mb-8 bg-white rounded-lg shadow-md p-6 md:p-8"> <div className="mb-6 md:mb-8 bg-white dark:bg-neutral-800 rounded-lg shadow-md p-6 md:p-8">
<div className="text-center"> <div className="text-center">
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div> <div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mb-1 md:mb-2"> <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 You are not in a team yet
</h2> </h2>
<p className="text-sm md:text-base text-gray-600 font-sans"> <p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 font-sans">
Use the sidebar to browse teams or create your own Use the sidebar to browse teams or create your own
</p> </p>
</div> </div>
</div> </div>
)} )}
<div className="mt-8 bg-linear-to-br from-white-50 to-primary-50 rounded-xl shadow-lg"> <div className="mt-8 bg-linear-to-br from-white-50 to-primary-50 dark:from-neutral-800 dark:to-neutral-900 rounded-xl shadow-lg">
<div className="bg-linear-to-r from-primary-600 to-indigo-600 h-24 rounded-t-xl"></div> <div className="bg-linear-to-r from-primary-600 to-indigo-600 h-24 rounded-t-xl"></div>
<div className="px-4 md:px-8 pb-4 md:pb-8"> <div className="px-4 md:px-8 pb-4 md:pb-8">
<div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6"> <div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6">
@@ -162,30 +162,30 @@ const DashboardPage: FC = (): ReactElement => {
<img <img
src={user.avatar} src={user.avatar}
alt={user.fullname || 'User'} 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" className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg object-cover"
/> />
) : ( ) : (
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white shadow-lg bg-gray-200 flex items-center justify-center"> <div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-400 text-3xl md:text-4xl font-sans"> <span className="text-gray-400 dark:text-neutral-500 text-3xl md:text-4xl font-sans">
U U
</span> </span>
</div> </div>
)} )}
<div className="md:ml-6 mt-4 md:mt-14"> <div className="md:ml-6 mt-4 md:mt-14">
<h2 className="text-xl md:text-2xl font-bold text-gray-900"> <h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">
{user?.fullname || {user?.fullname ||
user?.email?.split('@')[0] || user?.email?.split('@')[0] ||
'Unnamed User'} 'Unnamed User'}
</h2> </h2>
{user?.location ? ( {user?.location ? (
<p className="text-gray-600 flex items-center mt-1 text-sm md:text-base font-sans"> <p className="text-gray-600 dark:text-neutral-400 flex items-center mt-1 text-sm md:text-base font-sans">
{user.location} {user.location}
</p> </p>
) : ( ) : (
<Link <Link
to="/onboarding/user" to="/onboarding/user"
className="text-primary-500 hover:text-primary-600 text-sm md:text-sm mt-1 flex items-center" className="text-primary-500 dark:text-blue-400 hover:text-primary-600 dark:hover:text-blue-300 text-sm md:text-sm mt-1 flex items-center"
> >
<span className="font-sans">Complete your profile</span> <span className="font-sans">Complete your profile</span>
<Icon <Icon
@@ -208,24 +208,28 @@ const DashboardPage: FC = (): ReactElement => {
</div> </div>
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{user?.bio && ( {user?.bio && (
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2"> <h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-2">
About About
</h3> </h3>
<p className="text-gray-800 leading-relaxed">{user.bio}</p> <p className="text-gray-800 dark:text-neutral-200 leading-relaxed">
{user.bio}
</p>
</div> </div>
)} )}
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="font-semibold text-gray-700 tracking-wide mb-3"> <h3 className="font-semibold text-gray-700 dark:text-neutral-300 tracking-wide mb-3">
Contact Contact
</h3> </h3>
<div className="flex items-center text-gray-700"> <div className="flex items-center text-gray-700 dark:text-neutral-300">
<span className="text-gray-900">{user?.email}</span> <span className="text-gray-900 dark:text-white">
{user?.email}
</span>
</div> </div>
</div> </div>
{user?.skills && user.skills.length > 0 && ( {user?.skills && user.skills.length > 0 && (
<div className="bg-white rounded-lg p-4 shadow-sm"> <div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="font-semibold text-gray-700 tracking-wide mb-3"> <h3 className="font-semibold text-gray-700 dark:text-neutral-300 tracking-wide mb-3">
Skills Skills
</h3> </h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
+2 -2
View File
@@ -107,10 +107,10 @@ export default function RootLayout() {
// Show loading state while checking auth // Show loading state while checking auth
if (isChecking) { if (isChecking) {
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-center"> <div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div> <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
<p className="text-gray-600">Loading...</p> <p className="text-gray-600 dark:text-neutral-400">Loading...</p>
</div> </div>
</div> </div>
); );
+20 -15
View File
@@ -110,13 +110,13 @@ const UserOnboardingPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 px-4 py-8"> <div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 px-4 py-8">
<div className="bg-white w-full max-w-2xl p-8 rounded-xl shadow-lg"> <div className="bg-white dark:bg-neutral-900 w-full max-w-2xl p-8 rounded-xl shadow-lg dark:shadow-neutral-950/50">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 mb-2"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Complete Your Profile Complete Your Profile
</h1> </h1>
<p className="text-gray-600 font-sans"> <p className="text-gray-600 font-sans dark:text-neutral-400">
Tell us more about yourself to get started Tell us more about yourself to get started
</p> </p>
</div> </div>
@@ -129,22 +129,22 @@ const UserOnboardingPage: FC = (): ReactElement => {
<img <img
src={avatarPreview} src={avatarPreview}
alt="Avatar preview" alt="Avatar preview"
className="w-32 h-32 rounded-full object-cover border-4 border-gray-200" className="w-32 h-32 rounded-full object-cover border-4 border-gray-200 dark:border-neutral-700"
/> />
) : ( ) : (
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-32 h-32 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<Icon <Icon
icon="ic:baseline-person" icon="ic:baseline-person"
width="48" width="48"
height="48" height="48"
className="text-gray-400" className="text-gray-400 dark:text-neutral-500"
/> />
</div> </div>
)} )}
</div> </div>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<label htmlFor="avatar" className="cursor-pointer"> <label htmlFor="avatar" className="cursor-pointer">
<span className="px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 inline-block"> <span className="px-4 py-2 bg-primary-500 dark:bg-primary-600 text-white rounded-lg hover:bg-primary-600 dark:hover:bg-primary-700 inline-block">
{avatarPreview ? 'Change Photo' : 'Upload Photo'} {avatarPreview ? 'Change Photo' : 'Upload Photo'}
</span> </span>
<input <input
@@ -155,7 +155,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
onChange={handleAvatarChange} onChange={handleAvatarChange}
/> />
</label> </label>
<p className="text-xs text-gray-500 mt-2 text-center"> <p className="text-xs text-gray-500 dark:text-neutral-500 mt-2 text-center">
Optional, but highly recommended Optional, but highly recommended
</p> </p>
</div> </div>
@@ -174,7 +174,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
{/* City */} {/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-base font-medium text-gray-700"> <label className="block text-base font-medium text-gray-700 dark:text-neutral-300">
City <span className="text-red-500">*</span> City <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
@@ -193,7 +193,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
{/* Role/Skills */} {/* Role/Skills */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-base font-medium text-gray-700"> <label className="block text-base font-medium text-gray-700 dark:text-neutral-300">
Role / Skills Role / Skills
</label> </label>
<Controller <Controller
@@ -216,9 +216,11 @@ const UserOnboardingPage: FC = (): ReactElement => {
: (field.value || []).filter((v) => v !== role); : (field.value || []).filter((v) => v !== role);
field.onChange(newValue); field.onChange(newValue);
}} }}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" className="rounded border-gray-300 dark:border-neutral-600 text-blue-600 dark:text-primary-500 focus:ring-blue-500 dark:focus:ring-primary-500 dark:bg-neutral-800"
/> />
<span className="text-sm">{role}</span> <span className="text-sm dark:text-neutral-300">
{role}
</span>
</label> </label>
))} ))}
</div> </div>
@@ -229,8 +231,11 @@ const UserOnboardingPage: FC = (): ReactElement => {
{/* Bio */} {/* Bio */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-base font-medium text-gray-700"> <label className="block text-base font-medium text-gray-700 dark:text-neutral-300">
Bio <span className="text-gray-400">(Optional)</span> Bio{' '}
<span className="text-gray-400 dark:text-neutral-500">
(Optional)
</span>
</label> </label>
<Controller <Controller
control={form.control} control={form.control}
+205 -134
View File
@@ -2,11 +2,15 @@ import { useNavigate } from 'react-router';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { ThemeToggle } from '../components/theme-toggle';
import { useAuthStore } from '@imphnen-frontend-service/service';
export default function HomePage() { export default function HomePage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [openFaq, setOpenFaq] = useState<number | null>(0); const [openFaq, setOpenFaq] = useState<number | null>(0);
const { session } = useAuthStore();
const isAuthenticated = !!session?.token;
const faqs = [ const faqs = [
{ {
@@ -62,13 +66,15 @@ export default function HomePage() {
]; ];
return ( return (
<main className="min-h-screen bg-white"> <main className="min-h-screen bg-white dark:bg-neutral-950">
{/* Navigation */} {/* Navigation */}
<div id="#top" className="hidden"></div> <div id="#top" className="hidden"></div>
<nav className="border-b border-gray-200 bg-white sticky top-0 z-50"> <nav className="border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 sticky top-0 z-50">
<div className="flex items-center justify-between max-w-7xl mx-auto px-4 md:px-8 py-4"> <div className="flex items-center justify-between max-w-7xl mx-auto px-4 md:px-8 py-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg md:text-xl font-bold">IMPHNEN</span> <span className="text-lg md:text-xl font-bold dark:text-white">
IMPHNEN
</span>
<a <a
href="#top" href="#top"
className="text-lg md:text-xl font-bold text-primary-500 hover:cursor-pointer" className="text-lg md:text-xl font-bold text-primary-500 hover:cursor-pointer"
@@ -80,91 +86,121 @@ export default function HomePage() {
<div className="hidden md:flex text-label1 items-center gap-4 lg:gap-8"> <div className="hidden md:flex text-label1 items-center gap-4 lg:gap-8">
<a <a
href="#timeline" href="#timeline"
className="text-gray-600 hover:text-gray-900 transition-colors" className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
Timeline Timeline
</a> </a>
<a <a
href="#hadiah" href="#hadiah"
className="text-gray-600 hover:text-gray-900 transition-colors" className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
Hadiah Hadiah
</a> </a>
<a <a
href="#faq" href="#faq"
className="text-gray-600 hover:text-gray-900 transition-colors" className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
FAQ FAQ
</a> </a>
<Button <ThemeToggle />
onClick={() => navigate('/auth/login')} {isAuthenticated ? (
size="sm" <Button
variant="bordered" onClick={() => navigate('/dashboard')}
className="rounded-lg text-base" size="sm"
> className="rounded-lg text-base"
Masuk >
</Button> Dashboard
<Button </Button>
onClick={() => navigate('/auth/signup')} ) : (
size="sm" <>
className="rounded-lg text-base" <Button
> onClick={() => navigate('/auth/login')}
Daftar Sekarang size="sm"
</Button> variant="bordered"
className="rounded-lg text-base"
>
Masuk
</Button>
<Button
onClick={() => navigate('/auth/signup')}
size="sm"
className="rounded-lg text-base"
>
Daftar Sekarang
</Button>
</>
)}
</div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <div className="flex items-center gap-2 md:hidden">
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} <ThemeToggle />
className="md:hidden p-2 cursor-pointer" <button
> onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
<svg className="p-2 cursor-pointer text-gray-900 dark:text-white"
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-6 h-6"
strokeLinejoin="round" fill="none"
strokeWidth={2} stroke="currentColor"
d="M4 6h16M4 12h16M4 18h16" viewBox="0 0 24 24"
/> >
</svg> <path
</button> strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div> </div>
</nav> </nav>
{/* Mobile Menu */} {/* Mobile Menu */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<nav className="md:hidden sticky top-18 border-b border-gray-200 bg-white z-40"> <nav className="md:hidden sticky top-18 border-b border-gray-200 dark:border-neutral-800 bg-white z-40 dark:bg-neutral-900">
<div className="flex flex-col items-start gap-4 px-4 py-4"> <div className="flex flex-col items-start gap-4 px-4 py-4">
<a <a
href="#timeline" href="#timeline"
className="ms-3 text-gray-600 hover:text-gray-900" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
> >
Timeline Timeline
</a> </a>
<a <a
href="#hadiah" href="#hadiah"
className="ms-3 text-gray-600 hover:text-gray-900" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
> >
Hadiah Hadiah
</a> </a>
<a href="#faq" className="ms-3 text-gray-600 hover:text-gray-900"> <a
href="#faq"
className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
>
FAQ FAQ
</a> </a>
<a {isAuthenticated ? (
href="/auth/login" <button
className="ms-3 text-gray-600 hover:text-gray-900" onClick={() => navigate('/dashboard')}
> className="px-4 py-2 bg-primary-500 text-white text-base rounded-lg hover:bg-primary-600 transition-colors text-center cursor-pointer"
Masuk >
</a> Dashboard
<button </button>
onClick={() => navigate('/auth/login')} ) : (
className="px-4 py-2 bg-primary-500 text-white text-base rounded-lg hover:bg-primary-600 transition-colors text-center cursor-pointer" <>
> <a
Daftar Sekarang href="#masuk"
</button> className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
>
Masuk
</a>
<button
onClick={() => navigate('/auth/login')}
className="px-4 py-2 bg-primary-500 text-white text-base rounded-lg hover:bg-primary-600 transition-colors text-center cursor-pointer"
>
Daftar Sekarang
</button>
</>
)}
</div> </div>
</nav> </nav>
)} )}
@@ -180,7 +216,7 @@ export default function HomePage() {
className="absolute bottom-1/3 -right-20 w-100 h-100 rounded-full bg-linear-to-r from-blue-400/20 to-primary/20 blur-3xl" className="absolute bottom-1/3 -right-20 w-100 h-100 rounded-full bg-linear-to-r from-blue-400/20 to-primary/20 blur-3xl"
style={{ transform: 'translate(0px, 0px)', opacity: 1 }} style={{ transform: 'translate(0px, 0px)', opacity: 1 }}
></div> ></div>
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-size-[40px_40px]"></div> <div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] dark:bg-[linear-gradient(rgba(59,130,246,0.1)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.1)_1px,transparent_1px)] bg-size-[40px_40px]"></div>
</div> </div>
<div className="mx-auto container px-4 relative flex flex-col items-center"> <div className="mx-auto container px-4 relative flex flex-col items-center">
{/* Logos */} {/* Logos */}
@@ -192,19 +228,19 @@ export default function HomePage() {
className="h-12 md:h-16" className="h-12 md:h-16"
/> />
</div> </div>
<span className="text-3xl md:text-5xl font-bold text-gray-400"> <span className="text-3xl md:text-5xl font-bold text-gray-400 dark:text-neutral-500">
× ×
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<img <img
src="images/sponsors/kolosal-logo_rlxbck.svg" src="images/sponsors/kolosal-logo_rlxbck.svg"
alt="Kolosal.ai" alt="Kolosal.ai"
className="h-8 md:h-12" className="h-8 md:h-12 dark:invert"
/> />
</div> </div>
</div> </div>
{/* Title */} {/* Title */}
<h1 className="text-h1 font-bold text-gray-900 mb-4 text-center"> <h1 className="text-h1 font-bold text-gray-900 dark:text-white mb-4 text-center">
Hackathon Hackathon
</h1> </h1>
{/* Subtitle */} {/* Subtitle */}
@@ -212,12 +248,12 @@ export default function HomePage() {
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif" "Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
</p> </p>
{/* Description */} {/* Description */}
<p className="text-p3 text-gray-600 max-w-lg md:max-w-xl text-center mb-8 md:mb-12 px-4 font-sans"> <p className="text-p3 text-gray-600 dark:text-neutral-400 max-w-lg md:max-w-xl text-center mb-8 md:mb-12 px-4 font-sans">
Kompetisi pengembangan teknologi untuk menciptakan solusi inovatif Kompetisi pengembangan teknologi untuk menciptakan solusi inovatif
yang menghadirkan dampak nyata yang menghadirkan dampak nyata
</p> </p>
{/* Status */} {/* Status */}
<div className="flex items-center gap-4 md:gap-8 mb-10 md:mb-16 text-base text-gray-600"> <div className="flex items-center gap-4 md:gap-8 mb-10 md:mb-16 text-base text-gray-600 dark:text-neutral-400">
<div className="flex items-center gap-2 md:gap-3"> <div className="flex items-center gap-2 md:gap-3">
<Icon icon="streamline-plump:web" width="16" height="16" /> <Icon icon="streamline-plump:web" width="16" height="16" />
<span>Online</span> <span>Online</span>
@@ -239,7 +275,7 @@ export default function HomePage() {
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL" href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center justify-center px-4 py-2.5 border-2 border-gray-300 hover:border-gray-400 transition-colors text-center bg-transparent hover:text-gray-900 hover:bg-gray-50 text-base text-gray-600 rounded-lg font-bai-jamjuree font-semibold" className="inline-flex items-center justify-center px-4 py-2.5 border-2 border-gray-300 dark:border-neutral-600 hover:border-gray-400 dark:hover:border-neutral-500 transition-colors text-center bg-transparent hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 text-base text-gray-600 dark:text-neutral-400 rounded-lg font-bai-jamjuree font-semibold"
> >
Gabung Grup WA Hackathon Gabung Grup WA Hackathon
</a> </a>
@@ -247,7 +283,7 @@ export default function HomePage() {
{/* Scroll indicator */} {/* Scroll indicator */}
<div className="mt-12 md:mt-20"> <div className="mt-12 md:mt-20">
<svg <svg
className="w-6 h-6 text-gray-400 animate-bounce" className="w-6 h-6 text-gray-400 dark:text-neutral-500 animate-bounce"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -264,17 +300,19 @@ export default function HomePage() {
</section> </section>
{/* About Section */} {/* About Section */}
<section className="py-16 md:py-24 px-4 md:px-8 bg-white"> <section className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950">
<div className="max-w-4xl mx-auto text-center"> <div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-5xl font-bold mb-10"> <h2 className="text-3xl md:text-5xl font-bold mb-10 dark:text-white">
Tentang <span className="text-primary-500">Hackathon</span> Tentang <span className="text-primary-500">Hackathon</span>
</h2> </h2>
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 mb-6"> <p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400 mb-6">
<span className="font-semibold text-gray-900">Hackathon</span>{' '} <span className="font-semibold text-gray-900 dark:text-white">
Hackathon
</span>{' '}
adalah kompetisi pengembangan teknologi yang mengajak talenta muda adalah kompetisi pengembangan teknologi yang mengajak talenta muda
untuk berkolaborasi dalam menciptakan solusi inovatif. untuk berkolaborasi dalam menciptakan solusi inovatif.
</p> </p>
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600"> <p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400">
IMPHNEN bersama Kolosal.ai mengadakan Hackathon dengan tema lomba{' '} IMPHNEN bersama Kolosal.ai mengadakan Hackathon dengan tema lomba{' '}
<span className="font-semibold text-primary-500"> <span className="font-semibold text-primary-500">
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif" "Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
@@ -285,10 +323,13 @@ export default function HomePage() {
</section> </section>
{/* Prizes Section */} {/* Prizes Section */}
<section id="hadiah" className="py-16 md:py-24 px-4 md:px-8 bg-gray-50"> <section
id="hadiah"
className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900"
>
<div className="max-w-lg md:max-w-6xl mx-auto"> <div className="max-w-lg md:max-w-6xl mx-auto">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl md:text-5xl font-bold mb-4 font-bai-jamjuree"> <h2 className="text-3xl md:text-5xl font-bold mb-4 font-bai-jamjuree dark:text-white">
Hadiah <span className="text-primary-500">Menarik</span> Hadiah <span className="text-primary-500">Menarik</span>
</h2> </h2>
<p className="text-xl md:text-2xl text-primary-500 font-semibold font-sans"> <p className="text-xl md:text-2xl text-primary-500 font-semibold font-sans">
@@ -298,67 +339,73 @@ export default function HomePage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Prize 1 */} {/* Prize 1 */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-primary-500 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-primary-500 dark:hover:border-primary-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-primary-500" className="h-8 w-8 text-primary-500"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2">Juara 1</h3> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">
Juara 1
</h3>
<p className="text-2xl font-bold text-primary-500 text-center font-sans"> <p className="text-2xl font-bold text-primary-500 text-center font-sans">
Rp6.000.000 Rp6.000.000
</p> </p>
</div> </div>
{/* Prize 2 */} {/* Prize 2 */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-gray-500 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-gray-500 dark:hover:border-neutral-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-gray-100 dark:bg-neutral-700 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-gray-600" className="h-8 w-8 text-gray-600 dark:text-neutral-400"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2">Juara 2</h3> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">
Juara 2
</h3>
<p className="text-2xl font-bold text-primary-500 text-center font-sans"> <p className="text-2xl font-bold text-primary-500 text-center font-sans">
Rp4.000.000 Rp4.000.000
</p> </p>
</div> </div>
{/* Prize 3 */} {/* Prize 3 */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-orange-300 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-orange-300 dark:hover:border-orange-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-orange-600" className="h-8 w-8 text-orange-600 dark:text-orange-500"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2">Juara 3</h3> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">
<p className="text-2xl font-bold text-orange-600 text-center font-sans"> Juara 3
</h3>
<p className="text-2xl font-bold text-orange-600 dark:text-orange-500 text-center font-sans">
Rp2.500.000 Rp2.500.000
</p> </p>
</div> </div>
{/* Special Prize */} {/* Special Prize */}
<div className="bg-white rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 hover:border-purple-300 transition-colors"> <div className="bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-neutral-700 hover:border-purple-300 dark:hover:border-purple-500 transition-colors">
<div className="flex justify-center mb-4"> <div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
<Icon <Icon
icon="ic:round-star" icon="ic:round-star"
className="h-8 w-8 text-purple-600" className="h-8 w-8 text-purple-600 dark:text-purple-500"
/> />
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-center mb-2"> <h3 className="text-xl font-bold text-center mb-2 dark:text-white">
Juara Kategori Lainnya Juara Kategori Lainnya
</h3> </h3>
<p className="text-2xl font-bold text-purple-600 text-center font-sans"> <p className="text-2xl font-bold text-purple-600 dark:text-purple-500 text-center font-sans">
Rp2.000.000 Rp2.000.000
</p> </p>
</div> </div>
@@ -367,13 +414,16 @@ export default function HomePage() {
</section> </section>
{/* Timeline Section */} {/* Timeline Section */}
<section id="timeline" className="py-16 md:py-24 px-4 md:px-8 bg-white"> <section
id="timeline"
className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950"
>
<div className="max-w-4xl mx-auto font-sans"> <div className="max-w-4xl mx-auto font-sans">
<div className="text-center mb-12 font-bai-jamjuree"> <div className="text-center mb-12 font-bai-jamjuree">
<h2 className="text-3xl md:text-5xl font-bold mb-4"> <h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
Timeline <span className="text-primary-500">Acara</span> Timeline <span className="text-primary-500">Acara</span>
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600 dark:text-neutral-400">
Jadwal lengkap pelaksanaan hackathon Jadwal lengkap pelaksanaan hackathon
</p> </p>
</div> </div>
@@ -383,14 +433,16 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
30 November 2025 30 November 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Penutupan Registrasi</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">
<p className="text-gray-600"> Penutupan Registrasi
</h3>
<p className="text-gray-600 dark:text-neutral-400">
Batas akhir pendaftaran peserta hackathon. Batas akhir pendaftaran peserta hackathon.
</p> </p>
</div> </div>
@@ -400,14 +452,16 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
30 November 2025 30 November 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Technical Meeting</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">
<p className="text-gray-600"> Technical Meeting
</h3>
<p className="text-gray-600 dark:text-neutral-400">
Akan diadakan technical meeting terkait lomba melalui Google Akan diadakan technical meeting terkait lomba melalui Google
Meet. Stay tune di grup WA Hackathon. Meet. Stay tune di grup WA Hackathon.
</p> </p>
@@ -418,14 +472,16 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
1 - 7 Desember 2025 1 - 7 Desember 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Tahap Penyisihan</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">
<p className="text-gray-600"> Tahap Penyisihan
</h3>
<p className="text-gray-600 dark:text-neutral-400">
Peserta mengerjakan tantangan yang diberikan. Peserta mengerjakan tantangan yang diberikan.
</p> </p>
</div> </div>
@@ -435,14 +491,16 @@ export default function HomePage() {
<div className="flex gap-6"> <div className="flex gap-6">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-4 h-4 bg-primary-500 rounded-full"></div> <div className="w-4 h-4 bg-primary-500 rounded-full"></div>
<div className="w-0.5 h-full bg-gray-300"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
8 - 14 Desember 2025 8 - 14 Desember 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Penilaian & Webinar</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">
<p className="text-gray-600"> Penilaian & Webinar
</h3>
<p className="text-gray-600 dark:text-neutral-400">
Proses penilaian oleh juri dan sesi webinar. Proses penilaian oleh juri dan sesi webinar.
</p> </p>
</div> </div>
@@ -457,8 +515,10 @@ export default function HomePage() {
<p className="text-primary-500 font-semibold mb-2"> <p className="text-primary-500 font-semibold mb-2">
15 Desember 2025 15 Desember 2025
</p> </p>
<h3 className="text-xl font-bold mb-2">Pengumuman & Final</h3> <h3 className="text-xl font-bold mb-2 dark:text-white">
<p className="text-gray-600"> Pengumuman & Final
</h3>
<p className="text-gray-600 dark:text-neutral-400">
Presentasi final dan pengumuman pemenang. Presentasi final dan pengumuman pemenang.
</p> </p>
</div> </div>
@@ -468,47 +528,50 @@ export default function HomePage() {
</section> </section>
{/* Judges Section */} {/* Judges Section */}
<section className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 font-sans"> <section className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900 font-sans">
<div className="md:max-w-6xl mx-auto"> <div className="md:max-w-6xl mx-auto">
<div className="text-center mb-12 font-bai-jamjuree"> <div className="text-center mb-12 font-bai-jamjuree">
<h2 className="text-3xl md:text-5xl font-bold mb-4"> <h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
Dewan <span className="text-primary-500">Juri</span> Dewan <span className="text-primary-500">Juri</span>
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600 dark:text-neutral-400">
Perwakilan dari IMPHNEN dan Kolosal.ai yang akan menilai karya Perwakilan dari IMPHNEN dan Kolosal.ai yang akan menilai karya
</p> </p>
</div> </div>
<div className="w-full max-w-md md:max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="w-full max-w-md md:max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Judge 1 */} {/* Judge 1 */}
<div className="flex flex-col justify-between bg-white rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
{/* <div className="w-24 h-24 bg-gray-200 rounded-full mx-auto mb-4"></div> */} <h3 className="text-p3 font-bold mb-1 dark:text-white">
<h3 className="text-p3 font-bold mb-1">
Alifais Farrel Ramdhani Alifais Farrel Ramdhani
</h3> </h3>
<div> <div>
<p className="text-primary-500 font-semibold mb-1">CTO</p> <p className="text-primary-500 font-semibold mb-1">CTO</p>
<p className="text-gray-600">Kolosal.ai</p> <p className="text-gray-600 dark:text-neutral-400">
Kolosal.ai
</p>
</div> </div>
</div> </div>
{/* Judge 2 */} {/* Judge 2 */}
<div className="flex flex-col justify-between bg-white rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
{/* <div className="w-24 h-24 bg-gray-200 rounded-full mx-auto mb-4"></div> */} <h3 className="text-p3 font-bold mb-1 dark:text-white">
<h3 className="text-p3 font-bold mb-1">Anka Tama</h3> Anka Tama
</h3>
<div> <div>
<p className="text-primary-500 font-semibold mb-1">Admin</p> <p className="text-primary-500 font-semibold mb-1">Admin</p>
<p className="text-gray-600">IMPHNEN</p> <p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
</div> </div>
</div> </div>
{/* Judge 3 */} {/* Judge 3 */}
<div className="flex flex-col justify-between bg-white rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
{/* <div className="w-24 h-24 bg-gray-200 rounded-full mx-auto mb-4"></div> */} <h3 className="text-p3 font-bold mb-1 dark:text-white">
<h3 className="text-p3 font-bold mb-1">Hafid Nur</h3> Hafid Nur
</h3>
<div> <div>
<p className="text-primary-500 font-semibold mb-1">Moderator</p> <p className="text-primary-500 font-semibold mb-1">Moderator</p>
<p className="text-gray-600">IMPHNEN</p> <p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
</div> </div>
</div> </div>
</div> </div>
@@ -516,25 +579,31 @@ export default function HomePage() {
</section> </section>
{/* FAQ Section */} {/* FAQ Section */}
<section id="faq" className="py-16 md:py-24 px-4 md:px-8 bg-white"> <section
id="faq"
className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950"
>
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500"> <h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500">
FAQ FAQ
</h2> </h2>
<p className="text-lg text-gray-600"> <p className="text-lg text-gray-600 dark:text-neutral-400">
Pertanyaan yang Sering Diajukan Pertanyaan yang Sering Diajukan
</p> </p>
</div> </div>
<div className="space-y-4 font-sans"> <div className="space-y-4 font-sans">
{faqs.map((faq, index) => ( {faqs.map((faq, index) => (
<div key={index} className="border border-gray-200 rounded-lg"> <div
key={index}
className="border border-gray-200 dark:border-neutral-700 rounded-lg"
>
<button <button
onClick={() => setOpenFaq(openFaq === index ? null : index)} onClick={() => setOpenFaq(openFaq === index ? null : index)}
className="w-full px-6 py-4 flex items-center justify-between text-left hover:bg-gray-50 transition-colors cursor-pointer" className="w-full px-6 py-4 flex items-center justify-between text-left hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors cursor-pointer"
> >
<span className="font-semibold text-gray-900"> <span className="font-semibold text-gray-900 dark:text-white">
{faq.question} {faq.question}
</span> </span>
<svg <svg
@@ -554,7 +623,9 @@ export default function HomePage() {
</svg> </svg>
</button> </button>
{openFaq === index && ( {openFaq === index && (
<div className="px-6 pb-4 text-gray-600">{faq.answer}</div> <div className="px-6 pb-4 text-gray-600 dark:text-neutral-400">
{faq.answer}
</div>
)} )}
</div> </div>
))} ))}
@@ -563,12 +634,12 @@ export default function HomePage() {
</section> </section>
{/* Sponsors Section */} {/* Sponsors Section */}
<section className="py-20 md:py-28 px-4 md:px-8 bg-gray-50"> <section className="py-20 md:py-28 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900">
<div className="max-w-6xl mx-auto text-center"> <div className="max-w-6xl mx-auto text-center">
<h2 className="text-3xl md:text-5xl font-bold mb-4"> <h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
Sponsor & <span className="text-primary-500">Partner</span> Sponsor & <span className="text-primary-500">Partner</span>
</h2> </h2>
<p className="text-lg text-gray-600 mb-8"> <p className="text-lg text-gray-600 dark:text-neutral-400 mb-8">
Acara ini sepenuhnya disponsori oleh Acara ini sepenuhnya disponsori oleh
</p> </p>
@@ -577,12 +648,12 @@ export default function HomePage() {
href="https://kolosal.ai" href="https://kolosal.ai"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="bg-white rounded-xl p-8 shadow-lg inline-block border-2 border-transparent hover:border-gray-500 transition-colors" className="bg-white dark:bg-neutral-800 rounded-xl p-8 shadow-lg inline-block border-2 border-transparent hover:border-gray-500 dark:hover:border-neutral-500 transition-colors"
> >
<img <img
src="images/sponsors/kolosal-logo_rlxbck.svg" src="images/sponsors/kolosal-logo_rlxbck.svg"
alt="Kolosal.ai" alt="Kolosal.ai"
className="h-12 md:h-16" className="h-12 md:h-16 dark:invert"
/> />
</a> </a>
</div> </div>
@@ -592,13 +663,13 @@ export default function HomePage() {
{/* CTA Section */} {/* CTA Section */}
<section <section
id="masuk" id="masuk"
className="py-20 md:py-28 px-4 md:px-8 bg-linear-to-b from-white to-blue-50" className="py-20 md:py-28 px-4 md:px-8 bg-linear-to-b from-white to-blue-50 dark:from-neutral-950 dark:to-neutral-900"
> >
<div className="max-w-4xl mx-auto text-center font-sans"> <div className="max-w-4xl mx-auto text-center font-sans">
<h2 className="text-3xl md:text-5xl font-bold mb-6 font-bai-jamjuree"> <h2 className="text-3xl md:text-5xl font-bold mb-6 font-bai-jamjuree dark:text-white">
Segera Daftarkan <span className="text-primary-500">Timmu!</span> Segera Daftarkan <span className="text-primary-500">Timmu!</span>
</h2> </h2>
<p className="text-lg md:text-xl text-left md:text-center text-gray-600 mb-8"> <p className="text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400 mb-8">
Jangan lewatkan kesempatan emas untuk bersaing dengan developer Jangan lewatkan kesempatan emas untuk bersaing dengan developer
terbaik, terbaik,
<br className="hidden md:block" /> belajar dari para ahli, dan <br className="hidden md:block" /> belajar dari para ahli, dan
@@ -616,13 +687,13 @@ export default function HomePage() {
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL" href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="px-8 py-3 bg-white text-gray-900 text-lg rounded-lg border-2 border-gray-300 hover:border-gray-400 transition-colors font-semibold" className="px-8 py-3 bg-white dark:bg-neutral-800 text-gray-900 dark:text-white text-lg rounded-lg border-2 border-gray-300 dark:border-neutral-600 hover:border-gray-400 dark:hover:border-neutral-500 transition-colors font-semibold"
> >
Gabung Grup WA Hackathon Gabung Grup WA Hackathon
</a> </a>
</div> </div>
<p className="text-sm text-gray-500 mt-6"> <p className="text-sm text-gray-500 dark:text-neutral-500 mt-6">
Pendaftaran ditutup pada{' '} Pendaftaran ditutup pada{' '}
<span className="text-primary-500 font-semibold"> <span className="text-primary-500 font-semibold">
30 November 2025 30 November 2025
+27 -15
View File
@@ -138,12 +138,17 @@ const ProfilePage: FC = (): ReactElement => {
const isLoading = isUpdating || isUploading; const isLoading = isUpdating || isUploading;
return ( return (
<div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 px-4 py-8"> <div className="flex flex-col justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 px-4 py-8">
<div className="bg-white w-full max-w-md p-8 rounded-xl shadow-lg"> <div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-lg">
<div className="mb-6"> <div className="mb-6">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h1 className="text-2xl font-bold text-gray-900">Edit Profile</h1> <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
<Link to="/dashboard" className="text-gray-500 hover:text-gray-700"> Edit Profile
</h1>
<Link
to="/dashboard"
className="text-gray-500 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200"
>
<svg <svg
className="w-6 h-6" className="w-6 h-6"
fill="none" fill="none"
@@ -159,7 +164,9 @@ const ProfilePage: FC = (): ReactElement => {
</svg> </svg>
</Link> </Link>
</div> </div>
<p className="text-gray-600 font-sans">Update your photo and name</p> <p className="text-gray-600 font-sans dark:text-neutral-400">
Update your photo and name
</p>
</div> </div>
<form onSubmit={onSubmit} className="space-y-6"> <form onSubmit={onSubmit} className="space-y-6">
@@ -170,15 +177,15 @@ const ProfilePage: FC = (): ReactElement => {
<img <img
src={avatarPreview} src={avatarPreview}
alt="Avatar preview" alt="Avatar preview"
className="w-32 h-32 rounded-full object-cover border-4 border-gray-200 group-hover:border-blue-400 transition-colors" className="w-32 h-32 rounded-full object-cover border-4 border-gray-200 dark:border-neutral-700 group-hover:border-blue-400 dark:group-hover:border-blue-500 transition-colors"
/> />
) : ( ) : (
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors"> <div className="w-32 h-32 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center group-hover:bg-gray-300 dark:group-hover:bg-neutral-600 transition-colors">
<Icon <Icon
icon="ic:baseline-person" icon="ic:baseline-person"
width="48" width="48"
height="48" height="48"
className="text-gray-400" className="text-gray-400 dark:text-neutral-500"
/> />
</div> </div>
)} )}
@@ -216,7 +223,7 @@ const ProfilePage: FC = (): ReactElement => {
/> />
</label> </label>
</div> </div>
<p className="text-sm text-gray-500 text-center font-sans"> <p className="text-sm text-gray-500 dark:text-neutral-400 text-center font-sans">
Click the camera icon to change your photo Click the camera icon to change your photo
<br /> <br />
Format: JPG, PNG. Max 5MB Format: JPG, PNG. Max 5MB
@@ -234,7 +241,7 @@ const ProfilePage: FC = (): ReactElement => {
{/* City */} {/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700"> <label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
City City
</label> </label>
<Controller <Controller
@@ -253,7 +260,7 @@ const ProfilePage: FC = (): ReactElement => {
{/* Role/Skills */} {/* Role/Skills */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700"> <label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
Role / Skills Role / Skills
</label> </label>
<Controller <Controller
@@ -276,9 +283,11 @@ const ProfilePage: FC = (): ReactElement => {
: (field.value || []).filter((v) => v !== role); : (field.value || []).filter((v) => v !== role);
field.onChange(newValue); field.onChange(newValue);
}} }}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" className="rounded border-gray-300 dark:border-neutral-600 text-blue-600 focus:ring-blue-500 dark:bg-neutral-800"
/> />
<span className="text-sm">{role}</span> <span className="text-sm dark:text-neutral-300">
{role}
</span>
</label> </label>
))} ))}
</div> </div>
@@ -289,8 +298,11 @@ const ProfilePage: FC = (): ReactElement => {
{/* Bio */} {/* Bio */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700"> <label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
Bio <span className="text-gray-400">(Optional)</span> Bio{' '}
<span className="text-gray-400 dark:text-neutral-500">
(Optional)
</span>
</label> </label>
<Controller <Controller
control={form.control} control={form.control}
@@ -63,14 +63,14 @@ const TeamChatPage: FC = (): ReactElement => {
}; };
return ( return (
<div className="flex flex-col h-screen bg-gray-50"> <div className="flex flex-col h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header */} {/* Header */}
<div className="bg-white border-b shadow-sm"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 shadow-sm dark:shadow-neutral-950/50">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900">Team Chat</h1> <h1 className="text-2xl font-bold text-gray-900 dark:text-white">Team Chat</h1>
<p className="text-gray-600 text-sm mt-0.5">{team?.name}</p> <p className="text-gray-600 dark:text-neutral-400 text-sm mt-0.5">{team?.name}</p>
</div> </div>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}> <Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team Back to Team
@@ -84,7 +84,7 @@ const TeamChatPage: FC = (): ReactElement => {
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 dark:border-primary-400"></div>
</div> </div>
) : messages && messages.length > 0 ? ( ) : messages && messages.length > 0 ? (
<div className="space-y-4"> <div className="space-y-4">
@@ -118,18 +118,18 @@ const TeamChatPage: FC = (): ReactElement => {
<div className={`flex-1 ${isOwnMessage ? 'text-right' : 'text-left'}`}> <div className={`flex-1 ${isOwnMessage ? 'text-right' : 'text-left'}`}>
<div className={`inline-block ${isOwnMessage ? 'items-end' : 'items-start'}`}> <div className={`inline-block ${isOwnMessage ? 'items-end' : 'items-start'}`}>
<div className="flex items-baseline gap-2 mb-1"> <div className="flex items-baseline gap-2 mb-1">
<span className="font-semibold text-sm text-gray-900"> <span className="font-semibold text-sm text-gray-900 dark:text-white">
{isOwnMessage ? 'You' : msg.user?.fullname} {isOwnMessage ? 'You' : msg.user?.fullname}
</span> </span>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500 dark:text-neutral-500">
{formatTime(msg.created_at)} {formatTime(msg.created_at)}
</span> </span>
</div> </div>
<div <div
className={`relative group rounded-2xl px-4 py-2.5 ${ className={`relative group rounded-2xl px-4 py-2.5 ${
isOwnMessage isOwnMessage
? 'bg-blue-600 text-white' ? 'bg-blue-600 dark:bg-primary-600 text-white'
: 'bg-white text-gray-900 border border-gray-200' : 'bg-white dark:bg-neutral-800 text-gray-900 dark:text-white border border-gray-200 dark:border-neutral-700'
}`} }`}
> >
<p className="text-sm whitespace-pre-wrap break-words">{msg.message}</p> <p className="text-sm whitespace-pre-wrap break-words">{msg.message}</p>
@@ -138,7 +138,7 @@ const TeamChatPage: FC = (): ReactElement => {
{canDelete && ( {canDelete && (
<button <button
onClick={() => handleDeleteMessage(msg.id)} onClick={() => handleDeleteMessage(msg.id)}
className={`absolute top-1 ${isOwnMessage ? 'left-1' : 'right-1'} opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-200 ${isOwnMessage ? 'hover:bg-blue-700' : ''}`} className={`absolute top-1 ${isOwnMessage ? 'left-1' : 'right-1'} opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-200 dark:hover:bg-neutral-700 ${isOwnMessage ? 'hover:bg-blue-700 dark:hover:bg-primary-700' : ''}`}
title="Delete message" title="Delete message"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -158,10 +158,10 @@ const TeamChatPage: FC = (): ReactElement => {
) : ( ) : (
<div className="flex flex-col items-center justify-center h-64 text-center"> <div className="flex flex-col items-center justify-center h-64 text-center">
<div className="text-6xl mb-4">💬</div> <div className="text-6xl mb-4">💬</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2"> <h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
No messages yet No messages yet
</h3> </h3>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-neutral-400">
Be the first to start the conversation! Be the first to start the conversation!
</p> </p>
</div> </div>
@@ -170,7 +170,7 @@ const TeamChatPage: FC = (): ReactElement => {
</div> </div>
{/* Message Input */} {/* Message Input */}
<div className="bg-white border-t shadow-lg"> <div className="bg-white dark:bg-neutral-900 border-t dark:border-neutral-700 shadow-lg dark:shadow-neutral-950/50">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<form onSubmit={handleSendMessage} className="flex gap-3"> <form onSubmit={handleSendMessage} className="flex gap-3">
<input <input
@@ -178,7 +178,7 @@ const TeamChatPage: FC = (): ReactElement => {
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
placeholder="Type your message..." placeholder="Type your message..."
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="flex-1 px-4 py-3 border border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-primary-500 focus:border-transparent"
disabled={isSending} disabled={isSending}
/> />
<Button <Button
@@ -46,17 +46,17 @@ const EditTeamPage: FC = (): ReactElement => {
if (isLoadingTeam) { if (isLoadingTeam) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-gray-600">Loading team...</div> <div className="text-gray-600 dark:text-neutral-400">Loading team...</div>
</div> </div>
); );
} }
if (!isLeader) { if (!isLeader) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
<p className="text-gray-600 mb-4">Only the team leader can edit team information</p> <p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can edit team information</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> <Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
</div> </div>
); );
@@ -114,20 +114,20 @@ const EditTeamPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-gray-900">Edit Team Info</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Edit Team Info</h1>
<p className="text-gray-600 mt-1">Update your team details</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">Update your team details</p>
</div> </div>
</div> </div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-lg shadow-md p-8"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8">
<form onSubmit={onSubmit} className="space-y-6"> <form onSubmit={onSubmit} className="space-y-6">
{/* Banner Upload */} {/* Banner Upload */}
<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">
Team Banner Team Banner
</label> </label>
{bannerPreview ? ( {bannerPreview ? (
@@ -135,7 +135,7 @@ const EditTeamPage: FC = (): ReactElement => {
<img <img
src={bannerPreview} src={bannerPreview}
alt="Banner preview" alt="Banner preview"
className="w-full h-48 object-cover rounded-lg border-2 border-gray-200" className="w-full h-48 object-cover rounded-lg border-2 border-gray-200 dark:border-neutral-700"
/> />
<button <button
type="button" type="button"
@@ -150,10 +150,10 @@ const EditTeamPage: FC = (): ReactElement => {
</button> </button>
</div> </div>
) : ( ) : (
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50"> <label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
<div className="text-center"> <div className="text-center">
<p className="text-gray-500">Click to upload banner</p> <p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p>
<p className="text-xs text-gray-400 mt-1">1200x400 recommended</p> <p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended</p>
</div> </div>
<input <input
type="file" type="file"
@@ -167,7 +167,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Logo Upload */} {/* Logo Upload */}
<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">
Team Logo Team Logo
</label> </label>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
@@ -175,11 +175,11 @@ const EditTeamPage: FC = (): ReactElement => {
<img <img
src={logoPreview} src={logoPreview}
alt="Logo preview" alt="Logo preview"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200" className="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-neutral-700"
/> />
) : ( ) : (
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-400 text-3xl">👥</span> <span className="text-gray-400 dark:text-neutral-500 text-3xl">👥</span>
</div> </div>
)} )}
<div> <div>
@@ -222,7 +222,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* City */} {/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
City City
</label> </label>
<Controller <Controller
@@ -232,7 +232,7 @@ const EditTeamPage: FC = (): ReactElement => {
<div> <div>
<select <select
{...field} {...field}
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 dark:bg-neutral-800 dark:text-white rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="">Select city</option> <option value="">Select city</option>
{INDONESIAN_CITIES.map((city) => ( {INDONESIAN_CITIES.map((city) => (
@@ -251,7 +251,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Description */} {/* Description */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Description Description
</label> </label>
<Controller <Controller
@@ -276,7 +276,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Visibility */} {/* Visibility */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Team Visibility Team Visibility
</label> </label>
<Controller <Controller
@@ -284,7 +284,7 @@ const EditTeamPage: FC = (): ReactElement => {
name="visibility" name="visibility"
render={({ field }) => ( render={({ field }) => (
<div className="space-y-3"> <div className="space-y-3">
<label className="flex items-start space-x-3 cursor-pointer border rounded-lg p-4 hover:bg-gray-50"> <label className="flex items-start space-x-3 cursor-pointer border dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800">
<input <input
type="radio" type="radio"
{...field} {...field}
@@ -293,13 +293,13 @@ const EditTeamPage: FC = (): ReactElement => {
className="mt-1" className="mt-1"
/> />
<div> <div>
<p className="font-medium text-gray-900">Public</p> <p className="font-medium text-gray-900 dark:text-white">Public</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
Team will be visible in Browse Teams Team will be visible in Browse Teams
</p> </p>
</div> </div>
</label> </label>
<label className="flex items-start space-x-3 cursor-pointer border rounded-lg p-4 hover:bg-gray-50"> <label className="flex items-start space-x-3 cursor-pointer border dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800">
<input <input
type="radio" type="radio"
{...field} {...field}
@@ -308,8 +308,8 @@ const EditTeamPage: FC = (): ReactElement => {
className="mt-1" className="mt-1"
/> />
<div> <div>
<p className="font-medium text-gray-900">Private</p> <p className="font-medium text-gray-900 dark:text-white">Private</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
Team hidden, invite-only Team hidden, invite-only
</p> </p>
</div> </div>
@@ -6,21 +6,21 @@ const TeamDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto"> <div className="flex-1 flex flex-col overflow-auto">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
@@ -23,7 +23,7 @@ const ManageMembersPage: FC = (): ReactElement => {
const { session } = useAuthStore(); const { session } = useAuthStore();
const [showInviteModal, setShowInviteModal] = useState(false); const [showInviteModal, setShowInviteModal] = useState(false);
const { data: teamData } = useTeamById(teamId || ''); const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || ''); const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || '');
const { data: joinRequestsData } = useTeamJoinRequests(teamId || ''); const { data: joinRequestsData } = useTeamJoinRequests(teamId || '');
@@ -42,11 +42,20 @@ const ManageMembersPage: FC = (): ReactElement => {
mode: 'all', mode: 'all',
}); });
// Show loading state while fetching team data
if (isLoadingTeam) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<p className="text-gray-600 dark:text-neutral-400">Loading...</p>
</div>
);
}
if (!isLeader) { if (!isLeader) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
<p className="text-gray-600 mb-4">Only the team leader can manage members</p> <p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can manage members</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> <Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
</div> </div>
); );
@@ -94,13 +103,13 @@ const ManageMembersPage: FC = (): ReactElement => {
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Manage Members</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Manage Members</h1>
<p className="text-gray-600 mt-1">{team?.name}</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
</div> </div>
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button onClick={() => setShowInviteModal(true)}> <Button onClick={() => setShowInviteModal(true)}>
@@ -117,13 +126,13 @@ const ManageMembersPage: FC = (): ReactElement => {
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
{/* Join Requests */} {/* Join Requests */}
{joinRequests.length > 0 && ( {joinRequests.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4"> <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Join Requests ({joinRequests.length}) Join Requests ({joinRequests.length})
</h2> </h2>
<div className="space-y-3"> <div className="space-y-3">
{joinRequests.map((request) => ( {joinRequests.map((request) => (
<div key={request.id} className="border rounded-lg p-4"> <div key={request.id} className="border dark:border-neutral-700 rounded-lg p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-3 flex-1"> <div className="flex items-center space-x-3 flex-1">
{request.user.avatar ? ( {request.user.avatar ? (
@@ -133,17 +142,17 @@ const ManageMembersPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500">👤</span> <span className="text-gray-500 dark:text-neutral-400">👤</span>
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-gray-900">{request.user.fullname}</p> <p className="font-medium text-gray-900 dark:text-white">{request.user.fullname}</p>
<p className="text-sm text-gray-600">{request.user.email}</p> <p className="text-sm text-gray-600 dark:text-neutral-400">{request.user.email}</p>
{request.user.location && ( {request.user.location && (
<p className="text-sm text-gray-500">📍 {request.user.location}</p> <p className="text-sm text-gray-500 dark:text-neutral-500">📍 {request.user.location}</p>
)} )}
<p className="text-sm text-gray-700 mt-2 italic">"{request.message}"</p> <p className="text-sm text-gray-700 dark:text-neutral-300 mt-2 italic">"{request.message}"</p>
</div> </div>
</div> </div>
<div className="flex space-x-2 ml-4"> <div className="flex space-x-2 ml-4">
@@ -171,16 +180,16 @@ const ManageMembersPage: FC = (): ReactElement => {
)} )}
{/* Current Members */} {/* Current Members */}
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-6">
<h2 className="text-xl font-bold text-gray-900 mb-4"> <h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Current Members ({members.length}) Current Members ({members.length})
</h2> </h2>
{isLoadingMembers ? ( {isLoadingMembers ? (
<p className="text-gray-600">Loading members...</p> <p className="text-gray-600 dark:text-neutral-400">Loading members...</p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{members.map((member) => ( {members.map((member) => (
<div key={member.id} className="border rounded-lg p-4 flex items-center justify-between"> <div key={member.id} className="border dark:border-neutral-700 rounded-lg p-4 flex items-center justify-between">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{member.user.avatar ? ( {member.user.avatar ? (
<img <img
@@ -189,24 +198,24 @@ const ManageMembersPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500">👤</span> <span className="text-gray-500 dark:text-neutral-400">👤</span>
</div> </div>
)} )}
<div> <div>
<p className="font-medium text-gray-900">{member.user.fullname}</p> <p className="font-medium text-gray-900 dark:text-white">{member.user.fullname}</p>
<p className="text-sm text-gray-600">{member.user.email}</p> <p className="text-sm text-gray-600 dark:text-neutral-400">{member.user.email}</p>
{member.user.location && ( {member.user.location && (
<p className="text-sm text-gray-500">📍 {member.user.location}</p> <p className="text-sm text-gray-500 dark:text-neutral-500">📍 {member.user.location}</p>
)} )}
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-1">
{member.role === 'leader' && ( {member.role === 'leader' && (
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs font-medium"> <span className="px-2 py-1 bg-blue-100 dark:bg-primary-900/30 text-blue-800 dark:text-primary-300 rounded text-xs font-medium">
Leader Leader
</span> </span>
)} )}
{member.status === ETeamMemberStatus.PENDING && ( {member.status === ETeamMemberStatus.PENDING && (
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs font-medium"> <span className="px-2 py-1 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 rounded text-xs font-medium">
Pending Invitation Pending Invitation
</span> </span>
)} )}
@@ -230,8 +239,8 @@ const ManageMembersPage: FC = (): ReactElement => {
</div> </div>
{/* Warning */} {/* Warning */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4"> <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Note:</strong> Members cannot leave the team without your approval. Only you can remove members from the team. <strong>Note:</strong> Members cannot leave the team without your approval. Only you can remove members from the team.
</p> </p>
</div> </div>
@@ -239,22 +248,22 @@ const ManageMembersPage: FC = (): ReactElement => {
{/* Invite Member Modal */} {/* Invite Member Modal */}
{showInviteModal && ( {showInviteModal && (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl dark:shadow-neutral-950/50 max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Invite Member Invite Member
</h2> </h2>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 dark:text-neutral-400 mb-4">
Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in. Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in.
</p> </p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-6"> <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in. <strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in.
</p> </p>
</div> </div>
<form onSubmit={handleInvite} className="space-y-4"> <form onSubmit={handleInvite} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Email Address Email Address
</label> </label>
<Input <Input
+73 -62
View File
@@ -80,6 +80,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
const currentUserId = session?.user?.id; const currentUserId = session?.user?.id;
const isLeader = currentUserId === team?.leader_id; const isLeader = currentUserId === team?.leader_id;
const isMember = members.some(
(member: any) => member.user_id === currentUserId
);
const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS; const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS;
// Calculate total images to load // Calculate total images to load
@@ -218,7 +221,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<div className="fixed inset-0 bg-white/60 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-white/60 flex items-center justify-center z-50">
<div className="text-center"> <div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent"></div> <div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600 font-medium"> <p className="mt-4 text-gray-600 dark:text-neutral-400">
Loading team data... Loading team data...
</p> </p>
</div> </div>
@@ -230,7 +233,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (!team) { if (!team) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Team not found Team not found
</h2> </h2>
<Button onClick={() => navigate('/dashboard')}> <Button onClick={() => navigate('/dashboard')}>
@@ -241,7 +244,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
} }
return ( return (
<div className="min-h-screen bg-gray-50 relative"> <div className="min-h-screen bg-gray-50 relative dark:bg-neutral-950">
{/* Loading overlay while images are loading */} {/* Loading overlay while images are loading */}
{!imagesLoaded && totalImagesToLoad > 0 && ( {!imagesLoaded && totalImagesToLoad > 0 && (
<div className="fixed inset-0 bg-white/80 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-white/80 flex items-center justify-center z-50">
@@ -256,7 +259,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
)} )}
{/* Header with Banner */} {/* Header with Banner */}
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
{team.banner && ( {team.banner && (
<div className="w-full h-32 md:h-48 overflow-hidden relative"> <div className="w-full h-32 md:h-48 overflow-hidden relative">
<div <div
@@ -295,18 +298,18 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
)} )}
<div> <div>
<h1 className="text-xl md:text-3xl font-bold text-gray-900 line-clamp-2"> <h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white line-clamp-2">
{team.name} {team.name}
</h1> </h1>
<p className="text-sm md:text-base text-gray-600 mt-1 line-clamp-1"> <p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1 line-clamp-1">
📍 {team.city} 📍 {team.city}
</p> </p>
<div className="flex flex-wrap items-center gap-2 md:space-x-4 mt-2"> <div className="flex flex-wrap items-center gap-2 md:space-x-4 mt-2">
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500 dark:text-neutral-400">
{members.length}{' '} {members.length}{' '}
{members.length === 1 ? 'Member' : 'Members'} {members.length === 1 ? 'Member' : 'Members'}
</span> </span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500 dark:text-neutral-400">
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'} {team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
</span> </span>
{isLeader && ( {isLeader && (
@@ -326,19 +329,19 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Main Content */} {/* Main Content */}
<div className="lg:col-span-2 space-y-4 md:space-y-6"> <div className="lg:col-span-2 space-y-4 md:space-y-6">
{/* Team Description */} {/* Team Description */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4"> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
About Team About Team
</h2> </h2>
<p className="text-gray-700 whitespace-pre-wrap"> <p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">
{team.description} {team.description}
</p> </p>
</div> </div>
{/* Team Actions - Only for Leader */} {/* Team Actions - Only for Leader */}
{isLeader && ( {isLeader && (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4"> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Team Management Team Management
</h2> </h2>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
@@ -383,17 +386,17 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Link> </Link>
</div> </div>
{!canInvite && members.length >= MAX_TEAM_MEMBERS && ( {!canInvite && members.length >= MAX_TEAM_MEMBERS && (
<p className="text-sm text-gray-600 mt-3 text-center"> <p className="text-sm text-gray-600 dark:text-neutral-400 mt-3 text-center">
Maximum team size reached ({MAX_TEAM_MEMBERS} members) Maximum team size reached ({MAX_TEAM_MEMBERS} members)
</p> </p>
)} )}
</div> </div>
)} )}
{/* Quick Actions for Members */} {/* Quick Actions for Members (non-leaders) */}
{!isLeader && ( {!isLeader && isMember && (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4"> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Quick Actions Quick Actions
</h2> </h2>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
@@ -413,16 +416,16 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
)} )}
{/* Submission Status */} {/* Submission Status - Only show to members */}
{team.has_submission && ( {team.has_submission && isMember && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 md:p-6"> <div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 md:p-6">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<span className="text-3xl"></span> <span className="text-3xl"></span>
<div> <div>
<h3 className="font-bold text-green-900"> <h3 className="font-bold text-green-900 dark:text-green-400">
Project Submitted Project Submitted
</h3> </h3>
<p className="text-green-700 text-sm"> <p className="text-green-700 dark:text-green-300 text-sm">
Your team has successfully submitted a project Your team has successfully submitted a project
</p> </p>
</div> </div>
@@ -439,14 +442,14 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Sidebar */} {/* Sidebar */}
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{/* Team Leader */} {/* Team Leader */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 mb-3 md:mb-4"> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Team Leader Team Leader
</h3> </h3>
{team.leader && ( {team.leader && (
<button <button
onClick={() => navigate(`/users/${team.leader.id}`)} onClick={() => navigate(`/users/${team.leader.id}`)}
className="w-full flex items-center space-x-3 hover:bg-gray-100 rounded-lg p-2 transition-colors text-left cursor-pointer" className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
> >
{team.leader.avatar ? ( {team.leader.avatar ? (
<img <img
@@ -455,23 +458,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500">👤</span> <span className="text-gray-500 dark:text-neutral-400">
👤
</span>
</div> </div>
)} )}
<div> <div>
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900 dark:text-white">
{team.leader.fullname} {team.leader.fullname}
</p> </p>
<p className="text-sm text-gray-600">{team.leader.email}</p> <p className="text-sm text-gray-600 dark:text-neutral-400">
{team.leader.email}
</p>
</div> </div>
</button> </button>
)} )}
</div> </div>
{/* Team Members */} {/* Team Members */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 mb-3 md:mb-4"> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Members ({members.length}) Members ({members.length})
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
@@ -479,7 +486,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<button <button
key={member.id} key={member.id}
onClick={() => navigate(`/users/${member.user.id}`)} onClick={() => navigate(`/users/${member.user.id}`)}
className="w-full flex items-center space-x-3 hover:bg-gray-100 rounded-lg p-2 transition-colors text-left cursor-pointer" className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
> >
{member.user?.avatar ? ( {member.user?.avatar ? (
<img <img
@@ -490,15 +497,17 @@ const TeamDashboardPage: FC = (): ReactElement => {
onError={handleImageError} onError={handleImageError}
/> />
) : ( ) : (
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 text-sm">👤</span> <span className="text-gray-500 dark:text-neutral-400 text-sm">
👤
</span>
</div> </div>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 truncate"> <p className="font-medium text-gray-900 dark:text-white truncate">
{member.user?.fullname || 'Unknown'} {member.user?.fullname || 'Unknown'}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500 dark:text-neutral-400">
{member.role === ETeamMemberRole.LEADER {member.role === ETeamMemberRole.LEADER
? 'Leader' ? 'Leader'
: 'Member'} : 'Member'}
@@ -514,17 +523,17 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Invite Member Modal */} {/* Invite Member Modal */}
{showInviteModal && ( {showInviteModal && (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Invite Team Member Invite Team Member
</h2> </h2>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 dark:text-neutral-400 mb-4">
Send an invitation to join your team. The invited member will see Send an invitation to join your team. The invited member will see
the invitation on their dashboard after logging in. the invitation on their dashboard after logging in.
</p> </p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-6"> <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800 dark:text-yellow-300">
<strong>Important:</strong> The email you enter must match the <strong>Important:</strong> The email you enter must match the
GitHub email address the member uses to sign in. GitHub email address the member uses to sign in.
</p> </p>
@@ -533,7 +542,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<div> <div>
<label <label
htmlFor="invite-email" htmlFor="invite-email"
className="block text-sm font-medium text-gray-700 mb-2" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2"
> >
Email Address Email Address
</label> </label>
@@ -543,10 +552,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Enter email address..." placeholder="Enter email address..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
required required
/> />
<p className="text-sm text-gray-500 mt-1"> <p className="text-sm text-gray-500 dark:text-neutral-400 mt-1">
Current members: {members.length}/{MAX_TEAM_MEMBERS} Current members: {members.length}/{MAX_TEAM_MEMBERS}
</p> </p>
</div> </div>
@@ -577,15 +586,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Join Requests Modal */} {/* Join Requests Modal */}
{showJoinRequestsModal && ( {showJoinRequestsModal && (
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-900"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Join Requests ({pendingJoinRequests.length}) Join Requests ({pendingJoinRequests.length})
</h2> </h2>
<button <button
onClick={() => setShowJoinRequestsModal(false)} onClick={() => setShowJoinRequestsModal(false)}
className="text-gray-400 hover:text-gray-600 text-2xl" className="text-gray-400 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300 text-2xl"
> >
</button> </button>
@@ -593,10 +602,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
{pendingJoinRequests.length === 0 ? ( {pendingJoinRequests.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-600 text-lg"> <p className="text-gray-600 dark:text-neutral-400 text-lg">
No pending join requests No pending join requests
</p> </p>
<p className="text-gray-500 text-sm mt-2"> <p className="text-gray-500 dark:text-neutral-500 text-sm mt-2">
When users request to join your team, they'll appear here When users request to join your team, they'll appear here
</p> </p>
</div> </div>
@@ -605,7 +614,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
{pendingJoinRequests.map((request: any) => ( {pendingJoinRequests.map((request: any) => (
<div <div
key={request.id} key={request.id}
className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow" className="border border-gray-200 dark:border-neutral-700 rounded-lg p-4 hover:shadow-md transition-shadow"
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1"> <div className="flex items-start space-x-3 flex-1">
@@ -616,25 +625,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center shrink-0"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center shrink-0">
<span className="text-gray-500 text-xl">👤</span> <span className="text-gray-500 dark:text-neutral-400 text-xl">
👤
</span>
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-gray-900"> <p className="font-semibold text-gray-900 dark:text-white">
{request.user?.fullname || 'Unknown User'} {request.user?.fullname || 'Unknown User'}
</p> </p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
{request.user?.email} {request.user?.email}
</p> </p>
{request.message && ( {request.message && (
<div className="mt-2 bg-gray-50 rounded-lg p-3"> <div className="mt-2 bg-gray-50 dark:bg-neutral-800 rounded-lg p-3">
<p className="text-sm text-gray-700"> <p className="text-sm text-gray-700 dark:text-neutral-300">
<strong>Message:</strong> {request.message} <strong>Message:</strong> {request.message}
</p> </p>
</div> </div>
)} )}
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 dark:text-neutral-500 mt-2">
Requested{' '} Requested{' '}
{new Date(request.created_at).toLocaleDateString()} {new Date(request.created_at).toLocaleDateString()}
</p> </p>
@@ -665,8 +676,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
</div> </div>
{members.length >= MAX_TEAM_MEMBERS && ( {members.length >= MAX_TEAM_MEMBERS && (
<div className="mt-3 bg-yellow-50 border border-yellow-200 rounded-lg p-2"> <div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-2">
<p className="text-xs text-yellow-800"> <p className="text-xs text-yellow-800 dark:text-yellow-300">
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS}{' '} Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS}{' '}
members). Remove a member before accepting new members). Remove a member before accepting new
requests. requests.
@@ -15,18 +15,18 @@ const SubmissionViewPage: FC = (): ReactElement => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-gray-600">Loading submission...</div> <div className="text-gray-600 dark:text-neutral-400">Loading submission...</div>
</div> </div>
); );
} }
if (!submission) { if (!submission) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-6xl mb-4">📄</div> <div className="text-6xl mb-4">📄</div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">No Submission Yet</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">No Submission Yet</h2>
<p className="text-gray-600 mb-4">Your team hasn't submitted a project</p> <p className="text-gray-600 dark:text-neutral-400 mb-4">Your team hasn't submitted a project</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> <Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
</div> </div>
); );
@@ -43,13 +43,13 @@ const SubmissionViewPage: FC = (): ReactElement => {
: 'Not submitted'; : 'Not submitted';
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Project Submission</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Project Submission</h1>
<p className="text-gray-600 mt-1">{team?.name}</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
</div> </div>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}> <Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team Back to Team
@@ -60,22 +60,22 @@ const SubmissionViewPage: FC = (): ReactElement => {
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Status Banner */} {/* Status Banner */}
<div className="bg-green-50 border border-green-500 rounded-lg p-6 mb-6"> <div className="bg-green-50 dark:bg-green-900/20 border border-green-500 rounded-lg p-6 mb-6">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<span className="text-4xl">✅</span> <span className="text-4xl">✅</span>
<div> <div>
<h3 className="font-bold text-green-900 text-lg">Project Submitted Successfully</h3> <h3 className="font-bold text-green-900 dark:text-green-100 text-lg">Project Submitted Successfully</h3>
<p className="text-green-700 text-sm"> <p className="text-green-700 dark:text-green-300 text-sm">
Submitted on {submittedDate} Submitted on {submittedDate}
</p> </p>
<p className="text-green-600 text-xs mt-1"> <p className="text-green-600 dark:text-green-400 text-xs mt-1">
This submission is now read-only and cannot be edited This submission is now read-only and cannot be edited
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-lg shadow-md overflow-hidden"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 overflow-hidden">
{/* Project Header */} {/* Project Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-800 text-white p-8"> <div className="bg-gradient-to-r from-blue-600 to-blue-800 text-white p-8">
<h2 className="text-3xl font-bold mb-2">{submission.project_name}</h2> <h2 className="text-3xl font-bold mb-2">{submission.project_name}</h2>
@@ -86,9 +86,9 @@ const SubmissionViewPage: FC = (): ReactElement => {
<div className="p-8 space-y-6"> <div className="p-8 space-y-6">
{/* Description */} {/* Description */}
<div> <div>
<h3 className="text-lg font-bold text-gray-900 mb-3">Project Description</h3> <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">Project Description</h3>
<div className="bg-gray-50 rounded-lg p-4"> <div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-4">
<p className="text-gray-700 whitespace-pre-wrap">{submission.description}</p> <p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">{submission.description}</p>
</div> </div>
</div> </div>
@@ -96,12 +96,12 @@ const SubmissionViewPage: FC = (): ReactElement => {
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
{/* Repository */} {/* Repository */}
<div> <div>
<h3 className="text-lg font-bold text-gray-900 mb-3">Repository</h3> <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">Repository</h3>
<a <a
href={submission.repository_url} href={submission.repository_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center space-x-2 text-blue-600 hover:text-blue-800" className="flex items-center space-x-2 text-blue-600 dark:text-primary-400 hover:text-blue-800 dark:hover:text-primary-300"
> >
<span>🔗</span> <span>🔗</span>
<span className="break-all">{submission.repository_url}</span> <span className="break-all">{submission.repository_url}</span>
@@ -111,12 +111,12 @@ const SubmissionViewPage: FC = (): ReactElement => {
{/* Demo URL */} {/* Demo URL */}
{submission.demo_url && ( {submission.demo_url && (
<div> <div>
<h3 className="text-lg font-bold text-gray-900 mb-3">Live Demo</h3> <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">Live Demo</h3>
<a <a
href={submission.demo_url} href={submission.demo_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center space-x-2 text-blue-600 hover:text-blue-800" className="flex items-center space-x-2 text-blue-600 dark:text-primary-400 hover:text-blue-800 dark:hover:text-primary-300"
> >
<span>🌐</span> <span>🌐</span>
<span className="break-all">{submission.demo_url}</span> <span className="break-all">{submission.demo_url}</span>
@@ -127,12 +127,12 @@ const SubmissionViewPage: FC = (): ReactElement => {
{/* Presentation URL */} {/* Presentation URL */}
{submission.presentation_url && ( {submission.presentation_url && (
<div> <div>
<h3 className="text-lg font-bold text-gray-900 mb-3">Presentation</h3> <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">Presentation</h3>
<a <a
href={submission.presentation_url} href={submission.presentation_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center space-x-2 text-blue-600 hover:text-blue-800" className="flex items-center space-x-2 text-blue-600 dark:text-primary-400 hover:text-blue-800 dark:hover:text-primary-300"
> >
<span>📊</span> <span>📊</span>
<span className="break-all">{submission.presentation_url}</span> <span className="break-all">{submission.presentation_url}</span>
@@ -144,7 +144,7 @@ const SubmissionViewPage: FC = (): ReactElement => {
{/* Screenshots */} {/* Screenshots */}
{submission.screenshots && submission.screenshots.length > 0 && ( {submission.screenshots && submission.screenshots.length > 0 && (
<div> <div>
<h3 className="text-lg font-bold text-gray-900 mb-3"> <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-3">
Screenshots ({submission.screenshots.length}) Screenshots ({submission.screenshots.length})
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -159,7 +159,7 @@ const SubmissionViewPage: FC = (): ReactElement => {
<img <img
src={url} src={url}
alt={`Screenshot ${index + 1}`} alt={`Screenshot ${index + 1}`}
className="w-full h-48 object-cover rounded-lg border-2 border-gray-200 hover:border-blue-500 transition-colors cursor-pointer" className="w-full h-48 object-cover rounded-lg border-2 border-gray-200 dark:border-neutral-700 hover:border-blue-500 dark:hover:border-primary-500 transition-colors cursor-pointer"
/> />
</a> </a>
))} ))}
@@ -168,22 +168,22 @@ const SubmissionViewPage: FC = (): ReactElement => {
)} )}
{/* Submission Info */} {/* Submission Info */}
<div className="bg-gray-50 rounded-lg p-4 border-t-4 border-blue-600"> <div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-4 border-t-4 border-blue-600 dark:border-primary-500">
<h3 className="text-sm font-bold text-gray-900 mb-2">Submission Information</h3> <h3 className="text-sm font-bold text-gray-900 dark:text-white mb-2">Submission Information</h3>
<div className="grid gap-2 text-sm"> <div className="grid gap-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Status:</span> <span className="text-gray-600 dark:text-neutral-400">Status:</span>
<span className="font-medium text-green-600"> <span className="font-medium text-green-600 dark:text-green-400">
{submission.status === 'submitted' ? ' Submitted' : 'Draft'} {submission.status === 'submitted' ? ' Submitted' : 'Draft'}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Submitted:</span> <span className="text-gray-600 dark:text-neutral-400">Submitted:</span>
<span className="font-medium text-gray-900">{submittedDate}</span> <span className="font-medium text-gray-900 dark:text-white">{submittedDate}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-600">Submission ID:</span> <span className="text-gray-600 dark:text-neutral-400">Submission ID:</span>
<span className="font-medium text-gray-900 font-mono text-xs"> <span className="font-medium text-gray-900 dark:text-white font-mono text-xs">
{submission.id} {submission.id}
</span> </span>
</div> </div>
@@ -191,8 +191,8 @@ const SubmissionViewPage: FC = (): ReactElement => {
</div> </div>
{/* Read-only Notice */} {/* Read-only Notice */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4"> <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Note:</strong> This submission is now locked and cannot be edited or deleted. <strong>Note:</strong> This submission is now locked and cannot be edited or deleted.
If you need to make changes, please contact the hackathon organizers. If you need to make changes, please contact the hackathon organizers.
</p> </p>
@@ -38,9 +38,9 @@ const SubmitProjectPage: FC = (): ReactElement => {
if (!isLeader) { if (!isLeader) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
<p className="text-gray-600 mb-4">Only the team leader can submit projects</p> <p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can submit projects</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> <Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
</div> </div>
); );
@@ -48,10 +48,10 @@ const SubmitProjectPage: FC = (): ReactElement => {
if (hasSubmission) { if (hasSubmission) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-6xl mb-4"></div> <div className="text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-900 mb-4">Project Already Submitted</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Project Already Submitted</h2>
<p className="text-gray-600 mb-4">Your team has already submitted a project</p> <p className="text-gray-600 dark:text-neutral-400 mb-4">Your team has already submitted a project</p>
<div className="flex space-x-3"> <div className="flex space-x-3">
<Button onClick={() => navigate(`/teams/${teamId}/submission`)}> <Button onClick={() => navigate(`/teams/${teamId}/submission`)}>
View Submission View Submission
@@ -95,22 +95,22 @@ const SubmitProjectPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-gray-900">Submit Project</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Submit Project</h1>
<p className="text-gray-600 mt-1">{team?.name}</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
</div> </div>
</div> </div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Warning Banner */} {/* Warning Banner */}
<div className="bg-red-50 border-2 border-red-500 rounded-lg p-6 mb-6"> <div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-500 rounded-lg p-6 mb-6">
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-3">
<span className="text-3xl"></span> <span className="text-3xl"></span>
<div> <div>
<h3 className="font-bold text-red-900 text-lg">IMPORTANT WARNING</h3> <h3 className="font-bold text-red-900 dark:text-red-100 text-lg">IMPORTANT WARNING</h3>
<ul className="text-red-800 mt-2 space-y-1 text-sm"> <ul className="text-red-800 dark:text-red-200 mt-2 space-y-1 text-sm">
<li> You can only submit your project ONCE</li> <li> You can only submit your project ONCE</li>
<li> After submission, you CANNOT edit or change anything</li> <li> After submission, you CANNOT edit or change anything</li>
<li> Make sure all information is correct before submitting</li> <li> Make sure all information is correct before submitting</li>
@@ -120,7 +120,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
</div> </div>
</div> </div>
<div className="bg-white rounded-lg shadow-md p-8"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8">
<form onSubmit={(e) => { <form onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
setShowConfirmModal(true); setShowConfirmModal(true);
@@ -135,7 +135,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
{/* Description */} {/* Description */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Project Description <span className="text-red-500">*</span> Project Description <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
@@ -186,8 +186,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
{/* Screenshots */} {/* Screenshots */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Project Screenshots <span className="text-gray-400">(Optional)</span> Project Screenshots <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
</label> </label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4"> <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
{screenshots.map((url, index) => ( {screenshots.map((url, index) => (
@@ -195,7 +195,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
<img <img
src={url} src={url}
alt={`Screenshot ${index + 1}`} alt={`Screenshot ${index + 1}`}
className="w-full h-32 object-cover rounded-lg border-2 border-gray-200" className="w-full h-32 object-cover rounded-lg border-2 border-gray-200 dark:border-neutral-700"
/> />
<button <button
type="button" type="button"
@@ -207,10 +207,10 @@ const SubmitProjectPage: FC = (): ReactElement => {
</div> </div>
))} ))}
</div> </div>
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50"> <label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
<div className="text-center"> <div className="text-center">
<p className="text-gray-500">Click to upload screenshots</p> <p className="text-gray-500 dark:text-neutral-400">Click to upload screenshots</p>
<p className="text-xs text-gray-400 mt-1">PNG, JPG up to 5MB each</p> <p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">PNG, JPG up to 5MB each</p>
</div> </div>
<input <input
type="file" type="file"
@@ -247,22 +247,22 @@ const SubmitProjectPage: FC = (): ReactElement => {
{/* Confirmation Modal */} {/* Confirmation Modal */}
{showConfirmModal && ( {showConfirmModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl dark:shadow-neutral-950/50 max-w-md w-full p-6">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-5xl mb-4"></div> <div className="text-5xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-900 mb-2"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Final Confirmation Final Confirmation
</h2> </h2>
<p className="text-red-600 font-medium"> <p className="text-red-600 dark:text-red-400 font-medium">
This action is IRREVERSIBLE! This action is IRREVERSIBLE!
</p> </p>
</div> </div>
<div className="bg-gray-50 rounded-lg p-4 mb-6"> <div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-4 mb-6">
<p className="text-sm text-gray-700 mb-3"> <p className="text-sm text-gray-700 dark:text-neutral-300 mb-3">
By clicking "Submit Project", you confirm that: By clicking "Submit Project", you confirm that:
</p> </p>
<ul className="text-sm text-gray-600 space-y-2"> <ul className="text-sm text-gray-600 dark:text-neutral-400 space-y-2">
<li> All information is correct and complete</li> <li> All information is correct and complete</li>
<li> You understand this can only be done once</li> <li> You understand this can only be done once</li>
<li> You cannot edit after submission</li> <li> You cannot edit after submission</li>
+24 -24
View File
@@ -63,14 +63,14 @@ const BrowseTeamsPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header */} {/* Header */}
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Browse Teams</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Browse Teams</h1>
<p className="text-gray-600 mt-1">Find and join teams looking for members</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">Find and join teams looking for members</p>
</div> </div>
<Link to="/dashboard" className="hidden md:block"> <Link to="/dashboard" className="hidden md:block">
<Button variant="secondary">Back to Dashboard</Button> <Button variant="secondary">Back to Dashboard</Button>
@@ -81,10 +81,10 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Filters */} {/* Filters */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="bg-white p-6 rounded-lg shadow-sm mb-6"> <div className="bg-white dark:bg-neutral-900 p-6 rounded-lg shadow-sm mb-6">
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Search Teams Search Teams
</label> </label>
<input <input
@@ -92,11 +92,11 @@ const BrowseTeamsPage: FC = (): ReactElement => {
placeholder="Search by team name..." placeholder="Search by team name..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-[42px] px-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full h-[42px] px-3 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Filter by City Filter by City
</label> </label>
<CitySelect <CitySelect
@@ -111,17 +111,17 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Teams List */} {/* Teams List */}
{isLoading ? ( {isLoading ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-gray-600">Loading teams...</p> <p className="text-gray-600 dark:text-neutral-400">Loading teams...</p>
</div> </div>
) : teams.length === 0 ? ( ) : teams.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm p-12 text-center"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center">
<p className="text-gray-600 text-lg">No teams found</p> <p className="text-gray-600 dark:text-neutral-400 text-lg">No teams found</p>
<p className="text-gray-500 mt-2">Try adjusting your filters</p> <p className="text-gray-500 dark:text-neutral-500 mt-2">Try adjusting your filters</p>
</div> </div>
) : ( ) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{teams.map((team) => ( {teams.map((team) => (
<div key={team.id} className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col"> <div key={team.id} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col">
<img <img
src={team.banner || '/images/banner-imphnen.png'} src={team.banner || '/images/banner-imphnen.png'}
alt={team.name} alt={team.name}
@@ -136,19 +136,19 @@ const BrowseTeamsPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover" className="w-12 h-12 rounded-full object-cover"
/> />
) : ( ) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 text-xl">👥</span> <span className="text-gray-500 dark:text-neutral-400 text-xl">👥</span>
</div> </div>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-gray-900 line-clamp-2">{team.name}</h3> <h3 className="text-lg font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h3>
<div className="text-sm text-gray-600 flex gap-2"> <div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
<p className="truncate flex-1 min-w-0">📍 {team.city}</p> <p className="truncate flex-1 min-w-0">📍 {team.city}</p>
<p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p> <p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p>
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 text-sm mb-4 line-clamp-3"> <p className="text-gray-600 dark:text-neutral-400 text-sm mb-4 line-clamp-3">
{team.description} {team.description}
</p> </p>
<div className="space-y-3 mt-auto"> <div className="space-y-3 mt-auto">
@@ -189,23 +189,23 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Join Request Modal */} {/* Join Request Modal */}
{showJoinModal && ( {showJoinModal && (
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Request to Join Team Request to Join Team
</h2> </h2>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 dark:text-neutral-400 mb-6">
Send a message to the team leader explaining why you want to join Send a message to the team leader explaining why you want to join
</p> </p>
<form onSubmit={onSubmit} className="space-y-4"> <form onSubmit={onSubmit} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
Your Message Your Message
</label> </label>
<textarea <textarea
{...form.register('message')} {...form.register('message')}
rows={4} rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
placeholder="Tell the team leader why you want to join their team..." placeholder="Tell the team leader why you want to join their team..."
/> />
{form.formState.errors.message && ( {form.formState.errors.message && (
+35 -44
View File
@@ -5,7 +5,8 @@ import { useNavigate } from 'react-router';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { teamCreateSchema, TTeamCreateForm, useCreateTeam, ETeamVisibility, useUploadFile } from '@imphnen-frontend-service/service'; import { teamCreateSchema, TTeamCreateForm, useCreateTeam, ETeamVisibility, useUploadFile } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import INDONESIAN_CITIES from '../../../constants/cities';
import { CitySelect } from '../../../components/city-select';
const CreateTeamPage: FC = (): ReactElement => { const CreateTeamPage: FC = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -79,29 +80,29 @@ const CreateTeamPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header */} {/* Header */}
<div className="bg-white border-b"> <div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-gray-900">Create Your Team</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">Create Your Team</h1>
<p className="text-gray-600 mt-1">Build your hackathon dream team</p> <p className="text-gray-600 dark:text-neutral-400 mt-1">Build your hackathon dream team</p>
</div> </div>
</div> </div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-lg shadow-md p-8"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-900/50 p-8">
<form onSubmit={onSubmit} className="space-y-6"> <form onSubmit={onSubmit} className="space-y-6">
{/* Banner Upload */} {/* Banner Upload */}
<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">
Team Banner <span className="text-gray-400">(Optional)</span> Team Banner <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
</label> </label>
{bannerPreview ? ( {bannerPreview ? (
<div className="relative"> <div className="relative">
<img <img
src={bannerPreview} src={bannerPreview}
alt="Banner preview" alt="Banner preview"
className="w-full h-48 object-cover rounded-lg border-2 border-gray-200" className="w-full h-48 object-cover rounded-lg border-2 border-gray-200 dark:border-neutral-700"
/> />
<button <button
type="button" type="button"
@@ -115,10 +116,10 @@ const CreateTeamPage: FC = (): ReactElement => {
</button> </button>
</div> </div>
) : ( ) : (
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50"> <label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
<div className="text-center"> <div className="text-center">
<p className="text-gray-500">Click to upload banner</p> <p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p>
<p className="text-xs text-gray-400 mt-1">1200x400 recommended</p> <p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended</p>
</div> </div>
<input <input
type="file" type="file"
@@ -132,19 +133,19 @@ const CreateTeamPage: FC = (): ReactElement => {
{/* Logo Upload */} {/* Logo Upload */}
<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">
Team Logo <span className="text-gray-400">(Optional, but highly recommended)</span> Team Logo <span className="text-gray-400 dark:text-neutral-500">(Optional, but highly recommended)</span>
</label> </label>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{logoPreview ? ( {logoPreview ? (
<img <img
src={logoPreview} src={logoPreview}
alt="Logo preview" alt="Logo preview"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200" className="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-neutral-700"
/> />
) : ( ) : (
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center"> <div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-400 text-3xl">👥</span> <span className="text-gray-400 dark:text-neutral-500 text-3xl">👥</span>
</div> </div>
)} )}
<div> <div>
@@ -186,36 +187,26 @@ const CreateTeamPage: FC = (): ReactElement => {
{/* City */} {/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
City <span className="text-red-500">*</span> City <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
control={form.control} control={form.control}
name="city" name="city"
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<div> <CitySelect
<select value={field.value}
{...field} onChange={field.onChange}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" error={fieldState.error?.message}
> placeholder="Search your city..."
<option value="">Select city</option> />
{INDONESIAN_CITIES.map((city) => (
<option key={city} value={city}>
{city}
</option>
))}
</select>
{fieldState.error && (
<p className="text-sm text-red-500 mt-1">{fieldState.error.message}</p>
)}
</div>
)} )}
/> />
</div> </div>
{/* Description */} {/* Description */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Description <span className="text-red-500">*</span> Description <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
@@ -239,7 +230,7 @@ const CreateTeamPage: FC = (): ReactElement => {
{/* Visibility */} {/* Visibility */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700"> <label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Team Visibility <span className="text-red-500">*</span> Team Visibility <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
@@ -247,7 +238,7 @@ const CreateTeamPage: FC = (): ReactElement => {
name="visibility" name="visibility"
render={({ field }) => ( render={({ field }) => (
<div className="space-y-3"> <div className="space-y-3">
<label className="flex items-start space-x-3 cursor-pointer border rounded-lg p-4 hover:bg-gray-50"> <label className="flex items-start space-x-3 cursor-pointer border dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800">
<input <input
type="radio" type="radio"
{...field} {...field}
@@ -256,13 +247,13 @@ const CreateTeamPage: FC = (): ReactElement => {
className="mt-1" className="mt-1"
/> />
<div> <div>
<p className="font-medium text-gray-900">Public</p> <p className="font-medium text-gray-900 dark:text-white">Public</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
Team will be visible in Browse Teams. Anyone can request to join. Team will be visible in Browse Teams. Anyone can request to join.
</p> </p>
</div> </div>
</label> </label>
<label className="flex items-start space-x-3 cursor-pointer border rounded-lg p-4 hover:bg-gray-50"> <label className="flex items-start space-x-3 cursor-pointer border dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800">
<input <input
type="radio" type="radio"
{...field} {...field}
@@ -271,8 +262,8 @@ const CreateTeamPage: FC = (): ReactElement => {
className="mt-1" className="mt-1"
/> />
<div> <div>
<p className="font-medium text-gray-900">Private</p> <p className="font-medium text-gray-900 dark:text-white">Private</p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600 dark:text-neutral-400">
Team is hidden from Browse Teams. Members can only join via invitation. Team is hidden from Browse Teams. Members can only join via invitation.
</p> </p>
</div> </div>
@@ -283,8 +274,8 @@ const CreateTeamPage: FC = (): ReactElement => {
</div> </div>
{/* Warning */} {/* Warning */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4"> <div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<p className="text-sm text-yellow-800"> <p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Note:</strong> As team leader, you cannot leave or join another team after creating this team. <strong>Note:</strong> As team leader, you cannot leave or join another team after creating this team.
</p> </p>
</div> </div>
+4 -4
View File
@@ -6,19 +6,19 @@ const TeamsLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden sticky top-0 bg-white border-b px-4 py-3 z-10"> <div className="lg:hidden sticky top-0 bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 z-10">
<div className="flex items-center"> <div className="flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg <svg
className="w-6 h-6 text-gray-600" className="w-6 h-6 text-gray-600 dark:text-neutral-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -6,21 +6,21 @@ const UserDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
return ( return (
<div className="flex min-h-screen bg-gray-50"> <div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} /> <Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto"> <div className="flex-1 flex flex-col overflow-auto">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white border-b px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<button <button
onClick={() => setSidebarOpen(true)} onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 Hackathon</h1> <h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
</div> </div>
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
+34 -34
View File
@@ -14,26 +14,26 @@ const UserProfilePage: FC = (): ReactElement => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-gray-600">Loading user profile...</div> <div className="text-gray-600 dark:text-neutral-400">Loading user profile...</div>
</div> </div>
); );
} }
if (error || !user) { if (error || !user) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 mb-4">User not found</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">User not found</h2>
{error && <p className="text-red-600 mb-4">{String(error)}</p>} {error && <p className="text-red-600 dark:text-red-400 mb-4">{String(error)}</p>}
<Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button> <Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button>
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
{/* Header */} {/* 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-4 md:py-6"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-6">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-3 md:space-x-4"> <div className="flex items-center space-x-3 md:space-x-4">
@@ -41,19 +41,19 @@ const UserProfilePage: FC = (): ReactElement => {
<img <img
src={user.avatar} src={user.avatar}
alt={user.fullname} alt={user.fullname}
className="w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white shadow-lg" className="w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white dark:border-neutral-800 shadow-lg"
/> />
) : ( ) : (
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full bg-gray-200 flex items-center justify-center border-4 border-white shadow-lg"> <div className="w-16 h-16 md:w-20 md:h-20 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center border-4 border-white dark:border-neutral-800 shadow-lg">
<span className="text-gray-500 text-2xl md:text-3xl">👤</span> <span className="text-gray-500 dark:text-neutral-400 text-2xl md:text-3xl">👤</span>
</div> </div>
)} )}
<div> <div>
<h1 className="text-xl md:text-3xl font-bold text-gray-900">{user.fullname}</h1> <h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white">{user.fullname}</h1>
<p className="text-sm md:text-base text-gray-600 mt-1">{user.email}</p> <p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1">{user.email}</p>
{user.location && ( {user.location && (
<div className="flex items-center space-x-4 mt-2"> <div className="flex items-center space-x-4 mt-2">
<span className="text-sm text-gray-500">📍 {user.location}</span> <span className="text-sm text-gray-500 dark:text-neutral-500">📍 {user.location}</span>
</div> </div>
)} )}
</div> </div>
@@ -71,21 +71,21 @@ const UserProfilePage: FC = (): ReactElement => {
<div className="lg:col-span-2 space-y-4 md:space-y-6"> <div className="lg:col-span-2 space-y-4 md:space-y-6">
{/* About Section */} {/* About Section */}
{user.bio && ( {user.bio && (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6 overflow-hidden"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6 overflow-hidden">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">About</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">About</h2>
<p className="text-gray-700 break-all overflow-wrap-anywhere">{user.bio}</p> <p className="text-gray-700 dark:text-neutral-300 break-all overflow-wrap-anywhere">{user.bio}</p>
</div> </div>
)} )}
{/* Skills Section */} {/* Skills Section */}
{user.skills && user.skills.length > 0 && ( {user.skills && user.skills.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">Skills</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Skills</h2>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{user.skills.map((skill: string) => ( {user.skills.map((skill: string) => (
<span <span
key={skill} 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" className="inline-flex items-center px-4 py-2 bg-blue-600 dark:bg-primary-600 text-white rounded-lg text-sm font-medium shadow-sm hover:bg-blue-700 dark:hover:bg-primary-700 transition-colors"
> >
{skill} {skill}
</span> </span>
@@ -96,35 +96,35 @@ const UserProfilePage: FC = (): ReactElement => {
{/* Teams Section */} {/* Teams Section */}
{userTeams.length > 0 ? ( {userTeams.length > 0 ? (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">Teams ({userTeams.length})</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Teams ({userTeams.length})</h2>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{userTeams.map((team: any) => ( {userTeams.map((team: any) => (
<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 dark:shadow-neutral-950/50 overflow-hidden hover:shadow-lg transition-shadow">
<img src={team.banner || '/images/banner-imphnen.png'} alt={team.name} className="w-full h-24 object-cover" /> <img src={team.banner || '/images/banner-imphnen.png'} alt={team.name} className="w-full h-24 object-cover" />
<div className="p-4"> <div className="p-4">
<div className="flex items-center space-x-3 mb-2"> <div className="flex items-center space-x-3 mb-2">
{team.logo && <img src={team.logo} alt={team.name} className="w-10 h-10 rounded-full object-cover" />} {team.logo && <img src={team.logo} alt={team.name} className="w-10 h-10 rounded-full object-cover" />}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-bold text-gray-900 line-clamp-2">{team.name}</h3> <h3 className="font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h3>
<div className="text-sm text-gray-600 flex gap-2"> <div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
<p className="truncate flex-1 min-w-0">📍 {team.city}</p> <p className="truncate flex-1 min-w-0">📍 {team.city}</p>
<p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p> <p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p>
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 text-sm line-clamp-2">{team.description}</p> <p className="text-gray-600 dark:text-neutral-400 text-sm line-clamp-2">{team.description}</p>
</div> </div>
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
) : ( ) : (
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 mb-3 md:mb-4">Teams</h2> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Teams</h2>
<div className="text-center py-8"> <div className="text-center py-8">
<div className="text-4xl mb-3">👥</div> <div className="text-4xl mb-3">👥</div>
<p className="text-gray-600">Not in any team yet</p> <p className="text-gray-600 dark:text-neutral-400">Not in any team yet</p>
</div> </div>
</div> </div>
)} )}
@@ -133,17 +133,17 @@ const UserProfilePage: FC = (): ReactElement => {
{/* Sidebar */} {/* Sidebar */}
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
{/* Contact Info */} {/* Contact Info */}
<div className="bg-white rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 mb-3 md:mb-4">Contact Information</h3> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Contact Information</h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<p className="text-sm text-gray-600">Email</p> <p className="text-sm text-gray-600 dark:text-neutral-400">Email</p>
<p className="text-gray-900 font-medium">{user.email}</p> <p className="text-gray-900 dark:text-white font-medium">{user.email}</p>
</div> </div>
{user.location && ( {user.location && (
<div> <div>
<p className="text-sm text-gray-600">Location</p> <p className="text-sm text-gray-600 dark:text-neutral-400">Location</p>
<p className="text-gray-900 font-medium">{user.location}</p> <p className="text-gray-900 dark:text-white font-medium">{user.location}</p>
</div> </div>
)} )}
</div> </div>
@@ -70,15 +70,15 @@ export const CitySelect: FC<CitySelectProps> = ({
}} }}
onClick={handleInputClick} onClick={handleInputClick}
placeholder={placeholder} placeholder={placeholder}
className={`w-full h-[42px] px-3 text-[15px] border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ className={`w-full h-[42px] px-3 text-[15px] border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 ${
error ? 'border-red-500' : 'border-gray-300' error ? 'border-red-500' : 'border-gray-300 dark:border-neutral-600'
}`} }`}
/> />
{value && !isOpen && ( {value && !isOpen && (
<button <button
type="button" type="button"
onClick={handleClearSelection} onClick={handleClearSelection}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 cursor-pointer" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-neutral-500 hover:text-gray-600 cursor-pointer dark:hover:text-neutral-300"
> >
</button> </button>
@@ -88,15 +88,17 @@ export const CitySelect: FC<CitySelectProps> = ({
{error && <p className="text-sm text-red-500 mt-1">{error}</p>} {error && <p className="text-sm text-red-500 mt-1">{error}</p>}
{isOpen && ( {isOpen && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto text-label2"> <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 text-label2">
{filteredCities.length > 0 ? ( {filteredCities.length > 0 ? (
<ul className="py-1"> <ul className="py-1">
{filteredCities.map((city) => ( {filteredCities.map((city) => (
<li <li
key={city} key={city}
onClick={() => handleSelectCity(city)} onClick={() => handleSelectCity(city)}
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 ${ className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${
value === city ? 'bg-blue-100 text-blue-700' : '' value === city
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400'
: 'text-gray-900 dark:text-neutral-200'
}`} }`}
> >
{city} {city}
@@ -104,7 +106,7 @@ export const CitySelect: FC<CitySelectProps> = ({
))} ))}
</ul> </ul>
) : ( ) : (
<div className="px-3 py-2 text-gray-500 text-sm"> <div className="px-3 py-2 text-gray-500 dark:text-neutral-400 text-sm">
No cities found No cities found
</div> </div>
)} )}
+92 -15
View File
@@ -1,4 +1,4 @@
import { FC, useState, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { Link, useLocation } from 'react-router'; import { Link, useLocation } from 'react-router';
import { import {
useMyTeams, useMyTeams,
@@ -7,6 +7,7 @@ import {
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTheme } from './theme-provider';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
interface NavItem { interface NavItem {
@@ -26,6 +27,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { session, clearSession } = useAuthStore(); const { session, clearSession } = useAuthStore();
const { data: teamsData } = useMyTeams(); const { data: teamsData } = useMyTeams();
const { theme, setTheme, resolvedTheme } = useTheme();
const user = session?.user; const user = session?.user;
const myTeams = teamsData?.data || []; const myTeams = teamsData?.data || [];
@@ -94,18 +96,84 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
}, },
]; ];
const cycleTheme = () => {
if (theme === 'light') setTheme('dark');
else if (theme === 'dark') setTheme('system');
else setTheme('light');
};
const getThemeIcon = () => {
if (theme === 'system') {
return (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
);
}
if (resolvedTheme === 'dark') {
return (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
);
}
return (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
);
};
const getThemeLabel = () => {
if (theme === 'system') return 'System';
if (theme === 'dark') return 'Dark';
return 'Light';
};
const sidebarContent = ( const sidebarContent = (
<div className="w-64 bg-white border-r min-h-screen flex flex-col"> <div className="w-64 bg-white dark:bg-neutral-900 border-r dark:border-neutral-700 min-h-screen flex flex-col">
{/* Logo / Brand with Close Button */} {/* Logo / Brand with Close Button */}
<div className="p-6 flex items-center justify-between"> <div className="p-6 flex items-center justify-between border-b dark:border-neutral-700">
<h1 className="text-xl font-bold text-gray-900">Hackathon</h1> <h1 className="text-xl font-bold text-gray-900 dark:text-white">
Hackathon
</h1>
{onClose && ( {onClose && (
<button <button
onClick={onClose} onClick={onClose}
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors" className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
> >
<svg <svg
className="w-5 h-5 text-gray-500" className="w-5 h-5 text-gray-500 dark:text-neutral-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -122,7 +190,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</div> </div>
{/* User Info */} {/* User Info */}
<div className="p-4 border-y"> <div className="p-4 border-y dark:border-neutral-700">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{user?.avatar ? ( {user?.avatar ? (
<img <img
@@ -136,15 +204,17 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
icon="ic:baseline-person" icon="ic:baseline-person"
width="24" width="24"
height="24" height="24"
className="text-gray-400" className="text-gray-400 dark:text-neutral-400"
/> />
</div> </div>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate"> <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{user?.fullname || user?.email?.split('@')[0] || 'User'} {user?.fullname || user?.email?.split('@')[0] || 'User'}
</p> </p>
<p className="text-xs text-gray-500 truncate">{user?.email}</p> <p className="text-xs text-gray-500 dark:text-neutral-400 truncate">
{user?.email}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -162,8 +232,8 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
to={item.path} to={item.path}
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${ className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
isActive isActive
? 'bg-primary-50 text-primary-600 font-medium' ? 'bg-primary-50 dark:bg-blue-900/30 text-primary-600 dark:text-blue-400 font-medium'
: 'text-gray-700 hover:bg-gray-100' : 'text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800'
}`} }`}
> >
{item.icon} {item.icon}
@@ -175,11 +245,18 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</ul> </ul>
</nav> </nav>
{/* Logout Button */} {/* Theme Toggle & Logout */}
<div className="p-4 border-t"> <div className="p-4 border-t dark:border-neutral-700 space-y-2">
<button
onClick={cycleTheme}
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
>
{getThemeIcon()}
<span>{getThemeLabel()}</span>
</button>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 hover:bg-red-50 transition-colors cursor-pointer" 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 cursor-pointer"
> >
<Icon <Icon
icon="heroicons:arrow-right-end-on-rectangle" icon="heroicons:arrow-right-end-on-rectangle"
@@ -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;
+39 -6
View File
@@ -2,6 +2,9 @@
@import 'tailwindcss'; @import 'tailwindcss';
@source "../../../libs/ui/**/*.{ts,tsx}"; @source "../../../libs/ui/**/*.{ts,tsx}";
/* Enable class-based dark mode (using .dark class on html element) */
@custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
--color-primary-50: #f0f8ff; --color-primary-50: #f0f8ff;
--color-primary-100: #e1f0fd; --color-primary-100: #e1f0fd;
@@ -20,12 +23,12 @@
--color-neutral-200: #d1d1d1; --color-neutral-200: #d1d1d1;
--color-neutral-300: #b0b0b0; --color-neutral-300: #b0b0b0;
--color-neutral-400: #888888; --color-neutral-400: #888888;
--color-neutral-500: #6d6d6d; --color-neutral-500: #6e6a86;
--color-neutral-600: #5d5d5d; --color-neutral-600: #56526e;
--color-neutral-700: #4f4f4f; --color-neutral-700: #44415a;
--color-neutral-800: #454545; --color-neutral-800: #393552;
--color-neutral-900: #3d3d3d; --color-neutral-900: #2a273f;
--color-neutral-950: #2b2b2b; --color-neutral-950: #232136;
--color-success-100: #e0fbd8; --color-success-100: #e0fbd8;
--color-success-200: #bcf8b0; --color-success-200: #bcf8b0;
@@ -108,6 +111,14 @@
border-color: var(--color-neutral-200, currentColor); border-color: var(--color-neutral-200, currentColor);
} }
.dark *,
.dark ::after,
.dark ::before,
.dark ::backdrop,
.dark ::file-selector-button {
border-color: var(--color-neutral-700, currentColor);
}
/* Custom scrollbar */ /* Custom scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;
@@ -118,19 +129,41 @@
background: var(--color-neutral-50); background: var(--color-neutral-50);
} }
.dark ::-webkit-scrollbar-track {
background: var(--color-neutral-900);
}
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #cccccc; background: #cccccc;
border-radius: 3px; border-radius: 3px;
} }
.dark ::-webkit-scrollbar-thumb {
background: var(--color-neutral-600);
}
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-300); background: var(--color-neutral-300);
} }
.dark ::-webkit-scrollbar-thumb:hover {
background: var(--color-neutral-500);
}
html { html {
font-family: 'Bai Jamjuree', sans-serif; font-family: 'Bai Jamjuree', sans-serif;
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
line-height: 1.2; line-height: 1.2;
} }
/* Dark mode base styles */
.dark {
color-scheme: dark;
}
.dark body {
background-color: var(--color-neutral-950);
color: var(--color-neutral-100);
}
} }
+9 -6
View File
@@ -9,6 +9,7 @@ import {
QueryProvider, QueryProvider,
} from '@imphnen-frontend-service/utils'; } from '@imphnen-frontend-service/utils';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import { ThemeProvider } from './components/theme-provider';
import './index.css'; import './index.css';
const files = import.meta.glob('./app/**/*(page|layout).tsx'); const files = import.meta.glob('./app/**/*(page|layout).tsx');
@@ -36,11 +37,13 @@ if (!rootElement) throw new Error('Failed to find the root element');
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
<QueryProvider> <ThemeProvider defaultTheme="system">
<ModalLoginProvider> <QueryProvider>
<Toaster position="top-right" /> <ModalLoginProvider>
<RouterProvider router={router} /> <Toaster position="top-right" richColors />
</ModalLoginProvider> <RouterProvider router={router} />
</QueryProvider> </ModalLoginProvider>
</QueryProvider>
</ThemeProvider>
</StrictMode> </StrictMode>
); );
+40 -1
View File
@@ -312,9 +312,48 @@ export const useManageMember = (teamId: string) => {
export const useRemoveMember = (teamId: string) => { export const useRemoveMember = (teamId: string) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({ return useMutation({
mutationFn: (userId: string) => teamsApi.removeMember(teamId, userId), mutationFn: async (userId: string) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to remove a member');
}
// Verify the current user is the team leader
const { data: team, error: teamError } = await supabase
.from('teams')
.select('leader_id')
.eq('id', teamId)
.single();
if (teamError || !team) {
throw new Error('Team not found');
}
if (team.leader_id !== session.user.id) {
throw new Error('Only the team leader can remove members');
}
// Cannot remove the leader
if (userId === team.leader_id) {
throw new Error('Cannot remove the team leader');
}
// Delete the team member record
const { error: deleteError } = await supabase
.from('team_members')
.delete()
.eq('team_id', teamId)
.eq('user_id', userId);
if (deleteError) {
console.error('Failed to remove member:', deleteError);
throw new Error(deleteError.message || 'Failed to remove member');
}
return { success: true };
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) }); queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) }); queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
+4 -4
View File
@@ -26,12 +26,12 @@ type TButtonProps = DetailedHTMLProps<
const variantClasses: Record<TButtonVariant, string> = { const variantClasses: Record<TButtonVariant, string> = {
primary: 'bg-primary-500 hover:bg-primary-600 text-white shadow-md', primary: 'bg-primary-500 hover:bg-primary-600 text-white shadow-md',
secondary: secondary:
'bg-white hover:text-primary-600 hover:bg-gray-50 text-primary-500 shadow-md', 'bg-white dark:bg-neutral-800 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-neutral-700 text-primary-500 dark:text-primary-400 shadow-md dark:shadow-neutral-900/50',
text: 'bg-transparent hover:text-primary-600 hover:bg-gray-50 text-primary-500', text: 'bg-transparent hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-neutral-800 text-primary-500 dark:text-primary-400',
bordered: bordered:
'border border-primary-500 hover:border-primary-600 bg-transparent hover:text-primary-600 hover:bg-gray-50 text-primary-500', 'border border-primary-500 dark:border-primary-400 hover:border-primary-600 dark:hover:border-primary-300 bg-transparent hover:text-primary-600 dark:hover:text-primary-300 hover:bg-gray-50 dark:hover:bg-neutral-800 text-primary-500 dark:text-primary-400',
success: 'bg-success-500 hover:bg-success-600 text-white shadow-md', success: 'bg-success-500 hover:bg-success-600 text-white shadow-md',
danger: 'bg-danger-100 hover:bg-danger-200 text-danger-500 shadow-md', danger: 'bg-danger-100 dark:bg-danger-500/20 hover:bg-danger-200 dark:hover:bg-danger-500/30 text-danger-500 shadow-md dark:shadow-neutral-900/50',
}; };
const sizeClasses: Record<TButtonSize, string> = { const sizeClasses: Record<TButtonSize, string> = {