About
diff --git a/apps/hackathon/src/app/profile/page.tsx b/apps/hackathon/src/app/profile/page.tsx
index 7e07f3b..0163665 100644
--- a/apps/hackathon/src/app/profile/page.tsx
+++ b/apps/hackathon/src/app/profile/page.tsx
@@ -1,8 +1,8 @@
import { FC, ReactElement, useState, useEffect } from 'react';
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
-import { Button } from '@imphnen-frontend-service/ui/atoms';
+import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms';
import { useNavigate, Link } from 'react-router';
-import { useForm } from 'react-hook-form';
+import { useForm, Controller } from 'react-hook-form';
import {
userEditProfileSchema,
TUserEditProfileForm,
@@ -12,6 +12,18 @@ import {
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner';
+import { CitySelect } from '../../components/city-select';
+
+const ROLE_OPTIONS = [
+ 'Frontend Developer',
+ 'Backend Developer',
+ 'Full Stack Developer',
+ 'DevOps Engineer',
+ 'UI/UX Designer',
+ 'Product Manager',
+ 'Data Scientist',
+ 'Mobile Developer',
+];
const ProfilePage: FC = (): ReactElement => {
const navigate = useNavigate();
@@ -28,6 +40,9 @@ const ProfilePage: FC = (): ReactElement => {
defaultValues: {
fullname: session?.user?.fullname || '',
avatar: session?.user?.avatar || null,
+ location: session?.user?.location || '',
+ bio: session?.user?.bio || '',
+ skills: session?.user?.skills || [],
},
});
@@ -39,7 +54,16 @@ const ProfilePage: FC = (): ReactElement => {
if (session?.user?.fullname) {
form.setValue('fullname', session.user.fullname);
}
- }, [session?.user?.avatar, session?.user?.fullname, avatarPreview, form]);
+ if (session?.user?.location) {
+ form.setValue('location', session.user.location);
+ }
+ if (session?.user?.bio) {
+ form.setValue('bio', session.user.bio);
+ }
+ if (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]);
const handleAvatarChange = (e: React.ChangeEvent
) => {
const file = e.target.files?.[0];
@@ -79,6 +103,9 @@ const ProfilePage: FC = (): ReactElement => {
await updateUser({
fullname: data.fullname,
avatar: avatarUrl,
+ location: data.location,
+ bio: data.bio,
+ skills: data.skills,
});
toast.success('Profile updated successfully!');
@@ -192,6 +219,87 @@ const ProfilePage: FC = (): ReactElement => {
name="fullname"
/>
+ {/* City */}
+
+
+ City
+
+ (
+
+ )}
+ />
+
+
+ {/* Role/Skills */}
+
+
+ Role / Skills
+
+
(
+
+
+ {ROLE_OPTIONS.map((role) => (
+
+ {
+ const newValue = e.target.checked
+ ? [...(field.value || []), role]
+ : (field.value || []).filter((v) => v !== role);
+ field.onChange(newValue);
+ }}
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
+ />
+ {role}
+
+ ))}
+
+
+ )}
+ />
+
+
+ {/* Bio */}
+
+
+ Bio (Optional)
+
+
(
+
+
+ {fieldState.error && (
+
+ {fieldState.error.message}
+
+ )}
+
+ )}
+ />
+
+
{/* Action Buttons */}
{
const { teamId } = useParams<{ teamId: string }>();
@@ -75,16 +76,20 @@ const ManageMembersPage: FC = (): ReactElement => {
const handleApproveRequest = async (requestId: string) => {
try {
await respondToRequest({ requestId, action: 'approve' });
+ toast.success('Member added successfully!');
} catch (error) {
console.error('Failed to approve request:', error);
+ toast.error((error as Error).message || 'Failed to approve request');
}
};
const handleRejectRequest = async (requestId: string) => {
try {
await respondToRequest({ requestId, action: 'reject' });
+ toast.success('Request rejected');
} catch (error) {
console.error('Failed to reject request:', error);
+ toast.error((error as Error).message || 'Failed to reject request');
}
};
diff --git a/apps/hackathon/src/app/teams/[teamId]/page.tsx b/apps/hackathon/src/app/teams/[teamId]/page.tsx
index c720004..a68b918 100644
--- a/apps/hackathon/src/app/teams/[teamId]/page.tsx
+++ b/apps/hackathon/src/app/teams/[teamId]/page.tsx
@@ -1,7 +1,15 @@
import { FC, ReactElement, useState, useEffect } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms';
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';
const MAX_TEAM_MEMBERS = 5;
@@ -18,12 +26,16 @@ const ImageWithLoader: FC<{
return (
{!isLoaded && (
-
+
)}
{
setIsLoaded(true);
onLoad?.();
@@ -43,23 +55,36 @@ const TeamDashboardPage: FC = (): ReactElement => {
const [imagesLoaded, setImagesLoaded] = useState(false);
const [imageLoadCount, setImageLoadCount] = useState(0);
- const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
- const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || '');
- const { data: joinRequestsData } = useTeamJoinRequests(teamId || '', !!teamId);
- const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(teamId || '');
- const { mutateAsync: respondToJoinRequest, isPending: isResponding } = useRespondToJoinRequest(teamId || '');
+ const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
+ teamId || ''
+ );
+ const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(
+ 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 members = membersData?.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 isLeader = currentUserId === team?.leader_id;
const canInvite = isLeader && members.length < MAX_TEAM_MEMBERS;
// Calculate total images to load
- const totalImagesToLoad = (team?.banner ? 1 : 0) +
+ const totalImagesToLoad =
+ (team?.banner ? 1 : 0) +
(team?.logo ? 1 : 0) +
members.filter((m: any) => m.user?.avatar).length;
@@ -72,7 +97,13 @@ const TeamDashboardPage: FC = (): ReactElement => {
setImagesLoaded(true);
}
}
- }, [isLoadingTeam, isLoadingMembers, team, imageLoadCount, totalImagesToLoad]);
+ }, [
+ isLoadingTeam,
+ isLoadingMembers,
+ team,
+ imageLoadCount,
+ totalImagesToLoad,
+ ]);
// Fallback timeout - if images take too long, show content anyway
useEffect(() => {
@@ -93,7 +124,11 @@ const TeamDashboardPage: FC = (): ReactElement => {
setImageLoadCount((prev) => prev + 1);
};
- console.log('Leader check:', { currentUserId, leaderId: team?.leader_id, isLeader });
+ console.log('Leader check:', {
+ currentUserId,
+ leaderId: team?.leader_id,
+ isLeader,
+ });
const handleInviteMember = async (e: React.FormEvent) => {
e.preventDefault();
@@ -110,10 +145,15 @@ const TeamDashboardPage: FC = (): ReactElement => {
}
};
- const handleRespondToJoinRequest = async (requestId: string, action: 'approve' | 'reject') => {
+ const handleRespondToJoinRequest = async (
+ requestId: string,
+ action: 'approve' | 'reject'
+ ) => {
try {
await respondToJoinRequest({ requestId, action });
- toast.success(action === 'approve' ? 'Request approved!' : 'Request rejected');
+ toast.success(
+ action === 'approve' ? 'Request approved!' : 'Request rejected'
+ );
} catch (error) {
console.error('Failed to respond to join request:', error);
toast.error('Failed to process request');
@@ -178,7 +218,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
-
Loading team data...
+
+ Loading team data...
+
@@ -188,8 +230,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (!team) {
return (
-
Team not found
- navigate('/dashboard')}>Back to Dashboard
+
+ Team not found
+
+ navigate('/dashboard')}>
+ Back to Dashboard
+
);
}
@@ -202,7 +248,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
Loading images...
-
{imageLoadCount} / {totalImagesToLoad}
+
+ {imageLoadCount} / {totalImagesToLoad}
+
)}
@@ -210,20 +258,26 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Header with Banner */}
{team.banner && (
-
-
+
+
)}
-
+
-
+
{team.logo && (
{!imagesLoaded && (
@@ -232,51 +286,64 @@ const TeamDashboardPage: FC = (): ReactElement => {
)}
-
{team.name}
-
📍 {team.city}
-
+
+ {team.name}
+
+
+ 📍 {team.city}
+
+
- {members.length} {members.length === 1 ? 'Member' : 'Members'}
+ {members.length}{' '}
+ {members.length === 1 ? 'Member' : 'Members'}
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
{isLeader && (
-
+
👑 Team Leader
)}
-
+
Back to Dashboard
-
-
+
+
{/* Main Content */}
-
+
{/* Team Description */}
-
-
About Team
-
{team.description}
+
+
+ About Team
+
+
+ {team.description}
+
{/* Team Actions - Only for Leader */}
{isLeader && (
-
-
Team Management
+
+
+ Team Management
+
{
onClick={() => setShowInviteModal(true)}
disabled={!canInvite}
>
- ➕ Invite Member {!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
+ ➕ Invite Member{' '}
+ {!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
{
-
- 🚀 Submit Project
-
+ 🚀 Submit Project
{!canInvite && members.length >= MAX_TEAM_MEMBERS && (
@@ -329,8 +395,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Quick Actions for Members */}
{!isLeader && (
-
-
Quick Actions
+
+
+ Quick Actions
+
@@ -350,11 +418,13 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Submission Status */}
{team.has_submission && (
-
+
✅
-
Project Submitted
+
+ Project Submitted
+
Your team has successfully submitted a project
@@ -370,12 +440,17 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Sidebar */}
-
+
{/* Team Leader */}
-
-
Team Leader
+
+
+ Team Leader
+
{team.leader && (
-
+
navigate(`/users/${team.leader.id}`)}
+ className="w-full flex items-center space-x-3 hover:bg-gray-100 rounded-lg p-2 transition-colors text-left cursor-pointer"
+ >
{team.leader.avatar ? (
{
)}
-
{team.leader.fullname}
+
+ {team.leader.fullname}
+
{team.leader.email}
-
+
)}
{/* Team Members */}
-
-
+
+
Members ({members.length})
{members.map((member: any) => (
-
+
navigate(`/users/${member.user.id}`)}
+ className="w-full flex items-center space-x-3 hover:bg-gray-100 rounded-lg p-2 transition-colors text-left cursor-pointer"
+ >
{member.user?.avatar ? (
{
{member.user?.fullname || 'Unknown'}
- {member.role === ETeamMemberRole.LEADER ? 'Leader' : 'Member'}
+ {member.role === ETeamMemberRole.LEADER
+ ? 'Leader'
+ : 'Member'}
-
+
))}
@@ -440,16 +523,21 @@ const TeamDashboardPage: FC = (): ReactElement => {
Invite Team Member
- 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.
- Important: The email you enter must match the GitHub email address the member uses to sign in.
+ Important: The email you enter must match the
+ GitHub email address the member uses to sign in.