From 34708538589fae144e498f88235b8bc8331ba6b4 Mon Sep 17 00:00:00 2001 From: Hafid Nur <73023445+hafidnrzs@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:11:35 +0700 Subject: [PATCH] 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 --- apps/hackathon/src/app/404.tsx | 6 +- .../src/app/teams/[teamId]/chat/page.tsx | 174 +++++++++++--- .../src/app/teams/[teamId]/edit/page.tsx | 117 ++++++--- .../src/app/teams/[teamId]/layout.tsx | 26 +- .../src/app/teams/[teamId]/members/page.tsx | 136 +++++++---- .../hackathon/src/app/teams/[teamId]/page.tsx | 224 ++++++++++-------- .../src/app/teams/[teamId]/submit/page.tsx | 112 ++++++--- apps/hackathon/src/components/sidebar.tsx | 6 + libs/ui/src/atoms/input/input.tsx | 2 +- libs/ui/src/atoms/textarea/textarea.tsx | 2 +- .../src/molecules/input-field/input-field.tsx | 2 +- 11 files changed, 548 insertions(+), 259 deletions(-) diff --git a/apps/hackathon/src/app/404.tsx b/apps/hackathon/src/app/404.tsx index 2db4df6..3a2e703 100644 --- a/apps/hackathon/src/app/404.tsx +++ b/apps/hackathon/src/app/404.tsx @@ -2,13 +2,13 @@ import { Link } from 'react-router-dom'; export default function NotFoundPage() { return ( -
+

404

-

+

Page Not Found

-

+

The page you are looking for doesn't exist or has been moved.

{ 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(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(); + 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 ( -
+
{/* Header */} -
+
-

Team Chat

-

{team?.name}

+

+ Team Chat +

+

+ {team?.name} +

-
@@ -86,21 +137,27 @@ const TeamChatPage: FC = (): ReactElement => {
- ) : messages && messages.length > 0 ? ( + ) : displayMessages && displayMessages.length > 0 ? (
- {messages.map((msg) => { + {displayMessages.map((msg) => { const isOwnMessage = msg.user_id === currentUserId; const isLeader = team?.leader_id === currentUserId; const canDelete = isOwnMessage || isLeader; return (
-
+
{/* Avatar */} -
+
{msg.user?.avatar ? ( {
{/* Message Bubble */} -
-
+
+
{isOwnMessage ? 'You' : msg.user?.fullname} - + {formatTime(msg.created_at)}
@@ -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' }`} > -

{msg.message}

+

+ {msg.message} +

{/* Delete button */} {canDelete && ( )}
+ {canDelete && deleteTargetId === msg.id && ( +
+ + +
+ )}
@@ -157,11 +252,14 @@ const TeamChatPage: FC = (): ReactElement => {
) : (
-
💬
+

No messages yet

-

+

Be the first to start the conversation!

@@ -170,7 +268,7 @@ const TeamChatPage: FC = (): ReactElement => {
{/* Message Input */} -
+
{ 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} />
) : (
- - + + Send
diff --git a/apps/hackathon/src/app/teams/[teamId]/edit/page.tsx b/apps/hackathon/src/app/teams/[teamId]/edit/page.tsx index 92cb574..a6170f0 100644 --- a/apps/hackathon/src/app/teams/[teamId]/edit/page.tsx +++ b/apps/hackathon/src/app/teams/[teamId]/edit/page.tsx @@ -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(null); const [bannerPreview, setBannerPreview] = useState(''); - 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 ( -
-
Loading team...
+
+
Loading team...
); } if (!isLeader) { return ( -
-

Access Denied

-

Only the team leader can edit team information

- +
+

+ Access Denied +

+

+ Only the team leader can edit team information +

+
); } @@ -115,28 +134,35 @@ const EditTeamPage: FC = (): ReactElement => { }); return ( -
-
+
+
-

Edit Team Info

-

Update your team details

+

+ Edit Team Info +

+

+ Update your team details +

-
+
{/* Banner Upload */}
-