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:
Hafid Nur
2025-11-26 16:41:10 +07:00
committed by GitHub
co-authored by Maulana Sodiqin Muhammad Zakir Ramadhan
parent b3bdf25b5f
commit 19bd44a7ca
16 changed files with 813 additions and 251 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+17 -5
View File
@@ -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">
+126 -33
View File
@@ -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>
+29 -12
View File
@@ -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">
+70 -24
View File
@@ -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>
); );
} }
+39 -13
View File
@@ -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">
+259 -56
View File
@@ -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>
)} )}
+49 -20
View File
@@ -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"
+26 -12
View File
@@ -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>
+90 -27
View File
@@ -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}
+90 -37
View File
@@ -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>
+3 -3
View File
@@ -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}
/> />
); );
}; };