feat(hackathon): update team page

- update dark mode color scheme for team page: view, team edit,
  and submit project page
- fix bug in chat where messages are displayed double
- make the form field size consistent
This commit is contained in:
Hafid Nur
2025-11-27 14:53:38 +07:00
parent ff9a02c020
commit 3470853858
11 changed files with 548 additions and 259 deletions
+3 -3
View File
@@ -2,13 +2,13 @@ import { Link } from 'react-router-dom';
export default function NotFoundPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950">
<div className="text-center">
<h1 className="text-9xl font-bold text-gray-200 mb-4">404</h1>
<h2 className="text-3xl font-semibold text-gray-900 mb-4">
<h2 className="text-3xl font-semibold text-gray-900 dark:text-gray-300 mb-4">
Page Not Found
</h2>
<p className="text-gray-600 mb-8">
<p className="text-gray-600 dark:text-gray-400 mb-8">
The page you are looking for doesn't exist or has been moved.
</p>
<Link
@@ -1,8 +1,15 @@
import { FC, ReactElement, useState, useRef, useEffect } from 'react';
import { FC, ReactElement, useState, useRef, useEffect, useMemo } from 'react';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useNavigate, useParams } from 'react-router';
import { useTeamById, useTeamMessages, useSendMessage, useDeleteMessage, useAuthStore } from '@imphnen-frontend-service/service';
import {
useTeamById,
useTeamMessages,
useSendMessage,
useDeleteMessage,
useAuthStore,
} from '@imphnen-frontend-service/service';
import { toast } from 'sonner';
import { Icon } from '@iconify/react';
const TeamChatPage: FC = (): ReactElement => {
const { teamId } = useParams<{ teamId: string }>();
@@ -13,16 +20,54 @@ const TeamChatPage: FC = (): ReactElement => {
const { data: teamData } = useTeamById(teamId || '');
const { data: messages, isLoading } = useTeamMessages(teamId || '');
const { mutateAsync: sendMessage, isPending: isSending } = useSendMessage(teamId || '');
const { mutateAsync: sendMessage, isPending: isSending } = useSendMessage(
teamId || ''
);
const { mutateAsync: deleteMessage } = useDeleteMessage(teamId || '');
const team = teamData?.data;
const currentUserId = session?.user?.id;
// Message type used for UI rendering
interface ChatMessage {
id: string;
user_id: string;
user?: {
avatar?: string;
fullname?: string;
};
message: string;
created_at: string;
}
// Inline delete confirmation UI state
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
// Prepare messages: de-duplicate by id and sort by created_at
const displayMessages = useMemo(() => {
const raw: ChatMessage[] = Array.isArray(messages)
? (messages as ChatMessage[])
: [];
const seen = new Set<string>();
const dedup: ChatMessage[] = [];
for (const m of raw) {
const key = m.id ?? `${m.user_id}-${m.created_at}`;
if (!seen.has(key)) {
seen.add(key);
dedup.push(m);
}
}
dedup.sort(
(a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
return dedup;
}, [messages]);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
}, [displayMessages]);
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
@@ -38,11 +83,10 @@ const TeamChatPage: FC = (): ReactElement => {
};
const handleDeleteMessage = async (messageId: string) => {
if (!confirm('Are you sure you want to delete this message?')) return;
try {
await deleteMessage(messageId);
toast.success('Message deleted');
setDeleteTargetId(null);
} catch (error) {
console.error('Failed to delete message:', error);
toast.error('Failed to delete message');
@@ -63,16 +107,23 @@ const TeamChatPage: FC = (): ReactElement => {
};
return (
<div className="flex flex-col h-screen bg-gray-50 dark:bg-neutral-950">
<div className="flex flex-col h-screen bg-gray-50 dark:bg-gray-950">
{/* Header */}
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 shadow-sm dark:shadow-neutral-950/50">
<div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700 shadow-sm dark:shadow-gray-950/50">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Team Chat</h1>
<p className="text-gray-600 dark:text-neutral-400 text-sm mt-0.5">{team?.name}</p>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Team Chat
</h1>
<p className="text-gray-600 dark:text-gray-400 text-sm mt-0.5">
{team?.name}
</p>
</div>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
<Button
variant="secondary"
onClick={() => navigate(`/teams/${teamId}`)}
>
Back to Team
</Button>
</div>
@@ -86,21 +137,27 @@ const TeamChatPage: FC = (): ReactElement => {
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 dark:border-primary-400"></div>
</div>
) : messages && messages.length > 0 ? (
) : displayMessages && displayMessages.length > 0 ? (
<div className="space-y-4">
{messages.map((msg) => {
{displayMessages.map((msg) => {
const isOwnMessage = msg.user_id === currentUserId;
const isLeader = team?.leader_id === currentUserId;
const canDelete = isOwnMessage || isLeader;
return (
<div
key={msg.id}
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
key={`${msg.id ?? 'message'}-${msg.created_at}`}
className={`flex ${
isOwnMessage ? 'justify-end' : 'justify-start'
}`}
>
<div className={`flex gap-3 max-w-lg ${isOwnMessage ? 'flex-row-reverse' : 'flex-row'}`}>
<div
className={`flex gap-3 max-w-lg ${
isOwnMessage ? 'flex-row-reverse' : 'flex-row'
}`}
>
{/* Avatar */}
<div className="flex-shrink-0">
<div className="shrink-0">
{msg.user?.avatar ? (
<img
src={msg.user.avatar}
@@ -115,13 +172,17 @@ const TeamChatPage: FC = (): ReactElement => {
</div>
{/* Message Bubble */}
<div className={`flex-1 ${isOwnMessage ? 'text-right' : 'text-left'}`}>
<div className={`inline-block ${isOwnMessage ? 'items-end' : 'items-start'}`}>
<div className={`flex-1 text-left`}>
<div
className={`inline-block ${
isOwnMessage ? 'items-end' : 'items-start'
}`}
>
<div className="flex items-baseline gap-2 mb-1">
<span className="font-semibold text-sm text-gray-900 dark:text-white">
{isOwnMessage ? 'You' : msg.user?.fullname}
</span>
<span className="text-xs text-gray-500 dark:text-neutral-500">
<span className="text-xs text-gray-500 dark:text-gray-500">
{formatTime(msg.created_at)}
</span>
</div>
@@ -129,24 +190,58 @@ const TeamChatPage: FC = (): ReactElement => {
className={`relative group rounded-2xl px-4 py-2.5 ${
isOwnMessage
? 'bg-blue-600 dark:bg-primary-600 text-white'
: 'bg-white dark:bg-neutral-800 text-gray-900 dark:text-white border border-gray-200 dark:border-neutral-700'
: 'bg-white dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700'
}`}
>
<p className="text-sm whitespace-pre-wrap break-words">{msg.message}</p>
<p className="text-sm whitespace-pre-wrap wrap-break-word font-sans">
{msg.message}
</p>
{/* Delete button */}
{canDelete && (
<button
onClick={() => handleDeleteMessage(msg.id)}
className={`absolute top-1 ${isOwnMessage ? 'left-1' : 'right-1'} opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-200 dark:hover:bg-neutral-700 ${isOwnMessage ? 'hover:bg-blue-700 dark:hover:bg-primary-700' : ''}`}
onClick={() =>
setDeleteTargetId(
deleteTargetId === msg.id ? null : msg.id
)
}
className={`absolute top-1 ${
isOwnMessage ? 'left-1' : 'right-1'
} opacity-0 hover:opacity-100 p-1 rounded hover:bg-gray-200 dark:hover:bg-neutral-700 cursor-pointer ${
isOwnMessage
? 'hover:bg-blue-700 dark:hover:bg-primary-700'
: ''
}`}
title="Delete message"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
{/* Icon trash */}
<Icon
icon="mdi:trash-can-outline"
className="w-4 h-4"
/>
</button>
)}
</div>
{canDelete && deleteTargetId === msg.id && (
<div
className={`mt-2 flex gap-2 ${
isOwnMessage ? 'justify-end' : 'justify-start'
}`}
>
<button
onClick={() => handleDeleteMessage(msg.id)}
className="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700 cursor-pointer"
>
Delete
</button>
<button
onClick={() => setDeleteTargetId(null)}
className="px-2 py-1 text-xs rounded bg-gray-200 text-gray-800 hover:bg-gray-300 dark:bg-neutral-700 dark:text-white dark:hover:bg-neutral-600 cursor-pointer"
>
Cancel
</button>
</div>
)}
</div>
</div>
</div>
@@ -157,11 +252,14 @@ const TeamChatPage: FC = (): ReactElement => {
</div>
) : (
<div className="flex flex-col items-center justify-center h-64 text-center">
<div className="text-6xl mb-4">💬</div>
<Icon
icon="mdi:chat-outline"
className="text-6xl mb-4 text-gray-400"
/>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
No messages yet
</h3>
<p className="text-gray-600 dark:text-neutral-400">
<p className="text-gray-600 dark:text-gray-400 font-sans">
Be the first to start the conversation!
</p>
</div>
@@ -170,7 +268,7 @@ const TeamChatPage: FC = (): ReactElement => {
</div>
{/* Message Input */}
<div className="bg-white dark:bg-neutral-900 border-t dark:border-neutral-700 shadow-lg dark:shadow-neutral-950/50">
<div className="bg-white dark:bg-gray-900 border-t dark:border-gray-700 shadow-lg dark:shadow-gray-950/50">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<form onSubmit={handleSendMessage} className="flex gap-3">
<input
@@ -178,7 +276,7 @@ const TeamChatPage: FC = (): ReactElement => {
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type your message..."
className="flex-1 px-4 py-3 border border-gray-300 dark:border-neutral-600 dark:bg-neutral-800 dark:text-white dark:placeholder-neutral-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-primary-500 focus:border-transparent"
className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-primary-500 focus:border-transparent"
disabled={isSending}
/>
<Button
@@ -193,8 +291,18 @@ const TeamChatPage: FC = (): ReactElement => {
</div>
) : (
<div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
Send
</div>
@@ -3,10 +3,19 @@ import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms';
import { Button, Textarea } from '@imphnen-frontend-service/ui/atoms';
import { useNavigate, useParams } from 'react-router';
import { useForm, Controller } from 'react-hook-form';
import { teamUpdateSchema, TTeamUpdateForm, useUpdateTeam, useTeamById, ETeamVisibility, useUploadFile, useAuthStore } from '@imphnen-frontend-service/service';
import {
teamUpdateSchema,
TTeamUpdateForm,
useUpdateTeam,
useTeamById,
ETeamVisibility,
useUploadFile,
useAuthStore,
} from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod';
import { CitySelect } from '../../../../components/city-select';
import { Icon } from '@iconify/react';
const EditTeamPage: FC = (): ReactElement => {
const { teamId } = useParams<{ teamId: string }>();
@@ -17,8 +26,12 @@ const EditTeamPage: FC = (): ReactElement => {
const [bannerFile, setBannerFile] = useState<File | null>(null);
const [bannerPreview, setBannerPreview] = useState<string>('');
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
const { mutateAsync: updateTeam, isPending: isUpdating } = useUpdateTeam(teamId || '');
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
teamId || ''
);
const { mutateAsync: updateTeam, isPending: isUpdating } = useUpdateTeam(
teamId || ''
);
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const team = teamData?.data;
@@ -47,18 +60,24 @@ const EditTeamPage: FC = (): ReactElement => {
if (isLoadingTeam) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="text-gray-600 dark:text-neutral-400">Loading team...</div>
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-gray-600 dark:text-gray-400">Loading team...</div>
</div>
);
}
if (!isLeader) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can edit team information</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Access Denied
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Only the team leader can edit team information
</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team
</Button>
</div>
);
}
@@ -115,28 +134,35 @@ const EditTeamPage: FC = (): ReactElement => {
});
return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Edit Team Info</h1>
<p className="text-gray-600 dark:text-neutral-400 mt-1">Update your team details</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Edit Team Info
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Update your team details
</p>
</div>
</div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-8">
<form onSubmit={onSubmit} className="space-y-6">
{/* Banner Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-gray-300 mb-2">
Team Banner
</label>
{bannerPreview ? (
<div className="relative">
<div
className="relative w-full"
style={{ aspectRatio: '3 / 1' }}
>
<img
src={bannerPreview}
alt="Banner preview"
className="w-full h-48 object-cover rounded-lg border-2 border-gray-200 dark:border-neutral-700"
className="absolute inset-0 w-full h-full object-cover rounded-lg border-2 border-gray-200 dark:border-gray-700"
/>
<button
type="button"
@@ -145,16 +171,20 @@ const EditTeamPage: FC = (): ReactElement => {
setBannerPreview('');
form.setValue('banner', null);
}}
className="absolute top-2 right-2 bg-red-500 text-white px-3 py-1 rounded-lg text-sm hover:bg-red-600"
className="absolute top-2 right-2 bg-danger-600 text-white px-3 py-1 rounded-lg text-sm hover:bg-danger-700 cursor-pointer"
>
Remove
</button>
</div>
) : (
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
<label className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800">
<div className="text-center">
<p className="text-gray-500 dark:text-neutral-400">Click to upload banner</p>
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">1200x400 recommended</p>
<p className="text-gray-500 dark:text-gray-400">
Click to upload banner
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
1200x400 recommended
</p>
</div>
<input
type="file"
@@ -168,7 +198,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Logo Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
<label className="block text-label1 font-medium text-gray-700 dark:text-gray-300 mb-2">
Team Logo
</label>
<div className="flex items-center space-x-4">
@@ -176,16 +206,18 @@ const EditTeamPage: FC = (): ReactElement => {
<img
src={logoPreview}
alt="Logo preview"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-neutral-700"
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200 dark:border-gray-700"
/>
) : (
<div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-400 dark:text-neutral-500 text-3xl">👥</span>
<div className="w-24 h-24 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-400 dark:text-gray-500 text-3xl">
<Icon icon="mdi:account-group" />
</span>
</div>
)}
<div>
<div className="flex flex-col md:flex-row items-start justify-start gap-1">
<label htmlFor="logo" className="cursor-pointer">
<span className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 inline-block">
<span className="px-4 py-2 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 inline-block">
{logoPreview ? 'Change Logo' : 'Upload Logo'}
</span>
<input
@@ -204,7 +236,7 @@ const EditTeamPage: FC = (): ReactElement => {
setLogoPreview('');
form.setValue('logo', null);
}}
className="ml-3 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600"
className="px-4 py-2 text-sm bg-danger-600 text-white rounded-lg hover:bg-danger-700 cursor-pointer"
>
Remove
</button>
@@ -219,11 +251,13 @@ const EditTeamPage: FC = (): ReactElement => {
label="Team Name"
placeholder="Enter team name"
name="name"
size="lg"
isRequired={true}
/>
{/* City */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<Controller
@@ -242,7 +276,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Description */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
<label className="block text-label1 font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<Controller
@@ -256,9 +290,12 @@ const EditTeamPage: FC = (): ReactElement => {
placeholder="Tell others about your team..."
rows={4}
className="w-full"
size="lg"
/>
{fieldState.error && (
<p className="text-sm text-red-500 mt-1">{fieldState.error.message}</p>
<p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)}
</div>
)}
@@ -267,7 +304,7 @@ const EditTeamPage: FC = (): ReactElement => {
{/* Visibility */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
<label className="block text-label1 font-medium text-gray-700 dark:text-gray-300">
Team Visibility
</label>
<Controller
@@ -275,7 +312,7 @@ const EditTeamPage: FC = (): ReactElement => {
name="visibility"
render={({ field }) => (
<div className="space-y-3">
<label className="flex items-start space-x-3 cursor-pointer border dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800">
<label className="flex items-start space-x-3 cursor-pointer border dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<input
type="radio"
{...field}
@@ -284,13 +321,15 @@ const EditTeamPage: FC = (): ReactElement => {
className="mt-1"
/>
<div>
<p className="font-medium text-gray-900 dark:text-white">Public</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">
<p className="font-medium text-gray-900 dark:text-white">
Public
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 font-sans">
Team will be visible in Browse Teams
</p>
</div>
</label>
<label className="flex items-start space-x-3 cursor-pointer border dark:border-neutral-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-neutral-800">
<label className="flex items-start space-x-3 cursor-pointer border dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<input
type="radio"
{...field}
@@ -299,8 +338,10 @@ const EditTeamPage: FC = (): ReactElement => {
className="mt-1"
/>
<div>
<p className="font-medium text-gray-900 dark:text-white">Private</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">
<p className="font-medium text-gray-900 dark:text-white">
Private
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 font-sans">
Team hidden, invite-only
</p>
</div>
@@ -6,21 +6,33 @@ const TeamDetailLayout: FC = (): ReactElement => {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="flex min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-950">
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div className="flex-1 flex flex-col overflow-auto">
<div className="flex-1 flex flex-col">
{/* Mobile Header with Hamburger */}
<div className="lg:hidden bg-white dark:bg-neutral-900 border-b dark:border-neutral-700 px-4 py-3 flex items-center">
<div className="lg:hidden sticky top-0 z-30 bg-white dark:bg-gray-900 border-b dark:border-gray-700 px-4 py-3 flex items-center">
<button
onClick={() => setSidebarOpen(true)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors"
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<svg className="w-6 h-6 text-gray-600 dark:text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
<svg
className="w-6 h-6 text-gray-600 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">Hackathon</h1>
<h1 className="ml-3 text-lg font-bold text-gray-900 dark:text-white">
Hackathon
</h1>
</div>
<main className="flex-1 overflow-auto">
@@ -23,17 +23,28 @@ const ManageMembersPage: FC = (): ReactElement => {
const { session } = useAuthStore();
const [showInviteModal, setShowInviteModal] = useState(false);
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(teamId || '');
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(teamId || '');
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
teamId || ''
);
const { data: membersData, isLoading: isLoadingMembers } = useTeamMembers(
teamId || ''
);
const { data: joinRequestsData } = useTeamJoinRequests(teamId || '');
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(teamId || '');
const { mutateAsync: removeMember, isPending: isRemoving } = useRemoveMember(teamId || '');
const { mutateAsync: respondToRequest, isPending: isResponding } = useRespondToJoinRequest(teamId || '');
const { mutateAsync: inviteMember, isPending: isInviting } = useInviteMember(
teamId || ''
);
const { mutateAsync: removeMember, isPending: isRemoving } = useRemoveMember(
teamId || ''
);
const { mutateAsync: respondToRequest, isPending: isResponding } =
useRespondToJoinRequest(teamId || '');
const team = teamData?.data;
const members = membersData?.data || [];
const joinRequests = Array.isArray(joinRequestsData?.data) ? joinRequestsData.data : [];
const joinRequests = Array.isArray(joinRequestsData?.data)
? joinRequestsData.data
: [];
const currentUserId = session?.user?.id;
const isLeader = currentUserId === team?.leader_id;
@@ -45,18 +56,24 @@ const ManageMembersPage: FC = (): ReactElement => {
// Show loading state while fetching team data
if (isLoadingTeam) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<p className="text-gray-600 dark:text-neutral-400">Loading...</p>
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
</div>
);
}
if (!isLeader) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can manage members</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Access Denied
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Only the team leader can manage members
</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team
</Button>
</div>
);
}
@@ -103,19 +120,26 @@ const ManageMembersPage: FC = (): ReactElement => {
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-center justify-between">
<div className="flex flex-col md:flex-row gap-y-4 items-start md:items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Manage Members</h1>
<p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Manage Members
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
{team?.name}
</p>
</div>
<div className="flex space-x-3">
<Button onClick={() => setShowInviteModal(true)}>
Invite Member
</Button>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
<Button
variant="secondary"
onClick={() => navigate(`/teams/${teamId}`)}
>
Back to Team
</Button>
</div>
@@ -126,13 +150,16 @@ const ManageMembersPage: FC = (): ReactElement => {
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-6">
{/* Join Requests */}
{joinRequests.length > 0 && (
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Join Requests ({joinRequests.length})
</h2>
<div className="space-y-3">
{joinRequests.map((request) => (
<div key={request.id} className="border dark:border-neutral-700 rounded-lg p-4">
<div
key={request.id}
className="border dark:border-gray-700 rounded-lg p-4"
>
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3 flex-1">
{request.user.avatar ? (
@@ -142,17 +169,27 @@ const ManageMembersPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400">👤</span>
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-gray-400">
👤
</span>
</div>
)}
<div className="flex-1">
<p className="font-medium text-gray-900 dark:text-white">{request.user.fullname}</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">{request.user.email}</p>
<p className="font-medium text-gray-900 dark:text-white">
{request.user.fullname}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{request.user.email}
</p>
{request.user.location && (
<p className="text-sm text-gray-500 dark:text-neutral-500">📍 {request.user.location}</p>
<p className="text-sm text-gray-500 dark:text-gray-500">
📍 {request.user.location}
</p>
)}
<p className="text-sm text-gray-700 dark:text-neutral-300 mt-2 italic">"{request.message}"</p>
<p className="text-sm text-gray-700 dark:text-gray-300 mt-2 italic">
"{request.message}"
</p>
</div>
</div>
<div className="flex space-x-2 ml-4">
@@ -180,16 +217,21 @@ const ManageMembersPage: FC = (): ReactElement => {
)}
{/* Current Members */}
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Current Members ({members.length})
</h2>
{isLoadingMembers ? (
<p className="text-gray-600 dark:text-neutral-400">Loading members...</p>
<p className="text-gray-600 dark:text-gray-400">
Loading members...
</p>
) : (
<div className="space-y-3">
{members.map((member) => (
<div key={member.id} className="border dark:border-neutral-700 rounded-lg p-4 flex items-center justify-between">
<div
key={member.id}
className="border dark:border-gray-700 rounded-lg p-4 flex items-center justify-between"
>
<div className="flex items-center space-x-3">
{member.user.avatar ? (
<img
@@ -198,15 +240,23 @@ const ManageMembersPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400">👤</span>
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-gray-400">
👤
</span>
</div>
)}
<div>
<p className="font-medium text-gray-900 dark:text-white">{member.user.fullname}</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">{member.user.email}</p>
<p className="font-medium text-gray-900 dark:text-white">
{member.user.fullname}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{member.user.email}
</p>
{member.user.location && (
<p className="text-sm text-gray-500 dark:text-neutral-500">📍 {member.user.location}</p>
<p className="text-sm text-gray-500 dark:text-gray-500">
📍 {member.user.location}
</p>
)}
<div className="flex items-center space-x-2 mt-1">
{member.role === 'leader' && (
@@ -241,7 +291,8 @@ const ManageMembersPage: FC = (): ReactElement => {
{/* Warning */}
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Note:</strong> Members cannot leave the team without your approval. Only you can remove members from the team.
<strong>Note:</strong> Members cannot leave the team without your
approval. Only you can remove members from the team.
</p>
</div>
</div>
@@ -249,27 +300,30 @@ const ManageMembersPage: FC = (): ReactElement => {
{/* Invite Member Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl dark:shadow-neutral-950/50 max-w-md w-full p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl dark:shadow-gray-950/50 max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Invite Member
</h2>
<p className="text-gray-600 dark:text-neutral-400 mb-4">
Send an invitation to join your team. The invited member will see the invitation on their dashboard after logging in.
<p className="text-gray-600 dark:text-gray-400 mb-4 font-sans">
Send an invitation to join your team. The invited member will see
the invitation on their dashboard after logging in.
</p>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Important:</strong> The email you enter must match the GitHub email address the member uses to sign in.
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-sans">
<strong>Important:</strong> The email you enter must match the
GitHub email address the member uses to sign in.
</p>
</div>
<form onSubmit={handleInvite} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Address
</label>
<Input
{...form.register('email')}
type="email"
placeholder="member@example.com"
size="lg"
/>
{form.formState.errors.email && (
<p className="text-sm text-red-500 mt-1">
+127 -97
View File
@@ -11,6 +11,7 @@ import {
useAuthStore,
} from '@imphnen-frontend-service/service';
import { toast } from 'sonner';
import { Icon } from '@iconify/react';
const MAX_TEAM_MEMBERS = 5;
@@ -140,18 +141,18 @@ const TeamDashboardPage: FC = (): ReactElement => {
if (isLoadingTeam || isLoadingMembers) {
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
{/* Skeleton Header */}
<div className="bg-white border-b">
<div className="w-full h-48 bg-gray-200 animate-pulse" />
<div className="bg-white dark:bg-gray-950 border-b">
<div className="w-full h-48 bg-gray-200 dark:bg-gray-800 animate-pulse" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-4">
<div className="w-20 h-20 rounded-full bg-gray-300 animate-pulse -mt-10" />
<div className="w-20 h-20 rounded-full bg-gray-300 dark:bg-gray-700 animate-pulse -mt-10" />
<div>
<div className="h-8 w-48 bg-gray-300 rounded animate-pulse" />
<div className="h-4 w-32 bg-gray-200 rounded animate-pulse mt-2" />
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mt-2" />
<div className="h-8 w-48 bg-gray-300 dark:bg-gray-700 rounded animate-pulse" />
<div className="h-4 w-32 bg-gray-200 dark:bg-gray-800 rounded animate-pulse mt-2" />
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-800 rounded animate-pulse mt-2" />
</div>
</div>
</div>
@@ -162,27 +163,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
<div className="grid gap-6 lg:grid-cols-3">
{/* Main Content Skeleton */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="h-6 w-32 bg-gray-300 rounded animate-pulse mb-4" />
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6">
<div className="h-6 w-32 bg-gray-300 dark:bg-gray-700 rounded animate-pulse mb-4" />
<div className="space-y-2">
<div className="h-4 w-full bg-gray-200 rounded animate-pulse" />
<div className="h-4 w-3/4 bg-gray-200 rounded animate-pulse" />
<div className="h-4 w-5/6 bg-gray-200 rounded animate-pulse" />
<div className="h-4 w-full bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
<div className="h-4 w-3/4 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
<div className="h-4 w-5/6 bg-gray-200 dark:bg-gray-800 rounded animate-pulse" />
</div>
</div>
</div>
{/* Sidebar Skeleton */}
<div className="space-y-6">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="h-6 w-40 bg-gray-300 rounded animate-pulse mb-4" />
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6">
<div className="h-6 w-40 bg-gray-300 dark:bg-gray-700 rounded animate-pulse mb-4" />
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full bg-gray-300 animate-pulse" />
<div className="w-12 h-12 rounded-full bg-gray-300 dark:bg-gray-700 animate-pulse" />
<div>
<div className="h-4 w-24 bg-gray-300 rounded animate-pulse" />
<div className="h-3 w-16 bg-gray-200 rounded animate-pulse mt-1" />
<div className="h-4 w-24 bg-gray-300 dark:bg-gray-700 rounded animate-pulse" />
<div className="h-3 w-16 bg-gray-200 dark:bg-gray-800 rounded animate-pulse mt-1" />
</div>
</div>
))}
@@ -196,7 +197,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<div className="fixed inset-0 bg-white/60 flex items-center justify-center z-50">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-blue-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600 dark:text-neutral-400">
<p className="mt-4 text-gray-600 dark:text-white">
Loading team data...
</p>
</div>
@@ -219,7 +220,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
}
return (
<div className="min-h-screen bg-gray-50 relative dark:bg-neutral-950">
<div className="min-h-screen bg-gray-50 relative dark:bg-gray-950">
{/* Loading overlay while images are loading */}
{!imagesLoaded && totalImagesToLoad > 0 && (
<div className="fixed inset-0 bg-white/80 flex items-center justify-center z-50">
@@ -234,9 +235,9 @@ const TeamDashboardPage: FC = (): ReactElement => {
)}
{/* Header with Banner */}
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700">
{team.banner && (
<div className="w-full h-32 md:h-48 overflow-hidden relative">
<div className="relative w-full aspect-3/1 overflow-hidden">
<div
className={`absolute inset-0 bg-gray-200 animate-pulse ${
imagesLoaded ? 'hidden' : ''
@@ -245,26 +246,27 @@ const TeamDashboardPage: FC = (): ReactElement => {
<img
src={team.banner}
alt={team.name}
className={`w-full h-full object-cover transition-opacity duration-300 ${
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${
imagesLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={handleImageLoad}
onError={handleImageError}
/>
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-transparent via-black/20 to-black/60" />
</div>
)}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-6">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3 md:space-x-4">
{team.logo && (
<div className="relative">
<div className="relative shrink-0">
{!imagesLoaded && (
<div className="absolute inset-0 w-20 h-20 rounded-full bg-gray-300 animate-pulse -mt-10" />
<div className="absolute inset-0 w-20 h-20 rounded-full bg-gray-300 dark:bg-gray-700 animate-pulse -mt-10" />
)}
<img
src={team.logo}
alt={team.name}
className={`w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white shadow-lg -mt-8 md:-mt-10 transition-opacity duration-300 ${
className={`w-16 h-16 md:w-20 md:h-20 rounded-full object-cover border-4 border-white shadow-lg -mt-8 md:-mt-10 transition-opacity duration-300 shrink-0 ${
imagesLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={handleImageLoad}
@@ -276,20 +278,32 @@ const TeamDashboardPage: FC = (): ReactElement => {
<h1 className="text-xl md:text-3xl font-bold text-gray-900 dark:text-white line-clamp-2">
{team.name}
</h1>
<p className="text-sm md:text-base text-gray-600 dark:text-neutral-400 mt-1 line-clamp-1">
📍 {team.city}
<p className="text-sm md:text-base text-gray-600 dark:text-gray-400 mt-1 line-clamp-1 font-sans flex items-center">
<Icon icon="mdi:map-marker" className="inline-block mr-1" />
<span>{team.city}</span>
</p>
<div className="flex flex-wrap items-center gap-2 md:space-x-4 mt-2">
<span className="text-sm text-gray-500 dark:text-neutral-400">
<span className="text-sm text-gray-500 dark:text-gray-400 flex items-center">
<Icon
icon="mdi:account-multiple"
className="inline-block mr-1"
/>
{members.length}{' '}
{members.length === 1 ? 'Member' : 'Members'}
</span>
<span className="text-sm text-gray-500 dark:text-neutral-400">
{team.visibility === 'public' ? '🌐 Public' : '🔒 Private'}
<span className="text-sm text-gray-500 dark:text-gray-400 flex items-center">
<Icon
icon={
team.visibility === 'public' ? 'mdi:web' : 'mdi:lock'
}
className="inline-block mr-1"
/>
{team.visibility === 'public' ? 'Public' : 'Private'}
</span>
{isLeader && (
<span className="hidden md:inline-flex px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-semibold shadow-sm">
👑 Team Leader
<span className="hidden md:inline-flex px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-semibold shadow-sm items-center">
<Icon icon="mdi:crown" className="inline-block mr-1" />
<span>Team Leader</span>
</span>
)}
</div>
@@ -300,22 +314,22 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 md:py-8">
<div className="grid gap-4 md:gap-6 lg:grid-cols-3">
<div className="grid gap-4 md:gap-6 xl:grid-cols-3">
{/* Main Content */}
<div className="lg:col-span-2 space-y-4 md:space-y-6">
{/* Team Description */}
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
About Team
</h2>
<p className="text-gray-700 dark:text-neutral-300 whitespace-pre-wrap">
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-all font-sans">
{team.description}
</p>
</div>
{/* Team Actions - Only for Leader */}
{isLeader && (
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Team Management
</h2>
@@ -326,7 +340,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
onClick={() => setShowInviteModal(true)}
disabled={!canInvite}
>
Invite Member{' '}
<Icon icon="mdi:plus" className="inline-block mr-2" />
Invite Member{' '}
{!canInvite && `(${members.length}/${MAX_TEAM_MEMBERS})`}
</Button>
<Button
@@ -334,7 +349,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
variant="secondary"
onClick={() => setShowJoinRequestsModal(true)}
>
📩 Join Requests
<Icon icon="mdi:email" className="inline-block mr-2" /> Join
Requests
{pendingJoinRequests.length > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center">
{pendingJoinRequests.length}
@@ -343,25 +359,34 @@ const TeamDashboardPage: FC = (): ReactElement => {
</Button>
<Link to={`/teams/${teamId}/edit`}>
<Button className="w-full" variant="secondary">
Edit Team Info
<Icon icon="mdi:pencil" className="inline-block mr-2" />
Edit Team Info
</Button>
</Link>
<Link to={`/teams/${teamId}/members`}>
<Button className="w-full" variant="secondary">
👥 Manage Members
<Icon
icon="mdi:account-group"
className="inline-block mr-2"
/>
Manage Members
</Button>
</Link>
<Link to={`/teams/${teamId}/chat`}>
<Button className="w-full" variant="secondary">
💬 Team Chat
<Icon icon="mdi:chat" className="inline-block mr-2" />
Team Chat
</Button>
</Link>
<Link to={`/teams/${teamId}/submit`}>
<Button className="w-full">🚀 Submit Project</Button>
<Button className="w-full">
<Icon icon="mdi:rocket" className="inline-block mr-2" />
Submit Project
</Button>
</Link>
</div>
{!canInvite && members.length >= MAX_TEAM_MEMBERS && (
<p className="text-sm text-gray-600 dark:text-neutral-400 mt-3 text-center">
<p className="text-sm text-gray-600 dark:text-gray-400 mt-3 text-center">
Maximum team size reached ({MAX_TEAM_MEMBERS} members)
</p>
)}
@@ -370,7 +395,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Quick Actions for Members (non-leaders) */}
{!isLeader && isMember && (
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 md:p-6">
<h2 className="text-lg md:text-xl font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Quick Actions
</h2>
@@ -417,14 +442,14 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Sidebar */}
<div className="space-y-4 md:space-y-6">
{/* Team Leader */}
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Team Leader
</h3>
{team.leader && (
<button
onClick={() => navigate(`/users/${team.leader.id}`)}
className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
>
{team.leader.avatar ? (
<img
@@ -433,8 +458,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400">
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-gray-400">
👤
</span>
</div>
@@ -443,7 +468,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<p className="font-medium text-gray-900 dark:text-white">
{team.leader.fullname}
</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">
<p className="text-sm text-gray-600 dark:text-gray-400">
{team.leader.email}
</p>
</div>
@@ -452,7 +477,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
</div>
{/* Team Members */}
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-md p-4 md:p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 md:p-6">
<h3 className="text-base md:text-lg font-bold text-gray-900 dark:text-white mb-3 md:mb-4">
Members ({members.length})
</h3>
@@ -461,7 +486,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<button
key={member.id}
onClick={() => navigate(`/users/${member.user.id}`)}
className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
className="w-full flex items-center space-x-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg p-2 transition-colors text-left cursor-pointer"
>
{member.user?.avatar ? (
<img
@@ -472,8 +497,8 @@ const TeamDashboardPage: FC = (): ReactElement => {
onError={handleImageError}
/>
) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-neutral-400 text-sm">
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-gray-500 dark:text-gray-400 text-sm">
👤
</span>
</div>
@@ -482,7 +507,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<p className="font-medium text-gray-900 dark:text-white truncate">
{member.user?.fullname || 'Unknown'}
</p>
<p className="text-xs text-gray-500 dark:text-neutral-400">
<p className="text-xs text-gray-500 dark:text-gray-400">
{member.role === ETeamMemberRole.LEADER
? 'Leader'
: 'Member'}
@@ -499,16 +524,16 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Invite Member Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black/20 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-md w-full p-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Invite Team Member
</h2>
<p className="text-gray-600 dark:text-neutral-400 mb-4">
<p className="text-gray-600 dark:text-gray-400 mb-4 font-sans">
Send an invitation to join your team. The invited member will see
the invitation on their dashboard after logging in.
</p>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-6">
<p className="text-sm text-yellow-800 dark:text-yellow-300">
<p className="text-sm text-yellow-800 dark:text-yellow-300 font-sans">
<strong>Important:</strong> The email you enter must match the
GitHub email address the member uses to sign in.
</p>
@@ -517,7 +542,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
<div>
<label
htmlFor="invite-email"
className="block text-sm font-medium text-gray-700 dark:text-neutral-300 mb-2"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Email Address
</label>
@@ -527,10 +552,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Enter email address..."
className="w-full px-3 py-2 border border-gray-300 dark:border-neutral-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-neutral-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500"
required
/>
<p className="text-sm text-gray-500 dark:text-neutral-400 mt-1">
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Current members: {members.length}/{MAX_TEAM_MEMBERS}
</p>
</div>
@@ -562,14 +587,14 @@ const TeamDashboardPage: FC = (): ReactElement => {
{/* Join Requests Modal */}
{showJoinRequestsModal && (
<div className="fixed inset-0 bg-black/30 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full p-6 max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Join Requests ({pendingJoinRequests.length})
</h2>
<button
onClick={() => setShowJoinRequestsModal(false)}
className="text-gray-400 dark:text-neutral-500 hover:text-gray-600 dark:hover:text-neutral-300 text-2xl"
className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 text-2xl cursor-pointer"
>
</button>
@@ -577,10 +602,10 @@ const TeamDashboardPage: FC = (): ReactElement => {
{pendingJoinRequests.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600 dark:text-neutral-400 text-lg">
<p className="text-gray-600 dark:text-gray-400 text-lg">
No pending join requests
</p>
<p className="text-gray-500 dark:text-neutral-500 text-sm mt-2">
<p className="text-gray-500 dark:text-gray-500 text-sm mt-2 font-sans">
When users request to join your team, they'll appear here
</p>
</div>
@@ -589,7 +614,7 @@ const TeamDashboardPage: FC = (): ReactElement => {
{pendingJoinRequests.map((request: any) => (
<div
key={request.id}
className="border border-gray-200 dark:border-neutral-700 rounded-lg p-4 hover:shadow-md transition-shadow"
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-3 flex-1">
@@ -600,9 +625,12 @@ const TeamDashboardPage: FC = (): ReactElement => {
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-neutral-700 flex items-center justify-center shrink-0">
<span className="text-gray-500 dark:text-neutral-400 text-xl">
👤
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center shrink-0">
<span className="text-gray-500 dark:text-gray-400 text-xl">
<Icon
icon="mdi:account-circle"
className="text-3xl"
/>
</span>
</div>
)}
@@ -610,49 +638,51 @@ const TeamDashboardPage: FC = (): ReactElement => {
<p className="font-semibold text-gray-900 dark:text-white">
{request.user?.fullname || 'Unknown User'}
</p>
<p className="text-sm text-gray-600 dark:text-neutral-400">
<p className="text-sm text-gray-600 dark:text-gray-400">
{request.user?.email}
</p>
{request.message && (
<div className="mt-2 bg-gray-50 dark:bg-neutral-800 rounded-lg p-3">
<p className="text-sm text-gray-700 dark:text-neutral-300">
<div className="mt-2 bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<p className="text-sm text-gray-700 dark:text-gray-300">
<strong>Message:</strong> {request.message}
</p>
</div>
)}
<p className="text-xs text-gray-500 dark:text-neutral-500 mt-2">
<p className="text-sm text-gray-500 dark:text-gray-500 mt-2 font-sans">
Requested{' '}
{new Date(request.created_at).toLocaleDateString()}
</p>
</div>
</div>
<div className="flex space-x-2 ml-4">
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'approve')
}
disabled={
isResponding || members.length >= MAX_TEAM_MEMBERS
}
className="px-4 py-2 text-sm"
>
✓ Accept
</Button>
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'reject')
}
disabled={isResponding}
variant="secondary"
className="px-4 py-2 text-sm"
>
✕ Reject
</Button>
</div>
</div>
<div className="flex space-x-2 mt-4 justify-end">
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'approve')
}
disabled={
isResponding || members.length >= MAX_TEAM_MEMBERS
}
className="px-4 py-2 text-sm"
>
Accept
</Button>
<Button
onClick={() =>
handleRespondToJoinRequest(request.id, 'reject')
}
disabled={isResponding}
variant="secondary"
className="px-4 py-2 text-sm"
>
Reject
</Button>
</div>
{members.length >= MAX_TEAM_MEMBERS && (
<div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-2">
<p className="text-xs text-yellow-800 dark:text-yellow-300">
<div className="mt-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3">
<p className="text-sm text-yellow-800 dark:text-yellow-300 font-sans">
Team is full ({MAX_TEAM_MEMBERS}/{MAX_TEAM_MEMBERS}{' '}
members). Remove a member before accepting new
requests.
@@ -23,7 +23,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
const { data: teamData } = useTeamById(teamId || '');
const { data: submissionData } = useTeamSubmission(teamId || '', !!teamId);
const { mutateAsync: submitProject, isPending: isSubmitting } = useSubmitProject(teamId || '');
const { mutateAsync: submitProject, isPending: isSubmitting } =
useSubmitProject(teamId || '');
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const team = teamData?.data;
@@ -38,25 +39,38 @@ const SubmitProjectPage: FC = (): ReactElement => {
if (!isLeader) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Access Denied</h2>
<p className="text-gray-600 dark:text-neutral-400 mb-4">Only the team leader can submit projects</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>Back to Team</Button>
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Access Denied
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Only the team leader can submit projects
</p>
<Button onClick={() => navigate(`/teams/${teamId}`)}>
Back to Team
</Button>
</div>
);
}
if (hasSubmission) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-6xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Project Already Submitted</h2>
<p className="text-gray-600 dark:text-neutral-400 mb-4">Your team has already submitted a project</p>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Project Already Submitted
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Your team has already submitted a project
</p>
<div className="flex space-x-3">
<Button onClick={() => navigate(`/teams/${teamId}/submission`)}>
View Submission
</Button>
<Button variant="secondary" onClick={() => navigate(`/teams/${teamId}`)}>
<Button
variant="secondary"
onClick={() => navigate(`/teams/${teamId}`)}
>
Back to Team
</Button>
</div>
@@ -64,14 +78,16 @@ const SubmitProjectPage: FC = (): ReactElement => {
);
}
const handleScreenshotUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const handleScreenshotUpload = async (
e: React.ChangeEvent<HTMLInputElement>
) => {
const files = e.target.files;
if (!files) return;
try {
const uploadPromises = Array.from(files).map(file => uploadFile(file));
const uploadPromises = Array.from(files).map((file) => uploadFile(file));
const results = await Promise.all(uploadPromises);
const urls = results.map(r => r.data.url);
const urls = results.map((r) => r.data.url);
setScreenshots([...screenshots, ...urls]);
} catch (error) {
console.error('Failed to upload screenshots:', error);
@@ -95,11 +111,13 @@ const SubmitProjectPage: FC = (): ReactElement => {
});
return (
<div className="min-h-screen bg-gray-50 dark:bg-neutral-950">
<div className="bg-white dark:bg-neutral-900 border-b dark:border-neutral-700">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">Submit Project</h1>
<p className="text-gray-600 dark:text-neutral-400 mt-1">{team?.name}</p>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Submit Project
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">{team?.name}</p>
</div>
</div>
@@ -109,33 +127,42 @@ const SubmitProjectPage: FC = (): ReactElement => {
<div className="flex items-start space-x-3">
<span className="text-3xl"></span>
<div>
<h3 className="font-bold text-red-900 dark:text-red-100 text-lg">IMPORTANT WARNING</h3>
<ul className="text-red-800 dark:text-red-200 mt-2 space-y-1 text-sm">
<h3 className="font-bold text-red-900 dark:text-red-100 text-lg">
IMPORTANT WARNING
</h3>
<ul className="text-red-800 dark:text-red-200 mt-2 space-y-1 text-sm font-sans">
<li> You can only submit your project ONCE</li>
<li> After submission, you CANNOT edit or change anything</li>
<li> Make sure all information is correct before submitting</li>
<li>
Make sure all information is correct before submitting
</li>
<li> Review your project details carefully</li>
</ul>
</div>
</div>
</div>
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-md dark:shadow-neutral-950/50 p-8">
<form onSubmit={(e) => {
e.preventDefault();
setShowConfirmModal(true);
}} className="space-y-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-md dark:shadow-gray-950/50 p-8 border dark:border-gray-800">
<form
onSubmit={(e) => {
e.preventDefault();
setShowConfirmModal(true);
}}
className="space-y-6"
>
{/* Project Name */}
<ControlledInputField
control={form.control}
label="Project Name"
placeholder="Enter your project name"
name="project_name"
size="lg"
isRequired={true}
/>
{/* Description */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
<label className="block text-[15px] font-medium text-gray-700 dark:text-gray-300">
Project Description <span className="text-red-500">*</span>
</label>
<Controller
@@ -148,9 +175,12 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="Describe your project, its features, and what problem it solves..."
rows={6}
className="w-full"
size="lg"
/>
{fieldState.error && (
<p className="text-sm text-red-500 mt-1">{fieldState.error.message}</p>
<p className="text-sm text-red-500 mt-1">
{fieldState.error.message}
</p>
)}
</div>
)}
@@ -164,6 +194,8 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="https://github.com/username/project"
name="repository_url"
type="url"
size="lg"
isRequired={true}
/>
{/* Demo URL */}
@@ -173,6 +205,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="https://your-project-demo.com"
name="demo_url"
type="url"
size="lg"
/>
{/* Presentation URL */}
@@ -182,12 +215,13 @@ const SubmitProjectPage: FC = (): ReactElement => {
placeholder="https://slides.com/your-presentation or Google Drive link"
name="presentation_url"
type="url"
size="lg"
/>
{/* Screenshots */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-neutral-300">
Project Screenshots <span className="text-gray-400 dark:text-neutral-500">(Optional)</span>
<div>
<label className="block text-label1 font-medium text-neutral-800 dark:text-gray-300">
Project Screenshots (Optional)
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-4">
{screenshots.map((url, index) => (
@@ -195,7 +229,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
<img
src={url}
alt={`Screenshot ${index + 1}`}
className="w-full h-32 object-cover rounded-lg border-2 border-gray-200 dark:border-neutral-700"
className="w-full h-32 object-cover rounded-lg border-2 border-gray-200 dark:border-gray-700"
/>
<button
type="button"
@@ -207,10 +241,14 @@ const SubmitProjectPage: FC = (): ReactElement => {
</div>
))}
</div>
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 dark:border-neutral-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-neutral-800">
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800">
<div className="text-center">
<p className="text-gray-500 dark:text-neutral-400">Click to upload screenshots</p>
<p className="text-xs text-gray-400 dark:text-neutral-500 mt-1">PNG, JPG up to 5MB each</p>
<p className="text-gray-500 dark:text-gray-400">
Click to upload screenshots
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 font-sans">
PNG, JPG up to 5MB each
</p>
</div>
<input
type="file"
@@ -248,7 +286,7 @@ const SubmitProjectPage: FC = (): ReactElement => {
{/* Confirmation Modal */}
{showConfirmModal && (
<div className="fixed inset-0 bg-black/50 dark:bg-black/70 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-neutral-900 rounded-lg shadow-xl dark:shadow-neutral-950/50 max-w-md w-full p-6">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl dark:shadow-gray-950/50 max-w-md w-full p-6">
<div className="text-center mb-6">
<div className="text-5xl mb-4"></div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
@@ -258,11 +296,11 @@ const SubmitProjectPage: FC = (): ReactElement => {
This action is IRREVERSIBLE!
</p>
</div>
<div className="bg-gray-50 dark:bg-neutral-800 rounded-lg p-4 mb-6">
<p className="text-sm text-gray-700 dark:text-neutral-300 mb-3">
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-6">
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
By clicking "Submit Project", you confirm that:
</p>
<ul className="text-sm text-gray-600 dark:text-neutral-400 space-y-2">
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-2">
<li> All information is correct and complete</li>
<li> You understand this can only be done once</li>
<li> You cannot edit after submission</li>
@@ -62,6 +62,12 @@ export const Sidebar: FC<SidebarProps> = ({ isOpen = true, onClose }) => {
icon: <Icon icon="heroicons:home" className="w-5 h-5" />,
show: true,
},
{
name: 'My Teams',
path: '/teams/' + myTeams[0]?.id,
icon: <Icon icon="heroicons:users" className="w-5 h-5" />,
show: myTeams.length > 0,
},
{
name: 'Browse Teams',
path: '/teams/browse',