Compare commits

...
Author SHA1 Message Date
Hafid Nur bffa32dd67 add maintenance page 2025-11-27 23:44:56 +07:00
35c9539c08 feat(hackathon): Change "edit profile" into modal dialog (#65)
* feat: update landing page

* feat: Add favicon and SEO (JSON-LD schema, meta tags)

- Add favicon image
- Add JSON-LD schema for Hackathon event
- Add react-helmet-async for managing meta tags

* chore: remove react-helmet-async

* chore: update landing page

* chore: update UI for signup and login

- add back button for better UX

* feat: add image loading state and skeleton UI for team dashboard

* fix: landing navbar not sticky to top, fix onboarding UI

* feat(hackathon): update UI

- Update dashboard, onboarding user, edit profile, and team layout
  to make it more consistent

* fix: optimize placeholder banner image & minor fix dark mode

* feat(hackathon): update landing page dark mode color scheme

* feat(hackathon): update auth page dark mode color scheme

* feat(hackathon): dashboard UI improvement and dark mode adjustment

* feat(hackathon): browse team page

- update dark mode color scheme
- add some border style to give outline and depth
- change emoji to icon

* feat(hackathon): update team page

- update dark mode color scheme for team page: view, team edit,
  and submit project page
- fix bug in chat where messages are displayed double
- make the form field size consistent

* feat(hackathon): update dark mode onboarding and edit profile

* feat(hackathon): change "edit profile" to modal dialog

- Add "edit profile" modal dialog that can be triggered from dashboard
- Update background overlay for dark mode
- Update dark mode for auth callback

* feat(hackathon): dark mode for user page

---------

Co-authored-by: Maulana Sodiqin <sodiqincahyana1@gmail.com>
Co-authored-by: Muhammad Zakir Ramadhan <61570975+zakirkun@users.noreply.github.com>
2025-11-27 16:25:21 +07:00
bc5b072ad8 feat(hackathon): Improve dark mode and UI/UX in Hackathon website (#64)
* feat: update landing page

* feat: Add favicon and SEO (JSON-LD schema, meta tags)

- Add favicon image
- Add JSON-LD schema for Hackathon event
- Add react-helmet-async for managing meta tags

* chore: remove react-helmet-async

* chore: update landing page

* chore: update UI for signup and login

- add back button for better UX

* feat: add image loading state and skeleton UI for team dashboard

* fix: landing navbar not sticky to top, fix onboarding UI

* feat(hackathon): update UI

- Update dashboard, onboarding user, edit profile, and team layout
  to make it more consistent

* fix: optimize placeholder banner image & minor fix dark mode

* feat(hackathon): update landing page dark mode color scheme

* feat(hackathon): update auth page dark mode color scheme

* feat(hackathon): dashboard UI improvement and dark mode adjustment

* feat(hackathon): browse team page

- update dark mode color scheme
- add some border style to give outline and depth
- change emoji to icon

* feat(hackathon): update team page

- update dark mode color scheme for team page: view, team edit,
  and submit project page
- fix bug in chat where messages are displayed double
- make the form field size consistent

* feat(hackathon): update dark mode onboarding and edit profile

---------

Co-authored-by: Maulana Sodiqin <sodiqincahyana1@gmail.com>
Co-authored-by: Muhammad Zakir Ramadhan <61570975+zakirkun@users.noreply.github.com>
2025-11-27 15:12:45 +07:00
30 changed files with 1393 additions and 939 deletions
+3 -3
View File
@@ -2,13 +2,13 @@ import { Link } from 'react-router-dom';
export default function NotFoundPage() { export default function NotFoundPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950">
<div className="text-center"> <div className="text-center">
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1> <h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
<h2 className="text-3xl font-semibold text-gray-900 mb-4"> <h2 className="text-3xl font-semibold text-gray-900 dark:text-gray-300 mb-4">
Page Not Found Page Not Found
</h2> </h2>
<p className="text-gray-600 mb-8"> <p className="text-gray-600 dark:text-gray-400 mb-8">
The page you are looking for doesn't exist or has been moved. The page you are looking for doesn't exist or has been moved.
</p> </p>
<Link <Link
@@ -176,13 +176,13 @@ const CallbackPage: FC = (): ReactElement => {
} }
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-gray-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>
<h2 className="text-xl font-semibold text-gray-900 mb-2"> <h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Completing login... Completing login...
</h2> </h2>
<p className="text-gray-600">Please wait</p> <p className="text-gray-600 dark:text-gray-400">Please wait</p>
</div> </div>
</div> </div>
); );
@@ -3,6 +3,7 @@ import { supabase } from '@imphnen-frontend-service/service';
import { Link, useNavigate } from 'react-router'; import { Link, 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 ForgotPasswordPage() { export default function ForgotPasswordPage() {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@@ -80,13 +81,23 @@ export default function ForgotPasswordPage() {
} }
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-gray-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-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-6">
<button
onClick={() => navigate('/auth/login')}
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 Login
</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">
Forgot Password? Forgot Password?
</h2> </h2>
<p className="text-gray-600"> <p className="text-gray-600 dark:text-gray-400">
No worries, we'll send you reset instructions No worries, we'll send you reset instructions
</p> </p>
</div> </div>
@@ -95,7 +106,7 @@ export default function ForgotPasswordPage() {
<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-gray-300 mb-1"
> >
Email Address Email Address
</label> </label>
@@ -106,7 +117,7 @@ export default function ForgotPasswordPage() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com" placeholder="your@email.com"
disabled={isLoading} disabled={isLoading}
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="bg-white dark:bg-gray-800 w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -119,16 +130,6 @@ export default function ForgotPasswordPage() {
{isLoading ? 'Sending...' : 'Send Reset Link'} {isLoading ? 'Sending...' : 'Send Reset Link'}
</button> </button>
</form> </form>
<div className="mt-8">
<Link
to="/auth/login"
className="text-primary-500 hover:text-primary-600 flex items-center"
>
<Icon icon="ic:baseline-chevron-left" width="24" height="24" />
<span> Back to Login</span>
</Link>
</div>
</div> </div>
</div> </div>
); );
+16 -14
View File
@@ -115,8 +115,8 @@ export default function LoginPage() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<button <button
onClick={() => navigate('/')} onClick={() => navigate('/')}
@@ -132,7 +132,7 @@ export default function LoginPage() {
<h2 className="text-3xl font-bold text-gray-900 dark:text-white 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 dark:text-neutral-400 font-sans"> <p className="text-gray-600 dark:text-gray-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>
@@ -147,7 +147,7 @@ export default function LoginPage() {
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
> >
Email Email
</label> </label>
@@ -158,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 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" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -167,7 +167,7 @@ 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 dark:text-neutral-300" className="block text-sm font-medium text-gray-700 dark:text-gray-300"
> >
Password Password
</label> </label>
@@ -185,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 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" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -193,23 +193,25 @@ 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 dark:focus:ring-offset-neutral-900 disabled:bg-gray-400 dark:disabled:bg-neutral-600 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-gray-900 disabled:bg-gray-400 dark:disabled:bg-gray-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 dark:border-neutral-600"></div> <div className="flex-1 border-t border-gray-300 dark:border-gray-600"></div>
<span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span> <span className="px-4 text-sm text-gray-500 dark:text-gray-400">
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div> OR
</span>
<div className="flex-1 border-t border-gray-300 dark:border-gray-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 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" className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg font-semibold text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:bg-gray-100 dark:disabled:bg-gray-800 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
<GithubOutlined className="text-xl" /> <GithubOutlined className="text-xl" />
<span> <span>
@@ -218,7 +220,7 @@ export default function LoginPage() {
</button> </button>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600 dark:text-neutral-400 text-sm"> <p className="text-gray-600 dark:text-gray-400 text-sm">
Don't have an account?{' '} Don't have an account?{' '}
<a <a
href="/auth/signup" href="/auth/signup"
@@ -230,7 +232,7 @@ export default function LoginPage() {
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-500 dark:text-neutral-500 text-xs"> <p className="text-gray-500 dark:text-gray-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>
@@ -10,17 +10,17 @@ export default function ResetPasswordPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isValidToken, setIsValidToken] = useState(false); const [isValidToken, setIsValidToken] = useState(false);
useEffect(() => { // useEffect(() => {
// Check if we have a valid session (from the reset link) // // Check if we have a valid session (from the reset link)
supabase.auth.getSession().then(({ data: { session } }) => { // supabase.auth.getSession().then(({ data: { session } }) => {
if (session) { // if (session) {
setIsValidToken(true); // setIsValidToken(true);
} else { // } else {
toast.error('Invalid or expired reset link'); // toast.error('Invalid or expired reset link');
setTimeout(() => navigate('/auth/forgot-password'), 2000); // setTimeout(() => navigate('/auth/forgot-password'), 2000);
} // }
}); // });
}, [navigate]); // }, [navigate]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -59,32 +59,36 @@ export default function ResetPasswordPage() {
} }
}; };
if (!isValidToken) { // if (!isValidToken) {
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-gray-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">Verifying reset link...</p> // <p className="text-gray-600 dark:text-gray-400">
</div> // Verifying reset link...
</div> // </p>
); // </div>
} // </div>
// );
// }
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-gray-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-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700">
<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">
Set New Password Set New Password
</h2> </h2>
<p className="text-gray-600">Enter your new password below</p> <p className="text-gray-600 dark:text-gray-400">
Enter your new password below
</p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<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-gray-300 mb-1"
> >
New Password New Password
</label> </label>
@@ -95,7 +99,7 @@ export default function ResetPasswordPage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isLoading} disabled={isLoading}
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-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
required required
minLength={6} minLength={6}
/> />
@@ -104,7 +108,7 @@ export default function ResetPasswordPage() {
<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-gray-300 mb-1"
> >
Confirm New Password Confirm New Password
</label> </label>
@@ -115,7 +119,7 @@ export default function ResetPasswordPage() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••" placeholder="••••••••"
disabled={isLoading} disabled={isLoading}
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-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
required required
minLength={6} minLength={6}
/> />
+22 -18
View File
@@ -138,8 +138,8 @@ export default function SignupPage() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4"> <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<button <button
onClick={() => navigate('/')} onClick={() => navigate('/')}
@@ -155,7 +155,9 @@ export default function SignupPage() {
<h2 className="text-3xl font-bold text-gray-900 dark:text-white 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 dark:text-neutral-400">Join the hackathon community</p> <p className="text-gray-600 dark:text-gray-400">
Join the hackathon community
</p>
</div> </div>
{error && ( {error && (
@@ -168,7 +170,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="fullname" htmlFor="fullname"
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
> >
Full Name Full Name
</label> </label>
@@ -179,7 +181,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 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" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -187,7 +189,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="email" htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
> >
Email Email
</label> </label>
@@ -198,7 +200,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 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" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -206,7 +208,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="password" htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
> >
Password Password
</label> </label>
@@ -217,7 +219,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 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" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -225,7 +227,7 @@ export default function SignupPage() {
<div> <div>
<label <label
htmlFor="confirmPassword" htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-1" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
> >
Confirm Password Confirm Password
</label> </label>
@@ -236,7 +238,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 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" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed"
required required
/> />
</div> </div>
@@ -244,23 +246,25 @@ 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 dark:focus:ring-offset-neutral-900 disabled:bg-gray-400 dark:disabled:bg-neutral-600 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-gray-900 disabled:bg-gray-400 dark:disabled:bg-gray-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 dark:border-neutral-600"></div> <div className="flex-1 border-t border-gray-300 dark:border-gray-600"></div>
<span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span> <span className="px-4 text-sm text-gray-500 dark:text-gray-400">
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div> OR
</span>
<div className="flex-1 border-t border-gray-300 dark:border-gray-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 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" className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg font-semibold text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900 disabled:bg-gray-100 dark:disabled:bg-gray-800 disabled:cursor-not-allowed transition-colors cursor-pointer"
> >
<GithubOutlined className="text-xl" /> <GithubOutlined className="text-xl" />
<span> <span>
@@ -269,7 +273,7 @@ export default function SignupPage() {
</button> </button>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-600 dark:text-neutral-400 text-sm"> <p className="text-gray-600 dark:text-gray-400 text-sm">
Already have an account?{' '} Already have an account?{' '}
<a <a
href="/auth/login" href="/auth/login"
@@ -281,7 +285,7 @@ export default function SignupPage() {
</div> </div>
<div className="mt-6 text-center"> <div className="mt-6 text-center">
<p className="text-gray-500 dark:text-neutral-500 text-xs"> <p className="text-gray-500 dark:text-gray-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>
+4 -4
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 dark:bg-neutral-950"> <div className="flex min-h-screen bg-gray-50 dark:bg-gray-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 dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center"> <div className="lg:hidden sticky top-0 bg-white dark:bg-gray-900 border-b dark:border-gray-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 dark:hover:bg-neutral-800 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<svg <svg
className="w-6 h-6 text-gray-600 dark:text-neutral-400" className="w-6 h-6 text-gray-600 dark:text-gray-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
+249 -196
View File
@@ -1,4 +1,4 @@
import { FC, ReactElement } from 'react'; import { FC, ReactElement, useEffect, useState } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { import {
useMyTeams, useMyTeams,
@@ -9,16 +9,49 @@ import {
import { toast } from 'sonner'; import { toast } from 'sonner';
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 ProfilePage from '../profile/page';
type Invitation = {
id: string;
team: {
id?: string;
name?: string;
logo?: string;
banner?: string;
description?: string;
city?: string;
visibility?: string;
leader_id?: string;
};
inviter: {
id?: string;
fullname?: string;
email?: string;
avatar?: string;
};
};
const DashboardPage: FC = (): ReactElement => { const DashboardPage: FC = (): ReactElement => {
const { session } = useAuthStore(); const { session } = useAuthStore();
const [showProfileModal, setShowProfileModal] = useState(false);
// Lock background scroll when profile modal is open
useEffect(() => {
if (showProfileModal) {
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = originalOverflow || '';
};
}
}, [showProfileModal]);
const { data: teamsData } = useMyTeams(); const { data: teamsData } = useMyTeams();
const { data: invitationsData } = useMyInvitations(); const { data: invitationsData } = useMyInvitations();
const { mutateAsync: respondToInvitation } = useRespondToInvitation(); const { mutateAsync: respondToInvitation } = useRespondToInvitation();
const user = session?.user; const user = session?.user;
const myTeams = teamsData?.data || []; const myTeams = teamsData?.data || [];
const invitations = invitationsData?.data || []; const invitations: Invitation[] = (invitationsData?.data ||
[]) as Invitation[];
const handleAcceptInvitation = async (invitationId: string) => { const handleAcceptInvitation = async (invitationId: string) => {
try { try {
@@ -41,213 +74,233 @@ const DashboardPage: FC = (): ReactElement => {
}; };
return ( return (
<div className="p-8 max-w-7xl mx-auto"> <>
<div className="mb-8"> <div className="p-6 md:p-8 md:max-w-7xl w-full mx-auto">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white"> <div className="mb-8">
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}! <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
</h1> Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
{user?.location && ( </h1>
<p className="text-gray-600 dark:text-neutral-400 mt-1 font-sans flex items-center space-x-1"> {user?.location && (
<Icon icon="heroicons:map-pin-16-solid" width="24" height="24" /> <p className="text-gray-600 dark:text-gray-400 mt-1 font-sans flex items-center space-x-1">
<span>{user.location}</span> <Icon icon="heroicons:map-pin-16-solid" width="24" height="24" />
</p> <span>{user.location}</span>
)} </p>
</div> )}
</div>
{invitations.length > 0 && ( {invitations.length > 0 && (
<div className="mb-8 bg-primary-50 dark:bg-blue-900/20 border border-primary-200 dark:border-blue-800 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 dark:text-white 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 dark:bg-neutral-800 p-4 rounded-lg shadow-sm flex items-center justify-between" className="bg-white dark:bg-gray-900 p-4 rounded-lg shadow-sm flex items-center justify-between"
> >
<div> <div>
<p className="font-medium text-gray-900 dark:text-white"> <p className="font-medium text-gray-900 dark:text-white">
{invitation.team.name} {invitation.team?.name ?? 'Unnamed Team'}
</p> </p>
<p className="text-sm text-gray-600 dark:text-neutral-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Invited by {invitation.inviter.fullname} Invited by{' '}
</p> {invitation.inviter?.fullname ?? 'Unknown User'}
</p>
</div>
<div className="flex space-x-2">
<Button
size="sm"
onClick={() => handleAcceptInvitation(invitation.id)}
>
Accept
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => handleRejectInvitation(invitation.id)}
>
Decline
</Button>
</div>
</div> </div>
<div className="flex space-x-2"> ))}
<Button </div>
size="sm" </div>
onClick={() => handleAcceptInvitation(invitation.id)} )}
>
Accept {myTeams.length > 0 ? (
</Button> <div className="mb-6 md:mb-8">
<Button <h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
size="sm" My Team
variant="secondary" </h2>
onClick={() => handleRejectInvitation(invitation.id)} <div className="grid grid-cols-1 gap-4 md:gap-6 w-full md:max-w-lg">
> {myTeams.map((team) => (
Decline <Link
</Button> key={team.id}
to={'/teams/' + team.id}
className="bg-white dark:bg-gray-900 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
{team.banner && (
<div className="w-full aspect-3/1">
<img
src={team.banner}
alt={team.name}
className="w-full h-full object-cover"
/>
</div>
)}
<div className="p-4">
<div className="flex flex-col md:flex-row items-center space-y-3 space-x-3 -mt-14 md:mt-0 mb-2 md:mb-3 min-w-12 min-h-12 ">
{team.logo && (
<img
src={team.logo}
alt={team.name}
className="w-20 h-20 rounded-full object-cover shrink-0 border-black dark:border-gray-700"
/>
)}
<div>
<h3 className="text-xl md:text-lg font-bold text-gray-900 dark:text-white leading-tight">
{team.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 flex items-center">
<Icon
icon="heroicons:map-pin-16-solid"
width="16"
height="16"
/>
<span className="ml-1">{team.city}</span>
</p>
</div>
</div>
<p className="text-gray-600 dark:text-gray-400 line-clamp-3 text-ellipsis font-sans">
{team.description}
</p>
</div>
</Link>
))}
</div>
</div>
) : (
<div className="mb-6 md:mb-8 bg-white dark:bg-gray-900 rounded-lg shadow-md p-6 md:p-8">
<div className="text-center">
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-1 md:mb-2">
You are not in a team yet
</h2>
<p className="text-sm md:text-base text-gray-600 dark:text-gray-400 font-sans">
Use the sidebar to browse teams or create your own
</p>
</div>
</div>
)}
<div className="mt-8 bg-white dark:bg-gray-900 rounded-xl shadow-lg">
<div className="bg-linear-to-r from-primary-600 to-primary-500 h-24 rounded-t-xl"></div>
<div className="px-4 md:px-8 pb-4 md:pb-8 max-w-7xl">
<div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6">
<div className="flex flex-col md:flex-row items-start">
{user?.avatar ? (
<img
src={user.avatar}
alt={user.fullname || 'User'}
className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-gray-700 shadow-lg object-cover"
/>
) : (
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-gray-700 shadow-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-400 dark:text-gray-500 text-3xl md:text-4xl font-sans">
U
</span>
</div>
)}
<div className="md:ml-6 mt-4 md:mt-14">
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">
{user?.fullname ||
user?.email?.split('@')[0] ||
'Unnamed User'}
</h2>
{user?.location ? (
<p className="text-gray-600 dark:text-gray-400 flex items-center mt-1 text-sm md:text-base font-sans">
<Icon
icon="heroicons:map-pin-16-solid"
width="16"
height="16"
/>
<span className="ml-1">{user.location}</span>
</p>
) : (
<Link
to="/onboarding/user"
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>
<Icon
icon="ic:baseline-chevron-right"
width="24"
height="24"
/>
</Link>
)}
</div> </div>
</div> </div>
))} {user?.location && (
</div> <button
</div> type="button"
)} onClick={() => setShowProfileModal(true)}
className="mt-3 md:mt-14 md:ml-auto inline-flex items-center justify-center px-3 py-1.5 md:px-4 md:py-2 bg-primary-500 text-white rounded-lg text md:text-sm font-medium shadow-sm hover:bg-primary-600 transition-colors w-full md:w-auto cursor-pointer"
{myTeams.length > 0 ? ( >
<div className="mb-6 md:mb-8"> Edit Profile
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> </button>
My Team )}
</h2> </div>
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="space-y-4 md:space-y-6 w-full max-w-full">
{myTeams.map((team) => ( {user?.bio && (
<Link <div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm w-full max-w-full overflow-x-hidden">
key={team.id} <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-2">
to={'/teams/' + team.id} About
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow" </h3>
> <p className="text-gray-800 dark:text-gray-200 leading-relaxed line-clamp-3 wrap-break-word break-all overflow-hidden w-full max-w-full">
{team.banner && ( {user.bio}
<img
src={team.banner}
alt={team.name}
className="w-full h-32 object-cover"
/>
)}
<div className="p-4 md:p-6">
<div className="flex items-center space-x-3 mb-2 md:mb-3">
{team.logo && (
<img
src={team.logo}
alt={team.name}
className="w-10 h-10 md:w-12 md:h-12 rounded-full object-cover"
/>
)}
<div>
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white">
{team.name}
</h3>
<p className="text-xs md:text-sm text-gray-600 dark:text-neutral-400">
City: {team.city}
</p>
</div>
</div>
<p className="text-gray-600 dark:text-neutral-400 text-xs md:text-sm line-clamp-2">
{team.description}
</p> </p>
</div> </div>
</Link>
))}
</div>
</div>
) : (
<div className="mb-6 md:mb-8 bg-white dark:bg-neutral-800 rounded-lg shadow-md p-6 md:p-8">
<div className="text-center">
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-1 md:mb-2">
You are not in a team yet
</h2>
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 font-sans">
Use the sidebar to browse teams or create your own
</p>
</div>
</div>
)}
<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="px-4 md:px-8 pb-4 md:pb-8">
<div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6">
<div className="flex flex-col md:flex-row items-start">
{user?.avatar ? (
<img
src={user.avatar}
alt={user.fullname || 'User'}
className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white 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 dark:border-neutral-700 shadow-lg bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-400 dark:text-neutral-500 text-3xl md:text-4xl font-sans">
U
</span>
</div>
)} )}
<div className="md:ml-6 mt-4 md:mt-14"> <div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white"> <h3 className="font-semibold text-gray-700 dark:text-gray-300 tracking-wide mb-3">
{user?.fullname || Contact
user?.email?.split('@')[0] ||
'Unnamed User'}
</h2>
{user?.location ? (
<p className="text-gray-600 dark:text-neutral-400 flex items-center mt-1 text-sm md:text-base font-sans">
{user.location}
</p>
) : (
<Link
to="/onboarding/user"
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>
<Icon
icon="ic:baseline-chevron-right"
width="24"
height="24"
/>
</Link>
)}
</div>
</div>
{user?.location && (
<Link
to="/profile"
className="mt-3 md:mt-14 md:ml-auto inline-flex items-center justify-center px-3 py-1.5 md:px-4 md:py-2 bg-primary-500 text-white rounded-lg text md:text-sm font-medium shadow-sm hover:bg-primary-600 transition-colors w-full md:w-auto"
>
Edit Profile
</Link>
)}
</div>
<div className="space-y-4 md:space-y-6">
{user?.bio && (
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-2">
About
</h3> </h3>
<p className="text-gray-800 dark:text-neutral-200 leading-relaxed line-clamp-3 wrap-break-word"> <div className="flex items-center text-gray-700 dark:text-gray-300">
{user.bio} <span className="text-gray-900 dark:text-white">
</p> {user?.email}
</div> </span>
)}
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="font-semibold text-gray-700 dark:text-neutral-300 tracking-wide mb-3">
Contact
</h3>
<div className="flex items-center text-gray-700 dark:text-neutral-300">
<span className="text-gray-900 dark:text-white">
{user?.email}
</span>
</div>
</div>
{user?.skills && user.skills.length > 0 && (
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
<h3 className="font-semibold text-gray-700 dark:text-neutral-300 tracking-wide mb-3">
Skills
</h3>
<div className="flex flex-wrap gap-2">
{user.skills.map((skill: string) => (
<span
key={skill}
className="inline-flex items-center px-4 py-2 border border-primary-600 text-primary-600 rounded-4xl text-xs font-medium "
>
{skill}
</span>
))}
</div> </div>
</div> </div>
)} {user?.skills && user.skills.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<h3 className="font-semibold text-gray-700 dark:text-gray-300 tracking-wide mb-3">
Skills
</h3>
<div className="flex flex-wrap gap-2">
{user.skills.map((skill: string) => (
<span
key={skill}
className="inline-flex items-center px-4 py-2 border border-primary-600 dark:border-gray-500 text-primary-600 dark:text-white rounded-4xl text-xs font-medium dark:bg-gray-600"
>
{skill}
</span>
))}
</div>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <ProfilePage
open={showProfileModal}
onClose={() => setShowProfileModal(false)}
/>
</>
); );
}; };
+17 -4
View File
@@ -1,4 +1,9 @@
import { Outlet, ScrollRestoration, useLocation, useNavigate } from 'react-router-dom'; import {
Outlet,
ScrollRestoration,
useLocation,
useNavigate,
} from 'react-router-dom';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { supabase } from '@imphnen-frontend-service/service'; import { supabase } from '@imphnen-frontend-service/service';
@@ -27,14 +32,21 @@ export default function RootLayout() {
} }
// Check Supabase session // Check Supabase session
const { data: { session }, error } = await supabase.auth.getSession(); const {
data: { session },
error,
} = await supabase.auth.getSession();
if (error) { if (error) {
console.error('[Layout] Session error:', error); console.error('[Layout] Session error:', error);
} }
// Public auth pages (login, signup, forgot-password, reset-password) - allow unauthenticated access // Public auth pages (login, signup, forgot-password, reset-password) - allow unauthenticated access
const isPublicAuthPage = pathname === '/auth/login' || pathname === '/auth/signup' || pathname === '/auth/forgot-password' || pathname === '/auth/reset-password'; const isPublicAuthPage = pathname === '/maintenance';
// pathname === '/auth/login' ||
// pathname === '/auth/signup' ||
// pathname === '/auth/forgot-password' ||
// pathname === '/auth/reset-password';
if (isPublicAuthPage) { if (isPublicAuthPage) {
// If already authenticated and not on password reset pages, redirect to dashboard // If already authenticated and not on password reset pages, redirect to dashboard
@@ -56,7 +68,8 @@ export default function RootLayout() {
// Require authentication for all other routes // Require authentication for all other routes
if (!session) { if (!session) {
navigate('/auth/login', { replace: true }); navigate('/maintenance', { replace: true });
// navigate('/auth/login', { replace: true });
setIsChecking(false); setIsChecking(false);
return; return;
} }
@@ -0,0 +1,21 @@
export default function MaintenancePage() {
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 px-4">
<div className="text-center">
<h1 className="text-6xl font-bold text-yellow-600 mb-4">🚧</h1>
<h2 className="text-3xl font-semibold mb-2">We'll be back soon!</h2>
<p className="text-gray-600">
Our site is currently undergoing scheduled maintenance.
<br />
Thank you for your patience.
</p>
<a
href="/"
className="mt-4 inline-block text-primary-600 hover:underline"
>
Back to Homepage
</a>
</div>
</div>
);
}
+13 -13
View File
@@ -104,13 +104,13 @@ const UserOnboardingPage: FC = (): ReactElement => {
}); });
return ( return (
<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="flex flex-col justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 px-4 py-8">
<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="bg-white dark:bg-gray-900 w-full max-w-2xl p-8 rounded-xl shadow-lg dark:shadow-gray-950/50">
<div className="mb-6"> <div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white 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 dark:text-neutral-400"> <p className="text-gray-600 font-sans dark:text-gray-400">
Tell us more about yourself to get started Tell us more about yourself to get started
</p> </p>
</div> </div>
@@ -123,15 +123,15 @@ 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 dark:border-neutral-700" className="w-32 h-32 rounded-full object-cover border-4 border-gray-200 dark:border-gray-700"
/> />
) : ( ) : (
<div className="w-32 h-32 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-32 h-32 rounded-full bg-gray-200 dark:bg-gray-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 dark:text-neutral-500" className="text-gray-400 dark:text-gray-500"
/> />
</div> </div>
)} )}
@@ -149,7 +149,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
onChange={handleAvatarChange} onChange={handleAvatarChange}
/> />
</label> </label>
<p className="text-xs text-gray-500 dark:text-neutral-500 mt-2 text-center"> <p className="text-xs text-gray-500 dark:text-gray-500 mt-2 text-center">
Optional, but highly recommended Optional, but highly recommended
</p> </p>
</div> </div>
@@ -168,7 +168,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 dark:text-neutral-300"> <label className="block text-base font-medium text-gray-700 dark:text-gray-300">
City <span className="text-red-500">*</span> City <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
@@ -187,7 +187,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 dark:text-neutral-300"> <label className="block text-base font-medium text-gray-700 dark:text-gray-300">
Role / Skills Role / Skills
</label> </label>
<Controller <Controller
@@ -210,9 +210,9 @@ 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 dark:border-neutral-600 text-blue-600 dark:text-primary-500 focus:ring-blue-500 dark:focus:ring-primary-500 dark:bg-neutral-800" className="rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-primary-500 focus:ring-blue-500 dark:focus:ring-primary-500 dark:bg-gray-800"
/> />
<span className="text-sm dark:text-neutral-300"> <span className="text-sm dark:text-gray-300">
{role} {role}
</span> </span>
</label> </label>
@@ -225,9 +225,9 @@ 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 dark:text-neutral-300"> <label className="block text-base font-medium text-gray-700 dark:text-gray-300">
Bio{' '} Bio{' '}
<span className="text-gray-400 dark:text-neutral-500"> <span className="text-gray-400 dark:text-gray-500">
(Optional) (Optional)
</span> </span>
</label> </label>
+64 -66
View File
@@ -66,10 +66,10 @@ export default function HomePage() {
]; ];
return ( return (
<main className="min-h-screen bg-white dark:bg-neutral-950"> <main className="min-h-screen bg-white dark:bg-gray-950">
{/* Navigation */} {/* Navigation */}
<div id="#top" className="hidden"></div> <div id="#top" className="hidden"></div>
<nav className="border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 sticky top-0 z-50"> <nav className="border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-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 dark:text-white"> <span className="text-lg md:text-xl font-bold dark:text-white">
@@ -86,19 +86,19 @@ 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 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors" className="text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
Timeline Timeline
</a> </a>
<a <a
href="#hadiah" href="#hadiah"
className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors" className="text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
Hadiah Hadiah
</a> </a>
<a <a
href="#faq" href="#faq"
className="text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white transition-colors" className="text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white transition-colors"
> >
FAQ FAQ
</a> </a>
@@ -117,7 +117,7 @@ export default function HomePage() {
onClick={() => navigate('/auth/login')} onClick={() => navigate('/auth/login')}
size="sm" size="sm"
variant="bordered" variant="bordered"
className="rounded-lg text-base" className="rounded-lg text-base dark:bg-gray-800"
> >
Masuk Masuk
</Button> </Button>
@@ -158,23 +158,23 @@ export default function HomePage() {
{/* Mobile Menu */} {/* Mobile Menu */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<nav className="md:hidden sticky top-18 border-b border-gray-200 dark:border-neutral-800 bg-white z-40 dark:bg-neutral-900"> <nav className="md:hidden sticky top-18 border-b border-gray-200 dark:border-gray-800 bg-white z-40 dark:bg-gray-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 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white" className="ms-3 text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white"
> >
Timeline Timeline
</a> </a>
<a <a
href="#hadiah" href="#hadiah"
className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white" className="ms-3 text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white"
> >
Hadiah Hadiah
</a> </a>
<a <a
href="#faq" href="#faq"
className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white" className="ms-3 text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white"
> >
FAQ FAQ
</a> </a>
@@ -187,12 +187,12 @@ export default function HomePage() {
</button> </button>
) : ( ) : (
<> <>
<a <button
href="#masuk" onClick={() => navigate('/auth/login')}
className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white" className="ms-3 text-gray-600 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white cursor-pointer"
> >
Masuk Masuk
</a> </button>
<button <button
onClick={() => navigate('/auth/login')} 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" className="px-4 py-2 bg-primary-500 text-white text-base rounded-lg hover:bg-primary-600 transition-colors text-center cursor-pointer"
@@ -228,7 +228,7 @@ 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 dark:text-neutral-500"> <span className="text-3xl md:text-5xl font-bold text-gray-400 dark:text-gray-500">
× ×
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
@@ -248,23 +248,23 @@ 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 dark:text-neutral-400 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-gray-200 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 dark:text-neutral-400"> <div className="flex flex-col md:flex-row items-center gap-4 md:gap-8 mb-10 md:mb-16 text-base text-gray-600 dark:text-gray-200">
<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" className="w-4 h-4" />
<span>Online</span> <span>Online</span>
</div> </div>
<div className="flex items-center gap-2 md:gap-3 text-center"> <div className="flex items-center gap-2 md:gap-3 text-center">
<Icon icon="heroicons:clock" width="16" height="16" /> <Icon icon="heroicons:clock" className="w-4 h-4" />
<span>Pendaftaran hingga 30 November 2025</span> <span>Pendaftaran hingga 30 November 2025</span>
</div> </div>
</div> </div>
{/* CTA Buttons */} {/* CTA Buttons */}
<div className="flex items-center gap-4 px-4"> <div className="flex flex-col md:flex-row items-center gap-4 px-4">
<Button <Button
onClick={() => navigate('/auth/signup')} onClick={() => navigate('/auth/signup')}
className="rounded-lg text-base max-h-auto" className="rounded-lg text-base max-h-auto"
@@ -275,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 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" className="inline-flex items-center justify-center px-4 py-2.5 border-2 border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500 transition-colors text-center bg-transparent hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-800 text-base text-gray-600 dark:text-gray-200 dark:bg-gray-800 rounded-lg font-bai-jamjuree font-semibold"
> >
Gabung Grup WA Hackathon Gabung Grup WA Hackathon
</a> </a>
@@ -283,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 dark:text-neutral-500 animate-bounce" className="w-6 h-6 text-gray-400 dark:text-gray-500 animate-bounce"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -300,19 +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 dark:bg-neutral-950"> <section className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-gray-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 dark:text-white"> <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 dark:text-neutral-400 mb-6"> <p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-gray-200 mb-6">
<span className="font-semibold text-gray-900 dark:text-white"> <span className="font-semibold text-gray-900 dark:text-white">
Hackathon Hackathon
</span>{' '} </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 dark:text-neutral-400"> <p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-gray-200">
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"
@@ -325,7 +325,7 @@ export default function HomePage() {
{/* Prizes Section */} {/* Prizes Section */}
<section <section
id="hadiah" id="hadiah"
className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900" className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-linear-to-b dark:from-gray-950 dark:to-gray-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">
@@ -339,7 +339,7 @@ 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 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="bg-white dark:bg-gray-900 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-gray-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 dark:bg-primary-900/30 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
@@ -357,12 +357,12 @@ export default function HomePage() {
</div> </div>
{/* Prize 2 */} {/* Prize 2 */}
<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="bg-white dark:bg-gray-900 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-gray-700 hover:border-gray-500 dark:hover:border-gray-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 dark:bg-neutral-700 rounded-full flex items-center justify-center"> <div className="w-16 h-16 bg-gray-100 dark:bg-gray-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 dark:text-neutral-400" className="h-8 w-8 text-gray-600 dark:text-gray-200"
/> />
</div> </div>
</div> </div>
@@ -375,7 +375,7 @@ export default function HomePage() {
</div> </div>
{/* Prize 3 */} {/* Prize 3 */}
<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="bg-white dark:bg-gray-900 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-gray-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 dark:bg-orange-900/30 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
@@ -393,7 +393,7 @@ export default function HomePage() {
</div> </div>
{/* Special Prize */} {/* Special Prize */}
<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="bg-white dark:bg-gray-900 rounded-xl px-4 py-8 shadow-lg border-2 border-gray-300 dark:border-gray-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 dark:bg-purple-900/30 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
@@ -416,14 +416,14 @@ export default function HomePage() {
{/* Timeline Section */} {/* Timeline Section */}
<section <section
id="timeline" id="timeline"
className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950" className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-gray-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 dark:text-white"> <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 dark:text-neutral-400"> <p className="text-lg text-gray-600 dark:text-gray-200">
Jadwal lengkap pelaksanaan hackathon Jadwal lengkap pelaksanaan hackathon
</p> </p>
</div> </div>
@@ -433,7 +433,7 @@ 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 dark:bg-neutral-700"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-gray-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">
@@ -442,7 +442,7 @@ export default function HomePage() {
<h3 className="text-xl font-bold mb-2 dark:text-white"> <h3 className="text-xl font-bold mb-2 dark:text-white">
Penutupan Registrasi Penutupan Registrasi
</h3> </h3>
<p className="text-gray-600 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-200">
Batas akhir pendaftaran peserta hackathon. Batas akhir pendaftaran peserta hackathon.
</p> </p>
</div> </div>
@@ -452,7 +452,7 @@ 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 dark:bg-neutral-700"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-gray-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">
@@ -461,7 +461,7 @@ export default function HomePage() {
<h3 className="text-xl font-bold mb-2 dark:text-white"> <h3 className="text-xl font-bold mb-2 dark:text-white">
Technical Meeting Technical Meeting
</h3> </h3>
<p className="text-gray-600 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-200">
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>
@@ -472,7 +472,7 @@ 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 dark:bg-neutral-700"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-gray-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">
@@ -481,7 +481,7 @@ export default function HomePage() {
<h3 className="text-xl font-bold mb-2 dark:text-white"> <h3 className="text-xl font-bold mb-2 dark:text-white">
Tahap Penyisihan Tahap Penyisihan
</h3> </h3>
<p className="text-gray-600 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-200">
Peserta mengerjakan tantangan yang diberikan. Peserta mengerjakan tantangan yang diberikan.
</p> </p>
</div> </div>
@@ -491,7 +491,7 @@ 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 dark:bg-neutral-700"></div> <div className="w-0.5 h-full bg-gray-300 dark:bg-gray-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">
@@ -500,7 +500,7 @@ export default function HomePage() {
<h3 className="text-xl font-bold mb-2 dark:text-white"> <h3 className="text-xl font-bold mb-2 dark:text-white">
Penilaian & Webinar Penilaian & Webinar
</h3> </h3>
<p className="text-gray-600 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-200">
Proses penilaian oleh juri dan sesi webinar. Proses penilaian oleh juri dan sesi webinar.
</p> </p>
</div> </div>
@@ -518,7 +518,7 @@ export default function HomePage() {
<h3 className="text-xl font-bold mb-2 dark:text-white"> <h3 className="text-xl font-bold mb-2 dark:text-white">
Pengumuman & Final Pengumuman & Final
</h3> </h3>
<p className="text-gray-600 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-200">
Presentasi final dan pengumuman pemenang. Presentasi final dan pengumuman pemenang.
</p> </p>
</div> </div>
@@ -528,50 +528,48 @@ 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 dark:bg-neutral-900 font-sans"> <section className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-gray-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 dark:text-white"> <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 dark:text-neutral-400"> <p className="text-lg text-gray-600 dark:text-gray-200">
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 dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center"> <div className="flex flex-col justify-between bg-white dark:bg-gray-800 rounded-xl px-4 py-8 shadow-lg text-center">
<h3 className="text-p3 font-bold mb-1 dark:text-white"> <h3 className="text-p3 font-bold mb-1 dark:text-white">
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 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-200">Kolosal.ai</p>
Kolosal.ai
</p>
</div> </div>
</div> </div>
{/* Judge 2 */} {/* Judge 2 */}
<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="flex flex-col justify-between bg-white dark:bg-gray-800 rounded-xl px-4 py-8 shadow-lg text-center">
<h3 className="text-p3 font-bold mb-1 dark:text-white"> <h3 className="text-p3 font-bold mb-1 dark:text-white">
Anka Tama Anka Tama
</h3> </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 dark:text-neutral-400">IMPHNEN</p> <p className="text-gray-600 dark:text-gray-200">IMPHNEN</p>
</div> </div>
</div> </div>
{/* Judge 3 */} {/* Judge 3 */}
<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="flex flex-col justify-between bg-white dark:bg-gray-800 rounded-xl px-4 py-8 shadow-lg text-center">
<h3 className="text-p3 font-bold mb-1 dark:text-white"> <h3 className="text-p3 font-bold mb-1 dark:text-white">
Hafid Nur Hafid Nur
</h3> </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 dark:text-neutral-400">IMPHNEN</p> <p className="text-gray-600 dark:text-gray-200">IMPHNEN</p>
</div> </div>
</div> </div>
</div> </div>
@@ -581,14 +579,14 @@ export default function HomePage() {
{/* FAQ Section */} {/* FAQ Section */}
<section <section
id="faq" id="faq"
className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950" className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-gray-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 dark:text-neutral-400"> <p className="text-lg text-gray-600 dark:text-gray-200">
Pertanyaan yang Sering Diajukan Pertanyaan yang Sering Diajukan
</p> </p>
</div> </div>
@@ -597,11 +595,11 @@ export default function HomePage() {
{faqs.map((faq, index) => ( {faqs.map((faq, index) => (
<div <div
key={index} key={index}
className="border border-gray-200 dark:border-neutral-700 rounded-lg" className="border border-gray-200 dark:border-gray-700 rounded-lg dark:bg-gray-900"
> >
<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 dark:hover:bg-neutral-800 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-gray-800 transition-colors cursor-pointer"
> >
<span className="font-semibold text-gray-900 dark:text-white"> <span className="font-semibold text-gray-900 dark:text-white">
{faq.question} {faq.question}
@@ -623,7 +621,7 @@ export default function HomePage() {
</svg> </svg>
</button> </button>
{openFaq === index && ( {openFaq === index && (
<div className="px-6 pb-4 text-gray-600 dark:text-neutral-400"> <div className="px-6 pb-4 text-gray-600 dark:text-gray-200">
{faq.answer} {faq.answer}
</div> </div>
)} )}
@@ -634,12 +632,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 dark:bg-neutral-900"> <section className="py-20 md:py-28 px-4 md:px-8 bg-gray-50 dark:bg-gray-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 dark:text-white"> <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 dark:text-neutral-400 mb-8"> <p className="text-lg text-gray-600 dark:text-gray-200 mb-8">
Acara ini sepenuhnya disponsori oleh Acara ini sepenuhnya disponsori oleh
</p> </p>
@@ -648,7 +646,7 @@ 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 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" className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-lg inline-block border-2 border-transparent hover:border-gray-500 dark:hover:border-gray-500 transition-colors"
> >
<img <img
src="images/sponsors/kolosal-logo_rlxbck.svg" src="images/sponsors/kolosal-logo_rlxbck.svg"
@@ -663,13 +661,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 dark:from-neutral-950 dark:to-neutral-900" className="py-20 md:py-28 px-4 md:px-8 bg-linear-to-b from-white to-blue-50 dark:from-gray-950 dark:to-gray-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 dark:text-white"> <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 dark:text-neutral-400 mb-8"> <p className="text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-gray-200 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
@@ -687,13 +685,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 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" className="px-8 py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-lg rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500 transition-colors font-semibold"
> >
Gabung Grup WA Hackathon Gabung Grup WA Hackathon
</a> </a>
</div> </div>
<p className="text-sm text-gray-500 dark:text-neutral-500 mt-6"> <p className="text-sm text-gray-500 dark:text-gray-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
@@ -703,7 +701,7 @@ export default function HomePage() {
</section> </section>
{/* Footer */} {/* Footer */}
<footer className="bg-gray-900 text-white py-12 px-4 md:px-8 font-sans"> <footer className="bg-gray-950 text-white py-12 px-4 md:px-8 font-sans">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
{/* Brand */} {/* Brand */}
+224 -214
View File
@@ -1,7 +1,6 @@
import { FC, ReactElement, useState, useEffect } from 'react'; import { FC, ReactElement, useState, useEffect } from 'react';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms'; import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms';
import { useNavigate, Link } from 'react-router';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { import {
userEditProfileSchema, userEditProfileSchema,
@@ -26,8 +25,15 @@ const ROLE_OPTIONS = [
'Mobile Developer', 'Mobile Developer',
]; ];
const ProfilePage: FC = (): ReactElement => { type ProfileModalProps = {
const navigate = useNavigate(); open: boolean;
onClose: () => void;
};
const ProfilePage: FC<ProfileModalProps> = ({
open,
onClose,
}): ReactElement | null => {
const [avatarFile, setAvatarFile] = useState<File | null>(null); const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string>(''); const [avatarPreview, setAvatarPreview] = useState<string>('');
@@ -123,8 +129,8 @@ const ProfilePage: FC = (): ReactElement => {
// Wait a bit for the onSuccess handler to update localStorage // Wait a bit for the onSuccess handler to update localStorage
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
// Navigate back to dashboard // Close modal
navigate('/dashboard'); onClose();
} catch (error) { } catch (error) {
console.error('Profile update failed:', error); console.error('Profile update failed:', error);
toast.error( toast.error(
@@ -137,65 +143,23 @@ const ProfilePage: FC = (): ReactElement => {
const isLoading = isUpdating || isUploading; const isLoading = isUpdating || isUploading;
return ( return open ? (
<div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex flex-col justify-center items-center px-4 py-8 z-50 overflow-y-auto"> <div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-md z-50 overflow-y-auto">
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-xl dark:shadow-neutral-950/50"> <div className="min-h-full flex items-center justify-center">
<div className="mb-6"> <div className="bg-white dark:bg-gray-900 w-full max-w-md mx-4 my-6 sm:my-8 p-8 rounded-xl border dark:boder-gray-800">
<div className="flex items-center justify-between mb-2"> <div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white"> <div className="flex items-center justify-between mb-2">
Edit Profile <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
</h1> Edit Profile
<Link </h1>
to="/dashboard" <button
className="text-gray-500 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200" type="button"
> onClick={onClose}
<svg className="text-gray-500 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200 cursor-pointer"
className="w-6 h-6" aria-label="Close profile modal"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</Link>
</div>
<p className="text-gray-600 font-sans dark:text-neutral-400">
Update your photo and name
</p>
</div>
<form onSubmit={onSubmit} className="space-y-6">
{/* Avatar Upload */}
<div className="flex flex-col items-center space-y-4">
<div className="relative group">
{avatarPreview ? (
<img
src={avatarPreview}
alt="Avatar preview"
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 dark:bg-neutral-700 flex items-center justify-center group-hover:bg-gray-300 dark:group-hover:bg-neutral-600 transition-colors">
<Icon
icon="ic:baseline-person"
width="48"
height="48"
className="text-gray-400 dark:text-neutral-500"
/>
</div>
)}
{/* Camera overlay */}
<label
htmlFor="avatar"
className="absolute bottom-0 right-0 bg-primary-500 text-white p-2 rounded-full cursor-pointer hover:bg-primary-600 transition-colors shadow-lg"
> >
<svg <svg
className="w-5 h-5" className="w-6 h-6"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -204,176 +168,222 @@ const ProfilePage: FC = (): ReactElement => {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} strokeWidth={2}
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" d="M6 18L18 6M6 6l12 12"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/> />
</svg> </svg>
<input </button>
id="avatar"
type="file"
accept="image/*"
className="hidden"
onChange={handleAvatarChange}
disabled={isLoading}
/>
</label>
</div> </div>
<p className="text-sm text-gray-500 dark:text-neutral-400 text-center font-sans"> <p className="text-gray-600 font-sans dark:text-neutral-400">
Click the camera icon to change your photo Update your photo and name
<br />
Format: JPG, PNG. Max 5MB
</p> </p>
</div> </div>
{/* Full Name */} <form onSubmit={onSubmit} className="space-y-6">
<ControlledInputField {/* Avatar Upload */}
control={form.control} <div className="flex flex-col items-center space-y-4">
label="Full Name" <div className="relative group">
placeholder="Enter your full name" {avatarPreview ? (
name="fullname" <img
size="lg" src={avatarPreview}
/> alt="Avatar preview"
className="w-24 h-24 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"
{/* City */}
<div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
City
</label>
<Controller
control={form.control}
name="location"
render={({ field, fieldState }) => (
<CitySelect
value={field.value}
onChange={field.onChange}
error={fieldState.error?.message}
placeholder="Search your city..."
/>
)}
/>
</div>
{/* Role/Skills */}
<div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
Role / Skills
</label>
<Controller
control={form.control}
name="skills"
render={({ field }) => (
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
{ROLE_OPTIONS.map((role) => (
<label
key={role}
className="flex items-center space-x-2 cursor-pointer font-sans"
>
<input
type="checkbox"
checked={field.value?.includes(role)}
onChange={(e) => {
const newValue = e.target.checked
? [...(field.value || []), role]
: (field.value || []).filter((v) => v !== role);
field.onChange(newValue);
}}
className="rounded border-gray-300 dark:border-neutral-600 text-blue-600 focus:ring-blue-500 dark:bg-neutral-800"
/>
<span className="text-sm dark:text-neutral-300">
{role}
</span>
</label>
))}
</div>
</div>
)}
/>
</div>
{/* Bio */}
<div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
Bio{' '}
<span className="text-gray-400 dark:text-neutral-500">
(Optional)
</span>
</label>
<Controller
control={form.control}
name="bio"
render={({ field, fieldState }) => (
<div>
<Textarea
{...field}
placeholder="Tell us about yourself..."
rows={4}
className="w-full"
size="lg"
/> />
{fieldState.error && ( ) : (
<p className="text-sm text-red-500 mt-1"> <div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center group-hover:bg-gray-300 dark:group-hover:bg-gray-600 transition-colors">
{fieldState.error.message} <Icon
</p> icon="ic:baseline-person"
)} width="48"
</div> height="48"
)} className="text-gray-400 dark:text-neutral-500"
/> />
</div> </div>
)}
{/* Action Buttons */} {/* Camera overlay */}
<div className="flex space-x-3 pt-4"> <label
<Button htmlFor="avatar"
type="button" className="absolute bottom-0 right-0 bg-primary-500 text-white p-2 rounded-full cursor-pointer hover:bg-primary-600 transition-colors shadow-lg"
variant="secondary" >
className="flex-1"
onClick={() => navigate('/dashboard')}
disabled={isLoading}
>
Cancel
</Button>
<Button
type="submit"
className="flex-1"
disabled={isLoading || !form.formState.isValid}
>
{isLoading ? (
<span className="flex items-center justify-center">
<svg <svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" className="w-5 h-5"
fill="none" fill="none"
stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
> >
<circle <path
className="opacity-25" strokeLinecap="round"
cx="12" strokeLinejoin="round"
cy="12" strokeWidth={2}
r="10" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
stroke="currentColor"
strokeWidth="4"
/> />
<path <path
className="opacity-75" strokeLinecap="round"
fill="currentColor" strokeLinejoin="round"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" strokeWidth={2}
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/> />
</svg> </svg>
Saving... <input
id="avatar"
type="file"
accept="image/*"
className="hidden"
onChange={handleAvatarChange}
disabled={isLoading}
/>
</label>
</div>
<p className="text-sm text-gray-500 dark:text-neutral-400 text-center font-sans">
Click the camera icon to change your photo
<br />
Format: JPG, PNG. Max 5MB
</p>
</div>
{/* Full Name */}
<ControlledInputField
control={form.control}
label="Full Name"
placeholder="Enter your full name"
name="fullname"
size="lg"
/>
{/* City */}
<div className="space-y-2">
<label className="block text-label1 font-medium text-neutral-800 dark:text-neutral-300">
City
</label>
<Controller
control={form.control}
name="location"
render={({ field, fieldState }) => (
<CitySelect
value={field.value ?? ''}
onChange={field.onChange}
error={fieldState.error?.message}
placeholder="Search your city..."
/>
)}
/>
</div>
{/* Role/Skills */}
<div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
Role / Skills
</label>
<Controller
control={form.control}
name="skills"
render={({ field }) => (
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
{ROLE_OPTIONS.map((role) => (
<label
key={role}
className="flex items-center space-x-2 cursor-pointer font-sans"
>
<input
type="checkbox"
checked={field.value?.includes(role)}
onChange={(e) => {
const newValue = e.target.checked
? [...(field.value || []), role]
: (field.value || []).filter((v) => v !== role);
field.onChange(newValue);
}}
className="rounded border-gray-300 dark:border-neutral-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-800"
/>
<span className="text-sm dark:text-neutral-300">
{role}
</span>
</label>
))}
</div>
</div>
)}
/>
</div>
{/* Bio */}
<div className="space-y-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
Bio{' '}
<span className="text-gray-400 dark:text-neutral-500">
(Optional)
</span> </span>
) : ( </label>
'Save' <Controller
)} control={form.control}
</Button> name="bio"
</div> render={({ field, fieldState }) => (
</form> <div>
<Textarea
{...field}
placeholder="Tell us about yourself..."
rows={4}
className="w-full font-sans"
size="lg"
/>
{fieldState.error && (
<p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)}
</div>
)}
/>
</div>
{/* Action Buttons */}
<div className="flex space-x-3 pt-4">
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={onClose}
disabled={isLoading}
>
Cancel
</Button>
<Button
type="submit"
className="flex-1"
disabled={isLoading || !form.formState.isValid}
>
{isLoading ? (
<span className="flex items-center justify-center">
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Saving...
</span>
) : (
'Save'
)}
</Button>
</div>
</form>
</div>
</div> </div>
</div> </div>
); ) : null;
}; };
export default ProfilePage; export default ProfilePage;
@@ -1,8 +1,15 @@
import { FC, ReactElement, useState, useRef, useEffect } from 'react'; import { FC, ReactElement, useState, useRef, useEffect, useMemo } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms'; import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import { useTeamById, useTeamMessages, useSendMessage, useDeleteMessage, useAuthStore } from '@imphnen-frontend-service/service'; import {
useTeamById,
useTeamMessages,
useSendMessage,
useDeleteMessage,
useAuthStore,
} from '@imphnen-frontend-service/service';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Icon } from '@iconify/react';
const TeamChatPage: FC = (): ReactElement => { const TeamChatPage: FC = (): ReactElement => {
const { teamId } = useParams<{ teamId: string }>(); const { teamId } = useParams<{ teamId: string }>();
@@ -13,16 +20,54 @@ const TeamChatPage: FC = (): ReactElement => {
const { data: teamData } = useTeamById(teamId || ''); const { data: teamData } = useTeamById(teamId || '');
const { data: messages, isLoading } = useTeamMessages(teamId || ''); const { data: messages, isLoading } = useTeamMessages(teamId || '');
const { mutateAsync: sendMessage, isPending: isSending } = useSendMessage(teamId || ''); const { mutateAsync: sendMessage, isPending: isSending } = useSendMessage(
teamId || ''
);
const { mutateAsync: deleteMessage } = useDeleteMessage(teamId || ''); const { mutateAsync: deleteMessage } = useDeleteMessage(teamId || '');
const team = teamData?.data; const team = teamData?.data;
const currentUserId = session?.user?.id; const currentUserId = session?.user?.id;
// Message type used for UI rendering
interface ChatMessage {
id: string;
user_id: string;
user?: {
avatar?: string;
fullname?: string;
};
message: string;
created_at: string;
}
// Inline delete confirmation UI state
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
// Prepare messages: de-duplicate by id and sort by created_at
const displayMessages = useMemo(() => {
const raw: ChatMessage[] = Array.isArray(messages)
? (messages as ChatMessage[])
: [];
const seen = new Set<string>();
const dedup: ChatMessage[] = [];
for (const m of raw) {
const key = m.id ?? `${m.user_id}-${m.created_at}`;
if (!seen.has(key)) {
seen.add(key);
dedup.push(m);
}
}
dedup.sort(
(a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
return dedup;
}, [messages]);
// Auto-scroll to bottom when new messages arrive // Auto-scroll to bottom when new messages arrive
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]); }, [displayMessages]);
const handleSendMessage = async (e: React.FormEvent) => { const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -38,11 +83,10 @@ const TeamChatPage: FC = (): ReactElement => {
}; };
const handleDeleteMessage = async (messageId: string) => { const handleDeleteMessage = async (messageId: string) => {
if (!confirm('Are you sure you want to delete this message?')) return;
try { try {
await deleteMessage(messageId); await deleteMessage(messageId);
toast.success('Message deleted'); toast.success('Message deleted');
setDeleteTargetId(null);
} catch (error) { } catch (error) {
console.error('Failed to delete message:', error); console.error('Failed to delete message:', error);
toast.error('Failed to delete message'); toast.error('Failed to delete message');
@@ -63,16 +107,23 @@ const TeamChatPage: FC = (): ReactElement => {
}; };
return ( return (
<div className="flex flex-col h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col h-screen bg-gray-50 dark:bg-gray-950">
{/* Header */} {/* Header */}
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 shadow-sm dark:shadow-neutral-950/50"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700 shadow-sm dark:shadow-gray-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 dark:text-white">Team Chat</h1> <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
<p className="text-gray-600 dark:text-neutral-400 text-sm mt-0.5">{team?.name}</p> Team Chat
</h1>
<p className="text-gray-600 dark:text-gray-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
</Button> </Button>
</div> </div>
@@ -86,21 +137,27 @@ const TeamChatPage: FC = (): ReactElement => {
<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 dark:border-primary-400"></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 ? ( ) : displayMessages && displayMessages.length > 0 ? (
<div className="space-y-4"> <div className="space-y-4">
{messages.map((msg) => { {displayMessages.map((msg) => {
const isOwnMessage = msg.user_id === currentUserId; const isOwnMessage = msg.user_id === currentUserId;
const isLeader = team?.leader_id === currentUserId; const isLeader = team?.leader_id === currentUserId;
const canDelete = isOwnMessage || isLeader; const canDelete = isOwnMessage || isLeader;
return ( return (
<div <div
key={msg.id} key={`${msg.id ?? 'message'}-${msg.created_at}`}
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`} className={`flex ${
isOwnMessage ? 'justify-end' : 'justify-start'
}`}
> >
<div className={`flex gap-3 max-w-lg ${isOwnMessage ? 'flex-row-reverse' : 'flex-row'}`}> <div
className={`flex gap-3 max-w-lg ${
isOwnMessage ? 'flex-row-reverse' : 'flex-row'
}`}
>
{/* Avatar */} {/* Avatar */}
<div className="flex-shrink-0"> <div className="shrink-0">
{msg.user?.avatar ? ( {msg.user?.avatar ? (
<img <img
src={msg.user.avatar} src={msg.user.avatar}
@@ -115,13 +172,17 @@ const TeamChatPage: FC = (): ReactElement => {
</div> </div>
{/* Message Bubble */} {/* Message Bubble */}
<div className={`flex-1 ${isOwnMessage ? 'text-right' : 'text-left'}`}> <div className={`flex-1 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 dark:text-white"> <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 dark:text-neutral-500"> <span className="text-xs text-gray-500 dark:text-gray-500">
{formatTime(msg.created_at)} {formatTime(msg.created_at)}
</span> </span>
</div> </div>
@@ -129,24 +190,58 @@ const TeamChatPage: FC = (): ReactElement => {
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 dark:bg-primary-600 text-white' ? 'bg-blue-600 dark:bg-primary-600 text-white'
: 'bg-white dark:bg-neutral-800 text-gray-900 dark:text-white border border-gray-200 dark:border-neutral-700' : 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700'
}`} }`}
> >
<p className="text-sm whitespace-pre-wrap break-words">{msg.message}</p> <p className="text-sm whitespace-pre-wrap wrap-break-word font-sans">
{msg.message}
</p>
{/* Delete button */} {/* Delete button */}
{canDelete && ( {canDelete && (
<button <button
onClick={() => handleDeleteMessage(msg.id)} onClick={() =>
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' : ''}`} setDeleteTargetId(
deleteTargetId === msg.id ? null : msg.id
)
}
className={`absolute top-1 ${
isOwnMessage ? 'left-1' : 'right-1'
} opacity-0 hover:opacity-100 p-1 rounded hover:bg-gray-200 dark:hover:bg-neutral-700 cursor-pointer ${
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"> {/* Icon trash */}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <Icon
</svg> icon="mdi:trash-can-outline"
className="w-4 h-4"
/>
</button> </button>
)} )}
</div> </div>
{canDelete && deleteTargetId === msg.id && (
<div
className={`mt-2 flex gap-2 ${
isOwnMessage ? 'justify-end' : 'justify-start'
}`}
>
<button
onClick={() => handleDeleteMessage(msg.id)}
className="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700 cursor-pointer"
>
Delete
</button>
<button
onClick={() => setDeleteTargetId(null)}
className="px-2 py-1 text-xs rounded bg-gray-200 text-gray-800 hover:bg-gray-300 dark:bg-neutral-700 dark:text-white dark:hover:bg-neutral-600 cursor-pointer"
>
Cancel
</button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -157,11 +252,14 @@ const TeamChatPage: FC = (): ReactElement => {
</div> </div>
) : ( ) : (
<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> <Icon
icon="mdi:chat-outline"
className="text-6xl mb-4 text-gray-400"
/>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white 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 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-400 font-sans">
Be the first to start the conversation! Be the first to start the conversation!
</p> </p>
</div> </div>
@@ -170,7 +268,7 @@ const TeamChatPage: FC = (): ReactElement => {
</div> </div>
{/* Message Input */} {/* Message Input */}
<div className="bg-white dark:bg-neutral-900 border-t dark:border-neutral-700 shadow-lg dark:shadow-neutral-950/50"> <div className="bg-white dark:bg-gray-900 border-t dark:border-gray-700 shadow-lg dark:shadow-gray-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 +276,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 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" className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-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
@@ -193,8 +291,18 @@ const TeamChatPage: FC = (): ReactElement => {
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg> </svg>
Send Send
</div> </div>
@@ -3,10 +3,19 @@ import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms'; import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { teamUpdateSchema, TTeamUpdateForm, useUpdateTeam, useTeamById, ETeamVisibility, useUploadFile, useAuthStore } from '@imphnen-frontend-service/service'; import {
teamUpdateSchema,
TTeamUpdateForm,
useUpdateTeam,
useTeamById,
ETeamVisibility,
useUploadFile,
useAuthStore,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { CitySelect } from '../../../../components/city-select'; import { CitySelect } from '../../../../components/city-select';
import { Icon } from '@iconify/react';
const EditTeamPage: FC = (): ReactElement => { const EditTeamPage: FC = (): ReactElement => {
const { teamId } = useParams<{ teamId: string }>(); const { teamId } = useParams<{ teamId: string }>();
@@ -17,8 +26,12 @@ const EditTeamPage: FC = (): ReactElement => {
const [bannerFile, setBannerFile] = useState<File | null>(null); const [bannerFile, setBannerFile] = useState<File | null>(null);
const [bannerPreview, setBannerPreview] = useState<string>(''); const [bannerPreview, setBannerPreview] = useState<string>('');
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || ''); const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
const { mutateAsync: updateTeam, isPending: isUpdating } = useUpdateTeam(teamId || ''); teamId || ''
);
const { mutateAsync: updateTeam, isPending: isUpdating } = useUpdateTeam(
teamId || ''
);
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const team = teamData?.data; const team = teamData?.data;
@@ -47,18 +60,24 @@ const EditTeamPage: FC = (): ReactElement => {
if (isLoadingTeam) { if (isLoadingTeam) {
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-gray-600 dark:text-neutral-400">Loading team...</div> <div className="text-gray-600 dark:text-gray-400">Loading team...</div>
</div> </div>
); );
} }
if (!isLeader) { if (!isLeader) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can edit team information</p> Access Denied
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> </h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Only the team leader can edit team information
</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team
</Button>
</div> </div>
); );
} }
@@ -115,28 +134,35 @@ const EditTeamPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-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 dark:text-white">Edit Team Info</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
<p className="text-gray-600 dark:text-neutral-400 mt-1">Update your team details</p> Edit Team Info
</h1>
<p className="text-gray-600 dark:text-gray-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 dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-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 dark:text-neutral-300 mb-2"> <label className="block text-label1 font-medium text-gray-700 dark:text-gray-300 mb-2">
Team Banner Team Banner
</label> </label>
{bannerPreview ? ( {bannerPreview ? (
<div className="relative"> <div
className="relative w-full"
style={{ aspectRatio: '3 / 1' }}
>
<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 dark:border-neutral-700" className="absolute inset-0 w-full h-full object-cover rounded-lg border-2 border-gray-200 dark:border-gray-700"
/> />
<button <button
type="button" type="button"
@@ -145,16 +171,20 @@ const EditTeamPage: FC = (): ReactElement => {
setBannerPreview(''); setBannerPreview('');
form.setValue('banner', null); form.setValue('banner', null);
}} }}
className="absolute top-2 right-2 bg-red-500 text-white px-3 py-1 rounded-lg text-sm hover:bg-red-600" className="absolute top-2 right-2 bg-danger-600 text-white px-3 py-1 rounded-lg text-sm hover:bg-danger-700 cursor-pointer"
> >
Remove Remove
</button> </button>
</div> </div>
) : ( ) : (
<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"> <label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800">
<div className="text-center"> <div className="text-center">
<p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p> <p className="text-gray-500 dark:text-gray-400">
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended</p> Click to upload banner
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
1200x400 recommended
</p>
</div> </div>
<input <input
type="file" type="file"
@@ -168,7 +198,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Logo Upload */} {/* Logo Upload */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2"> <label className="block text-label1 font-medium text-gray-700 dark:text-gray-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">
@@ -176,16 +206,18 @@ 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 dark:border-neutral-700" className="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-gray-700"
/> />
) : ( ) : (
<div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-400 dark:text-neutral-500 text-3xl">👥</span> <span className="text-gray-400 dark:text-gray-500 text-3xl">
<Icon icon="mdi:account-group" />
</span>
</div> </div>
)} )}
<div> <div className="flex flex-col md:flex-row items-start justify-start gap-1">
<label htmlFor="logo" className="cursor-pointer"> <label htmlFor="logo" className="cursor-pointer">
<span className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 inline-block"> <span className="px-4 py-2 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 inline-block">
{logoPreview ? 'Change Logo' : 'Upload Logo'} {logoPreview ? 'Change Logo' : 'Upload Logo'}
</span> </span>
<input <input
@@ -204,7 +236,7 @@ const EditTeamPage: FC = (): ReactElement => {
setLogoPreview(''); setLogoPreview('');
form.setValue('logo', null); form.setValue('logo', null);
}} }}
className="ml-3 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600" className="px-4 py-2 text-sm bg-danger-600 text-white rounded-lg hover:bg-danger-700 cursor-pointer"
> >
Remove Remove
</button> </button>
@@ -219,11 +251,13 @@ const EditTeamPage: FC = (): ReactElement => {
label="Team Name" label="Team Name"
placeholder="Enter team name" placeholder="Enter team name"
name="name" name="name"
size="lg"
isRequired={true}
/> />
{/* City */} {/* City */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
City City
</label> </label>
<Controller <Controller
@@ -242,7 +276,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 dark:text-neutral-300"> <label className="block text-label1 font-medium text-gray-700 dark:text-gray-300">
Description Description
</label> </label>
<Controller <Controller
@@ -256,9 +290,12 @@ const EditTeamPage: FC = (): ReactElement => {
placeholder="Tell others about your team..." placeholder="Tell others about your team..."
rows={4} rows={4}
className="w-full" className="w-full"
size="lg"
/> />
{fieldState.error && ( {fieldState.error && (
<p className="text-sm text-red-500 mt-1">{fieldState.error.message}</p> <p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)} )}
</div> </div>
)} )}
@@ -267,7 +304,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 dark:text-neutral-300"> <label className="block text-label1 font-medium text-gray-700 dark:text-gray-300">
Team Visibility Team Visibility
</label> </label>
<Controller <Controller
@@ -275,7 +312,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 dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800"> <label className="flex items-start space-x-3 cursor-pointer border dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<input <input
type="radio" type="radio"
{...field} {...field}
@@ -284,13 +321,15 @@ const EditTeamPage: FC = (): ReactElement => {
className="mt-1" className="mt-1"
/> />
<div> <div>
<p className="font-medium text-gray-900 dark:text-white">Public</p> <p className="font-medium text-gray-900 dark:text-white">
<p className="text-sm text-gray-600 dark:text-neutral-400"> Public
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 font-sans">
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 dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800"> <label className="flex items-start space-x-3 cursor-pointer border dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<input <input
type="radio" type="radio"
{...field} {...field}
@@ -299,8 +338,10 @@ const EditTeamPage: FC = (): ReactElement => {
className="mt-1" className="mt-1"
/> />
<div> <div>
<p className="font-medium text-gray-900 dark:text-white">Private</p> <p className="font-medium text-gray-900 dark:text-white">
<p className="text-sm text-gray-600 dark:text-neutral-400"> Private
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 font-sans">
Team hidden, invite-only Team hidden, invite-only
</p> </p>
</div> </div>
@@ -6,21 +6,33 @@ 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 dark:bg-neutral-950"> <div className="flex min-h-screen bg-gray-50 dark:bg-gray-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">
{/* Mobile Header with Hamburger */} {/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center"> <div className="lg:hidden sticky top-0 z-30 bg-white dark:bg-gray-900 border-b dark:border-gray-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 dark:hover:bg-neutral-800 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> className="w-6 h-6 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<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 dark:text-white">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,17 +23,28 @@ const ManageMembersPage: FC = (): ReactElement => {
const { session } = useAuthStore(); const { session } = useAuthStore();
const [showInviteModal, setShowInviteModal] = useState(false); const [showInviteModal, setShowInviteModal] = useState(false);
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || ''); const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || ''); teamId || ''
);
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(
teamId || ''
);
const { data: joinRequestsData } = useTeamJoinRequests(teamId || ''); const { data: joinRequestsData } = useTeamJoinRequests(teamId || '');
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(teamId || ''); const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(
const { mutateAsync: removeMember, isPending: isRemoving } = useRemoveMember(teamId || ''); teamId || ''
const { mutateAsync: respondToRequest, isPending: isResponding } = useRespondToJoinRequest(teamId || ''); );
const { mutateAsync: removeMember, isPending: isRemoving } = useRemoveMember(
teamId || ''
);
const { mutateAsync: respondToRequest, isPending: isResponding } =
useRespondToJoinRequest(teamId || '');
const team = teamData?.data; const team = teamData?.data;
const members = membersData?.data || []; const members = membersData?.data || [];
const joinRequests = Array.isArray(joinRequestsData?.data) ? joinRequestsData.data : []; const joinRequests = Array.isArray(joinRequestsData?.data)
? joinRequestsData.data
: [];
const currentUserId = session?.user?.id; const currentUserId = session?.user?.id;
const isLeader = currentUserId === team?.leader_id; const isLeader = currentUserId === team?.leader_id;
@@ -45,18 +56,24 @@ const ManageMembersPage: FC = (): ReactElement => {
// Show loading state while fetching team data // Show loading state while fetching team data
if (isLoadingTeam) { if (isLoadingTeam) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<p className="text-gray-600 dark:text-neutral-400">Loading...</p> <p className="text-gray-600 dark:text-gray-400">Loading...</p>
</div> </div>
); );
} }
if (!isLeader) { if (!isLeader) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can manage members</p> Access Denied
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> </h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Only the team leader can manage members
</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team
</Button>
</div> </div>
); );
} }
@@ -103,19 +120,26 @@ const ManageMembersPage: FC = (): ReactElement => {
}; };
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-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 flex-col md:flex-row gap-y-4 items-start md:items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Manage Members</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
<p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p> Manage Members
</h1>
<p className="text-gray-600 dark:text-gray-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)}>
Invite Member Invite Member
</Button> </Button>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}> <Button
variant="secondary"
onClick={() => navigate(`/teams/${teamId}`)}
>
Back to Team Back to Team
</Button> </Button>
</div> </div>
@@ -126,13 +150,16 @@ 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 dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white 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 dark:border-neutral-700 rounded-lg p-4"> <div
key={request.id}
className="border dark:border-gray-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 ? (
@@ -142,17 +169,27 @@ 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 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400">👤</span> <span className="text-gray-500 dark:text-gray-400">
👤
</span>
</div> </div>
)} )}
<div className="flex-1"> <div className="flex-1">
<p className="font-medium text-gray-900 dark:text-white">{request.user.fullname}</p> <p className="font-medium text-gray-900 dark:text-white">
<p className="text-sm text-gray-600 dark:text-neutral-400">{request.user.email}</p> {request.user.fullname}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{request.user.email}
</p>
{request.user.location && ( {request.user.location && (
<p className="text-sm text-gray-500 dark:text-neutral-500">📍 {request.user.location}</p> <p className="text-sm text-gray-500 dark:text-gray-500">
📍 {request.user.location}
</p>
)} )}
<p className="text-sm text-gray-700 dark:text-neutral-300 mt-2 italic">"{request.message}"</p> <p className="text-sm text-gray-700 dark:text-gray-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">
@@ -180,16 +217,21 @@ const ManageMembersPage: FC = (): ReactElement => {
)} )}
{/* Current Members */} {/* Current Members */}
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white 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 dark:text-neutral-400">Loading members...</p> <p className="text-gray-600 dark:text-gray-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 dark:border-neutral-700 rounded-lg p-4 flex items-center justify-between"> <div
key={member.id}
className="border dark:border-gray-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
@@ -198,15 +240,23 @@ 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 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400">👤</span> <span className="text-gray-500 dark:text-gray-400">
👤
</span>
</div> </div>
)} )}
<div> <div>
<p className="font-medium text-gray-900 dark:text-white">{member.user.fullname}</p> <p className="font-medium text-gray-900 dark:text-white">
<p className="text-sm text-gray-600 dark:text-neutral-400">{member.user.email}</p> {member.user.fullname}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{member.user.email}
</p>
{member.user.location && ( {member.user.location && (
<p className="text-sm text-gray-500 dark:text-neutral-500">📍 {member.user.location}</p> <p className="text-sm text-gray-500 dark:text-gray-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' && (
@@ -241,7 +291,8 @@ const ManageMembersPage: FC = (): ReactElement => {
{/* Warning */} {/* Warning */}
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 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 dark:text-yellow-200"> <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>
</div> </div>
@@ -249,27 +300,30 @@ const ManageMembersPage: FC = (): ReactElement => {
{/* Invite Member Modal */} {/* Invite Member Modal */}
{showInviteModal && ( {showInviteModal && (
<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="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl dark:shadow-neutral-950/50 max-w-md w-full p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl dark:shadow-gray-950/50 max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white 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 dark:text-neutral-400 mb-4"> <p className="text-gray-600 dark:text-gray-400 mb-4 font-sans">
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 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 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 dark:text-yellow-200"> <p className="text-sm text-yellow-800 dark:text-yellow-200 font-sans">
<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 dark:text-neutral-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Address Email Address
</label> </label>
<Input <Input
{...form.register('email')} {...form.register('email')}
type="email" type="email"
placeholder="member@example.com" placeholder="member@example.com"
size="lg"
/> />
{form.formState.errors.email && ( {form.formState.errors.email && (
<p className="text-sm text-red-500 mt-1"> <p className="text-sm text-red-500 mt-1">
+128 -98
View File
@@ -11,6 +11,7 @@ import {
useAuthStore, useAuthStore,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Icon } from '@iconify/react';
const MAX_TEAM_MEMBERS = 5; const MAX_TEAM_MEMBERS = 5;
@@ -140,18 +141,18 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (isLoadingTeam || isLoadingMembers) { if (isLoadingTeam || isLoadingMembers) {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
{/* Skeleton Header */} {/* Skeleton Header */}
<div className="bg-white border-b"> <div className="bg-white dark:bg-gray-950 border-b">
<div className="w-full h-48 bg-gray-200 animate-pulse" /> <div className="w-full h-48 bg-gray-200 dark:bg-gray-800 animate-pulse" />
<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-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<div className="w-20 h-20 rounded-full bg-gray-300 animate-pulse -mt-10" /> <div className="w-20 h-20 rounded-full bg-gray-300 dark:bg-gray-700 animate-pulse -mt-10" />
<div> <div>
<div className="h-8 w-48 bg-gray-300 rounded animate-pulse" /> <div className="h-8 w-48 bg-gray-300 dark:bg-gray-700 rounded animate-pulse" />
<div className="h-4 w-32 bg-gray-200 rounded animate-pulse mt-2" /> <div className="h-4 w-32 bg-gray-200 dark:bg-gray-800 rounded animate-pulse mt-2" />
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mt-2" /> <div className="h-4 w-24 bg-gray-200 dark:bg-gray-800 rounded animate-pulse mt-2" />
</div> </div>
</div> </div>
</div> </div>
@@ -162,27 +163,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
<div className="grid gap-6 lg:grid-cols-3"> <div className="grid gap-6 lg:grid-cols-3">
{/* Main Content Skeleton */} {/* Main Content Skeleton */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6">
<div className="h-6 w-32 bg-gray-300 rounded animate-pulse mb-4" /> <div className="h-6 w-32 bg-gray-300 dark:bg-gray-700 rounded animate-pulse mb-4" />
<div className="space-y-2"> <div className="space-y-2">
<div className="h-4 w-full bg-gray-200 rounded animate-pulse" /> <div className="h-4 w-full bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
<div className="h-4 w-3/4 bg-gray-200 rounded animate-pulse" /> <div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
<div className="h-4 w-5/6 bg-gray-200 rounded animate-pulse" /> <div className="h-4 w-5/6 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
</div> </div>
</div> </div>
</div> </div>
{/* Sidebar Skeleton */} {/* Sidebar Skeleton */}
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6">
<div className="h-6 w-40 bg-gray-300 rounded animate-pulse mb-4" /> <div className="h-6 w-40 bg-gray-300 dark:bg-gray-700 rounded animate-pulse mb-4" />
<div className="space-y-4"> <div className="space-y-4">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div key={i} className="flex items-center space-x-3"> <div key={i} className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full bg-gray-300 animate-pulse" /> <div className="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-700 animate-pulse" />
<div> <div>
<div className="h-4 w-24 bg-gray-300 rounded animate-pulse" /> <div className="h-4 w-24 bg-gray-300 dark:bg-gray-700 rounded animate-pulse" />
<div className="h-3 w-16 bg-gray-200 rounded animate-pulse mt-1" /> <div className="h-3 w-16 bg-gray-200 dark:bg-gray-800 rounded animate-pulse mt-1" />
</div> </div>
</div> </div>
))} ))}
@@ -193,10 +194,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
{/* Loading Overlay */} {/* Loading Overlay */}
<div className="fixed inset-0 bg-white/60 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-white/60 dark:bg-black/50 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 dark:text-neutral-400"> <p className="mt-4 text-gray-600 dark:text-white">
Loading team data... Loading team data...
</p> </p>
</div> </div>
@@ -219,7 +220,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
} }
return ( return (
<div className="min-h-screen bg-gray-50 relative dark:bg-neutral-950"> <div className="min-h-screen bg-gray-50 relative dark:bg-gray-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">
@@ -234,9 +235,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
)} )}
{/* Header with Banner */} {/* Header with Banner */}
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700">
{team.banner && ( {team.banner && (
<div className="w-full h-32 md:h-48 overflow-hidden relative"> <div className="relative w-full aspect-3/1 overflow-hidden">
<div <div
className={`absolute inset-0 bg-gray-200 animate-pulse ${ className={`absolute inset-0 bg-gray-200 animate-pulse ${
imagesLoaded ? 'hidden' : '' imagesLoaded ? 'hidden' : ''
@@ -245,26 +246,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
<img <img
src={team.banner} src={team.banner}
alt={team.name} alt={team.name}
className={`w-full h-full object-cover transition-opacity duration-300 ${ className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${
imagesLoaded ? 'opacity-100' : 'opacity-0' imagesLoaded ? 'opacity-100' : 'opacity-0'
}`} }`}
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageError} onError={handleImageError}
/> />
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent via-black/20 to-black/60" />
</div> </div>
)} )}
<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">
{team.logo && ( {team.logo && (
<div className="relative"> <div className="relative shrink-0">
{!imagesLoaded && ( {!imagesLoaded && (
<div className="absolute inset-0 w-20 h-20 rounded-full bg-gray-300 animate-pulse -mt-10" /> <div className="absolute inset-0 w-20 h-20 rounded-full bg-gray-300 dark:bg-gray-700 animate-pulse -mt-10" />
)} )}
<img <img
src={team.logo} src={team.logo}
alt={team.name} alt={team.name}
className={`w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white shadow-lg -mt-8 md:-mt-10 transition-opacity duration-300 ${ className={`w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white shadow-lg -mt-8 md:-mt-10 transition-opacity duration-300 shrink-0 ${
imagesLoaded ? 'opacity-100' : 'opacity-0' imagesLoaded ? 'opacity-100' : 'opacity-0'
}`} }`}
onLoad={handleImageLoad} onLoad={handleImageLoad}
@@ -276,20 +278,32 @@ const TeamDashboardPage: FC = (): ReactElement => {
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white 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 dark:text-neutral-400 mt-1 line-clamp-1"> <p className="text-sm md:text-base text-gray-600 dark:text-gray-400 mt-1 line-clamp-1 font-sans flex items-center">
📍 {team.city} <Icon icon="mdi:map-marker" className="inline-block mr-1" />
<span>{team.city}</span>
</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 dark:text-neutral-400"> <span className="text-sm text-gray-500 dark:text-gray-400 flex items-center">
<Icon
icon="mdi:account-multiple"
className="inline-block mr-1"
/>
{members.length}{' '} {members.length}{' '}
{members.length === 1 ? 'Member' : 'Members'} {members.length === 1 ? 'Member' : 'Members'}
</span> </span>
<span className="text-sm text-gray-500 dark:text-neutral-400"> <span className="text-sm text-gray-500 dark:text-gray-400 flex items-center">
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'} <Icon
icon={
team.visibility === 'public' ? 'mdi:web' : 'mdi:lock'
}
className="inline-block mr-1"
/>
{team.visibility === 'public' ? 'Public' : 'Private'}
</span> </span>
{isLeader && ( {isLeader && (
<span className="hidden md:inline-flex px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-semibold shadow-sm"> <span className="hidden md:inline-flex px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-semibold shadow-sm items-center">
👑 Team Leader <Icon icon="mdi:crown" className="inline-block mr-1" />
<span>Team Leader</span>
</span> </span>
)} )}
</div> </div>
@@ -300,22 +314,22 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8">
<div className="grid gap-4 md:gap-6 lg:grid-cols-3"> <div className="grid gap-4 md:gap-6 xl:grid-cols-3">
{/* 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 dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <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 dark:text-neutral-300 whitespace-pre-wrap"> <p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-all font-sans">
{team.description} {team.description}
</p> </p>
</div> </div>
{/* Team Actions - Only for Leader */} {/* Team Actions - Only for Leader */}
{isLeader && ( {isLeader && (
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <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>
@@ -326,7 +340,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
disabled={!canInvite} disabled={!canInvite}
> >
Invite Member{' '} <Icon icon="mdi:plus" className="inline-block mr-2" />
Invite Member{' '}
{!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`} {!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
</Button> </Button>
<Button <Button
@@ -334,7 +349,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
variant="secondary" variant="secondary"
onClick={() => setShowJoinRequestsModal(true)} onClick={() => setShowJoinRequestsModal(true)}
> >
📩 Join Requests <Icon icon="mdi:email" className="inline-block mr-2" /> Join
Requests
{pendingJoinRequests.length > 0 && ( {pendingJoinRequests.length > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center"> <span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center">
{pendingJoinRequests.length} {pendingJoinRequests.length}
@@ -343,25 +359,34 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Button> </Button>
<Link to={`/teams/${teamId}/edit`}> <Link to={`/teams/${teamId}/edit`}>
<Button className="w-full" variant="secondary"> <Button className="w-full" variant="secondary">
Edit Team Info <Icon icon="mdi:pencil" className="inline-block mr-2" />
Edit Team Info
</Button> </Button>
</Link> </Link>
<Link to={`/teams/${teamId}/members`}> <Link to={`/teams/${teamId}/members`}>
<Button className="w-full" variant="secondary"> <Button className="w-full" variant="secondary">
👥 Manage Members <Icon
icon="mdi:account-group"
className="inline-block mr-2"
/>
Manage Members
</Button> </Button>
</Link> </Link>
<Link to={`/teams/${teamId}/chat`}> <Link to={`/teams/${teamId}/chat`}>
<Button className="w-full" variant="secondary"> <Button className="w-full" variant="secondary">
💬 Team Chat <Icon icon="mdi:chat" className="inline-block mr-2" />
Team Chat
</Button> </Button>
</Link> </Link>
<Link to={`/teams/${teamId}/submit`}> <Link to={`/teams/${teamId}/submit`}>
<Button className="w-full">🚀 Submit Project</Button> <Button className="w-full">
<Icon icon="mdi:rocket" className="inline-block mr-2" />
Submit Project
</Button>
</Link> </Link>
</div> </div>
{!canInvite && members.length >= MAX_TEAM_MEMBERS && ( {!canInvite && members.length >= MAX_TEAM_MEMBERS && (
<p className="text-sm text-gray-600 dark:text-neutral-400 mt-3 text-center"> <p className="text-sm text-gray-600 dark:text-gray-400 mt-3 text-center">
Maximum team size reached ({MAX_TEAM_MEMBERS} members) Maximum team size reached ({MAX_TEAM_MEMBERS} members)
</p> </p>
)} )}
@@ -370,7 +395,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Quick Actions for Members (non-leaders) */} {/* Quick Actions for Members (non-leaders) */}
{!isLeader && isMember && ( {!isLeader && isMember && (
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <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>
@@ -417,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 dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <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 dark:hover:bg-neutral-700 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-gray-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
> >
{team.leader.avatar ? ( {team.leader.avatar ? (
<img <img
@@ -433,8 +458,8 @@ 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 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400"> <span className="text-gray-500 dark:text-gray-400">
👤 👤
</span> </span>
</div> </div>
@@ -443,7 +468,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<p className="font-medium text-gray-900 dark:text-white"> <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 dark:text-neutral-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
{team.leader.email} {team.leader.email}
</p> </p>
</div> </div>
@@ -452,7 +477,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div> </div>
{/* Team Members */} {/* Team Members */}
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <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>
@@ -461,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 dark:hover:bg-neutral-700 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-gray-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
> >
{member.user?.avatar ? ( {member.user?.avatar ? (
<img <img
@@ -472,8 +497,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
onError={handleImageError} onError={handleImageError}
/> />
) : ( ) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400 text-sm"> <span className="text-gray-500 dark:text-gray-400 text-sm">
👤 👤
</span> </span>
</div> </div>
@@ -482,7 +507,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<p className="font-medium text-gray-900 dark:text-white 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 dark:text-neutral-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
{member.role === ETeamMemberRole.LEADER {member.role === ETeamMemberRole.LEADER
? 'Leader' ? 'Leader'
: 'Member'} : 'Member'}
@@ -499,16 +524,16 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Invite Member Modal */} {/* Invite Member Modal */}
{showInviteModal && ( {showInviteModal && (
<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="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <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 dark:text-neutral-400 mb-4"> <p className="text-gray-600 dark:text-gray-400 mb-4 font-sans">
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 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 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 dark:text-yellow-300"> <p className="text-sm text-yellow-800 dark:text-yellow-300 font-sans">
<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>
@@ -517,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 dark:text-neutral-300 mb-2" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
> >
Email Address Email Address
</label> </label>
@@ -527,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 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" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500"
required required
/> />
<p className="text-sm text-gray-500 dark:text-neutral-400 mt-1"> <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Current members: {members.length}/{MAX_TEAM_MEMBERS} Current members: {members.length}/{MAX_TEAM_MEMBERS}
</p> </p>
</div> </div>
@@ -562,14 +587,14 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Join Requests Modal */} {/* Join Requests Modal */}
{showJoinRequestsModal && ( {showJoinRequestsModal && (
<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="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto"> <div className="bg-white dark:bg-gray-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 dark:text-white"> <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 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300 text-2xl" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 text-2xl cursor-pointer"
> >
</button> </button>
@@ -577,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 dark:text-neutral-400 text-lg"> <p className="text-gray-600 dark:text-gray-400 text-lg">
No pending join requests No pending join requests
</p> </p>
<p className="text-gray-500 dark:text-neutral-500 text-sm mt-2"> <p className="text-gray-500 dark:text-gray-500 text-sm mt-2 font-sans">
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>
@@ -589,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 dark:border-neutral-700 rounded-lg p-4 hover:shadow-md transition-shadow" className="border border-gray-200 dark:border-gray-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">
@@ -600,9 +625,12 @@ 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 dark:bg-neutral-700 flex items-center justify-center shrink-0"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center shrink-0">
<span className="text-gray-500 dark:text-neutral-400 text-xl"> <span className="text-gray-500 dark:text-gray-400 text-xl">
👤 <Icon
icon="mdi:account-circle"
className="text-3xl"
/>
</span> </span>
</div> </div>
)} )}
@@ -610,49 +638,51 @@ const TeamDashboardPage: FC = (): ReactElement => {
<p className="font-semibold text-gray-900 dark:text-white"> <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 dark:text-neutral-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
{request.user?.email} {request.user?.email}
</p> </p>
{request.message && ( {request.message && (
<div className="mt-2 bg-gray-50 dark:bg-neutral-800 rounded-lg p-3"> <div className="mt-2 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<p className="text-sm text-gray-700 dark:text-neutral-300"> <p className="text-sm text-gray-700 dark:text-gray-300">
<strong>Message:</strong> {request.message} <strong>Message:</strong> {request.message}
</p> </p>
</div> </div>
)} )}
<p className="text-xs text-gray-500 dark:text-neutral-500 mt-2"> <p className="text-sm text-gray-500 dark:text-gray-500 mt-2 font-sans">
Requested{' '} Requested{' '}
{new Date(request.created_at).toLocaleDateString()} {new Date(request.created_at).toLocaleDateString()}
</p> </p>
</div> </div>
</div> </div>
<div className="flex space-x-2 ml-4">
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'approve')
}
disabled={
isResponding || members.length >= MAX_TEAM_MEMBERS
}
className="px-4 py-2 text-sm"
>
✓ Accept
</Button>
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'reject')
}
disabled={isResponding}
variant="secondary"
className="px-4 py-2 text-sm"
>
✕ Reject
</Button>
</div>
</div> </div>
<div className="flex space-x-2 mt-4 justify-end">
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'approve')
}
disabled={
isResponding || members.length >= MAX_TEAM_MEMBERS
}
className="px-4 py-2 text-sm"
>
Accept
</Button>
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'reject')
}
disabled={isResponding}
variant="secondary"
className="px-4 py-2 text-sm"
>
Reject
</Button>
</div>
{members.length >= MAX_TEAM_MEMBERS && ( {members.length >= MAX_TEAM_MEMBERS && (
<div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 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-3">
<p className="text-xs text-yellow-800 dark:text-yellow-300"> <p className="text-sm text-yellow-800 dark:text-yellow-300 font-sans">
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.
@@ -23,7 +23,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
const { data: teamData } = useTeamById(teamId || ''); const { data: teamData } = useTeamById(teamId || '');
const { data: submissionData } = useTeamSubmission(teamId || '', !!teamId); const { data: submissionData } = useTeamSubmission(teamId || '', !!teamId);
const { mutateAsync: submitProject, isPending: isSubmitting } = useSubmitProject(teamId || ''); const { mutateAsync: submitProject, isPending: isSubmitting } =
useSubmitProject(teamId || '');
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const team = teamData?.data; const team = teamData?.data;
@@ -38,25 +39,38 @@ const SubmitProjectPage: FC = (): ReactElement => {
if (!isLeader) { if (!isLeader) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can submit projects</p> Access Denied
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button> </h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Only the team leader can submit projects
</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team
</Button>
</div> </div>
); );
} }
if (hasSubmission) { if (hasSubmission) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-6xl mb-4"></div> <div className="text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Project Already Submitted</h2> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
<p className="text-gray-600 dark:text-neutral-400 mb-4">Your team has already submitted a project</p> Project Already Submitted
</h2>
<p className="text-gray-600 dark:text-gray-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
</Button> </Button>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}> <Button
variant="secondary"
onClick={() => navigate(`/teams/${teamId}`)}
>
Back to Team Back to Team
</Button> </Button>
</div> </div>
@@ -64,14 +78,16 @@ const SubmitProjectPage: FC = (): ReactElement => {
); );
} }
const handleScreenshotUpload = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleScreenshotUpload = async (
e: React.ChangeEvent<HTMLInputElement>
) => {
const files = e.target.files; const files = e.target.files;
if (!files) return; if (!files) return;
try { try {
const uploadPromises = Array.from(files).map(file => uploadFile(file)); const uploadPromises = Array.from(files).map((file) => uploadFile(file));
const results = await Promise.all(uploadPromises); const results = await Promise.all(uploadPromises);
const urls = results.map(r => r.data.url); const urls = results.map((r) => r.data.url);
setScreenshots([...screenshots, ...urls]); setScreenshots([...screenshots, ...urls]);
} catch (error) { } catch (error) {
console.error('Failed to upload screenshots:', error); console.error('Failed to upload screenshots:', error);
@@ -95,11 +111,13 @@ const SubmitProjectPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-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 dark:text-white">Submit Project</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
<p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p> Submit Project
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">{team?.name}</p>
</div> </div>
</div> </div>
@@ -109,33 +127,42 @@ const SubmitProjectPage: FC = (): ReactElement => {
<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 dark:text-red-100 text-lg">IMPORTANT WARNING</h3> <h3 className="font-bold text-red-900 dark:text-red-100 text-lg">
<ul className="text-red-800 dark:text-red-200 mt-2 space-y-1 text-sm"> IMPORTANT WARNING
</h3>
<ul className="text-red-800 dark:text-red-200 mt-2 space-y-1 text-sm font-sans">
<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>
<li> Review your project details carefully</li> <li> Review your project details carefully</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-8 border dark:border-gray-800">
<form onSubmit={(e) => { <form
e.preventDefault(); onSubmit={(e) => {
setShowConfirmModal(true); e.preventDefault();
}} className="space-y-6"> setShowConfirmModal(true);
}}
className="space-y-6"
>
{/* Project Name */} {/* Project Name */}
<ControlledInputField <ControlledInputField
control={form.control} control={form.control}
label="Project Name" label="Project Name"
placeholder="Enter your project name" placeholder="Enter your project name"
name="project_name" name="project_name"
size="lg"
isRequired={true}
/> />
{/* Description */} {/* Description */}
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300"> <label className="block text-[15px] font-medium text-gray-700 dark:text-gray-300">
Project Description <span className="text-red-500">*</span> Project Description <span className="text-red-500">*</span>
</label> </label>
<Controller <Controller
@@ -148,9 +175,12 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="Describe your project, its features, and what problem it solves..." placeholder="Describe your project, its features, and what problem it solves..."
rows={6} rows={6}
className="w-full" className="w-full"
size="lg"
/> />
{fieldState.error && ( {fieldState.error && (
<p className="text-sm text-red-500 mt-1">{fieldState.error.message}</p> <p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)} )}
</div> </div>
)} )}
@@ -164,6 +194,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="https://github.com/username/project" placeholder="https://github.com/username/project"
name="repository_url" name="repository_url"
type="url" type="url"
size="lg"
isRequired={true}
/> />
{/* Demo URL */} {/* Demo URL */}
@@ -173,6 +205,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="https://your-project-demo.com" placeholder="https://your-project-demo.com"
name="demo_url" name="demo_url"
type="url" type="url"
size="lg"
/> />
{/* Presentation URL */} {/* Presentation URL */}
@@ -182,12 +215,13 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="https://slides.com/your-presentation or Google Drive link" placeholder="https://slides.com/your-presentation or Google Drive link"
name="presentation_url" name="presentation_url"
type="url" type="url"
size="lg"
/> />
{/* Screenshots */} {/* Screenshots */}
<div className="space-y-2"> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300"> <label className="block text-label1 font-medium text-neutral-800 dark:text-gray-300">
Project Screenshots <span className="text-gray-400 dark:text-neutral-500">(Optional)</span> Project Screenshots (Optional)
</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 +229,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 dark:border-neutral-700" className="w-full h-32 object-cover rounded-lg border-2 border-gray-200 dark:border-gray-700"
/> />
<button <button
type="button" type="button"
@@ -207,10 +241,14 @@ 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 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800"> <label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800">
<div className="text-center"> <div className="text-center">
<p className="text-gray-500 dark:text-neutral-400">Click to upload screenshots</p> <p className="text-gray-500 dark:text-gray-400">
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">PNG, JPG up to 5MB each</p> Click to upload screenshots
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 font-sans">
PNG, JPG up to 5MB each
</p>
</div> </div>
<input <input
type="file" type="file"
@@ -248,7 +286,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
{/* Confirmation Modal */} {/* Confirmation Modal */}
{showConfirmModal && ( {showConfirmModal && (
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 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 dark:bg-neutral-900 rounded-lg shadow-xl dark:shadow-neutral-950/50 max-w-md w-full p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl dark:shadow-gray-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 dark:text-white mb-2"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
@@ -258,11 +296,11 @@ const SubmitProjectPage: FC = (): ReactElement => {
This action is IRREVERSIBLE! This action is IRREVERSIBLE!
</p> </p>
</div> </div>
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-4 mb-6"> <div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6">
<p className="text-sm text-gray-700 dark:text-neutral-300 mb-3"> <p className="text-sm text-gray-700 dark:text-gray-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 dark:text-neutral-400 space-y-2"> <ul className="text-sm text-gray-600 dark:text-gray-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>
+32 -31
View File
@@ -12,6 +12,7 @@ import {
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { CitySelect } from '../../../components/city-select'; import { CitySelect } from '../../../components/city-select';
import { Icon } from '@iconify/react';
const BrowseTeamsPage: FC = (): ReactElement => { const BrowseTeamsPage: FC = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -70,16 +71,16 @@ const BrowseTeamsPage: FC = (): ReactElement => {
}); });
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
{/* Header */} {/* Header */}
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-800">
<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 dark:text-white"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Browse Teams Browse Teams
</h1> </h1>
<p className="text-gray-600 dark:text-neutral-400 mt-1"> <p className="text-gray-600 dark:text-gray-400 mt-1">
Find and join teams looking for members Find and join teams looking for members
</p> </p>
</div> </div>
@@ -92,10 +93,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 dark:bg-neutral-900 p-6 rounded-lg shadow-sm mb-6"> <div className="bg-white dark:bg-gray-900 p-6 rounded-lg shadow-sm mb-6 border dark:border-gray-800">
<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 dark:text-neutral-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Search Teams Search Teams
</label> </label>
<input <input
@@ -103,11 +104,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 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" className="w-full h-[42px] px-3 border border-gray-300 dark:border-gray-700 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Filter by City Filter by City
</label> </label>
<CitySelect <CitySelect
@@ -122,30 +123,28 @@ 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 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-400">Loading teams...</p>
Loading teams...
</p>
</div> </div>
) : teams.length === 0 ? ( ) : teams.length === 0 ? (
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm p-12 text-center">
<p className="text-gray-600 dark:text-neutral-400 text-lg"> <p className="text-gray-600 dark:text-gray-400 text-lg">
No teams found No teams found
</p> </p>
<p className="text-gray-500 dark:text-neutral-500 mt-2"> <p className="text-gray-500 dark:text-gray-500 mt-2">
Try adjusting your filters Try adjusting your filters
</p> </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 xl:grid-cols-3">
{teams.map((team) => ( {teams.map((team) => (
<div <div
key={team.id} key={team.id}
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col" className="bg-white dark:bg-gray-900 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col border dark:border-gray-800"
> >
<img <img
src={team.banner || '/images/banner-imphnen.webp'} src={team.banner || '/images/banner-imphnen.webp'}
alt={team.name} alt={team.name}
className="w-full h-32 object-cover" className="w-full aspect-3/1 object-cover"
/> />
<div className="p-6 flex flex-col flex-1"> <div className="p-6 flex flex-col flex-1">
<div className="flex items-center space-x-3 mb-3"> <div className="flex items-center space-x-3 mb-3">
@@ -156,27 +155,29 @@ 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 dark:bg-neutral-700 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400 text-xl"> <span className="text-gray-500 dark:text-gray-300 text-xl">
👥 <Icon icon="mdi:account-group" />
</span> </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 dark:text-white line-clamp-2"> <h3 className="text-lg font-bold text-gray-900 dark:text-white line-clamp-2 leading-tight">
{team.name} {team.name}
</h3> </h3>
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2"> <div className="text-sm font-sans text-gray-600 dark:text-gray-400 flex gap-2">
<p className="truncate flex-1 min-w-0"> <p className="truncate flex-1 min-w-0 flex items-center gap-x-1">
📍 {team.city} <Icon icon="mdi:map-marker" />{' '}
<span>{team.city}</span>
</p> </p>
<p className="whitespace-nowrap shrink-0"> <p className="whitespace-nowrap shrink-0 flex items-center gap-x-1">
👥 {team.members?.length || 0} members <Icon icon="mdi:account-group" />{' '}
{team.members?.length || 0} members
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 dark:text-neutral-400 text-sm mb-4 line-clamp-3"> <p className="text-gray-600 dark:text-gray-300 text-sm mb-4 line-clamp-3 font-sans">
{team.description} {team.description}
</p> </p>
<div className="space-y-3 mt-auto"> <div className="space-y-3 mt-auto">
@@ -219,26 +220,26 @@ const BrowseTeamsPage: FC = (): ReactElement => {
{/* Join Request Modal */} {/* Join Request Modal */}
{showJoinModal && ( {showJoinModal && (
<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="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <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 dark:text-neutral-400 mb-6"> <p className="text-gray-600 dark:text-gray-400 mb-6 font-sans">
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 dark:text-neutral-300 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-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 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" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-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 && (
<p className="text-sm text-red-500 mt-1"> <p className="text-sm text-red-500 mt-1 font-sans">
{form.formState.errors.message.message} {form.formState.errors.message.message}
</p> </p>
)} )}
+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 dark:bg-neutral-950"> <div className="flex min-h-screen bg-gray-50 dark:bg-gray-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 dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 z-10"> <div className="lg:hidden sticky top-0 bg-white dark:bg-gray-900 border-b dark:border-gray-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 dark:hover:bg-neutral-800 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<svg <svg
className="w-6 h-6 text-gray-600 dark:text-neutral-400" className="w-6 h-6 text-gray-600 dark:text-gray-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -6,21 +6,33 @@ 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 dark:bg-neutral-950"> <div className="flex min-h-screen bg-gray-50 dark:bg-gray-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 dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center"> <div className="lg:hidden bg-white dark:bg-gray-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 dark:hover:bg-neutral-800 transition-colors" className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> className="w-6 h-6 text-gray-600 dark:text-neutral-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg> </svg>
</button> </button>
<h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">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">
+49 -34
View File
@@ -5,6 +5,7 @@ import {
useUserDetailsById, useUserDetailsById,
useTeamsByUserId, useTeamsByUserId,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { Icon } from '@iconify/react';
const UserProfilePage: FC = (): ReactElement => { const UserProfilePage: FC = (): ReactElement => {
const { userId } = useParams<{ userId: string }>(); const { userId } = useParams<{ userId: string }>();
@@ -17,8 +18,8 @@ const UserProfilePage: FC = (): ReactElement => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-gray-600 dark:text-neutral-400"> <div className="text-gray-600 dark:text-gray-400">
Loading user profile... Loading user profile...
</div> </div>
</div> </div>
@@ -27,7 +28,7 @@ const UserProfilePage: FC = (): ReactElement => {
if (error || !user) { if (error || !user) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4"> <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
User not found User not found
</h2> </h2>
@@ -42,9 +43,9 @@ const UserProfilePage: FC = (): ReactElement => {
} }
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950"> <div className="min-h-screen bg-gray-50 dark:bg-gray-950">
{/* Header */} {/* Header */}
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700"> <div className="bg-white dark:bg-gray-900 border-b dark:border-gray-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">
@@ -52,12 +53,12 @@ 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 dark:border-neutral-800 shadow-lg" className="w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white dark:border-gray-800 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"> <div className="w-16 h-16 md:w-20 md:h-20 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center border-4 border-white dark:border-gray-800 shadow-lg">
<span className="text-gray-500 dark:text-neutral-400 text-2xl md:text-3xl"> <span className="text-gray-500 dark:text-gray-400 text-2xl md:text-3xl">
👤 <Icon icon="mdi:account-circle" className="w-10 h-10" />
</span> </span>
</div> </div>
)} )}
@@ -65,13 +66,17 @@ const UserProfilePage: FC = (): ReactElement => {
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white"> <h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white">
{user.fullname} {user.fullname}
</h1> </h1>
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1"> <p className="text-sm md:text-base text-gray-600 dark:text-gray-400 mt-1">
{user.email} {user.email}
</p> </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 dark:text-neutral-500"> <span className="text-sm text-gray-500 dark:text-gray-500">
📍 {user.location} <Icon
icon="mdi:map-marker"
className="inline-block w-4 h-4 mr-1"
/>
{user.location}
</span> </span>
</div> </div>
)} )}
@@ -90,11 +95,11 @@ 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 dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6 overflow-hidden"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-4 md:p-6 overflow-hidden">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white 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 About
</h2> </h2>
<p className="text-gray-700 dark:text-neutral-300 break-all overflow-wrap-anywhere"> <p className="text-gray-700 dark:text-gray-300 break-all overflow-wrap-anywhere font-sans">
{user.bio} {user.bio}
</p> </p>
</div> </div>
@@ -102,7 +107,7 @@ const UserProfilePage: FC = (): ReactElement => {
{/* Skills Section */} {/* Skills Section */}
{user.skills && user.skills.length > 0 && ( {user.skills && user.skills.length > 0 && (
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Skills Skills
</h2> </h2>
@@ -110,7 +115,7 @@ const UserProfilePage: FC = (): ReactElement => {
{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 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" className="inline-flex items-center px-4 py-2 border border-primary-600 dark:border-gray-500 text-primary-600 dark:text-white rounded-4xl text-xs font-medium dark:bg-gray-600"
> >
{skill} {skill}
</span> </span>
@@ -121,7 +126,7 @@ const UserProfilePage: FC = (): ReactElement => {
{/* Teams Section */} {/* Teams Section */}
{userTeams.length > 0 ? ( {userTeams.length > 0 ? (
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Teams ({userTeams.length}) Teams ({userTeams.length})
</h2> </h2>
@@ -130,37 +135,47 @@ const UserProfilePage: FC = (): ReactElement => {
<Link <Link
key={team.id} key={team.id}
to={'/teams/' + 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" className="bg-white dark:bg-gray-800 rounded-lg shadow-md dark:shadow-gray-950/50 overflow-hidden hover:shadow-lg transition-shadow"
> >
<img <div className="w-full aspect-3/1">
src={team.banner || '/images/banner-imphnen.webp'} <img
alt={team.name} src={team.banner}
className="w-full h-24 object-cover" alt={team.name}
/> className="w-full h-full object-cover"
/>
</div>
<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 && ( {team.logo && (
<img <img
src={team.logo} src={team.logo}
alt={team.name} alt={team.name}
className="w-10 h-10 rounded-full object-cover" className="w-12 h-12 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 dark:text-white line-clamp-2"> <h3 className="font-bold text-gray-900 dark:text-white line-clamp-2 mb-1">
{team.name} {team.name}
</h3> </h3>
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2"> <div className="text-sm text-gray-600 dark:text-gray-400 flex gap-2 font-sans">
<p className="truncate flex-1 min-w-0"> <p className="truncate flex-1 min-w-0">
📍 {team.city} <Icon
icon="mdi:map-marker"
className="inline-block w-4 h-4 mr-1"
/>
{team.city}
</p> </p>
<p className="whitespace-nowrap shrink-0"> <p className="whitespace-nowrap shrink-0">
👥 {team.members?.length || 0} members <Icon
icon="mdi:account-group"
className="inline-block w-4 h-4 mr-1"
/>
{team.members?.length || 0} members
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<p className="text-gray-600 dark:text-neutral-400 text-sm line-clamp-2"> <p className="text-gray-600 dark:text-gray-400 text-base line-clamp-2 font-sans">
{team.description} {team.description}
</p> </p>
</div> </div>
@@ -169,13 +184,13 @@ const UserProfilePage: FC = (): ReactElement => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Teams Teams
</h2> </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 dark:text-neutral-400"> <p className="text-gray-600 dark:text-gray-400">
Not in any team yet Not in any team yet
</p> </p>
</div> </div>
@@ -186,13 +201,13 @@ 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 dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6"> <div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4"> <h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Contact Information Contact Information
</h3> </h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<p className="text-sm text-gray-600 dark:text-neutral-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Email Email
</p> </p>
<p className="text-gray-900 dark:text-white font-medium"> <p className="text-gray-900 dark:text-white font-medium">
@@ -201,7 +216,7 @@ const UserProfilePage: FC = (): ReactElement => {
</div> </div>
{user.location && ( {user.location && (
<div> <div>
<p className="text-sm text-gray-600 dark:text-neutral-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Location Location
</p> </p>
<p className="text-gray-900 dark:text-white font-medium"> <p className="text-gray-900 dark:text-white font-medium">
@@ -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 bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 ${ 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-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 ${
error ? 'border-red-500' : 'border-gray-300 dark:border-neutral-600' error ? 'border-red-500' : 'border-gray-300 dark:border-gray-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 dark:text-neutral-500 hover:text-gray-600 cursor-pointer dark:hover:text-neutral-300" className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500 hover:text-gray-600 cursor-pointer dark:hover:text-gray-300"
> >
</button> </button>
@@ -88,7 +88,7 @@ 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 dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 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-gray-800 border border-gray-300 dark:border-gray-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) => (
@@ -98,7 +98,7 @@ export const CitySelect: FC<CitySelectProps> = ({
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${ className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${
value === city value === city
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400' ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400'
: 'text-gray-900 dark:text-neutral-200' : 'text-gray-900 dark:text-gray-200'
}`} }`}
> >
{city} {city}
@@ -106,7 +106,7 @@ export const CitySelect: FC<CitySelectProps> = ({
))} ))}
</ul> </ul>
) : ( ) : (
<div className="px-3 py-2 text-gray-500 dark:text-neutral-400 text-sm"> <div className="px-3 py-2 text-gray-500 dark:text-gray-400 text-sm">
No cities found No cities found
</div> </div>
)} )}
+16 -10
View File
@@ -62,6 +62,12 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
icon: <Icon icon="heroicons:home" className="w-5 h-5" />, icon: <Icon icon="heroicons:home" className="w-5 h-5" />,
show: true, show: true,
}, },
{
name: 'My Teams',
path: '/teams/' + myTeams[0]?.id,
icon: <Icon icon="heroicons:users" className="w-5 h-5" />,
show: myTeams.length > 0,
},
{ {
name: 'Browse Teams', name: 'Browse Teams',
path: '/teams/browse', path: '/teams/browse',
@@ -161,19 +167,19 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
}; };
const sidebarContent = ( const sidebarContent = (
<div className="w-64 bg-white dark:bg-neutral-900 border-r dark:border-neutral-700 min-h-screen flex flex-col"> <div className="w-64 bg-white dark:bg-gray-900 border-r dark:border-gray-800 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 border-b dark:border-neutral-700"> <div className="p-6 flex items-center justify-between border-b dark:border-gray-800">
<h1 className="text-xl font-bold text-gray-900 dark:text-white"> <h1 className="text-xl font-bold text-gray-900 dark:text-white">
Hackathon Hackathon
</h1> </h1>
{onClose && ( {onClose && (
<button <button
onClick={onClose} onClick={onClose}
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors" className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
> >
<svg <svg
className="w-5 h-5 text-gray-500 dark:text-neutral-400" className="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -190,7 +196,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</div> </div>
{/* User Info */} {/* User Info */}
<div className="p-4 border-y dark:border-neutral-700"> <div className="p-4 border-b dark:border-gray-800">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{user?.avatar ? ( {user?.avatar ? (
<img <img
@@ -204,7 +210,7 @@ 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 dark:text-neutral-400" className="text-gray-400 dark:text-gray-400"
/> />
</div> </div>
)} )}
@@ -212,7 +218,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
<p className="text-sm font-medium text-gray-900 dark:text-white 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 dark:text-neutral-400 truncate"> <p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{user?.email} {user?.email}
</p> </p>
</div> </div>
@@ -233,7 +239,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
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 dark:bg-blue-900/30 text-primary-600 dark:text-blue-400 font-medium' ? 'bg-primary-50 dark:bg-blue-900/30 text-primary-600 dark:text-blue-400 font-medium'
: 'text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
}`} }`}
> >
{item.icon} {item.icon}
@@ -246,10 +252,10 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
</nav> </nav>
{/* Theme Toggle & Logout */} {/* Theme Toggle & Logout */}
<div className="p-4 border-t dark:border-neutral-700 space-y-2"> <div className="p-4 border-t dark:border-gray-800 space-y-2">
<button <button
onClick={cycleTheme} 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" className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors cursor-pointer"
> >
{getThemeIcon()} {getThemeIcon()}
<span>{getThemeLabel()}</span> <span>{getThemeLabel()}</span>
+37 -7
View File
@@ -7,22 +7,52 @@ interface ThemeToggleProps {
// Sun icon for light mode // Sun icon for light mode
const SunIcon = () => ( const SunIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<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" /> 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> </svg>
); );
// Moon icon for dark mode // Moon icon for dark mode
const MoonIcon = () => ( const MoonIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<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" /> 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> </svg>
); );
// System icon (monitor) // System icon (monitor)
const SystemIcon = () => ( const SystemIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<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" /> 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> </svg>
); );
@@ -51,7 +81,7 @@ export const ThemeToggle: FC<ThemeToggleProps> = ({ className = '' }) => {
<button <button
type="button" type="button"
onClick={cycleTheme} 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}`} className={`p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors cursor-pointer text-gray-600 dark:text-neutral-200 ${className}`}
title={`Theme: ${getThemeLabel()}`} title={`Theme: ${getThemeLabel()}`}
aria-label={`Current theme: ${getThemeLabel()}. Click to change.`} aria-label={`Current theme: ${getThemeLabel()}. Click to change.`}
> >
+5 -4
View File
@@ -26,12 +26,13 @@ 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 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', 'bg-white dark:bg-gray-800 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700 text-primary-500 dark:text-primary-400 shadow-md dark:shadow-gray-900/50 border dark:border-gray-700',
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', text: 'bg-transparent hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-800 text-primary-500 dark:text-primary-400',
bordered: bordered:
'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', '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-gray-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 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', 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-gray-900/50',
}; };
const sizeClasses: Record<TButtonSize, string> = { const sizeClasses: Record<TButtonSize, string> = {
+1 -1
View File
@@ -56,7 +56,7 @@ export const Input: FC<TInputProps> = ({
}; };
const mergedClassName = cn( const mergedClassName = cn(
`px-[12px] py-[8px] text-neutral-800 dark:text-white bg-white dark:bg-neutral-800 placeholder:text-neutral-300 dark:placeholder:text-neutral-500 border border-neutral-200 dark:border-neutral-600 hover:border-blue-300 dark:hover:border-blue-500 focus:outline-1 focus:outline-blue-500 dark:focus:outline-primary-500 rounded-md font-bai-jamjuree w-full ${ `px-[12px] py-[8px] text-neutral-800 dark:text-white bg-white dark:bg-gray-800 placeholder:text-neutral-300 dark:placeholder:text-neutral-500 border border-neutral-200 dark:border-neutral-600 hover:border-blue-300 dark:hover:border-blue-500 focus:outline-1 focus:outline-blue-500 dark:focus:outline-primary-500 rounded-md font-bai-jamjuree w-full ${
widthform === 'standard' ? 'min-w-70' : '' widthform === 'standard' ? 'min-w-70' : ''
}`, }`,
sizeClasses[size].textSize, sizeClasses[size].textSize,
+1 -1
View File
@@ -38,7 +38,7 @@ export const Textarea: FC<TTextareaProps> = ({
...rest ...rest
}): ReactElement => { }): ReactElement => {
const mergedClassName = cn( const mergedClassName = cn(
'rounded-md border border-neutral-200 dark:border-neutral-600 hover:border-blue-300 dark:hover:border-blue-500 focus:outline-1 focus:outline-blue-500 dark:focus:outline-primary-500 px-[12px] py-[8px] bg-white dark:bg-neutral-800 text-neutral-800 dark:text-white placeholder:text-neutral-300 dark:placeholder:text-neutral-500 invalid:border-danger-500 invalid:text-danger-500', 'rounded-md border border-neutral-200 dark:border-neutral-600 hover:border-blue-300 dark:hover:border-blue-500 focus:outline-1 focus:outline-blue-500 dark:focus:outline-primary-500 px-[12px] py-[8px] bg-white dark:bg-gray-800 text-neutral-800 dark:text-white placeholder:text-neutral-300 dark:placeholder:text-neutral-500 invalid:border-danger-500 invalid:text-danger-500',
sizeClasses[size], sizeClasses[size],
disabled && disabledClass, disabled && disabledClass,
error && errorClass, error && errorClass,
@@ -56,7 +56,7 @@ export const InputField: FC<TInputFieldProps> = ({
<label <label
htmlFor={htmlFor} htmlFor={htmlFor}
className={cn( className={cn(
'items-start justify-item-start text-start !text-neutral-800', 'items-start justify-item-start text-start text-neutral-800! dark:text-neutral-300!',
sizeClasses[size].label sizeClasses[size].label
)} )}
> >