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>
This commit is contained in:
co-authored by
Maulana Sodiqin
Muhammad Zakir Ramadhan
parent
66b8b064ba
commit
bc5b072ad8
@@ -2,13 +2,13 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
export default function NotFoundPage() {
|
||||
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">
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
<Link
|
||||
|
||||
@@ -3,6 +3,7 @@ import { supabase } from '@imphnen-frontend-service/service';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
import ThemeToggle from '../../../components/theme-toggle';
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -80,13 +81,23 @@ export default function ForgotPasswordPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||
<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-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">
|
||||
<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?
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
No worries, we'll send you reset instructions
|
||||
</p>
|
||||
</div>
|
||||
@@ -95,7 +106,7 @@ export default function ForgotPasswordPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
@@ -106,7 +117,7 @@ export default function ForgotPasswordPage() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
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
|
||||
/>
|
||||
</div>
|
||||
@@ -119,16 +130,6 @@ export default function ForgotPasswordPage() {
|
||||
{isLoading ? 'Sending...' : 'Send Reset Link'}
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -115,8 +115,8 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
|
||||
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700">
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
|
||||
<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('/')}
|
||||
@@ -132,7 +132,7 @@ export default function LoginPage() {
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Welcome Back
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@@ -158,7 +158,7 @@ export default function LoginPage() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 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
|
||||
/>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@ export default function LoginPage() {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-neutral-300"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
@@ -185,7 +185,7 @@ export default function LoginPage() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 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
|
||||
/>
|
||||
</div>
|
||||
@@ -193,23 +193,25 @@ export default function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 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'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="my-6 flex items-center">
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
|
||||
<span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-gray-600"></div>
|
||||
<span className="px-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
OR
|
||||
</span>
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGithubLogin}
|
||||
disabled={isGithubLoading}
|
||||
type="button"
|
||||
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 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" />
|
||||
<span>
|
||||
@@ -218,7 +220,7 @@ export default function LoginPage() {
|
||||
</button>
|
||||
|
||||
<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?{' '}
|
||||
<a
|
||||
href="/auth/signup"
|
||||
@@ -230,7 +232,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,17 +10,17 @@ export default function ResetPasswordPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isValidToken, setIsValidToken] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if we have a valid session (from the reset link)
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
if (session) {
|
||||
setIsValidToken(true);
|
||||
} else {
|
||||
toast.error('Invalid or expired reset link');
|
||||
setTimeout(() => navigate('/auth/forgot-password'), 2000);
|
||||
}
|
||||
});
|
||||
}, [navigate]);
|
||||
// useEffect(() => {
|
||||
// // Check if we have a valid session (from the reset link)
|
||||
// supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
// if (session) {
|
||||
// setIsValidToken(true);
|
||||
// } else {
|
||||
// toast.error('Invalid or expired reset link');
|
||||
// setTimeout(() => navigate('/auth/forgot-password'), 2000);
|
||||
// }
|
||||
// });
|
||||
// }, [navigate]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -59,32 +59,36 @@ export default function ResetPasswordPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (!isValidToken) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
|
||||
<p className="text-gray-600">Verifying reset link...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if (!isValidToken) {
|
||||
// return (
|
||||
// <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
// <div className="text-center">
|
||||
// <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
|
||||
// <p className="text-gray-600 dark:text-gray-400">
|
||||
// Verifying reset link...
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
|
||||
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200">
|
||||
<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-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">
|
||||
<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
|
||||
</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>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
New Password
|
||||
</label>
|
||||
@@ -95,7 +99,7 @@ export default function ResetPasswordPage() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
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
|
||||
minLength={6}
|
||||
/>
|
||||
@@ -104,7 +108,7 @@ export default function ResetPasswordPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="confirmPassword"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Confirm New Password
|
||||
</label>
|
||||
@@ -115,7 +119,7 @@ export default function ResetPasswordPage() {
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
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
|
||||
minLength={6}
|
||||
/>
|
||||
|
||||
@@ -138,8 +138,8 @@ export default function SignupPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-neutral-950 p-4">
|
||||
<div className="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-neutral-700">
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
|
||||
<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('/')}
|
||||
@@ -155,7 +155,9 @@ export default function SignupPage() {
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Create Account
|
||||
</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>
|
||||
|
||||
{error && (
|
||||
@@ -168,7 +170,7 @@ export default function SignupPage() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@@ -179,7 +181,7 @@ export default function SignupPage() {
|
||||
onChange={(e) => setFullname(e.target.value)}
|
||||
placeholder="John Doe"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 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
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +189,7 @@ export default function SignupPage() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@@ -198,7 +200,7 @@ export default function SignupPage() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 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
|
||||
/>
|
||||
</div>
|
||||
@@ -206,7 +208,7 @@ export default function SignupPage() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@@ -217,7 +219,7 @@ export default function SignupPage() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 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
|
||||
/>
|
||||
</div>
|
||||
@@ -225,7 +227,7 @@ export default function SignupPage() {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@@ -236,7 +238,7 @@ export default function SignupPage() {
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 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
|
||||
/>
|
||||
</div>
|
||||
@@ -244,23 +246,25 @@ export default function SignupPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isEmailLoading}
|
||||
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 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'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="my-6 flex items-center">
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
|
||||
<span className="px-4 text-sm text-gray-500 dark:text-neutral-400">OR</span>
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-neutral-600"></div>
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-gray-600"></div>
|
||||
<span className="px-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
OR
|
||||
</span>
|
||||
<div className="flex-1 border-t border-gray-300 dark:border-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGithubLogin}
|
||||
disabled={isGithubLoading}
|
||||
type="button"
|
||||
className="w-full py-3 flex items-center justify-center gap-2 bg-gray-100 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" />
|
||||
<span>
|
||||
@@ -269,7 +273,7 @@ export default function SignupPage() {
|
||||
</button>
|
||||
|
||||
<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?{' '}
|
||||
<a
|
||||
href="/auth/login"
|
||||
@@ -281,7 +285,7 @@ export default function SignupPage() {
|
||||
</div>
|
||||
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -6,18 +6,18 @@ const DashboardLayout: FC = (): ReactElement => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
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)} />
|
||||
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* 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
|
||||
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"
|
||||
className="w-6 h-6 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -41,13 +41,13 @@ const DashboardPage: FC = (): ReactElement => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
<div className="p-6 md:p-8 md:max-w-7xl w-full mx-auto overflow-hidden">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
|
||||
</h1>
|
||||
{user?.location && (
|
||||
<p className="text-gray-600 dark:text-neutral-400 mt-1 font-sans flex items-center space-x-1">
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1 font-sans flex items-center space-x-1">
|
||||
<Icon icon="heroicons:map-pin-16-solid" width="24" height="24" />
|
||||
<span>{user.location}</span>
|
||||
</p>
|
||||
@@ -63,13 +63,13 @@ const DashboardPage: FC = (): ReactElement => {
|
||||
{invitations.map((invitation) => (
|
||||
<div
|
||||
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>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{invitation.team.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Invited by {invitation.inviter.fullname}
|
||||
</p>
|
||||
</div>
|
||||
@@ -99,39 +99,46 @@ const DashboardPage: FC = (): ReactElement => {
|
||||
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||
My Team
|
||||
</h2>
|
||||
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-4 md:gap-6 w-full md:max-w-lg">
|
||||
{myTeams.map((team) => (
|
||||
<Link
|
||||
key={team.id}
|
||||
to={'/teams/' + team.id}
|
||||
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
|
||||
className="bg-white dark:bg-gray-900 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
|
||||
>
|
||||
{team.banner && (
|
||||
<img
|
||||
src={team.banner}
|
||||
alt={team.name}
|
||||
className="w-full h-32 object-cover"
|
||||
/>
|
||||
<div className="w-full aspect-3/1">
|
||||
<img
|
||||
src={team.banner}
|
||||
alt={team.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex items-center space-x-3 mb-2 md:mb-3">
|
||||
<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-10 h-10 md:w-12 md:h-12 rounded-full object-cover"
|
||||
className="w-20 h-20 rounded-full object-cover shrink-0 border-black dark:border-gray-700"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white">
|
||||
<h3 className="text-xl md:text-lg font-bold text-gray-900 dark:text-white leading-tight">
|
||||
{team.name}
|
||||
</h3>
|
||||
<p className="text-xs md:text-sm text-gray-600 dark:text-neutral-400">
|
||||
City: {team.city}
|
||||
<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-neutral-400 text-xs md:text-sm line-clamp-2">
|
||||
<p className="text-gray-600 dark:text-gray-400 line-clamp-3 text-ellipsis font-sans">
|
||||
{team.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -140,33 +147,33 @@ const DashboardPage: FC = (): ReactElement => {
|
||||
</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="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-neutral-400 font-sans">
|
||||
<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-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="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-neutral-700 shadow-lg object-cover"
|
||||
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-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">
|
||||
<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>
|
||||
@@ -179,8 +186,13 @@ const DashboardPage: FC = (): ReactElement => {
|
||||
</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 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
|
||||
@@ -206,37 +218,37 @@ const DashboardPage: FC = (): ReactElement => {
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<div className="space-y-4 md:space-y-6 w-full max-w-full">
|
||||
{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">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm w-full max-w-full overflow-x-hidden">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-2">
|
||||
About
|
||||
</h3>
|
||||
<p className="text-gray-800 dark:text-neutral-200 leading-relaxed line-clamp-3 wrap-break-word">
|
||||
<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">
|
||||
{user.bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<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">
|
||||
<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">
|
||||
Contact
|
||||
</h3>
|
||||
<div className="flex items-center text-gray-700 dark:text-neutral-300">
|
||||
<div className="flex items-center text-gray-700 dark:text-gray-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">
|
||||
<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 text-primary-600 rounded-4xl text-xs font-medium "
|
||||
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>
|
||||
|
||||
@@ -104,13 +104,13 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
});
|
||||
|
||||
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="bg-white dark:bg-neutral-900 w-full max-w-2xl p-8 rounded-xl shadow-lg dark:shadow-neutral-950/50">
|
||||
<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-gray-900 w-full max-w-2xl p-8 rounded-xl shadow-lg dark:shadow-gray-950/50">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Complete Your Profile
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -123,15 +123,15 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
<img
|
||||
src={avatarPreview}
|
||||
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="ic:baseline-person"
|
||||
width="48"
|
||||
height="48"
|
||||
className="text-gray-400 dark:text-neutral-500"
|
||||
className="text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -149,7 +149,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
onChange={handleAvatarChange}
|
||||
/>
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
|
||||
{/* City */}
|
||||
<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>
|
||||
</label>
|
||||
<Controller
|
||||
@@ -187,7 +187,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Role/Skills */}
|
||||
<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
|
||||
</label>
|
||||
<Controller
|
||||
@@ -210,9 +210,9 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
: (field.value || []).filter((v) => v !== role);
|
||||
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}
|
||||
</span>
|
||||
</label>
|
||||
@@ -225,9 +225,9 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Bio */}
|
||||
<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{' '}
|
||||
<span className="text-gray-400 dark:text-neutral-500">
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
(Optional)
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -66,10 +66,10 @@ export default function HomePage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white dark:bg-neutral-950">
|
||||
<main className="min-h-screen bg-white dark:bg-gray-950">
|
||||
{/* Navigation */}
|
||||
<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 gap-2">
|
||||
<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">
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@@ -117,7 +117,7 @@ export default function HomePage() {
|
||||
onClick={() => navigate('/auth/login')}
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
className="rounded-lg text-base"
|
||||
className="rounded-lg text-base dark:bg-gray-800"
|
||||
>
|
||||
Masuk
|
||||
</Button>
|
||||
@@ -158,23 +158,23 @@ export default function HomePage() {
|
||||
|
||||
{/* Mobile Menu */}
|
||||
{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">
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@@ -189,7 +189,7 @@ export default function HomePage() {
|
||||
<>
|
||||
<a
|
||||
href="#masuk"
|
||||
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"
|
||||
>
|
||||
Masuk
|
||||
</a>
|
||||
@@ -228,7 +228,7 @@ export default function HomePage() {
|
||||
className="h-12 md:h-16"
|
||||
/>
|
||||
</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>
|
||||
<div className="flex items-center">
|
||||
@@ -248,23 +248,23 @@ export default function HomePage() {
|
||||
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
|
||||
</p>
|
||||
{/* 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
|
||||
yang menghadirkan dampak nyata
|
||||
</p>
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-4 md:gap-8 mb-10 md:mb-16 text-base text-gray-600 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">
|
||||
<Icon icon="streamline-plump:web" width="16" height="16" />
|
||||
<Icon icon="streamline-plump:web" className="w-4 h-4" />
|
||||
<span>Online</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{/* 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
|
||||
onClick={() => navigate('/auth/signup')}
|
||||
className="rounded-lg text-base max-h-auto"
|
||||
@@ -275,7 +275,7 @@ export default function HomePage() {
|
||||
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center px-4 py-2.5 border-2 border-gray-300 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
|
||||
</a>
|
||||
@@ -283,7 +283,7 @@ export default function HomePage() {
|
||||
{/* Scroll indicator */}
|
||||
<div className="mt-12 md:mt-20">
|
||||
<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"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -300,19 +300,19 @@ export default function HomePage() {
|
||||
</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">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-10 dark:text-white">
|
||||
Tentang <span className="text-primary-500">Hackathon</span>
|
||||
</h2>
|
||||
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 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">
|
||||
Hackathon
|
||||
</span>{' '}
|
||||
adalah kompetisi pengembangan teknologi yang mengajak talenta muda
|
||||
untuk berkolaborasi dalam menciptakan solusi inovatif.
|
||||
</p>
|
||||
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 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{' '}
|
||||
<span className="font-semibold text-primary-500">
|
||||
"Inovasi AI: Mendorong Usaha Lokal dengan AI Inklusif"
|
||||
@@ -325,7 +325,7 @@ export default function HomePage() {
|
||||
{/* Prizes Section */}
|
||||
<section
|
||||
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="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">
|
||||
{/* 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="w-16 h-16 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
|
||||
<Icon
|
||||
@@ -357,12 +357,12 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* 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="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="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>
|
||||
@@ -375,7 +375,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* 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="w-16 h-16 bg-orange-100 dark:bg-orange-900/30 rounded-full flex items-center justify-center">
|
||||
<Icon
|
||||
@@ -393,7 +393,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* 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="w-16 h-16 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center">
|
||||
<Icon
|
||||
@@ -416,14 +416,14 @@ export default function HomePage() {
|
||||
{/* Timeline Section */}
|
||||
<section
|
||||
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="text-center mb-12 font-bai-jamjuree">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
|
||||
Timeline <span className="text-primary-500">Acara</span>
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-200">
|
||||
Jadwal lengkap pelaksanaan hackathon
|
||||
</p>
|
||||
</div>
|
||||
@@ -433,7 +433,7 @@ export default function HomePage() {
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-4 h-4 bg-primary-500 rounded-full"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
<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">
|
||||
Penutupan Registrasi
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-gray-600 dark:text-gray-200">
|
||||
Batas akhir pendaftaran peserta hackathon.
|
||||
</p>
|
||||
</div>
|
||||
@@ -452,7 +452,7 @@ export default function HomePage() {
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-4 h-4 bg-primary-500 rounded-full"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
<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">
|
||||
Technical Meeting
|
||||
</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
|
||||
Meet. Stay tune di grup WA Hackathon.
|
||||
</p>
|
||||
@@ -472,7 +472,7 @@ export default function HomePage() {
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-4 h-4 bg-primary-500 rounded-full"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
<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">
|
||||
Tahap Penyisihan
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-gray-600 dark:text-gray-200">
|
||||
Peserta mengerjakan tantangan yang diberikan.
|
||||
</p>
|
||||
</div>
|
||||
@@ -491,7 +491,7 @@ export default function HomePage() {
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-4 h-4 bg-primary-500 rounded-full"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-neutral-700"></div>
|
||||
<div className="w-0.5 h-full bg-gray-300 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div className="flex-1 pb-8">
|
||||
<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">
|
||||
Penilaian & Webinar
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -518,7 +518,7 @@ export default function HomePage() {
|
||||
<h3 className="text-xl font-bold mb-2 dark:text-white">
|
||||
Pengumuman & Final
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-gray-600 dark:text-gray-200">
|
||||
Presentasi final dan pengumuman pemenang.
|
||||
</p>
|
||||
</div>
|
||||
@@ -528,50 +528,48 @@ export default function HomePage() {
|
||||
</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="text-center mb-12 font-bai-jamjuree">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
|
||||
Dewan <span className="text-primary-500">Juri</span>
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md md:max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Judge 1 */}
|
||||
<div className="flex flex-col justify-between bg-white 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">
|
||||
Alifais Farrel Ramdhani
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-primary-500 font-semibold mb-1">CTO</p>
|
||||
<p className="text-gray-600 dark:text-neutral-400">
|
||||
Kolosal.ai
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-200">Kolosal.ai</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
Anka Tama
|
||||
</h3>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
Hafid Nur
|
||||
</h3>
|
||||
<div>
|
||||
<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>
|
||||
@@ -581,14 +579,14 @@ export default function HomePage() {
|
||||
{/* FAQ Section */}
|
||||
<section
|
||||
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="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500">
|
||||
FAQ
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-200">
|
||||
Pertanyaan yang Sering Diajukan
|
||||
</p>
|
||||
</div>
|
||||
@@ -597,11 +595,11 @@ export default function HomePage() {
|
||||
{faqs.map((faq, index) => (
|
||||
<div
|
||||
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
|
||||
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">
|
||||
{faq.question}
|
||||
@@ -623,7 +621,7 @@ export default function HomePage() {
|
||||
</svg>
|
||||
</button>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
@@ -634,12 +632,12 @@ export default function HomePage() {
|
||||
</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">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
|
||||
Sponsor & <span className="text-primary-500">Partner</span>
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-neutral-400 mb-8">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-200 mb-8">
|
||||
Acara ini sepenuhnya disponsori oleh
|
||||
</p>
|
||||
|
||||
@@ -648,7 +646,7 @@ export default function HomePage() {
|
||||
href="https://kolosal.ai"
|
||||
target="_blank"
|
||||
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
|
||||
src="images/sponsors/kolosal-logo_rlxbck.svg"
|
||||
@@ -663,13 +661,13 @@ export default function HomePage() {
|
||||
{/* CTA Section */}
|
||||
<section
|
||||
id="masuk"
|
||||
className="py-20 md:py-28 px-4 md:px-8 bg-linear-to-b from-white to-blue-50 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">
|
||||
<h2 className="text-3xl md:text-5xl font-bold mb-6 font-bai-jamjuree dark:text-white">
|
||||
Segera Daftarkan <span className="text-primary-500">Timmu!</span>
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl text-left md:text-center text-gray-600 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
|
||||
terbaik,
|
||||
<br className="hidden md:block" /> belajar dari para ahli, dan
|
||||
@@ -687,13 +685,13 @@ export default function HomePage() {
|
||||
href="https://chat.whatsapp.com/BlxrYh9uSC37d7VPhJslGL"
|
||||
target="_blank"
|
||||
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
|
||||
</a>
|
||||
</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{' '}
|
||||
<span className="text-primary-500 font-semibold">
|
||||
30 November 2025
|
||||
@@ -703,7 +701,7 @@ export default function HomePage() {
|
||||
</section>
|
||||
|
||||
{/* 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="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
|
||||
{/* Brand */}
|
||||
|
||||
@@ -138,8 +138,8 @@ const ProfilePage: FC = (): ReactElement => {
|
||||
const isLoading = isUpdating || isUploading;
|
||||
|
||||
return (
|
||||
<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="bg-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-xl dark:shadow-neutral-950/50">
|
||||
<div className=" 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="bg-white dark:bg-gray-900 w-full max-w-md p-8 rounded-xl border dark:boder-gray-800">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
@@ -180,7 +180,7 @@ const ProfilePage: FC = (): ReactElement => {
|
||||
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">
|
||||
<div className="w-32 h-32 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">
|
||||
<Icon
|
||||
icon="ic:baseline-person"
|
||||
width="48"
|
||||
@@ -241,7 +241,7 @@ const ProfilePage: FC = (): ReactElement => {
|
||||
|
||||
{/* City */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
|
||||
<label className="block text-label1 font-medium text-neutral-800 dark:text-neutral-300">
|
||||
City
|
||||
</label>
|
||||
<Controller
|
||||
@@ -283,7 +283,7 @@ const ProfilePage: FC = (): ReactElement => {
|
||||
: (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"
|
||||
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}
|
||||
@@ -313,7 +313,7 @@ const ProfilePage: FC = (): ReactElement => {
|
||||
{...field}
|
||||
placeholder="Tell us about yourself..."
|
||||
rows={4}
|
||||
className="w-full"
|
||||
className="w-full font-sans"
|
||||
size="lg"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
|
||||
@@ -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 { 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 { Icon } from '@iconify/react';
|
||||
|
||||
const TeamChatPage: FC = (): ReactElement => {
|
||||
const { teamId } = useParams<{ teamId: string }>();
|
||||
@@ -13,16 +20,54 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
|
||||
const { data: teamData } = useTeamById(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 team = teamData?.data;
|
||||
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
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
}, [displayMessages]);
|
||||
|
||||
const handleSendMessage = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -38,11 +83,10 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
};
|
||||
|
||||
const handleDeleteMessage = async (messageId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this message?')) return;
|
||||
|
||||
try {
|
||||
await deleteMessage(messageId);
|
||||
toast.success('Message deleted');
|
||||
setDeleteTargetId(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete message:', error);
|
||||
toast.error('Failed to delete message');
|
||||
@@ -63,16 +107,23 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<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="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Team Chat</h1>
|
||||
<p className="text-gray-600 dark:text-neutral-400 text-sm mt-0.5">{team?.name}</p>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Team Chat
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm mt-0.5">
|
||||
{team?.name}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/teams/${teamId}`)}
|
||||
>
|
||||
Back to Team
|
||||
</Button>
|
||||
</div>
|
||||
@@ -86,21 +137,27 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
<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>
|
||||
) : messages && messages.length > 0 ? (
|
||||
) : displayMessages && displayMessages.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{messages.map((msg) => {
|
||||
{displayMessages.map((msg) => {
|
||||
const isOwnMessage = msg.user_id === currentUserId;
|
||||
const isLeader = team?.leader_id === currentUserId;
|
||||
const canDelete = isOwnMessage || isLeader;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
|
||||
key={`${msg.id ?? 'message'}-${msg.created_at}`}
|
||||
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 */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className="shrink-0">
|
||||
{msg.user?.avatar ? (
|
||||
<img
|
||||
src={msg.user.avatar}
|
||||
@@ -115,13 +172,17 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
|
||||
{/* Message Bubble */}
|
||||
<div className={`flex-1 ${isOwnMessage ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`inline-block ${isOwnMessage ? 'items-end' : 'items-start'}`}>
|
||||
<div className={`flex-1 text-left`}>
|
||||
<div
|
||||
className={`inline-block ${
|
||||
isOwnMessage ? 'items-end' : 'items-start'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span className="font-semibold text-sm text-gray-900 dark:text-white">
|
||||
{isOwnMessage ? 'You' : msg.user?.fullname}
|
||||
</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)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -129,24 +190,58 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
className={`relative group rounded-2xl px-4 py-2.5 ${
|
||||
isOwnMessage
|
||||
? '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 */}
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={() => handleDeleteMessage(msg.id)}
|
||||
className={`absolute top-1 ${isOwnMessage ? 'left-1' : 'right-1'} opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-200 dark:hover:bg-neutral-700 ${isOwnMessage ? 'hover:bg-blue-700 dark:hover:bg-primary-700' : ''}`}
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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" />
|
||||
</svg>
|
||||
{/* Icon trash */}
|
||||
<Icon
|
||||
icon="mdi:trash-can-outline"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</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>
|
||||
@@ -157,11 +252,14 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
) : (
|
||||
<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">
|
||||
No messages yet
|
||||
</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!
|
||||
</p>
|
||||
</div>
|
||||
@@ -170,7 +268,7 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<form onSubmit={handleSendMessage} className="flex gap-3">
|
||||
<input
|
||||
@@ -178,7 +276,7 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
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}
|
||||
/>
|
||||
<Button
|
||||
@@ -193,8 +291,18 @@ const TeamChatPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg 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
|
||||
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>
|
||||
Send
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,19 @@ import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
|
||||
import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
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 { CitySelect } from '../../../../components/city-select';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const EditTeamPage: FC = (): ReactElement => {
|
||||
const { teamId } = useParams<{ teamId: string }>();
|
||||
@@ -17,8 +26,12 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
const [bannerFile, setBannerFile] = useState<File | null>(null);
|
||||
const [bannerPreview, setBannerPreview] = useState<string>('');
|
||||
|
||||
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
|
||||
const { mutateAsync: updateTeam, isPending: isUpdating } = useUpdateTeam(teamId || '');
|
||||
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
|
||||
teamId || ''
|
||||
);
|
||||
const { mutateAsync: updateTeam, isPending: isUpdating } = useUpdateTeam(
|
||||
teamId || ''
|
||||
);
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
|
||||
const team = teamData?.data;
|
||||
@@ -47,18 +60,24 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
|
||||
if (isLoadingTeam) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<div className="text-gray-600 dark:text-neutral-400">Loading team...</div>
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<div className="text-gray-600 dark:text-gray-400">Loading team...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLeader) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can edit team information</p>
|
||||
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -115,28 +134,35 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<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">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Edit Team Info</h1>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mt-1">Update your team details</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Edit Team Info
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Update your team details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{/* Banner Upload */}
|
||||
<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
|
||||
</label>
|
||||
{bannerPreview ? (
|
||||
<div className="relative">
|
||||
<div
|
||||
className="relative w-full"
|
||||
style={{ aspectRatio: '3 / 1' }}
|
||||
>
|
||||
<img
|
||||
src={bannerPreview}
|
||||
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
|
||||
type="button"
|
||||
@@ -145,16 +171,20 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
setBannerPreview('');
|
||||
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
|
||||
</button>
|
||||
</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">
|
||||
<p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p>
|
||||
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Click to upload banner
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
1200x400 recommended
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@@ -168,7 +198,7 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Logo Upload */}
|
||||
<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
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
@@ -176,16 +206,18 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
<img
|
||||
src={logoPreview}
|
||||
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">
|
||||
<span className="text-gray-400 dark:text-neutral-500 text-3xl">👥</span>
|
||||
<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-gray-500 text-3xl">
|
||||
<Icon icon="mdi:account-group" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="flex flex-col md:flex-row items-start justify-start gap-1">
|
||||
<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'}
|
||||
</span>
|
||||
<input
|
||||
@@ -204,7 +236,7 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
setLogoPreview('');
|
||||
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
|
||||
</button>
|
||||
@@ -219,11 +251,13 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
label="Team Name"
|
||||
placeholder="Enter team name"
|
||||
name="name"
|
||||
size="lg"
|
||||
isRequired={true}
|
||||
/>
|
||||
|
||||
{/* City */}
|
||||
<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
|
||||
</label>
|
||||
<Controller
|
||||
@@ -242,7 +276,7 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Description */}
|
||||
<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
|
||||
</label>
|
||||
<Controller
|
||||
@@ -256,9 +290,12 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
placeholder="Tell others about your team..."
|
||||
rows={4}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
@@ -267,7 +304,7 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Visibility */}
|
||||
<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
|
||||
</label>
|
||||
<Controller
|
||||
@@ -275,7 +312,7 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
name="visibility"
|
||||
render={({ field }) => (
|
||||
<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
|
||||
type="radio"
|
||||
{...field}
|
||||
@@ -284,13 +321,15 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
className="mt-1"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">Public</p>
|
||||
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
Public
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-sans">
|
||||
Team will be visible in Browse Teams
|
||||
</p>
|
||||
</div>
|
||||
</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
|
||||
type="radio"
|
||||
{...field}
|
||||
@@ -299,8 +338,10 @@ const EditTeamPage: FC = (): ReactElement => {
|
||||
className="mt-1"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">Private</p>
|
||||
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
Private
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 font-sans">
|
||||
Team hidden, invite-only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -6,21 +6,33 @@ const TeamDetailLayout: FC = (): ReactElement => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
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)} />
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* 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
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
<svg
|
||||
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>
|
||||
</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>
|
||||
|
||||
<main className="flex-1 overflow-auto">
|
||||
|
||||
@@ -23,17 +23,28 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
const { session } = useAuthStore();
|
||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||
|
||||
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
|
||||
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || '');
|
||||
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
|
||||
teamId || ''
|
||||
);
|
||||
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(
|
||||
teamId || ''
|
||||
);
|
||||
const { data: joinRequestsData } = useTeamJoinRequests(teamId || '');
|
||||
|
||||
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(teamId || '');
|
||||
const { mutateAsync: removeMember, isPending: isRemoving } = useRemoveMember(teamId || '');
|
||||
const { mutateAsync: respondToRequest, isPending: isResponding } = useRespondToJoinRequest(teamId || '');
|
||||
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(
|
||||
teamId || ''
|
||||
);
|
||||
const { mutateAsync: removeMember, isPending: isRemoving } = useRemoveMember(
|
||||
teamId || ''
|
||||
);
|
||||
const { mutateAsync: respondToRequest, isPending: isResponding } =
|
||||
useRespondToJoinRequest(teamId || '');
|
||||
|
||||
const team = teamData?.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 isLeader = currentUserId === team?.leader_id;
|
||||
|
||||
@@ -45,18 +56,24 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
// Show loading state while fetching team data
|
||||
if (isLoadingTeam) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<p className="text-gray-600 dark:text-neutral-400">Loading...</p>
|
||||
<div 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-gray-400">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLeader) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can manage members</p>
|
||||
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -103,19 +120,26 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<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="flex items-center justify-between">
|
||||
<div className="flex flex-col md:flex-row gap-y-4 items-start md:items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Manage Members</h1>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Manage Members
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
{team?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<Button onClick={() => setShowInviteModal(true)}>
|
||||
Invite Member
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/teams/${teamId}`)}
|
||||
>
|
||||
Back to Team
|
||||
</Button>
|
||||
</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">
|
||||
{/* Join Requests */}
|
||||
{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">
|
||||
Join Requests ({joinRequests.length})
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{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-center space-x-3 flex-1">
|
||||
{request.user.avatar ? (
|
||||
@@ -142,17 +169,27 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
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">
|
||||
<span className="text-gray-500 dark:text-neutral-400">👤</span>
|
||||
<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-gray-400">
|
||||
👤
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{request.user.fullname}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-neutral-400">{request.user.email}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{request.user.fullname}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{request.user.email}
|
||||
</p>
|
||||
{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 className="flex space-x-2 ml-4">
|
||||
@@ -180,16 +217,21 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
Current Members ({members.length})
|
||||
</h2>
|
||||
{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">
|
||||
{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">
|
||||
{member.user.avatar ? (
|
||||
<img
|
||||
@@ -198,15 +240,23 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
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">
|
||||
<span className="text-gray-500 dark:text-neutral-400">👤</span>
|
||||
<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-gray-400">
|
||||
👤
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{member.user.fullname}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-neutral-400">{member.user.email}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{member.user.fullname}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{member.user.email}
|
||||
</p>
|
||||
{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">
|
||||
{member.role === 'leader' && (
|
||||
@@ -241,7 +291,8 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
{/* Warning */}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,27 +300,30 @@ const ManageMembersPage: FC = (): ReactElement => {
|
||||
{/* Invite Member Modal */}
|
||||
{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="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">
|
||||
Invite Member
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mb-4">
|
||||
Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in.
|
||||
<p 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.
|
||||
</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">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in.
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleInvite} className="space-y-4">
|
||||
<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
|
||||
</label>
|
||||
<Input
|
||||
{...form.register('email')}
|
||||
type="email"
|
||||
placeholder="member@example.com"
|
||||
size="lg"
|
||||
/>
|
||||
{form.formState.errors.email && (
|
||||
<p className="text-sm text-red-500 mt-1">
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useAuthStore,
|
||||
} from '@imphnen-frontend-service/service';
|
||||
import { toast } from 'sonner';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const MAX_TEAM_MEMBERS = 5;
|
||||
|
||||
@@ -140,18 +141,18 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
|
||||
if (isLoadingTeam || isLoadingMembers) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
{/* Skeleton Header */}
|
||||
<div className="bg-white border-b">
|
||||
<div className="w-full h-48 bg-gray-200 animate-pulse" />
|
||||
<div className="bg-white dark:bg-gray-950 border-b">
|
||||
<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="flex items-start justify-between">
|
||||
<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 className="h-8 w-48 bg-gray-300 rounded animate-pulse" />
|
||||
<div className="h-4 w-32 bg-gray-200 rounded animate-pulse mt-2" />
|
||||
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mt-2" />
|
||||
<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 dark:bg-gray-800 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>
|
||||
@@ -162,27 +163,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main Content Skeleton */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="h-6 w-32 bg-gray-300 rounded animate-pulse mb-4" />
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6">
|
||||
<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="h-4 w-full bg-gray-200 rounded animate-pulse" />
|
||||
<div className="h-4 w-3/4 bg-gray-200 rounded animate-pulse" />
|
||||
<div className="h-4 w-5/6 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 dark:bg-gray-800 rounded animate-pulse" />
|
||||
<div className="h-4 w-5/6 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Skeleton */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<div className="h-6 w-40 bg-gray-300 rounded animate-pulse mb-4" />
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6">
|
||||
<div className="h-6 w-40 bg-gray-300 dark:bg-gray-700 rounded animate-pulse mb-4" />
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<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 className="h-4 w-24 bg-gray-300 rounded animate-pulse" />
|
||||
<div className="h-3 w-16 bg-gray-200 rounded animate-pulse mt-1" />
|
||||
<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 dark:bg-gray-800 rounded animate-pulse mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -196,7 +197,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<div className="fixed inset-0 bg-white/60 flex items-center justify-center z-50">
|
||||
<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>
|
||||
<p className="mt-4 text-gray-600 dark:text-neutral-400">
|
||||
<p className="mt-4 text-gray-600 dark:text-white">
|
||||
Loading team data...
|
||||
</p>
|
||||
</div>
|
||||
@@ -219,7 +220,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
}
|
||||
|
||||
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 */}
|
||||
{!imagesLoaded && totalImagesToLoad > 0 && (
|
||||
<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 */}
|
||||
<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 && (
|
||||
<div className="w-full h-32 md:h-48 overflow-hidden relative">
|
||||
<div className="relative w-full aspect-3/1 overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 bg-gray-200 animate-pulse ${
|
||||
imagesLoaded ? 'hidden' : ''
|
||||
@@ -245,26 +246,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<img
|
||||
src={team.banner}
|
||||
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'
|
||||
}`}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent via-black/20 to-black/60" />
|
||||
</div>
|
||||
)}
|
||||
<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-center space-x-3 md:space-x-4">
|
||||
{team.logo && (
|
||||
<div className="relative">
|
||||
<div className="relative shrink-0">
|
||||
{!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
|
||||
src={team.logo}
|
||||
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'
|
||||
}`}
|
||||
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">
|
||||
{team.name}
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1 line-clamp-1">
|
||||
📍 {team.city}
|
||||
<p className="text-sm md:text-base text-gray-600 dark:text-gray-400 mt-1 line-clamp-1 font-sans flex items-center">
|
||||
<Icon icon="mdi:map-marker" className="inline-block mr-1" />
|
||||
<span>{team.city}</span>
|
||||
</p>
|
||||
<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 === 1 ? 'Member' : 'Members'}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-neutral-400">
|
||||
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
||||
<Icon
|
||||
icon={
|
||||
team.visibility === 'public' ? 'mdi:web' : 'mdi:lock'
|
||||
}
|
||||
className="inline-block mr-1"
|
||||
/>
|
||||
{team.visibility === 'public' ? 'Public' : 'Private'}
|
||||
</span>
|
||||
{isLeader && (
|
||||
<span className="hidden md:inline-flex px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-semibold shadow-sm">
|
||||
👑 Team Leader
|
||||
<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">
|
||||
<Icon icon="mdi:crown" className="inline-block mr-1" />
|
||||
<span>Team Leader</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -300,22 +314,22 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
|
||||
<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 */}
|
||||
<div className="lg:col-span-2 space-y-4 md:space-y-6">
|
||||
{/* 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">
|
||||
About Team
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Team Actions - Only for Leader */}
|
||||
{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">
|
||||
Team Management
|
||||
</h2>
|
||||
@@ -326,7 +340,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
onClick={() => setShowInviteModal(true)}
|
||||
disabled={!canInvite}
|
||||
>
|
||||
➕ Invite Member{' '}
|
||||
<Icon icon="mdi:plus" className="inline-block mr-2" />
|
||||
Invite Member{' '}
|
||||
{!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -334,7 +349,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
variant="secondary"
|
||||
onClick={() => setShowJoinRequestsModal(true)}
|
||||
>
|
||||
📩 Join Requests
|
||||
<Icon icon="mdi:email" className="inline-block mr-2" /> Join
|
||||
Requests
|
||||
{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">
|
||||
{pendingJoinRequests.length}
|
||||
@@ -343,25 +359,34 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
</Button>
|
||||
<Link to={`/teams/${teamId}/edit`}>
|
||||
<Button className="w-full" variant="secondary">
|
||||
✏️ Edit Team Info
|
||||
<Icon icon="mdi:pencil" className="inline-block mr-2" />
|
||||
Edit Team Info
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={`/teams/${teamId}/members`}>
|
||||
<Button className="w-full" variant="secondary">
|
||||
👥 Manage Members
|
||||
<Icon
|
||||
icon="mdi:account-group"
|
||||
className="inline-block mr-2"
|
||||
/>
|
||||
Manage Members
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={`/teams/${teamId}/chat`}>
|
||||
<Button className="w-full" variant="secondary">
|
||||
💬 Team Chat
|
||||
<Icon icon="mdi:chat" className="inline-block mr-2" />
|
||||
Team Chat
|
||||
</Button>
|
||||
</Link>
|
||||
<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>
|
||||
</div>
|
||||
{!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)
|
||||
</p>
|
||||
)}
|
||||
@@ -370,7 +395,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Quick Actions for Members (non-leaders) */}
|
||||
{!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">
|
||||
Quick Actions
|
||||
</h2>
|
||||
@@ -417,14 +442,14 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* 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">
|
||||
Team Leader
|
||||
</h3>
|
||||
{team.leader && (
|
||||
<button
|
||||
onClick={() => navigate(`/users/${team.leader.id}`)}
|
||||
className="w-full flex items-center space-x-3 hover:bg-gray-100 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 ? (
|
||||
<img
|
||||
@@ -433,8 +458,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
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">
|
||||
<span className="text-gray-500 dark:text-neutral-400">
|
||||
<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-gray-400">
|
||||
👤
|
||||
</span>
|
||||
</div>
|
||||
@@ -443,7 +468,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{team.leader.fullname}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{team.leader.email}
|
||||
</p>
|
||||
</div>
|
||||
@@ -452,7 +477,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
Members ({members.length})
|
||||
</h3>
|
||||
@@ -461,7 +486,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<button
|
||||
key={member.id}
|
||||
onClick={() => navigate(`/users/${member.user.id}`)}
|
||||
className="w-full flex items-center space-x-3 hover:bg-gray-100 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 ? (
|
||||
<img
|
||||
@@ -472,8 +497,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
onError={handleImageError}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
||||
<span className="text-gray-500 dark:text-neutral-400 text-sm">
|
||||
<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-gray-400 text-sm">
|
||||
👤
|
||||
</span>
|
||||
</div>
|
||||
@@ -482,7 +507,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<p className="font-medium text-gray-900 dark:text-white truncate">
|
||||
{member.user?.fullname || 'Unknown'}
|
||||
</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
|
||||
? 'Leader'
|
||||
: 'Member'}
|
||||
@@ -499,16 +524,16 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
{/* Invite Member Modal */}
|
||||
{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="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">
|
||||
Invite Team Member
|
||||
</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.
|
||||
</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">
|
||||
<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
|
||||
GitHub email address the member uses to sign in.
|
||||
</p>
|
||||
@@ -517,7 +542,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<div>
|
||||
<label
|
||||
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
|
||||
</label>
|
||||
@@ -527,10 +552,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
placeholder="Enter email address..."
|
||||
className="w-full px-3 py-2 border border-gray-300 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
|
||||
/>
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -562,14 +587,14 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
{/* Join Requests Modal */}
|
||||
{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="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">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Join Requests ({pendingJoinRequests.length})
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowJoinRequestsModal(false)}
|
||||
className="text-gray-400 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>
|
||||
@@ -577,10 +602,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
|
||||
{pendingJoinRequests.length === 0 ? (
|
||||
<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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -589,7 +614,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
{pendingJoinRequests.map((request: any) => (
|
||||
<div
|
||||
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 space-x-3 flex-1">
|
||||
@@ -600,9 +625,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
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">
|
||||
<span className="text-gray-500 dark:text-neutral-400 text-xl">
|
||||
👤
|
||||
<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-gray-400 text-xl">
|
||||
<Icon
|
||||
icon="mdi:account-circle"
|
||||
className="text-3xl"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -610,49 +638,51 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
||||
<p className="font-semibold text-gray-900 dark:text-white">
|
||||
{request.user?.fullname || 'Unknown User'}
|
||||
</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}
|
||||
</p>
|
||||
{request.message && (
|
||||
<div className="mt-2 bg-gray-50 dark:bg-neutral-800 rounded-lg p-3">
|
||||
<p className="text-sm text-gray-700 dark:text-neutral-300">
|
||||
<div className="mt-2 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<strong>Message:</strong> {request.message}
|
||||
</p>
|
||||
</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{' '}
|
||||
{new Date(request.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</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 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 && (
|
||||
<div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-2">
|
||||
<p className="text-xs text-yellow-800 dark:text-yellow-300">
|
||||
<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-sm text-yellow-800 dark:text-yellow-300 font-sans">
|
||||
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS}{' '}
|
||||
members). Remove a member before accepting new
|
||||
requests.
|
||||
|
||||
@@ -23,7 +23,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
|
||||
const { data: teamData } = useTeamById(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 team = teamData?.data;
|
||||
@@ -38,25 +39,38 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
|
||||
if (!isLeader) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can submit projects</p>
|
||||
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasSubmission) {
|
||||
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>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Project Already Submitted</h2>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mb-4">Your team has already submitted a project</p>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
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">
|
||||
<Button onClick={() => navigate(`/teams/${teamId}/submission`)}>
|
||||
View Submission
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => navigate(`/teams/${teamId}`)}
|
||||
>
|
||||
Back to Team
|
||||
</Button>
|
||||
</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;
|
||||
if (!files) return;
|
||||
|
||||
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 urls = results.map(r => r.data.url);
|
||||
const urls = results.map((r) => r.data.url);
|
||||
setScreenshots([...screenshots, ...urls]);
|
||||
} catch (error) {
|
||||
console.error('Failed to upload screenshots:', error);
|
||||
@@ -95,11 +111,13 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||
<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">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Submit Project</h1>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Submit Project
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">{team?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,33 +127,42 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
<div className="flex items-start space-x-3">
|
||||
<span className="text-3xl">⚠️</span>
|
||||
<div>
|
||||
<h3 className="font-bold text-red-900 dark:text-red-100 text-lg">IMPORTANT WARNING</h3>
|
||||
<ul className="text-red-800 dark:text-red-200 mt-2 space-y-1 text-sm">
|
||||
<h3 className="font-bold text-red-900 dark:text-red-100 text-lg">
|
||||
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>• 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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8">
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setShowConfirmModal(true);
|
||||
}} className="space-y-6">
|
||||
<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) => {
|
||||
e.preventDefault();
|
||||
setShowConfirmModal(true);
|
||||
}}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Project Name */}
|
||||
<ControlledInputField
|
||||
control={form.control}
|
||||
label="Project Name"
|
||||
placeholder="Enter your project name"
|
||||
name="project_name"
|
||||
size="lg"
|
||||
isRequired={true}
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
<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>
|
||||
</label>
|
||||
<Controller
|
||||
@@ -148,9 +175,12 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
placeholder="Describe your project, its features, and what problem it solves..."
|
||||
rows={6}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
@@ -164,6 +194,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
placeholder="https://github.com/username/project"
|
||||
name="repository_url"
|
||||
type="url"
|
||||
size="lg"
|
||||
isRequired={true}
|
||||
/>
|
||||
|
||||
{/* Demo URL */}
|
||||
@@ -173,6 +205,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
placeholder="https://your-project-demo.com"
|
||||
name="demo_url"
|
||||
type="url"
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{/* Presentation URL */}
|
||||
@@ -182,12 +215,13 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
placeholder="https://slides.com/your-presentation or Google Drive link"
|
||||
name="presentation_url"
|
||||
type="url"
|
||||
size="lg"
|
||||
/>
|
||||
|
||||
{/* Screenshots */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
||||
Project Screenshots <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
|
||||
<div>
|
||||
<label className="block text-label1 font-medium text-neutral-800 dark:text-gray-300">
|
||||
Project Screenshots (Optional)
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
|
||||
{screenshots.map((url, index) => (
|
||||
@@ -195,7 +229,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
<img
|
||||
src={url}
|
||||
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
|
||||
type="button"
|
||||
@@ -207,10 +241,14 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
</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">
|
||||
<p className="text-gray-500 dark:text-neutral-400">Click to upload screenshots</p>
|
||||
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">PNG, JPG up to 5MB each</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
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>
|
||||
<input
|
||||
type="file"
|
||||
@@ -248,7 +286,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
|
||||
{/* Confirmation Modal */}
|
||||
{showConfirmModal && (
|
||||
<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-5xl mb-4">⚠️</div>
|
||||
<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!
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm text-gray-700 dark:text-neutral-300 mb-3">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
By clicking "Submit Project", you confirm that:
|
||||
</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>✓ You understand this can only be done once</li>
|
||||
<li>✓ You cannot edit after submission</li>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { CitySelect } from '../../../components/city-select';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const BrowseTeamsPage: FC = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
@@ -70,16 +71,16 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
||||
});
|
||||
|
||||
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 */}
|
||||
<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="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Browse Teams
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-neutral-400 mt-1">
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Find and join teams looking for members
|
||||
</p>
|
||||
</div>
|
||||
@@ -92,10 +93,10 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
||||
|
||||
{/* Filters */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="bg-white 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>
|
||||
<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
|
||||
</label>
|
||||
<input
|
||||
@@ -103,11 +104,11 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
||||
placeholder="Search by team name..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full h-[42px] px-3 border border-gray-300 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>
|
||||
<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
|
||||
</label>
|
||||
<CitySelect
|
||||
@@ -122,30 +123,28 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
||||
{/* Teams List */}
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-600 dark:text-neutral-400">
|
||||
Loading teams...
|
||||
</p>
|
||||
<p className="text-gray-600 dark:text-gray-400">Loading teams...</p>
|
||||
</div>
|
||||
) : teams.length === 0 ? (
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center">
|
||||
<p className="text-gray-600 dark:text-neutral-400 text-lg">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm p-12 text-center">
|
||||
<p className="text-gray-600 dark:text-gray-400 text-lg">
|
||||
No teams found
|
||||
</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
|
||||
</p>
|
||||
</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) => (
|
||||
<div
|
||||
key={team.id}
|
||||
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col"
|
||||
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
|
||||
src={team.banner || '/images/banner-imphnen.webp'}
|
||||
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="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"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
||||
<span className="text-gray-500 dark:text-neutral-400 text-xl">
|
||||
👥
|
||||
<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-gray-300 text-xl">
|
||||
<Icon icon="mdi:account-group" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<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}
|
||||
</h3>
|
||||
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
|
||||
<p className="truncate flex-1 min-w-0">
|
||||
📍 {team.city}
|
||||
<div className="text-sm font-sans text-gray-600 dark:text-gray-400 flex gap-2">
|
||||
<p className="truncate flex-1 min-w-0 flex items-center gap-x-1">
|
||||
<Icon icon="mdi:map-marker" />{' '}
|
||||
<span>{team.city}</span>
|
||||
</p>
|
||||
<p className="whitespace-nowrap shrink-0">
|
||||
👥 {team.members?.length || 0} members
|
||||
<p className="whitespace-nowrap shrink-0 flex items-center gap-x-1">
|
||||
<Icon icon="mdi:account-group" />{' '}
|
||||
{team.members?.length || 0} members
|
||||
</p>
|
||||
</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}
|
||||
</p>
|
||||
<div className="space-y-3 mt-auto">
|
||||
@@ -219,26 +220,26 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
||||
{/* Join Request Modal */}
|
||||
{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="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">
|
||||
Request to Join Team
|
||||
</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
|
||||
</p>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<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
|
||||
</label>
|
||||
<textarea
|
||||
{...form.register('message')}
|
||||
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..."
|
||||
/>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -6,19 +6,19 @@ const TeamsLayout: FC = (): ReactElement => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
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)} />
|
||||
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* 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">
|
||||
<button
|
||||
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"
|
||||
className="w-6 h-6 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -70,15 +70,15 @@ export const CitySelect: FC<CitySelectProps> = ({
|
||||
}}
|
||||
onClick={handleInputClick}
|
||||
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 ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-neutral-600'
|
||||
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-gray-600'
|
||||
}`}
|
||||
/>
|
||||
{value && !isOpen && (
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
@@ -88,7 +88,7 @@ export const CitySelect: FC<CitySelectProps> = ({
|
||||
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 w-full mt-1 bg-white 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 ? (
|
||||
<ul className="py-1">
|
||||
{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 ${
|
||||
value === city
|
||||
? '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}
|
||||
@@ -106,7 +106,7 @@ export const CitySelect: FC<CitySelectProps> = ({
|
||||
))}
|
||||
</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
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -62,6 +62,12 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
||||
icon: <Icon icon="heroicons:home" className="w-5 h-5" />,
|
||||
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',
|
||||
path: '/teams/browse',
|
||||
@@ -161,19 +167,19 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<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">
|
||||
Hackathon
|
||||
</h1>
|
||||
{onClose && (
|
||||
<button
|
||||
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
|
||||
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"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -190,7 +196,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{user?.avatar ? (
|
||||
<img
|
||||
@@ -204,7 +210,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
||||
icon="ic:baseline-person"
|
||||
width="24"
|
||||
height="24"
|
||||
className="text-gray-400 dark:text-neutral-400"
|
||||
className="text-gray-400 dark:text-gray-400"
|
||||
/>
|
||||
</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">
|
||||
{user?.fullname || user?.email?.split('@')[0] || 'User'}
|
||||
</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}
|
||||
</p>
|
||||
</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 ${
|
||||
isActive
|
||||
? '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}
|
||||
@@ -246,10 +252,10 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
||||
</nav>
|
||||
|
||||
{/* 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
|
||||
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()}
|
||||
<span>{getThemeLabel()}</span>
|
||||
|
||||
@@ -7,22 +7,52 @@ interface ThemeToggleProps {
|
||||
|
||||
// Sun icon for light mode
|
||||
const SunIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Moon icon for dark mode
|
||||
const MoonIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// System icon (monitor)
|
||||
const SystemIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<svg
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -51,7 +81,7 @@ export const ThemeToggle: FC<ThemeToggleProps> = ({ className = '' }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={cycleTheme}
|
||||
className={`p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors cursor-pointer text-gray-600 dark:text-neutral-400 ${className}`}
|
||||
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()}`}
|
||||
aria-label={`Current theme: ${getThemeLabel()}. Click to change.`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user