Skip to content

Commit

Permalink
Merge pull request #379 from gsainfoteam/342-feature-push-noti-is-now…
Browse files Browse the repository at this point in the history
…-delayed-15-min
  • Loading branch information
enc2586 authored Nov 18, 2024
2 parents 9912cc1 + 10a46d1 commit 257c5f1
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 7 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"editor.formatOnSave": false,
"cSpell.words": [
"Swal",
"Zabo",
"Ziggle"
],
Expand Down
1 change: 1 addition & 0 deletions src/api/notice/notice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface Notice {
uuid: string;
};
createdAt: dayjs.Dayjs | string;
publishedAt: dayjs.Dayjs | string;
tags: string[];
views: number;
imageUrls: string[];
Expand Down
5 changes: 5 additions & 0 deletions src/api/notice/send-alarm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ziggleApi } from '..';
import type { Notice } from './notice';

export const sendNoticeAlarm = ({ id }: Pick<Notice, 'id'>) =>
ziggleApi.post(`/notice/${id}/alarm`).then((res) => res.data);
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
'use client';

import { useSession } from 'next-auth/react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Swal from 'sweetalert2';

import { NoticeDetail } from '@/api/notice/notice';
import { sendNoticeAlarm } from '@/api/notice/send-alarm';
import { PropsWithLng } from '@/app/i18next';
import { useTranslation } from '@/app/i18next/client';

import { getTimeDiff } from './getTimeDiff';

interface SendPushAlarmProps
extends Pick<NoticeDetail, 'id' | 'author' | 'publishedAt'> {}

const SendPushAlarm = ({
id,
author,
publishedAt,
lng,
}: PropsWithLng<SendPushAlarmProps>): JSX.Element | null => {
const { t } = useTranslation(lng);

const [isManuallyAlarmed, setIsManuallyAlarmed] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const handleSendPushNotification = useCallback(async () => {
if (isLoading) return;
const result = await Swal.fire({
text: t('write.alerts.sendPushNotice'),
icon: 'question',
showCancelButton: true,
confirmButtonText: t('alertResponse.confirm'),
cancelButtonText: t('alertResponse.cancel'),
});

if (!result.isConfirmed) return;
setIsLoading(true);

try {
Swal.fire({
text: t('write.alerts.sendingAlarmNotice'),
icon: 'info',
showConfirmButton: false,
allowOutsideClick: false,
});

const newNotice = await sendNoticeAlarm({ id }).catch(() => null);

if (!newNotice) throw new Error('No newNotice returned');

setIsManuallyAlarmed(true);

Swal.fire({
text: t('write.alerts.sendPushNoticeSuccess'),
icon: 'success',
confirmButtonText: t('alertResponse.confirm'),
});
} catch (error) {
Swal.fire({
text: t('write.alerts.sendPushNoticeFail'),
icon: 'error',
confirmButtonText: t('alertResponse.confirm'),
});
} finally {
setIsLoading(false);
}
}, [t, id, isLoading]);

const { data: user } = useSession();
const isMyNotice = user?.user.uuid === author.uuid;

const MINUTE = 60000;
const TEN_SECONDS = 10000;
const ONE_SECOND = 1000;

const getIntervalDuration = (minutes: number, seconds: number) => {
if (minutes > 1) return MINUTE;
if (seconds > 15) return TEN_SECONDS;
return ONE_SECOND;
};

const [timeRemaining, setTimeRemaining] = useState(getTimeDiff(publishedAt));
useEffect(() => {
let isSubscribed = true;
if (
isManuallyAlarmed ||
timeRemaining.minutes < 0 ||
timeRemaining.seconds < 0
) {
return;
}

const intervalDuration = getIntervalDuration(
timeRemaining.minutes,
timeRemaining.seconds
);

const interval = setInterval(() => {
if (isSubscribed) {
const newTimeRemaining = getTimeDiff(publishedAt);
if (
newTimeRemaining.minutes !== timeRemaining.minutes ||
newTimeRemaining.seconds !== timeRemaining.seconds
) {
setTimeRemaining(newTimeRemaining);
}
}
}, intervalDuration);

return () => {
isSubscribed = false;
clearInterval(interval);
};
}, [timeRemaining, publishedAt, isManuallyAlarmed]);

const isEditable = timeRemaining.minutes >= 0 && timeRemaining.seconds >= 0;

const showComponent = useMemo(
() => isMyNotice && isEditable && !isManuallyAlarmed,
[isMyNotice, isEditable, isManuallyAlarmed],
);

return (
<div
className={`transform transition-all duration-1000 ease-in-out ${
showComponent ? 'max-h-screen' : 'max-h-0 overflow-hidden'
}`}
>
<div
className={`inline-flex w-full items-start justify-start gap-1.5 rounded-[15px] bg-[#fff4f0] px-5 py-[15px] font-normal text-primary`}
>
<span>{t('zabo.sentPushNotificationAlert.title')} </span>
<span
className="cursor-pointer font-bold underline"
onClick={handleSendPushNotification}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSendPushNotification();
}
}}
>
{t('zabo.sentPushNotificationAlert.action')}
</span>
</div>
</div>
);
};

export default SendPushAlarm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import dayjs, { Dayjs } from 'dayjs';

export const isServer = typeof window === 'undefined';

const CLIENT_SERVER_TIME_OFFSET_SECONDS = 10
export const getTimeDiff = (createdAt: Dayjs | string) => {
const currentTime = dayjs();
const diffInSeconds = dayjs(createdAt)
.subtract(CLIENT_SERVER_TIME_OFFSET_SECONDS, 'second')
.diff(currentTime, 'second');
const minutes = Math.floor(diffInSeconds / 60);
const seconds = diffInSeconds % 60;
return { minutes, seconds };
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AdditionalNotices from './AdditionalNotices';
import Content from './Content';
import ImageStack from './ImageStack';
import NoticeInfo from './NoticeInfo';
import SendPushAlarm from './SendPushNotificationAlert';

export const generateMetadata = async (
{
Expand Down Expand Up @@ -71,6 +72,8 @@ const DetailedNoticePage = async ({
</div>

<div className="flex flex-col gap-[18px] md:w-[60%]">
<SendPushAlarm {...notice} lng={lng} />

<NoticeInfo
{...notice}
currentDeadline={notice.currentDeadline ?? null}
Expand Down
6 changes: 6 additions & 0 deletions src/app/[lng]/write/NoticeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ const NoticeEditor = ({
const handleSubmit = async () => {
if (isLoading) return;

await Swal.fire({
text: t('write.alerts.pushWillDelayedNotice'),
icon: 'info',
confirmButtonText: t('alertResponse.confirm'),
});

setIsLoading(true);
const noticeId = await handleNoticeSubmit({
title: state.korean.title,
Expand Down
10 changes: 5 additions & 5 deletions src/app/[lng]/write/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import 'react-calendar/dist/Calendar.css';
import 'react-clock/dist/Clock.css';
import 'react-datetime-picker/dist/DateTimePicker.css';

import { notFound, redirect } from 'next/navigation';

import { auth } from '@/api/auth/auth';
Expand All @@ -6,10 +10,6 @@ import { createTranslation, PropsWithLng } from '@/app/i18next';

import NoticeEditor from './NoticeEditor';

import 'react-calendar/dist/Calendar.css';
import 'react-clock/dist/Clock.css';
import 'react-datetime-picker/dist/DateTimePicker.css';

const WritePage = async ({
params: { lng },
searchParams,
Expand All @@ -32,7 +32,7 @@ const WritePage = async ({
searchParams?.noticeId && notice?.langs.includes('en')
? await getNotice(Number.parseInt(searchParams.noticeId), 'en')
: null;
if ((searchParams?.noticeId) && !notice) notFound();
if (searchParams?.noticeId && !notice) notFound();

const isAuthor = userData.user.uuid === notice?.author.uuid;
if (notice && !isAuthor) redirect(`/${lng}/notice/${notice.id}`);
Expand Down
12 changes: 11 additions & 1 deletion src/app/i18next/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@
"writeTitle": "Write a title of English notice here"
},

"sentPushNotificationAlert": {
"title": "You can't edit once you send a notification.",
"action": "Send Push Notification"
},

"authorActions": {
"edit": "Edit / Add English Version",
"remove": "Remove",
Expand Down Expand Up @@ -324,7 +329,12 @@
"attachAdditionalNoticeFail": "Failed to post additional notice. Please try posting it again.",
"copyAdditionalNotice": "Copy additional notice",
"attachInternationalAdditionalNoticeFail": "Failed to post English additional notice. Please try posting it again.",
"copyInternationalAdditionalNotice": "Copy En. add. notice"
"copyInternationalAdditionalNotice": "Copy En. add. notice",
"pushWillDelayedNotice": "Users will receive a push notification after 15 minutes. If it's urgent, you can use Send Notification button in your notice page.",
"sendPushNotice": "Notifications will be sent to all users. Are you sure you want to send them now?",
"sendingAlarmNotice":"Sending notifications...",
"sendPushNoticeFail": "Failed to send push notification",
"sendPushNoticeSuccess": "Push notification sent successfully"
}
},
"group": {
Expand Down
12 changes: 11 additions & 1 deletion src/app/i18next/locales/ko/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@
"writeTitle": "영어 공지 제목을 입력하세요"
},

"sentPushNotificationAlert": {
"title": "알림을 보내면 수정이 불가능합니다.",
"action": "알림 보내기"
},

"authorActions": {
"edit": "수정 / 영어공지 작성",
"remove": "삭제",
Expand Down Expand Up @@ -310,7 +315,12 @@
"attachAdditionalNoticeFail": "추가 공지 작성에 실패했습니다",
"copyAdditionalNotice": "추가 공지 복사",
"attachInternationalAdditionalNoticeFail": "영어 추가 공지 작성에 실패했습니다",
"copyInternationalAdditionalNotice": "영어 추가 공지 복사"
"copyInternationalAdditionalNotice": "영어 추가 공지 복사",
"pushWillDelayedNotice": "15분 뒤에 사용자들에게 푸시알람이 전송됩니다. 긴급한 경우 내 공지 > 알림 보내기 기능을 사용하세요.",
"sendPushNotice": "모든 사용자들에게 알림이 전송됩니다. 정말 지금 알림을 보낼까요?",
"sendingAlarmNotice":"알림 보내는 중...",
"sendPushNoticeFail": "알림 전송에 실패했습니다",
"sendPushNoticeSuccess": "알림을 성공적으로 보냈습니다"
}
},
"group": {
Expand Down

0 comments on commit 257c5f1

Please sign in to comment.