feat(hackathon): Update UI landing page, dashboard, onboarding (#62)
* 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 --------- 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
b3bdf25b5f
commit
19bd44a7ca
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -9,18 +9,30 @@ const DashboardLayout: FC = (): ReactElement => {
|
|||||||
<div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
|
<div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||||
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-auto">
|
<div className="flex-1 flex flex-col">
|
||||||
{/* Mobile Header with Hamburger */}
|
{/* Mobile Header with Hamburger */}
|
||||||
<div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
|
<div className="lg:hidden sticky top-0 bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen(true)}
|
onClick={() => setSidebarOpen(true)}
|
||||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
className="w-6 h-6 text-gray-600 dark:text-neutral-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
|
<h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">
|
||||||
|
Hackathon
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const DashboardPage: FC = (): ReactElement => {
|
const DashboardPage: FC = (): ReactElement => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
@@ -40,31 +41,52 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8 max-w-7xl mx-auto">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
|
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
|
||||||
</h1>
|
</h1>
|
||||||
{user?.location && (
|
{user?.location && (
|
||||||
<p className="text-gray-600 dark:text-neutral-400 mt-1">Location: {user.location}</p>
|
<p className="text-gray-600 dark:text-neutral-400 mt-1 font-sans flex items-center space-x-1">
|
||||||
|
<Icon icon="heroicons:map-pin-16-solid" width="24" height="24" />
|
||||||
|
<span>{user.location}</span>
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{invitations.length > 0 && (
|
{invitations.length > 0 && (
|
||||||
<div className="mb-8 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6">
|
<div className="mb-8 bg-primary-50 dark:bg-blue-900/20 border border-primary-200 dark:border-blue-800 rounded-lg p-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
Team Invitations ({invitations.length})
|
Team Invitations ({invitations.length})
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 font-sans">
|
||||||
{invitations.map((invitation) => (
|
{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">
|
<div
|
||||||
|
key={invitation.id}
|
||||||
|
className="bg-white dark:bg-neutral-800 p-4 rounded-lg shadow-sm flex items-center justify-between"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 dark:text-white">{invitation.team.name}</p>
|
<p className="font-medium text-gray-900 dark:text-white">
|
||||||
<p className="text-sm text-gray-600 dark:text-neutral-400">Invited by {invitation.inviter.fullname}</p>
|
{invitation.team.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
Invited by {invitation.inviter.fullname}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button size="sm" onClick={() => handleAcceptInvitation(invitation.id)}>Accept</Button>
|
<Button
|
||||||
<Button size="sm" variant="secondary" onClick={() => handleRejectInvitation(invitation.id)}>Decline</Button>
|
size="sm"
|
||||||
|
onClick={() => handleAcceptInvitation(invitation.id)}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleRejectInvitation(invitation.id)}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -74,20 +96,44 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{myTeams.length > 0 ? (
|
{myTeams.length > 0 ? (
|
||||||
<div className="mb-6 md:mb-8">
|
<div className="mb-6 md:mb-8">
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">My Team</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
My Team
|
||||||
|
</h2>
|
||||||
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{myTeams.map((team) => (
|
{myTeams.map((team) => (
|
||||||
<Link key={team.id} to={'/teams/' + team.id} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
<Link
|
||||||
{team.banner && <img src={team.banner} alt={team.name} className="w-full h-32 object-cover" />}
|
key={team.id}
|
||||||
|
to={'/teams/' + team.id}
|
||||||
|
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
{team.banner && (
|
||||||
|
<img
|
||||||
|
src={team.banner}
|
||||||
|
alt={team.name}
|
||||||
|
className="w-full h-32 object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="p-4 md:p-6">
|
<div className="p-4 md:p-6">
|
||||||
<div className="flex items-center space-x-3 mb-2 md:mb-3">
|
<div className="flex items-center space-x-3 mb-2 md:mb-3">
|
||||||
{team.logo && <img src={team.logo} alt={team.name} className="w-10 h-10 md:w-12 md:h-12 rounded-full object-cover" />}
|
{team.logo && (
|
||||||
|
<img
|
||||||
|
src={team.logo}
|
||||||
|
alt={team.name}
|
||||||
|
className="w-10 h-10 md:w-12 md:h-12 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white">{team.name}</h3>
|
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-xs md:text-sm text-gray-600 dark:text-neutral-400">City: {team.city}</p>
|
{team.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs md:text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
City: {team.city}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-neutral-400 text-xs md:text-sm line-clamp-2">{team.description}</p>
|
<p className="text-gray-600 dark:text-neutral-400 text-xs md:text-sm line-clamp-2">
|
||||||
|
{team.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -97,56 +143,103 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
<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-neutral-800 rounded-lg shadow-md p-6 md:p-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
|
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-1 md:mb-2">You are not in a team yet</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-1 md:mb-2">
|
||||||
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400">Use the sidebar to browse teams or create your own</p>
|
You are not in a team yet
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 font-sans">
|
||||||
|
Use the sidebar to browse teams or create your own
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-8 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-neutral-800 dark:to-neutral-900 rounded-xl shadow-lg">
|
<div className="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-gradient-to-r from-blue-600 to-indigo-600 h-24 rounded-t-xl"></div>
|
<div className="bg-linear-to-r from-primary-600 to-indigo-600 h-24 rounded-t-xl"></div>
|
||||||
<div className="px-4 md:px-8 pb-4 md:pb-8">
|
<div className="px-4 md:px-8 pb-4 md:pb-8">
|
||||||
<div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6">
|
<div className="flex flex-col md:flex-row md:items-start -mt-12 mb-4 md:mb-6">
|
||||||
<div className="flex flex-col md:flex-row items-start">
|
<div className="flex flex-col md:flex-row items-start">
|
||||||
{user?.avatar ? (
|
{user?.avatar ? (
|
||||||
<img src={user.avatar} alt={user.fullname || 'User'} className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg object-cover" />
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.fullname || 'User'}
|
||||||
|
className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg object-cover"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white dark:border-neutral-700 shadow-lg bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
||||||
<span className="text-gray-400 dark:text-neutral-500 text-3xl md:text-4xl">User</span>
|
<span className="text-gray-400 dark:text-neutral-500 text-3xl md:text-4xl font-sans">
|
||||||
|
U
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="md:ml-6 mt-4 md:mt-14">
|
<div className="md:ml-6 mt-4 md:mt-14">
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">{user?.fullname || user?.email?.split('@')[0] || 'Unnamed User'}</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{user?.fullname ||
|
||||||
|
user?.email?.split('@')[0] ||
|
||||||
|
'Unnamed User'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{user?.location ? (
|
{user?.location ? (
|
||||||
<p className="text-gray-600 dark:text-neutral-400 flex items-center mt-1 text-sm md:text-base">{user.location}</p>
|
<p className="text-gray-600 dark:text-neutral-400 flex items-center mt-1 text-sm md:text-base font-sans">
|
||||||
|
{user.location}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Link to="/onboarding/user" className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-xs md:text-sm mt-1 inline-block">Complete your profile</Link>
|
<Link
|
||||||
|
to="/onboarding/user"
|
||||||
|
className="text-primary-500 dark:text-blue-400 hover:text-primary-600 dark:hover:text-blue-300 text-sm md:text-sm mt-1 flex items-center"
|
||||||
|
>
|
||||||
|
<span className="font-sans">Complete your profile</span>
|
||||||
|
<Icon
|
||||||
|
icon="ic:baseline-chevron-right"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.location && (
|
{user?.location && (
|
||||||
<Link to="/profile" className="mt-3 md:mt-14 md:ml-auto inline-flex items-center justify-center px-3 py-1.5 md:px-4 md:py-2 bg-blue-600 text-white rounded-lg text-xs md:text-sm font-medium shadow-sm hover:bg-blue-700 transition-colors w-full md:w-auto">Edit Profile</Link>
|
<Link
|
||||||
|
to="/profile"
|
||||||
|
className="mt-3 md:mt-14 md:ml-auto inline-flex items-center justify-center px-3 py-1.5 md:px-4 md:py-2 bg-primary-500 text-white rounded-lg text md:text-sm font-medium shadow-sm hover:bg-primary-600 transition-colors w-full md:w-auto"
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4 md:space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
{user?.bio && (
|
{user?.bio && (
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-2">About</h3>
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-2">
|
||||||
<p className="text-gray-800 dark:text-neutral-200 leading-relaxed">{user.bio}</p>
|
About
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-800 dark:text-neutral-200 leading-relaxed">
|
||||||
|
{user.bio}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-3">Contact</h3>
|
<h3 className="font-semibold text-gray-700 dark:text-neutral-300 tracking-wide mb-3">
|
||||||
|
Contact
|
||||||
|
</h3>
|
||||||
<div className="flex items-center text-gray-700 dark:text-neutral-300">
|
<div className="flex items-center text-gray-700 dark:text-neutral-300">
|
||||||
<span className="text-gray-900 dark:text-white">{user?.email}</span>
|
<span className="text-gray-900 dark:text-white">
|
||||||
|
{user?.email}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.skills && user.skills.length > 0 && (
|
{user?.skills && user.skills.length > 0 && (
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-neutral-300 uppercase tracking-wide mb-3">Skills</h3>
|
<h3 className="font-semibold text-gray-700 dark:text-neutral-300 tracking-wide mb-3">
|
||||||
<div className="flex flex-wrap gap-3">
|
Skills
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
{user.skills.map((skill: string) => (
|
{user.skills.map((skill: string) => (
|
||||||
<span key={skill} className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium shadow-sm hover:bg-blue-700 transition-colors">{skill}</span>
|
<span
|
||||||
|
key={skill}
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-primary-600 text-primary-600 rounded-4xl text-xs font-medium "
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { CitySelect } from '../../../components/city-select';
|
import { CitySelect } from '../../../components/city-select';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const ROLE_OPTIONS = [
|
const ROLE_OPTIONS = [
|
||||||
'Frontend Developer',
|
'Frontend Developer',
|
||||||
@@ -95,7 +96,9 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
globalThis.location.href = '/dashboard';
|
globalThis.location.href = '/dashboard';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : 'Onboarding failed. Please try again.'
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Onboarding failed. Please try again.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -107,7 +110,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
Complete Your Profile
|
Complete Your Profile
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-neutral-400">
|
<p className="text-gray-600 font-sans dark:text-neutral-400">
|
||||||
Tell us more about yourself to get started
|
Tell us more about yourself to get started
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,13 +127,18 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<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-neutral-700 flex items-center justify-center">
|
||||||
<span className="text-gray-400 dark:text-neutral-500 text-4xl">👤</span>
|
<Icon
|
||||||
|
icon="ic:baseline-person"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
className="text-gray-400 dark:text-neutral-500"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-col items-center">
|
||||||
<label htmlFor="avatar" className="cursor-pointer">
|
<label htmlFor="avatar" className="cursor-pointer">
|
||||||
<span className="px-4 py-2 bg-blue-600 dark:bg-primary-600 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-primary-700 inline-block">
|
<span className="px-4 py-2 bg-primary-500 dark:bg-primary-600 text-white rounded-lg hover:bg-primary-600 dark:hover:bg-primary-700 inline-block">
|
||||||
{avatarPreview ? 'Change Photo' : 'Upload Photo'}
|
{avatarPreview ? 'Change Photo' : 'Upload Photo'}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -153,11 +161,14 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
label="Full Name"
|
label="Full Name"
|
||||||
placeholder="Enter your full name"
|
placeholder="Enter your full name"
|
||||||
name="fullname"
|
name="fullname"
|
||||||
|
className="px-3 py-2 text-label2 rounded-lg max-h-auto text-base"
|
||||||
|
size="md"
|
||||||
|
isRequired={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* City */}
|
{/* City */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
<label className="block text-base font-medium text-gray-700 dark:text-neutral-300">
|
||||||
City <span className="text-red-500">*</span>
|
City <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -176,7 +187,7 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Role/Skills */}
|
{/* Role/Skills */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
<label className="block text-base font-medium text-gray-700 dark:text-neutral-300">
|
||||||
Role / Skills
|
Role / Skills
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -201,7 +212,9 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
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-neutral-600 text-blue-600 dark:text-primary-500 focus:ring-blue-500 dark:focus:ring-primary-500 dark:bg-neutral-800"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm dark:text-neutral-300">{role}</span>
|
<span className="text-sm dark:text-neutral-300">
|
||||||
|
{role}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -212,19 +225,23 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
<label className="block text-base font-medium text-gray-700 dark:text-neutral-300">
|
||||||
Bio <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
|
Bio{' '}
|
||||||
|
<span className="text-gray-400 dark:text-neutral-500">
|
||||||
|
(Optional)
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="bio"
|
name="bio"
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<div>
|
<div className="flex">
|
||||||
<Textarea
|
<Textarea
|
||||||
{...field}
|
{...field}
|
||||||
placeholder="Tell us about yourself..."
|
placeholder="Tell us about yourself..."
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full"
|
className="w-full rounded-lg text-sm"
|
||||||
|
style={{ resize: 'vertical' }}
|
||||||
/>
|
/>
|
||||||
{fieldState.error && (
|
{fieldState.error && (
|
||||||
<p className="text-sm text-red-500 mt-1">
|
<p className="text-sm text-red-500 mt-1">
|
||||||
|
|||||||
@@ -66,13 +66,15 @@ export default function HomePage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white dark:bg-neutral-950">
|
<main className="min-h-screen bg-white dark:bg-neutral-950">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<div id="#top" className="hidden"></div>
|
<div id="#top" className="hidden"></div>
|
||||||
<nav className="border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 sticky top-0 z-50">
|
<nav className="border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 sticky top-0 z-50">
|
||||||
<div className="flex items-center justify-between max-w-7xl mx-auto px-4 md:px-8 py-4">
|
<div className="flex items-center justify-between max-w-7xl mx-auto px-4 md:px-8 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-lg md:text-xl font-bold dark:text-white">IMPHNEN</span>
|
<span className="text-lg md:text-xl font-bold dark:text-white">
|
||||||
|
IMPHNEN
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="#top"
|
||||||
className="text-lg md:text-xl font-bold text-primary-500 hover:cursor-pointer"
|
className="text-lg md:text-xl font-bold text-primary-500 hover:cursor-pointer"
|
||||||
@@ -156,7 +158,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* Mobile Menu */}
|
{/* Mobile Menu */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="md:hidden border-b border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900">
|
<nav className="md:hidden sticky top-18 border-b border-gray-200 dark:border-neutral-800 bg-white z-40 dark:bg-neutral-900">
|
||||||
<div className="flex flex-col items-start gap-4 px-4 py-4">
|
<div className="flex flex-col items-start gap-4 px-4 py-4">
|
||||||
<a
|
<a
|
||||||
href="#timeline"
|
href="#timeline"
|
||||||
@@ -170,7 +172,10 @@ export default function HomePage() {
|
|||||||
>
|
>
|
||||||
Hadiah
|
Hadiah
|
||||||
</a>
|
</a>
|
||||||
<a href="#faq" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white">
|
<a
|
||||||
|
href="#faq"
|
||||||
|
className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
|
||||||
|
>
|
||||||
FAQ
|
FAQ
|
||||||
</a>
|
</a>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
@@ -182,7 +187,10 @@ export default function HomePage() {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<a href="#masuk" className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white">
|
<a
|
||||||
|
href="#masuk"
|
||||||
|
className="ms-3 text-gray-600 dark:text-neutral-400 hover:text-gray-900 dark:hover:text-white"
|
||||||
|
>
|
||||||
Masuk
|
Masuk
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
@@ -194,7 +202,7 @@ export default function HomePage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
@@ -298,7 +306,9 @@ export default function HomePage() {
|
|||||||
Tentang <span className="text-primary-500">Hackathon</span>
|
Tentang <span className="text-primary-500">Hackathon</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400 mb-6">
|
<p className="font-sans text-lg md:text-xl text-left md:text-center text-gray-600 dark:text-neutral-400 mb-6">
|
||||||
<span className="font-semibold text-gray-900 dark:text-white">Hackathon</span>{' '}
|
<span className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Hackathon
|
||||||
|
</span>{' '}
|
||||||
adalah kompetisi pengembangan teknologi yang mengajak talenta muda
|
adalah kompetisi pengembangan teknologi yang mengajak talenta muda
|
||||||
untuk berkolaborasi dalam menciptakan solusi inovatif.
|
untuk berkolaborasi dalam menciptakan solusi inovatif.
|
||||||
</p>
|
</p>
|
||||||
@@ -313,7 +323,10 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Prizes Section */}
|
{/* Prizes Section */}
|
||||||
<section id="hadiah" className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900">
|
<section
|
||||||
|
id="hadiah"
|
||||||
|
className="py-16 md:py-24 px-4 md:px-8 bg-gray-50 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
<div className="max-w-lg md:max-w-6xl mx-auto">
|
<div className="max-w-lg md:max-w-6xl mx-auto">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 font-bai-jamjuree dark:text-white">
|
<h2 className="text-3xl md:text-5xl font-bold mb-4 font-bai-jamjuree dark:text-white">
|
||||||
@@ -335,7 +348,9 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-center mb-2 dark:text-white">Juara 1</h3>
|
<h3 className="text-xl font-bold text-center mb-2 dark:text-white">
|
||||||
|
Juara 1
|
||||||
|
</h3>
|
||||||
<p className="text-2xl font-bold text-primary-500 text-center font-sans">
|
<p className="text-2xl font-bold text-primary-500 text-center font-sans">
|
||||||
Rp6.000.000
|
Rp6.000.000
|
||||||
</p>
|
</p>
|
||||||
@@ -351,7 +366,9 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-center mb-2 dark:text-white">Juara 2</h3>
|
<h3 className="text-xl font-bold text-center mb-2 dark:text-white">
|
||||||
|
Juara 2
|
||||||
|
</h3>
|
||||||
<p className="text-2xl font-bold text-primary-500 text-center font-sans">
|
<p className="text-2xl font-bold text-primary-500 text-center font-sans">
|
||||||
Rp4.000.000
|
Rp4.000.000
|
||||||
</p>
|
</p>
|
||||||
@@ -367,7 +384,9 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold text-center mb-2 dark:text-white">Juara 3</h3>
|
<h3 className="text-xl font-bold text-center mb-2 dark:text-white">
|
||||||
|
Juara 3
|
||||||
|
</h3>
|
||||||
<p className="text-2xl font-bold text-orange-600 dark:text-orange-500 text-center font-sans">
|
<p className="text-2xl font-bold text-orange-600 dark:text-orange-500 text-center font-sans">
|
||||||
Rp2.500.000
|
Rp2.500.000
|
||||||
</p>
|
</p>
|
||||||
@@ -395,7 +414,10 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Timeline Section */}
|
{/* Timeline Section */}
|
||||||
<section id="timeline" className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950">
|
<section
|
||||||
|
id="timeline"
|
||||||
|
className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950"
|
||||||
|
>
|
||||||
<div className="max-w-4xl mx-auto font-sans">
|
<div className="max-w-4xl mx-auto font-sans">
|
||||||
<div className="text-center mb-12 font-bai-jamjuree">
|
<div className="text-center mb-12 font-bai-jamjuree">
|
||||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
|
<h2 className="text-3xl md:text-5xl font-bold mb-4 dark:text-white">
|
||||||
@@ -417,7 +439,9 @@ export default function HomePage() {
|
|||||||
<p className="text-primary-500 font-semibold mb-2">
|
<p className="text-primary-500 font-semibold mb-2">
|
||||||
30 November 2025
|
30 November 2025
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-xl font-bold mb-2 dark:text-white">Penutupan Registrasi</h3>
|
<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-neutral-400">
|
||||||
Batas akhir pendaftaran peserta hackathon.
|
Batas akhir pendaftaran peserta hackathon.
|
||||||
</p>
|
</p>
|
||||||
@@ -434,7 +458,9 @@ export default function HomePage() {
|
|||||||
<p className="text-primary-500 font-semibold mb-2">
|
<p className="text-primary-500 font-semibold mb-2">
|
||||||
30 November 2025
|
30 November 2025
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-xl font-bold mb-2 dark:text-white">Technical Meeting</h3>
|
<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-neutral-400">
|
||||||
Akan diadakan technical meeting terkait lomba melalui Google
|
Akan diadakan technical meeting terkait lomba melalui Google
|
||||||
Meet. Stay tune di grup WA Hackathon.
|
Meet. Stay tune di grup WA Hackathon.
|
||||||
@@ -452,7 +478,9 @@ export default function HomePage() {
|
|||||||
<p className="text-primary-500 font-semibold mb-2">
|
<p className="text-primary-500 font-semibold mb-2">
|
||||||
1 - 7 Desember 2025
|
1 - 7 Desember 2025
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-xl font-bold mb-2 dark:text-white">Tahap Penyisihan</h3>
|
<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-neutral-400">
|
||||||
Peserta mengerjakan tantangan yang diberikan.
|
Peserta mengerjakan tantangan yang diberikan.
|
||||||
</p>
|
</p>
|
||||||
@@ -469,7 +497,9 @@ export default function HomePage() {
|
|||||||
<p className="text-primary-500 font-semibold mb-2">
|
<p className="text-primary-500 font-semibold mb-2">
|
||||||
8 - 14 Desember 2025
|
8 - 14 Desember 2025
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-xl font-bold mb-2 dark:text-white">Penilaian & Webinar</h3>
|
<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-neutral-400">
|
||||||
Proses penilaian oleh juri dan sesi webinar.
|
Proses penilaian oleh juri dan sesi webinar.
|
||||||
</p>
|
</p>
|
||||||
@@ -485,7 +515,9 @@ export default function HomePage() {
|
|||||||
<p className="text-primary-500 font-semibold mb-2">
|
<p className="text-primary-500 font-semibold mb-2">
|
||||||
15 Desember 2025
|
15 Desember 2025
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-xl font-bold mb-2 dark:text-white">Pengumuman & Final</h3>
|
<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-neutral-400">
|
||||||
Presentasi final dan pengumuman pemenang.
|
Presentasi final dan pengumuman pemenang.
|
||||||
</p>
|
</p>
|
||||||
@@ -515,13 +547,17 @@ export default function HomePage() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-primary-500 font-semibold mb-1">CTO</p>
|
<p className="text-primary-500 font-semibold mb-1">CTO</p>
|
||||||
<p className="text-gray-600 dark:text-neutral-400">Kolosal.ai</p>
|
<p className="text-gray-600 dark:text-neutral-400">
|
||||||
|
Kolosal.ai
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Judge 2 */}
|
{/* Judge 2 */}
|
||||||
<div className="flex flex-col justify-between bg-white 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-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
|
||||||
<h3 className="text-p3 font-bold mb-1 dark:text-white">Anka Tama</h3>
|
<h3 className="text-p3 font-bold mb-1 dark:text-white">
|
||||||
|
Anka Tama
|
||||||
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-primary-500 font-semibold mb-1">Admin</p>
|
<p className="text-primary-500 font-semibold mb-1">Admin</p>
|
||||||
<p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
|
<p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
|
||||||
@@ -530,7 +566,9 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* Judge 3 */}
|
{/* Judge 3 */}
|
||||||
<div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
|
<div className="flex flex-col justify-between bg-white dark:bg-neutral-800 rounded-xl px-4 py-8 shadow-lg text-center">
|
||||||
<h3 className="text-p3 font-bold mb-1 dark:text-white">Hafid Nur</h3>
|
<h3 className="text-p3 font-bold mb-1 dark:text-white">
|
||||||
|
Hafid Nur
|
||||||
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-primary-500 font-semibold mb-1">Moderator</p>
|
<p className="text-primary-500 font-semibold mb-1">Moderator</p>
|
||||||
<p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
|
<p className="text-gray-600 dark:text-neutral-400">IMPHNEN</p>
|
||||||
@@ -541,7 +579,10 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<section id="faq" className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950">
|
<section
|
||||||
|
id="faq"
|
||||||
|
className="py-16 md:py-24 px-4 md:px-8 bg-white dark:bg-neutral-950"
|
||||||
|
>
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500">
|
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-primary-500">
|
||||||
@@ -554,7 +595,10 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<div className="space-y-4 font-sans">
|
<div className="space-y-4 font-sans">
|
||||||
{faqs.map((faq, index) => (
|
{faqs.map((faq, index) => (
|
||||||
<div key={index} className="border border-gray-200 dark:border-neutral-700 rounded-lg">
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 dark:border-neutral-700 rounded-lg"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setOpenFaq(openFaq === index ? null : index)}
|
onClick={() => setOpenFaq(openFaq === index ? null : index)}
|
||||||
className="w-full px-6 py-4 flex items-center justify-between text-left hover:bg-gray-50 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-neutral-800 transition-colors cursor-pointer"
|
||||||
@@ -579,7 +623,9 @@ export default function HomePage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{openFaq === index && (
|
{openFaq === index && (
|
||||||
<div className="px-6 pb-4 text-gray-600 dark:text-neutral-400">{faq.answer}</div>
|
<div className="px-6 pb-4 text-gray-600 dark:text-neutral-400">
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -820,6 +866,6 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { CitySelect } from '../../components/city-select';
|
import { CitySelect } from '../../components/city-select';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const ROLE_OPTIONS = [
|
const ROLE_OPTIONS = [
|
||||||
'Frontend Developer',
|
'Frontend Developer',
|
||||||
@@ -31,7 +32,8 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
const [avatarPreview, setAvatarPreview] = useState<string>('');
|
const [avatarPreview, setAvatarPreview] = useState<string>('');
|
||||||
|
|
||||||
const { mutateAsync: updateUser, isPending: isUpdating } = useUpdateUserMe();
|
const { mutateAsync: updateUser, isPending: isUpdating } = useUpdateUserMe();
|
||||||
const { mutateAsync: uploadAvatar, isPending: isUploading } = useUploadAvatar();
|
const { mutateAsync: uploadAvatar, isPending: isUploading } =
|
||||||
|
useUploadAvatar();
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
|
|
||||||
const form = useForm<TUserEditProfileForm>({
|
const form = useForm<TUserEditProfileForm>({
|
||||||
@@ -63,7 +65,15 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
if (session?.user?.skills) {
|
if (session?.user?.skills) {
|
||||||
form.setValue('skills', session.user.skills);
|
form.setValue('skills', session.user.skills);
|
||||||
}
|
}
|
||||||
}, [session?.user?.avatar, session?.user?.fullname, session?.user?.location, session?.user?.bio, session?.user?.skills, avatarPreview, form]);
|
}, [
|
||||||
|
session?.user?.avatar,
|
||||||
|
session?.user?.fullname,
|
||||||
|
session?.user?.location,
|
||||||
|
session?.user?.bio,
|
||||||
|
session?.user?.skills,
|
||||||
|
avatarPreview,
|
||||||
|
form,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -132,7 +142,9 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
<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-white dark:bg-neutral-900 w-full max-w-md p-8 rounded-xl shadow-xl dark:shadow-neutral-950/50">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Edit Profile</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Edit Profile
|
||||||
|
</h1>
|
||||||
<Link
|
<Link
|
||||||
to="/dashboard"
|
to="/dashboard"
|
||||||
className="text-gray-500 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200"
|
className="text-gray-500 dark:text-neutral-400 hover:text-gray-700 dark:hover:text-neutral-200"
|
||||||
@@ -152,7 +164,9 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-neutral-400">Update your photo and name</p>
|
<p className="text-gray-600 font-sans dark:text-neutral-400">
|
||||||
|
Update your photo and name
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-6">
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
@@ -167,13 +181,18 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<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-neutral-700 flex items-center justify-center group-hover:bg-gray-300 dark:group-hover:bg-neutral-600 transition-colors">
|
||||||
<span className="text-gray-400 dark:text-neutral-500 text-4xl">👤</span>
|
<Icon
|
||||||
|
icon="ic:baseline-person"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
className="text-gray-400 dark:text-neutral-500"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Camera overlay */}
|
{/* Camera overlay */}
|
||||||
<label
|
<label
|
||||||
htmlFor="avatar"
|
htmlFor="avatar"
|
||||||
className="absolute bottom-0 right-0 bg-blue-600 text-white p-2 rounded-full cursor-pointer hover:bg-blue-700 transition-colors shadow-lg"
|
className="absolute bottom-0 right-0 bg-primary-500 text-white p-2 rounded-full cursor-pointer hover:bg-primary-600 transition-colors shadow-lg"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-5 h-5"
|
className="w-5 h-5"
|
||||||
@@ -204,7 +223,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 dark:text-neutral-400 text-center">
|
<p className="text-sm text-gray-500 dark:text-neutral-400 text-center font-sans">
|
||||||
Click the camera icon to change your photo
|
Click the camera icon to change your photo
|
||||||
<br />
|
<br />
|
||||||
Format: JPG, PNG. Max 5MB
|
Format: JPG, PNG. Max 5MB
|
||||||
@@ -217,11 +236,12 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
label="Full Name"
|
label="Full Name"
|
||||||
placeholder="Enter your full name"
|
placeholder="Enter your full name"
|
||||||
name="fullname"
|
name="fullname"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* City */}
|
{/* City */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
|
||||||
City
|
City
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -240,7 +260,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Role/Skills */}
|
{/* Role/Skills */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
|
||||||
Role / Skills
|
Role / Skills
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -252,7 +272,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
{ROLE_OPTIONS.map((role) => (
|
{ROLE_OPTIONS.map((role) => (
|
||||||
<label
|
<label
|
||||||
key={role}
|
key={role}
|
||||||
className="flex items-center space-x-2 cursor-pointer"
|
className="flex items-center space-x-2 cursor-pointer font-sans"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -265,7 +285,9 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
}}
|
}}
|
||||||
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-neutral-800"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm dark:text-neutral-300">{role}</span>
|
<span className="text-sm dark:text-neutral-300">
|
||||||
|
{role}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -276,8 +298,11 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
|
<label className="block text-label1 font-medium text-gray-700 dark:text-neutral-300">
|
||||||
Bio <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
|
Bio{' '}
|
||||||
|
<span className="text-gray-400 dark:text-neutral-500">
|
||||||
|
(Optional)
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -289,6 +314,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
placeholder="Tell us about yourself..."
|
placeholder="Tell us about yourself..."
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
size="lg"
|
||||||
/>
|
/>
|
||||||
{fieldState.error && (
|
{fieldState.error && (
|
||||||
<p className="text-sm text-red-500 mt-1">
|
<p className="text-sm text-red-500 mt-1">
|
||||||
|
|||||||
@@ -1,11 +1,50 @@
|
|||||||
import { FC, ReactElement, useState } from 'react';
|
import { FC, ReactElement, useState, useEffect } from 'react';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { Link, useParams, useNavigate } from 'react-router';
|
import { Link, useParams, useNavigate } from 'react-router';
|
||||||
import { useTeamById, useTeamMembers, useInviteMember, useTeamJoinRequests, useRespondToJoinRequest, ETeamMemberRole, useAuthStore } from '@imphnen-frontend-service/service';
|
import {
|
||||||
|
useTeamById,
|
||||||
|
useTeamMembers,
|
||||||
|
useInviteMember,
|
||||||
|
useTeamJoinRequests,
|
||||||
|
useRespondToJoinRequest,
|
||||||
|
ETeamMemberRole,
|
||||||
|
useAuthStore,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const MAX_TEAM_MEMBERS = 5;
|
const MAX_TEAM_MEMBERS = 5;
|
||||||
|
|
||||||
|
// Image component with loading state
|
||||||
|
const ImageWithLoader: FC<{
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
onLoad?: () => void;
|
||||||
|
}> = ({ src, alt, className, onLoad }) => {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{!isLoaded && (
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-gray-200 animate-pulse ${className}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={`${className} ${
|
||||||
|
isLoaded ? 'opacity-100' : 'opacity-0'
|
||||||
|
} transition-opacity duration-300`}
|
||||||
|
onLoad={() => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
onLoad?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const TeamDashboardPage: FC = (): ReactElement => {
|
const TeamDashboardPage: FC = (): ReactElement => {
|
||||||
const { teamId } = useParams<{ teamId: string }>();
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -13,21 +52,37 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
const [showInviteModal, setShowInviteModal] = useState(false);
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
||||||
const [showJoinRequestsModal, setShowJoinRequestsModal] = useState(false);
|
const [showJoinRequestsModal, setShowJoinRequestsModal] = useState(false);
|
||||||
const [inviteEmail, setInviteEmail] = useState('');
|
const [inviteEmail, setInviteEmail] = useState('');
|
||||||
|
const [imagesLoaded, setImagesLoaded] = useState(false);
|
||||||
|
const [imageLoadCount, setImageLoadCount] = useState(0);
|
||||||
|
|
||||||
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
|
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
|
||||||
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || '');
|
teamId || ''
|
||||||
const { data: joinRequestsData } = useTeamJoinRequests(teamId || '', !!teamId);
|
);
|
||||||
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(teamId || '');
|
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(
|
||||||
const { mutateAsync: respondToJoinRequest, isPending: isResponding } = useRespondToJoinRequest(teamId || '');
|
teamId || ''
|
||||||
|
);
|
||||||
|
const { data: joinRequestsData } = useTeamJoinRequests(
|
||||||
|
teamId || '',
|
||||||
|
!!teamId
|
||||||
|
);
|
||||||
|
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(
|
||||||
|
teamId || ''
|
||||||
|
);
|
||||||
|
const { mutateAsync: respondToJoinRequest, isPending: isResponding } =
|
||||||
|
useRespondToJoinRequest(teamId || '');
|
||||||
|
|
||||||
const team = teamData?.data;
|
const team = teamData?.data;
|
||||||
const members = membersData?.data || [];
|
const members = membersData?.data || [];
|
||||||
const joinRequests = joinRequestsData?.data || [];
|
const joinRequests = joinRequestsData?.data || [];
|
||||||
const pendingJoinRequests = joinRequests.filter((req: any) => req.status === 'pending');
|
const pendingJoinRequests = joinRequests.filter(
|
||||||
|
(req: any) => req.status === 'pending'
|
||||||
|
);
|
||||||
const currentUserId = session?.user?.id;
|
const currentUserId = session?.user?.id;
|
||||||
|
|
||||||
const isLeader = currentUserId === team?.leader_id;
|
const isLeader = currentUserId === team?.leader_id;
|
||||||
const isMember = members.some((member: any) => member.user_id === currentUserId);
|
const isMember = members.some(
|
||||||
|
(member: any) => member.user_id === currentUserId
|
||||||
|
);
|
||||||
const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS;
|
const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS;
|
||||||
|
|
||||||
const handleInviteMember = async (e: React.FormEvent) => {
|
const handleInviteMember = async (e: React.FormEvent) => {
|
||||||
@@ -45,10 +100,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRespondToJoinRequest = async (requestId: string, action: 'approve' | 'reject') => {
|
const handleRespondToJoinRequest = async (
|
||||||
|
requestId: string,
|
||||||
|
action: 'approve' | 'reject'
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await respondToJoinRequest({ requestId, action });
|
await respondToJoinRequest({ requestId, action });
|
||||||
toast.success(action === 'approve' ? 'Request approved!' : 'Request rejected');
|
toast.success(
|
||||||
|
action === 'approve' ? 'Request approved!' : 'Request rejected'
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to respond to join request:', error);
|
console.error('Failed to respond to join request:', error);
|
||||||
toast.error('Failed to process request');
|
toast.error('Failed to process request');
|
||||||
@@ -57,8 +117,67 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
if (isLoadingTeam || isLoadingMembers) {
|
if (isLoadingTeam || isLoadingMembers) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="text-gray-600 dark:text-neutral-400">Loading team...</div>
|
{/* Skeleton Header */}
|
||||||
|
<div className="bg-white border-b">
|
||||||
|
<div className="w-full h-48 bg-gray-200 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<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="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>
|
||||||
|
</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="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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading Overlay */}
|
||||||
|
<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">
|
||||||
|
Loading team data...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -66,22 +185,48 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
if (!team) {
|
if (!team) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Team not found</h2>
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
<Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button>
|
Team not found
|
||||||
|
</h2>
|
||||||
|
<Button onClick={() => navigate('/dashboard')}>
|
||||||
|
Back to Dashboard
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
|
<div className="min-h-screen bg-gray-50 relative dark:bg-neutral-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">
|
||||||
|
<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 font-medium">Loading images...</p>
|
||||||
|
<p className="text-sm text-gray-400 mt-1">
|
||||||
|
{imageLoadCount} / {totalImagesToLoad}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Header with Banner */}
|
{/* Header with Banner */}
|
||||||
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
|
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
|
||||||
{team.banner && (
|
{team.banner && (
|
||||||
<div className="w-full h-32 md:h-48 overflow-hidden">
|
<div className="w-full h-32 md:h-48 overflow-hidden relative">
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-gray-200 animate-pulse ${
|
||||||
|
imagesLoaded ? 'hidden' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
src={team.banner}
|
src={team.banner}
|
||||||
alt={team.name}
|
alt={team.name}
|
||||||
className="w-full h-full object-cover"
|
className={`w-full h-full object-cover transition-opacity duration-300 ${
|
||||||
|
imagesLoaded ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
onError={handleImageError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -89,18 +234,32 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-center space-x-3 md:space-x-4">
|
<div className="flex items-center space-x-3 md:space-x-4">
|
||||||
{team.logo && (
|
{team.logo && (
|
||||||
<img
|
<div className="relative">
|
||||||
src={team.logo}
|
{!imagesLoaded && (
|
||||||
alt={team.name}
|
<div className="absolute inset-0 w-20 h-20 rounded-full bg-gray-300 animate-pulse -mt-10" />
|
||||||
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"
|
)}
|
||||||
/>
|
<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 ${
|
||||||
|
imagesLoaded ? 'opacity-100' : 'opacity-0'
|
||||||
|
}`}
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h1>
|
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white line-clamp-2">
|
||||||
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1 line-clamp-1">📍 {team.city}</p>
|
{team.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1 line-clamp-1">
|
||||||
|
📍 {team.city}
|
||||||
|
</p>
|
||||||
<div className="flex flex-wrap items-center gap-2 md:space-x-4 mt-2">
|
<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-neutral-400">
|
||||||
{members.length} {members.length === 1 ? 'Member' : 'Members'}
|
{members.length}{' '}
|
||||||
|
{members.length === 1 ? 'Member' : 'Members'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-500 dark:text-neutral-400">
|
<span className="text-sm text-gray-500 dark:text-neutral-400">
|
||||||
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
|
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
|
||||||
@@ -123,14 +282,20 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="lg:col-span-2 space-y-4 md:space-y-6">
|
<div className="lg:col-span-2 space-y-4 md:space-y-6">
|
||||||
{/* Team Description */}
|
{/* Team Description */}
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">About Team</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
<p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">{team.description}</p>
|
About Team
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">
|
||||||
|
{team.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team Actions - Only for Leader */}
|
{/* Team Actions - Only for Leader */}
|
||||||
{isLeader && (
|
{isLeader && (
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Team Management</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Team Management
|
||||||
|
</h2>
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -138,7 +303,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
onClick={() => setShowInviteModal(true)}
|
onClick={() => setShowInviteModal(true)}
|
||||||
disabled={!canInvite}
|
disabled={!canInvite}
|
||||||
>
|
>
|
||||||
➕ Invite Member {!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
|
➕ Invite Member{' '}
|
||||||
|
{!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-full relative"
|
className="w-full relative"
|
||||||
@@ -168,9 +334,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to={`/teams/${teamId}/submit`}>
|
<Link to={`/teams/${teamId}/submit`}>
|
||||||
<Button className="w-full">
|
<Button className="w-full">🚀 Submit Project</Button>
|
||||||
🚀 Submit Project
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{!canInvite && members.length >= MAX_TEAM_MEMBERS && (
|
{!canInvite && members.length >= MAX_TEAM_MEMBERS && (
|
||||||
@@ -184,7 +348,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
{/* Quick Actions for Members (non-leaders) */}
|
{/* Quick Actions for Members (non-leaders) */}
|
||||||
{!isLeader && isMember && (
|
{!isLeader && isMember && (
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Quick Actions</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Quick Actions
|
||||||
|
</h2>
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<Link to={`/teams/${teamId}/chat`}>
|
<Link to={`/teams/${teamId}/chat`}>
|
||||||
<Button className="w-full" variant="secondary">
|
<Button className="w-full" variant="secondary">
|
||||||
@@ -208,7 +374,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<span className="text-3xl">✅</span>
|
<span className="text-3xl">✅</span>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-green-900 dark:text-green-400">Project Submitted</h3>
|
<h3 className="font-bold text-green-900 dark:text-green-400">
|
||||||
|
Project Submitted
|
||||||
|
</h3>
|
||||||
<p className="text-green-700 dark:text-green-300 text-sm">
|
<p className="text-green-700 dark:text-green-300 text-sm">
|
||||||
Your team has successfully submitted a project
|
Your team has successfully submitted a project
|
||||||
</p>
|
</p>
|
||||||
@@ -227,7 +395,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="space-y-4 md:space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
{/* Team Leader */}
|
{/* Team Leader */}
|
||||||
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
|
||||||
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Team Leader</h3>
|
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Team Leader
|
||||||
|
</h3>
|
||||||
{team.leader && (
|
{team.leader && (
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/users/${team.leader.id}`)}
|
onClick={() => navigate(`/users/${team.leader.id}`)}
|
||||||
@@ -241,12 +411,18 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
||||||
<span className="text-gray-500 dark:text-neutral-400">👤</span>
|
<span className="text-gray-500 dark:text-neutral-400">
|
||||||
|
👤
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 dark:text-white">{team.leader.fullname}</p>
|
<p className="font-medium text-gray-900 dark:text-white">
|
||||||
<p className="text-sm text-gray-600 dark:text-neutral-400">{team.leader.email}</p>
|
{team.leader.fullname}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
{team.leader.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -258,29 +434,35 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
Members ({members.length})
|
Members ({members.length})
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{members.map((member) => (
|
{members.map((member: any) => (
|
||||||
<button
|
<button
|
||||||
key={member.id}
|
key={member.id}
|
||||||
onClick={() => navigate(`/users/${member.user.id}`)}
|
onClick={() => navigate(`/users/${member.user.id}`)}
|
||||||
className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
|
className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
|
||||||
>
|
>
|
||||||
{member.user.avatar ? (
|
{member.user?.avatar ? (
|
||||||
<img
|
<img
|
||||||
src={member.user.avatar}
|
src={member.user.avatar}
|
||||||
alt={member.user.fullname}
|
alt={member.user.fullname || 'Member'}
|
||||||
className="w-10 h-10 rounded-full object-cover"
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
onError={handleImageError}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
||||||
<span className="text-gray-500 dark:text-neutral-400 text-sm">👤</span>
|
<span className="text-gray-500 dark:text-neutral-400 text-sm">
|
||||||
|
👤
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium text-gray-900 dark:text-white truncate">
|
<p className="font-medium text-gray-900 dark:text-white truncate">
|
||||||
{member.user.fullname}
|
{member.user?.fullname || 'Unknown'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-neutral-400">
|
<p className="text-xs text-gray-500 dark:text-neutral-400">
|
||||||
{member.role === ETeamMemberRole.LEADER ? 'Leader' : 'Member'}
|
{member.role === ETeamMemberRole.LEADER
|
||||||
|
? 'Leader'
|
||||||
|
: 'Member'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -299,16 +481,21 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
Invite Team Member
|
Invite Team Member
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-neutral-400 mb-4">
|
<p className="text-gray-600 dark:text-neutral-400 mb-4">
|
||||||
Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in.
|
Send an invitation to join your team. The invited member will see
|
||||||
|
the invitation on their dashboard after logging in.
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
|
||||||
<p className="text-sm text-yellow-800 dark:text-yellow-300">
|
<p className="text-sm text-yellow-800 dark:text-yellow-300">
|
||||||
<strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in.
|
<strong>Important:</strong> The email you enter must match the
|
||||||
|
GitHub email address the member uses to sign in.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={handleInviteMember} className="space-y-4">
|
<form onSubmit={handleInviteMember} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="invite-email" className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
|
<label
|
||||||
|
htmlFor="invite-email"
|
||||||
|
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2"
|
||||||
|
>
|
||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -367,7 +554,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
{pendingJoinRequests.length === 0 ? (
|
{pendingJoinRequests.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-gray-600 dark:text-neutral-400 text-lg">No pending join requests</p>
|
<p className="text-gray-600 dark:text-neutral-400 text-lg">
|
||||||
|
No pending join requests
|
||||||
|
</p>
|
||||||
<p className="text-gray-500 dark:text-neutral-500 text-sm mt-2">
|
<p className="text-gray-500 dark:text-neutral-500 text-sm mt-2">
|
||||||
When users request to join your team, they'll appear here
|
When users request to join your team, they'll appear here
|
||||||
</p>
|
</p>
|
||||||
@@ -375,7 +564,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{pendingJoinRequests.map((request: any) => (
|
{pendingJoinRequests.map((request: any) => (
|
||||||
<div key={request.id} className="border border-gray-200 dark:border-neutral-700 rounded-lg p-4 hover:shadow-md transition-shadow">
|
<div
|
||||||
|
key={request.id}
|
||||||
|
className="border border-gray-200 dark:border-neutral-700 rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-start space-x-3 flex-1">
|
<div className="flex items-start space-x-3 flex-1">
|
||||||
{request.user?.avatar ? (
|
{request.user?.avatar ? (
|
||||||
@@ -386,7 +578,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center shrink-0">
|
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center shrink-0">
|
||||||
<span className="text-gray-500 dark:text-neutral-400 text-xl">👤</span>
|
<span className="text-gray-500 dark:text-neutral-400 text-xl">
|
||||||
|
👤
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -404,20 +598,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-gray-500 dark:text-neutral-500 mt-2">
|
<p className="text-xs text-gray-500 dark:text-neutral-500 mt-2">
|
||||||
Requested {new Date(request.created_at).toLocaleDateString()}
|
Requested{' '}
|
||||||
|
{new Date(request.created_at).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2 ml-4">
|
<div className="flex space-x-2 ml-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRespondToJoinRequest(request.id, 'approve')}
|
onClick={() =>
|
||||||
disabled={isResponding || members.length >= MAX_TEAM_MEMBERS}
|
handleRespondToJoinRequest(request.id, 'approve')
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
isResponding || members.length >= MAX_TEAM_MEMBERS
|
||||||
|
}
|
||||||
className="px-4 py-2 text-sm"
|
className="px-4 py-2 text-sm"
|
||||||
>
|
>
|
||||||
✓ Accept
|
✓ Accept
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRespondToJoinRequest(request.id, 'reject')}
|
onClick={() =>
|
||||||
|
handleRespondToJoinRequest(request.id, 'reject')
|
||||||
|
}
|
||||||
disabled={isResponding}
|
disabled={isResponding}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="px-4 py-2 text-sm"
|
className="px-4 py-2 text-sm"
|
||||||
@@ -429,7 +630,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
|
|||||||
{members.length >= MAX_TEAM_MEMBERS && (
|
{members.length >= MAX_TEAM_MEMBERS && (
|
||||||
<div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-2">
|
<div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-2">
|
||||||
<p className="text-xs text-yellow-800 dark:text-yellow-300">
|
<p className="text-xs text-yellow-800 dark:text-yellow-300">
|
||||||
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS} members). Remove a member before accepting new requests.
|
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS}{' '}
|
||||||
|
members). Remove a member before accepting new
|
||||||
|
requests.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { FC, ReactElement, useState, useEffect } from 'react';
|
import { FC, ReactElement, useState, useEffect } from 'react';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { Link, useNavigate } from 'react-router';
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { useTeams, useJoinTeam, useMyTeams, ETeamVisibility, joinTeamSchema, TJoinTeamForm } from '@imphnen-frontend-service/service';
|
import {
|
||||||
|
useTeams,
|
||||||
|
useJoinTeam,
|
||||||
|
useMyTeams,
|
||||||
|
ETeamVisibility,
|
||||||
|
joinTeamSchema,
|
||||||
|
TJoinTeamForm,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { CitySelect } from '../../../components/city-select';
|
import { CitySelect } from '../../../components/city-select';
|
||||||
@@ -69,8 +76,12 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
|||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Browse Teams</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-gray-600 dark:text-neutral-400 mt-1">Find and join teams looking for members</p>
|
Browse Teams
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-neutral-400 mt-1">
|
||||||
|
Find and join teams looking for members
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/dashboard" className="hidden md:block">
|
<Link to="/dashboard" className="hidden md:block">
|
||||||
<Button variant="secondary">Back to Dashboard</Button>
|
<Button variant="secondary">Back to Dashboard</Button>
|
||||||
@@ -111,19 +122,28 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
|||||||
{/* Teams List */}
|
{/* Teams List */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<p className="text-gray-600 dark:text-neutral-400">Loading teams...</p>
|
<p className="text-gray-600 dark:text-neutral-400">
|
||||||
|
Loading teams...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : teams.length === 0 ? (
|
) : teams.length === 0 ? (
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-sm p-12 text-center">
|
||||||
<p className="text-gray-600 dark:text-neutral-400 text-lg">No teams found</p>
|
<p className="text-gray-600 dark:text-neutral-400 text-lg">
|
||||||
<p className="text-gray-500 dark:text-neutral-500 mt-2">Try adjusting your filters</p>
|
No teams found
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 dark:text-neutral-500 mt-2">
|
||||||
|
Try adjusting your filters
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{teams.map((team) => (
|
{teams.map((team) => (
|
||||||
<div key={team.id} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col">
|
<div
|
||||||
|
key={team.id}
|
||||||
|
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow flex flex-col"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={team.banner || '/images/banner-imphnen.png'}
|
src={team.banner || '/images/banner-imphnen.webp'}
|
||||||
alt={team.name}
|
alt={team.name}
|
||||||
className="w-full h-32 object-cover"
|
className="w-full h-32 object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -137,14 +157,22 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
||||||
<span className="text-gray-500 dark:text-neutral-400 text-xl">👥</span>
|
<span className="text-gray-500 dark:text-neutral-400 text-xl">
|
||||||
|
👥
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h3>
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white line-clamp-2">
|
||||||
|
{team.name}
|
||||||
|
</h3>
|
||||||
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
|
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
|
||||||
<p className="truncate flex-1 min-w-0">📍 {team.city}</p>
|
<p className="truncate flex-1 min-w-0">
|
||||||
<p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p>
|
📍 {team.city}
|
||||||
|
</p>
|
||||||
|
<p className="whitespace-nowrap shrink-0">
|
||||||
|
👥 {team.members?.length || 0} members
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,14 +190,15 @@ const BrowseTeamsPage: FC = (): ReactElement => {
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{myTeams.length === 0 && (team.members?.length || 0) < 5 && (
|
{myTeams.length === 0 &&
|
||||||
<Button
|
(team.members?.length || 0) < 5 && (
|
||||||
className="w-full"
|
<Button
|
||||||
onClick={() => handleJoinRequest(team.id)}
|
className="w-full"
|
||||||
>
|
onClick={() => handleJoinRequest(team.id)}
|
||||||
Request to Join
|
>
|
||||||
</Button>
|
Request to Join
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -9,21 +9,35 @@ const TeamsLayout: FC = (): ReactElement => {
|
|||||||
<div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
|
<div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||||
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-auto">
|
<div className="flex-1 flex flex-col">
|
||||||
{/* Mobile Header with Hamburger */}
|
{/* Mobile Header with Hamburger */}
|
||||||
<div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
|
<div className="lg:hidden sticky top-0 bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 z-10">
|
||||||
<button
|
<div className="flex items-center">
|
||||||
onClick={() => setSidebarOpen(true)}
|
<button
|
||||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
onClick={() => setSidebarOpen(true)}
|
||||||
>
|
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
||||||
<svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
<svg
|
||||||
</svg>
|
className="w-6 h-6 text-gray-600 dark:text-neutral-400"
|
||||||
</button>
|
fill="none"
|
||||||
<h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
|
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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { useParams, useNavigate, Link } from 'react-router';
|
import { useParams, useNavigate, Link } from 'react-router';
|
||||||
import { useUserDetailsById, useTeamsByUserId } from '@imphnen-frontend-service/service';
|
import {
|
||||||
|
useUserDetailsById,
|
||||||
|
useTeamsByUserId,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
|
|
||||||
const UserProfilePage: FC = (): ReactElement => {
|
const UserProfilePage: FC = (): ReactElement => {
|
||||||
const { userId } = useParams<{ userId: string }>();
|
const { userId } = useParams<{ userId: string }>();
|
||||||
@@ -15,7 +18,9 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||||
<div className="text-gray-600 dark:text-neutral-400">Loading user profile...</div>
|
<div className="text-gray-600 dark:text-neutral-400">
|
||||||
|
Loading user profile...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -23,9 +28,15 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
if (error || !user) {
|
if (error || !user) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">User not found</h2>
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
{error && <p className="text-red-600 dark:text-red-400 mb-4">{String(error)}</p>}
|
User not found
|
||||||
<Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button>
|
</h2>
|
||||||
|
{error && (
|
||||||
|
<p className="text-red-600 dark:text-red-400 mb-4">{String(error)}</p>
|
||||||
|
)}
|
||||||
|
<Button onClick={() => navigate('/dashboard')}>
|
||||||
|
Back to Dashboard
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -45,15 +56,23 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center border-4 border-white dark:border-neutral-800 shadow-lg">
|
<div className="w-16 h-16 md:w-20 md:h-20 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center border-4 border-white dark:border-neutral-800 shadow-lg">
|
||||||
<span className="text-gray-500 dark:text-neutral-400 text-2xl md:text-3xl">👤</span>
|
<span className="text-gray-500 dark:text-neutral-400 text-2xl md:text-3xl">
|
||||||
|
👤
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white">{user.fullname}</h1>
|
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1">{user.email}</p>
|
{user.fullname}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1">
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
{user.location && (
|
{user.location && (
|
||||||
<div className="flex items-center space-x-4 mt-2">
|
<div className="flex items-center space-x-4 mt-2">
|
||||||
<span className="text-sm text-gray-500 dark:text-neutral-500">📍 {user.location}</span>
|
<span className="text-sm text-gray-500 dark:text-neutral-500">
|
||||||
|
📍 {user.location}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,15 +91,21 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
{/* About Section */}
|
{/* About Section */}
|
||||||
{user.bio && (
|
{user.bio && (
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6 overflow-hidden">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6 overflow-hidden">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">About</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
<p className="text-gray-700 dark:text-neutral-300 break-all overflow-wrap-anywhere">{user.bio}</p>
|
About
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-700 dark:text-neutral-300 break-all overflow-wrap-anywhere">
|
||||||
|
{user.bio}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skills Section */}
|
{/* Skills Section */}
|
||||||
{user.skills && user.skills.length > 0 && (
|
{user.skills && user.skills.length > 0 && (
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Skills</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Skills
|
||||||
|
</h2>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{user.skills.map((skill: string) => (
|
{user.skills.map((skill: string) => (
|
||||||
<span
|
<span
|
||||||
@@ -97,23 +122,47 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
{/* Teams Section */}
|
{/* Teams Section */}
|
||||||
{userTeams.length > 0 ? (
|
{userTeams.length > 0 ? (
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Teams ({userTeams.length})</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Teams ({userTeams.length})
|
||||||
|
</h2>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{userTeams.map((team: any) => (
|
{userTeams.map((team: any) => (
|
||||||
<Link key={team.id} to={'/teams/' + team.id} className="bg-white dark:bg-neutral-800 rounded-lg shadow-md dark:shadow-neutral-950/50 overflow-hidden hover:shadow-lg transition-shadow">
|
<Link
|
||||||
<img src={team.banner || '/images/banner-imphnen.png'} alt={team.name} className="w-full h-24 object-cover" />
|
key={team.id}
|
||||||
|
to={'/teams/' + team.id}
|
||||||
|
className="bg-white dark:bg-neutral-800 rounded-lg shadow-md dark:shadow-neutral-950/50 overflow-hidden hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={team.banner || '/images/banner-imphnen.webp'}
|
||||||
|
alt={team.name}
|
||||||
|
className="w-full h-24 object-cover"
|
||||||
|
/>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
{team.logo && <img src={team.logo} alt={team.name} className="w-10 h-10 rounded-full object-cover" />}
|
{team.logo && (
|
||||||
|
<img
|
||||||
|
src={team.logo}
|
||||||
|
alt={team.name}
|
||||||
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-bold text-gray-900 dark:text-white line-clamp-2">{team.name}</h3>
|
<h3 className="font-bold text-gray-900 dark:text-white line-clamp-2">
|
||||||
|
{team.name}
|
||||||
|
</h3>
|
||||||
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
|
<div className="text-sm text-gray-600 dark:text-neutral-400 flex gap-2">
|
||||||
<p className="truncate flex-1 min-w-0">📍 {team.city}</p>
|
<p className="truncate flex-1 min-w-0">
|
||||||
<p className="whitespace-nowrap shrink-0">👥 {team.members?.length || 0} members</p>
|
📍 {team.city}
|
||||||
|
</p>
|
||||||
|
<p className="whitespace-nowrap shrink-0">
|
||||||
|
👥 {team.members?.length || 0} members
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-neutral-400 text-sm line-clamp-2">{team.description}</p>
|
<p className="text-gray-600 dark:text-neutral-400 text-sm line-clamp-2">
|
||||||
|
{team.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -121,10 +170,14 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Teams</h2>
|
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Teams
|
||||||
|
</h2>
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="text-4xl mb-3">👥</div>
|
<div className="text-4xl mb-3">👥</div>
|
||||||
<p className="text-gray-600 dark:text-neutral-400">Not in any team yet</p>
|
<p className="text-gray-600 dark:text-neutral-400">
|
||||||
|
Not in any team yet
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -134,16 +187,26 @@ const UserProfilePage: FC = (): ReactElement => {
|
|||||||
<div className="space-y-4 md:space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-4 md:p-6">
|
||||||
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">Contact Information</h3>
|
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
|
||||||
|
Contact Information
|
||||||
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-neutral-400">Email</p>
|
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
<p className="text-gray-900 dark:text-white font-medium">{user.email}</p>
|
Email
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{user.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{user.location && (
|
{user.location && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600 dark:text-neutral-400">Location</p>
|
<p className="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
<p className="text-gray-900 dark:text-white font-medium">{user.location}</p>
|
Location
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{user.location}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const CitySelect: FC<CitySelectProps> = ({
|
|||||||
}}
|
}}
|
||||||
onClick={handleInputClick}
|
onClick={handleInputClick}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={`w-full h-[42px] px-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 ${
|
className={`w-full h-[42px] px-3 text-[15px] border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-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'
|
error ? 'border-red-500' : 'border-gray-300 dark:border-neutral-600'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
@@ -78,7 +78,7 @@ export const CitySelect: FC<CitySelectProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClearSelection}
|
onClick={handleClearSelection}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300"
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-neutral-500 hover:text-gray-600 cursor-pointer dark:hover:text-neutral-300"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@@ -88,7 +88,7 @@ export const CitySelect: FC<CitySelectProps> = ({
|
|||||||
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
|
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-neutral-800 border border-gray-300 dark:border-neutral-600 rounded-lg shadow-lg max-h-60 overflow-y-auto text-label2">
|
||||||
{filteredCities.length > 0 ? (
|
{filteredCities.length > 0 ? (
|
||||||
<ul className="py-1">
|
<ul className="py-1">
|
||||||
{filteredCities.map((city) => (
|
{filteredCities.map((city) => (
|
||||||
@@ -96,7 +96,9 @@ export const CitySelect: FC<CitySelectProps> = ({
|
|||||||
key={city}
|
key={city}
|
||||||
onClick={() => handleSelectCity(city)}
|
onClick={() => handleSelectCity(city)}
|
||||||
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${
|
className={`px-3 py-2 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/30 ${
|
||||||
value === city ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400' : 'text-gray-900 dark:text-neutral-200'
|
value === city
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400'
|
||||||
|
: 'text-gray-900 dark:text-neutral-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{city}
|
{city}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { Link, useLocation } from 'react-router';
|
import { Link, useLocation } from 'react-router';
|
||||||
import { useMyTeams, useAuthStore, supabase } from '@imphnen-frontend-service/service';
|
import {
|
||||||
|
useMyTeams,
|
||||||
|
useAuthStore,
|
||||||
|
supabase,
|
||||||
|
} from '@imphnen-frontend-service/service';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTheme } from './theme-provider';
|
import { useTheme } from './theme-provider';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -54,39 +59,37 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
{
|
{
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
icon: (
|
icon: <Icon icon="heroicons:home" className="w-5 h-5" />,
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Browse Teams',
|
name: 'Browse Teams',
|
||||||
path: '/teams/browse',
|
path: '/teams/browse',
|
||||||
icon: (
|
icon: <Icon icon="heroicons-outline:search" className="w-5 h-5" />,
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Create Team',
|
name: 'Create Team',
|
||||||
path: '/teams/create',
|
path: '/teams/create',
|
||||||
icon: (
|
icon: <Icon icon="heroicons:plus" className="h-5 w-5" />,
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
show: !hasTeam,
|
show: !hasTeam,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Edit Profile',
|
name: 'Edit Profile',
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
show: false,
|
show: false,
|
||||||
@@ -102,21 +105,51 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
const getThemeIcon = () => {
|
const getThemeIcon = () => {
|
||||||
if (theme === 'system') {
|
if (theme === 'system') {
|
||||||
return (
|
return (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (resolvedTheme === 'dark') {
|
if (resolvedTheme === 'dark') {
|
||||||
return (
|
return (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -130,22 +163,34 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<div className="w-64 bg-white dark:bg-neutral-900 border-r dark:border-neutral-700 min-h-screen flex flex-col">
|
<div className="w-64 bg-white dark:bg-neutral-900 border-r dark:border-neutral-700 min-h-screen flex flex-col">
|
||||||
{/* Logo / Brand with Close Button */}
|
{/* Logo / Brand with Close Button */}
|
||||||
<div className="p-6 border-b dark:border-neutral-700 flex items-center justify-between">
|
<div className="p-6 flex items-center justify-between border-b dark:border-neutral-700">
|
||||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Hackathon</h1>
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Hackathon
|
||||||
|
</h1>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 text-gray-500 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
className="w-5 h-5 text-gray-500 dark:text-neutral-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="p-4 border-b dark:border-neutral-700">
|
<div className="p-4 border-y dark:border-neutral-700">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{user?.avatar ? (
|
{user?.avatar ? (
|
||||||
<img
|
<img
|
||||||
@@ -154,15 +199,22 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
className="w-10 h-10 rounded-full object-cover"
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
|
||||||
<span className="text-gray-500 dark:text-neutral-400 text-lg">👤</span>
|
<Icon
|
||||||
|
icon="ic:baseline-person"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
className="text-gray-400 dark:text-neutral-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
{user?.fullname || user?.email?.split('@')[0] || 'User'}
|
{user?.fullname || user?.email?.split('@')[0] || 'User'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-neutral-400 truncate">{user?.email}</p>
|
<p className="text-xs text-gray-500 dark:text-neutral-400 truncate">
|
||||||
|
{user?.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +232,7 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
to={item.path}
|
to={item.path}
|
||||||
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 font-medium'
|
? 'bg-primary-50 dark:bg-blue-900/30 text-primary-600 dark:text-blue-400 font-medium'
|
||||||
: 'text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800'
|
: 'text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -204,11 +256,12 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Icon
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
icon="heroicons:arrow-right-end-on-rectangle"
|
||||||
</svg>
|
className="w-5 h-5"
|
||||||
|
/>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ type TInputProps = Omit<
|
|||||||
|
|
||||||
const sizeClasses: Record<TInputSize, { textSize: string; iconSize: string }> =
|
const sizeClasses: Record<TInputSize, { textSize: string; iconSize: string }> =
|
||||||
{
|
{
|
||||||
sm: { textSize: 'text-[10px] max-h-[28px]', iconSize: 'text-[10px]' },
|
sm: { textSize: 'text-[10px] h-[28px]', iconSize: 'text-[10px]' },
|
||||||
md: { textSize: 'text-[12px] max-h-[30px]', iconSize: 'text-[12px]' },
|
md: { textSize: 'text-[12px] h-[30px]', iconSize: 'text-[12px]' },
|
||||||
lg: { textSize: 'text-[15px] max-h-[34px]', iconSize: 'text-[15px]' },
|
lg: { textSize: 'text-[15px] h-[34px]', iconSize: 'text-[15px]' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
||||||
|
|||||||
@@ -20,19 +20,20 @@ export type TInputFieldProps = Omit<
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
htmlFor?: string;
|
htmlFor?: string;
|
||||||
|
isRequired?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<TInputSize, { label: string; helperText: string }> = {
|
const sizeClasses: Record<TInputSize, { label: string; helperText: string }> = {
|
||||||
lg: {
|
lg: {
|
||||||
label: 'text-p3 font-medium',
|
label: 'text-label1 font-medium',
|
||||||
helperText: 'text-label3 font-normal',
|
helperText: 'text-label3 font-normal',
|
||||||
},
|
},
|
||||||
md: {
|
md: {
|
||||||
label: 'text-label1 font-medium',
|
label: 'text-label2 font-medium',
|
||||||
helperText: 'text-label2 font-normal',
|
helperText: 'text-label2 font-normal',
|
||||||
},
|
},
|
||||||
sm: {
|
sm: {
|
||||||
label: 'text-label2 font-medium',
|
label: 'text-label3 font-medium',
|
||||||
helperText: 'text-label2 font-normal',
|
helperText: 'text-label2 font-normal',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -47,6 +48,7 @@ export const InputField: FC<TInputFieldProps> = ({
|
|||||||
htmlFor,
|
htmlFor,
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
|
isRequired = false,
|
||||||
...rest
|
...rest
|
||||||
}): ReactElement => {
|
}): ReactElement => {
|
||||||
return (
|
return (
|
||||||
@@ -58,7 +60,7 @@ export const InputField: FC<TInputFieldProps> = ({
|
|||||||
sizeClasses[size].label
|
sizeClasses[size].label
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label} {isRequired ? <span className="text-red-500">*</span> : null}
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
{...(htmlFor && { id: htmlFor })}
|
{...(htmlFor && { id: htmlFor })}
|
||||||
@@ -75,7 +77,7 @@ export const InputField: FC<TInputFieldProps> = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<p className="text-danger-500 text-label1 text-left">{error}</p>
|
<p className="text-danger-500 text-label2 text-left">{error}</p>
|
||||||
) : (
|
) : (
|
||||||
helperText && (
|
helperText && (
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const ControlledInputField = <T extends FieldValues>(
|
|||||||
error={fieldState.error?.message}
|
error={fieldState.error?.message}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
isRequired={props.isRequired}
|
||||||
|
size={props.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user