All files / src/components/pages/notification/notification-card index.tsx

0% Statements 0/153
0% Branches 0/1
0% Functions 0/1
0% Lines 0/153

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154                                                                                                                                                                                                                                                                                                                   
import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';
import { Toast } from '@/components/ui';
import { useToast } from '@/components/ui/toast/core';
import { useUpdateNotificationRead } from '@/hooks/use-notification/use-notification-update-read';
import { formatTimeAgo } from '@/lib/formatDateTime';
import { cn } from '@/lib/utils';
import { NotificationItem, NotificationType } from '@/types/service/notification';

interface Props {
  item: NotificationItem;
}

export const NotificationCard = ({ item }: Props) => {
  const router = useRouter();
  const { mutateAsync } = useUpdateNotificationRead();
  const { run } = useToast();

  const NotificationIcon = IconMap[item.type];
  const title = getTitle(item);
  const description = getDescription(item);
  const timeAgo = getTimeAgo(item);
  const redirectUrl = getRedirectUrl(item);

  const handleNotificationClick = () => {
    try {
      mutateAsync(item.id);
      if (redirectUrl) {
        router.push(redirectUrl);
      } else {
        run(<Toast>이미 마감되었거나 삭제된 모임입니다.</Toast>);
      }
    } catch {}
  };

  const handleNotificationHover = () => {
    if (!redirectUrl) return;
    router.prefetch(redirectUrl);
  };

  return (
    <article
      className={cn(
        'bg-mono-white flex cursor-pointer flex-row gap-3 px-5 py-6',
        !item.readAt && 'bg-mint-50',
      )}
      onClick={handleNotificationClick}
      onMouseEnter={handleNotificationHover}
    >
      <div className={cn('flex-center mt-0.5 size-10 shrink-0 rounded-xl bg-gray-100')}>
        {NotificationIcon}
      </div>
      <div className='w-full'>
        <div className='flex flex-row justify-between'>
          <p className='text-text-md-bold mb-1 text-gray-800'>{title}</p>
          <span className='text-text-xs-medium text-gray-500'>{timeAgo}</span>
        </div>
        <p className='text-gray-600'>{description}</p>
      </div>
    </article>
  );
};

const IconMap: Record<NotificationType, React.ReactNode> = {
  FOLLOW: <Icon id='heart' className='text-mint-500 size-6' />,
  GROUP_JOIN: <Icon id='symbol' className='text-mint-500 size-6' />,
  GROUP_LEAVE: <Icon id='x-2' className='size-6 text-gray-500' />,
  GROUP_CREATE: <Icon id='map-pin-2' className='size-6 text-[#FFBA1A]' />,
  GROUP_DELETE: <Icon id='x-2' className='size-6 text-gray-500' />,
  GROUP_JOIN_REQUEST: <Icon id='send' className='text-mint-500 size-6' />,
  GROUP_JOIN_APPROVED: <Icon id='congratulate' className='size-6' />,
  GROUP_JOIN_REJECTED: <Icon id='kick' className='size-6' />,
  GROUP_JOIN_KICKED: <Icon id='kick' className='size-6' />,
};

const getTitle = (data: NotificationItem) => {
  switch (data.type) {
    case 'FOLLOW':
      return `새 팔로워`;
    case 'GROUP_JOIN':
      return `모임 현황`;
    case 'GROUP_LEAVE':
      return `모임 현황`;
    case 'GROUP_CREATE':
      return `모임 생성`;
    case 'GROUP_DELETE':
      return `모임 취소`;
    case 'GROUP_JOIN_REQUEST':
      return `모임 참여 신청`;
    case 'GROUP_JOIN_APPROVED':
      return `모임 신청 승인`;
    case 'GROUP_JOIN_REJECTED':
      return `모임 신청 거절`;
    case 'GROUP_JOIN_KICKED':
      return `모임 강퇴`;
  }
};

const getDescription = (data: NotificationItem) => {
  // user type 알림
  switch (data.type) {
    case 'FOLLOW':
      return `${data.user.nickname} 님이 팔로우했어요.`;
  }

  // group type 알림
  // 알림 필드 type 변경 전 데이터는 group 필드가 null로 조회되므로 fallback 처리
  if (!data.group) return data.message;

  // group 필드가 null이 아닐 경우
  switch (data.type) {
    case 'GROUP_JOIN':
      return `${data.user.nickname} 님이 "${data.group.title}" 모임에 참여했어요.`;
    case 'GROUP_LEAVE':
      return `${data.user.nickname} 님이 "${data.group.title}" 모임을 탈퇴했어요.`;
    case 'GROUP_CREATE':
      return `${data.user.nickname} 님이 "${data.group.title}" 모임을 생성했어요.`;
    case 'GROUP_DELETE':
      return `${data.user.nickname} 님이 "${data.group.title}" 모임을 취소했어요.`;
    case 'GROUP_JOIN_REQUEST':
      return `${data.user.nickname} 님이 "${data.group.title}" 모임에 참여를 요청했어요.`;
    case 'GROUP_JOIN_APPROVED':
      return `"${data.group.title}" 모임 참여 신청이 승인됐어요.`;
    case 'GROUP_JOIN_REJECTED':
      return `"${data.group.title}" 모임 참여 신청이 거절됐어요.`;
    case 'GROUP_JOIN_KICKED':
      return `"${data.group.title}" 모임에서 퇴장됐어요.`;
  }
};

const getTimeAgo = (data: NotificationItem) => {
  const { createdAt } = data;
  return formatTimeAgo(createdAt);
};

const getRedirectUrl = (data: NotificationItem) => {
  // user type 알림
  switch (data.type) {
    case 'FOLLOW':
      return `/profile/${data.user.id}`;
  }

  // 알림 필드 type 변경 전 데이터는 group 필드가 null로 조회되므로 fallback 처리
  if (!data.group) return null;

  switch (data.type) {
    case 'GROUP_JOIN_REQUEST':
      return `/pending/${data.group.id}`;
    default:
      return `/group/${data.group.id}`;
  }
};