feat(hackathon): update UI
- Update dashboard, onboarding user, edit profile, and team layout to make it more consistent
This commit is contained in:
@@ -9,18 +9,28 @@ const DashboardLayout: FC = (): ReactElement => {
|
|||||||
<div className="flex min-h-screen bg-gray-50">
|
<div className="flex min-h-screen bg-gray-50">
|
||||||
<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 border-b px-4 py-3 flex items-center">
|
<div className="lg:hidden sticky top-0 bg-white border-b px-4 py-3 flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen(true)}
|
onClick={() => setSidebarOpen(true)}
|
||||||
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6 text-gray-600" 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"
|
||||||
|
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">🏆 Hackathon</h1>
|
<h1 className="ml-3 text-lg font-bold text-gray-900">Hackathon</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const DashboardPage: FC = (): ReactElement => {
|
const DashboardPage: FC = (): ReactElement => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
@@ -40,31 +41,52 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8">
|
<div className="p-8 max-w-7xl mx-auto">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
|
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
|
||||||
</h1>
|
</h1>
|
||||||
{user?.location && (
|
{user?.location && (
|
||||||
<p className="text-gray-600 mt-1">Location: {user.location}</p>
|
<p className="text-gray-600 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 border border-blue-200 rounded-lg p-6">
|
<div className="mb-8 bg-primary-50 border border-primary-200 rounded-lg p-6">
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
<h2 className="text-xl font-bold text-gray-900 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 p-4 rounded-lg shadow-sm flex items-center justify-between">
|
<div
|
||||||
|
key={invitation.id}
|
||||||
|
className="bg-white p-4 rounded-lg shadow-sm flex items-center justify-between"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">{invitation.team.name}</p>
|
<p className="font-medium text-gray-900">
|
||||||
<p className="text-sm text-gray-600">Invited by {invitation.inviter.fullname}</p>
|
{invitation.team.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
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 mb-3 md:mb-4">My Team</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900 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 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 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">{team.name}</h3>
|
<h3 className="text-base md:text-lg font-bold text-gray-900">
|
||||||
<p className="text-xs md:text-sm text-gray-600">City: {team.city}</p>
|
{team.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs md:text-sm text-gray-600">
|
||||||
|
City: {team.city}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 text-xs md:text-sm line-clamp-2">{team.description}</p>
|
<p className="text-gray-600 text-xs md:text-sm line-clamp-2">
|
||||||
|
{team.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -97,56 +143,99 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="mb-6 md:mb-8 bg-white rounded-lg shadow-md p-6 md:p-8">
|
<div className="mb-6 md:mb-8 bg-white rounded-lg shadow-md p-6 md:p-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
|
<div className="text-3xl md:text-4xl mb-2 md:mb-3">👋</div>
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mb-1 md:mb-2">You are not in a team yet</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900 mb-1 md:mb-2">
|
||||||
<p className="text-sm md:text-base text-gray-600">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 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 rounded-xl shadow-lg">
|
<div className="mt-8 bg-linear-to-br from-white-50 to-primary-50 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 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 shadow-lg object-cover"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white shadow-lg bg-gray-200 flex items-center justify-center">
|
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full border-4 border-white shadow-lg bg-gray-200 flex items-center justify-center">
|
||||||
<span className="text-gray-400 text-3xl md:text-4xl">User</span>
|
<span className="text-gray-400 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">{user?.fullname || user?.email?.split('@')[0] || 'Unnamed User'}</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-gray-900">
|
||||||
|
{user?.fullname ||
|
||||||
|
user?.email?.split('@')[0] ||
|
||||||
|
'Unnamed User'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{user?.location ? (
|
{user?.location ? (
|
||||||
<p className="text-gray-600 flex items-center mt-1 text-sm md:text-base">{user.location}</p>
|
<p className="text-gray-600 flex items-center mt-1 text-sm md:text-base font-sans">
|
||||||
|
{user.location}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<Link to="/onboarding/user" className="text-blue-600 hover:text-blue-700 text-xs md:text-sm mt-1 inline-block">Complete your profile</Link>
|
<Link
|
||||||
|
to="/onboarding/user"
|
||||||
|
className="text-primary-500 hover:text-primary-600 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 rounded-lg p-4 shadow-sm">
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2">About</h3>
|
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2">
|
||||||
|
About
|
||||||
|
</h3>
|
||||||
<p className="text-gray-800 leading-relaxed">{user.bio}</p>
|
<p className="text-gray-800 leading-relaxed">{user.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Contact</h3>
|
<h3 className="font-semibold text-gray-700 tracking-wide mb-3">
|
||||||
|
Contact
|
||||||
|
</h3>
|
||||||
<div className="flex items-center text-gray-700">
|
<div className="flex items-center text-gray-700">
|
||||||
<span className="text-gray-900">{user?.email}</span>
|
<span className="text-gray-900">{user?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{user?.skills && user.skills.length > 0 && (
|
{user?.skills && user.skills.length > 0 && (
|
||||||
<div className="bg-white rounded-lg p-4 shadow-sm">
|
<div className="bg-white rounded-lg p-4 shadow-sm">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide mb-3">Skills</h3>
|
<h3 className="font-semibold text-gray-700 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>
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ const UserOnboardingPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center">
|
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center">
|
||||||
{/* <span className="text-gray-400 text-4xl">👤</span> */}
|
|
||||||
<Icon
|
<Icon
|
||||||
icon="ic:baseline-person"
|
icon="ic:baseline-person"
|
||||||
width="48"
|
width="48"
|
||||||
|
|||||||
@@ -153,7 +153,10 @@ export default function HomePage() {
|
|||||||
<a href="#faq" className="ms-3 text-gray-600 hover:text-gray-900">
|
<a href="#faq" className="ms-3 text-gray-600 hover:text-gray-900">
|
||||||
FAQ
|
FAQ
|
||||||
</a>
|
</a>
|
||||||
<a href="#masuk" className="ms-3 text-gray-600 hover:text-gray-900">
|
<a
|
||||||
|
href="/auth/login"
|
||||||
|
className="ms-3 text-gray-600 hover:text-gray-900"
|
||||||
|
>
|
||||||
Masuk
|
Masuk
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -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];
|
||||||
@@ -133,10 +143,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Edit Profile</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Edit Profile</h1>
|
||||||
<Link
|
<Link to="/dashboard" className="text-gray-500 hover:text-gray-700">
|
||||||
to="/dashboard"
|
|
||||||
className="text-gray-500 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -152,7 +159,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600">Update your photo and name</p>
|
<p className="text-gray-600 font-sans">Update your photo and name</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-6">
|
<form onSubmit={onSubmit} className="space-y-6">
|
||||||
@@ -167,13 +174,18 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors">
|
<div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center group-hover:bg-gray-300 transition-colors">
|
||||||
<span className="text-gray-400 text-4xl">👤</span>
|
<Icon
|
||||||
|
icon="ic:baseline-person"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
className="text-gray-400"
|
||||||
|
/>
|
||||||
</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 +216,7 @@ const ProfilePage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500 text-center">
|
<p className="text-sm text-gray-500 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 +229,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">
|
<label className="block text-label1 font-medium text-gray-700">
|
||||||
City
|
City
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -240,7 +253,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">
|
<label className="block text-label1 font-medium text-gray-700">
|
||||||
Role / Skills
|
Role / Skills
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -252,7 +265,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"
|
||||||
@@ -276,7 +289,7 @@ 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">
|
<label className="block text-label1 font-medium text-gray-700">
|
||||||
Bio <span className="text-gray-400">(Optional)</span>
|
Bio <span className="text-gray-400">(Optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
@@ -289,6 +302,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">
|
||||||
|
|||||||
@@ -9,21 +9,33 @@ const TeamsLayout: FC = (): ReactElement => {
|
|||||||
<div className="flex min-h-screen bg-gray-50">
|
<div className="flex min-h-screen bg-gray-50">
|
||||||
<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 border-b px-4 py-3 flex items-center">
|
<div className="lg:hidden sticky top-0 bg-white border-b 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 transition-colors"
|
onClick={() => setSidebarOpen(true)}
|
||||||
>
|
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
<svg className="w-6 h-6 text-gray-600" 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"
|
||||||
</button>
|
fill="none"
|
||||||
<h1 className="ml-3 text-lg font-bold text-gray-900">🏆 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">Hackathon</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</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 ${
|
className={`w-full h-[42px] px-3 text-[15px] border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
|
||||||
error ? 'border-red-500' : 'border-gray-300'
|
error ? 'border-red-500' : 'border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
import { FC, useState, 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 { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -52,39 +57,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,
|
||||||
@@ -94,22 +97,32 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<div className="w-64 bg-white border-r min-h-screen flex flex-col">
|
<div className="w-64 bg-white border-r min-h-screen flex flex-col">
|
||||||
{/* Logo / Brand with Close Button */}
|
{/* Logo / Brand with Close Button */}
|
||||||
<div className="p-6 border-b flex items-center justify-between">
|
<div className="p-6 flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold text-gray-900">🏆 Hackathon</h1>
|
<h1 className="text-xl font-bold text-gray-900">Hackathon</h1>
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 text-gray-500" 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"
|
||||||
|
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">
|
<div className="p-4 border-y">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{user?.avatar ? (
|
{user?.avatar ? (
|
||||||
<img
|
<img
|
||||||
@@ -119,7 +132,12 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-full bg-gray-200 flex items-center justify-center">
|
||||||
<span className="text-gray-500 text-lg">👤</span>
|
<Icon
|
||||||
|
icon="ic:baseline-person"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
className="text-gray-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -144,7 +162,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 text-blue-700 font-medium'
|
? 'bg-primary-50 text-primary-600 font-medium'
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -161,11 +179,12 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
|
|||||||
<div className="p-4 border-t">
|
<div className="p-4 border-t">
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 hover:bg-red-50 transition-colors"
|
className="flex items-center space-x-3 px-4 py-3 w-full rounded-lg text-red-600 hover:bg-red-50 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Icon
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
icon="heroicons:arrow-right-end-on-rectangle"
|
||||||
</svg>
|
className="w-5 h-5"
|
||||||
|
/>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ type TInputProps = Omit<
|
|||||||
|
|
||||||
const sizeClasses: Record<TInputSize, { textSize: string; iconSize: string }> =
|
const sizeClasses: Record<TInputSize, { textSize: string; iconSize: string }> =
|
||||||
{
|
{
|
||||||
sm: { textSize: 'text-[10px] max-h-[28px]', iconSize: 'text-[10px]' },
|
sm: { textSize: 'text-[10px] h-[28px]', iconSize: 'text-[10px]' },
|
||||||
md: { textSize: 'text-[12px] max-h-[30px]', iconSize: 'text-[12px]' },
|
md: { textSize: 'text-[12px] h-[30px]', iconSize: 'text-[12px]' },
|
||||||
lg: { textSize: 'text-[15px] max-h-[34px]', iconSize: 'text-[15px]' },
|
lg: { textSize: 'text-[15px] h-[34px]', iconSize: 'text-[15px]' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ export type TInputFieldProps = Omit<
|
|||||||
|
|
||||||
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',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const ControlledInputField = <T extends FieldValues>(
|
|||||||
{...inputProps}
|
{...inputProps}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
isRequired={props.isRequired}
|
isRequired={props.isRequired}
|
||||||
|
size={props.size}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user