diff --git a/apps/service/package.json b/apps/service/package.json index 4da5e7c8..81c5f573 100644 --- a/apps/service/package.json +++ b/apps/service/package.json @@ -10,6 +10,7 @@ "openapi": "pnpm dlx openapi-typescript https://dev.math-pointer.com/v3/api-docs --output ./src/types/api/schema.d.ts && prettier --write ./src/types/api/schema.d.ts" }, "dependencies": { + "@next/third-parties": "^15.2.4", "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "dayjs": "^1.11.13", diff --git a/apps/service/src/app/(home)/page.tsx b/apps/service/src/app/(home)/page.tsx index 0d7d7345..12884a10 100644 --- a/apps/service/src/app/(home)/page.tsx +++ b/apps/service/src/app/(home)/page.tsx @@ -1,10 +1,11 @@ 'use client'; -import Link from 'next/link'; import { Button } from '@components'; import { IcSearch } from '@svg'; import { getHomeFeed } from '@apis'; import dayjs from 'dayjs'; import { DailyProgress } from '@types'; +import { useTrackEvent } from '@hooks'; +import { useRouter } from 'next/navigation'; import { GuideButton, @@ -15,6 +16,8 @@ import { } from '@/components/home'; const Page = () => { + const router = useRouter(); + const { trackEvent } = useTrackEvent(); const { data } = getHomeFeed(); const homeFeedData = data?.data; @@ -26,6 +29,11 @@ const Page = () => { const progress: DailyProgress[] = dailyProgresses?.map((progress) => progress.progressStatus ?? 'NOT_STARTED') ?? []; + const handleClickAllProblem = () => { + trackEvent('home_all_problem_button_click'); + router.push('/problem/calandar'); + }; + return ( <> @@ -43,12 +51,10 @@ const Page = () => {
- - - +
); diff --git a/apps/service/src/app/layout.tsx b/apps/service/src/app/layout.tsx index 845903b1..c172743d 100644 --- a/apps/service/src/app/layout.tsx +++ b/apps/service/src/app/layout.tsx @@ -1,5 +1,6 @@ import { Suspense } from 'react'; import type { Metadata, Viewport } from 'next'; +import { GoogleAnalytics } from '@next/third-parties/google'; import Providers from './providers'; @@ -57,6 +58,7 @@ export default function RootLayout({ + ); diff --git a/apps/service/src/app/problem/solve/[publishId]/[problemId]/SolveButtonsClient.tsx b/apps/service/src/app/problem/solve/[publishId]/[problemId]/SolveButtonsClient.tsx index e84352cd..992ea96d 100644 --- a/apps/service/src/app/problem/solve/[publishId]/[problemId]/SolveButtonsClient.tsx +++ b/apps/service/src/app/problem/solve/[publishId]/[problemId]/SolveButtonsClient.tsx @@ -2,6 +2,7 @@ import { SolveButton } from '@components'; import { getChildData, postChildProblemSubmit, postProblemSubmit } from '@apis'; import { useRouter } from 'next/navigation'; +import { useTrackEvent } from '@hooks'; interface SolveButtonsClientProps { publishId: string; @@ -10,15 +11,18 @@ interface SolveButtonsClientProps { const SolveButtonsClient = ({ publishId, problemId }: SolveButtonsClientProps) => { const router = useRouter(); + const { trackEvent } = useTrackEvent(); const { data } = getChildData(publishId, problemId); const childProblemId = data?.data?.childProblemIds[0].toString(); const handleClickDirect = async () => { + trackEvent('problem_solve_direct_button_click'); await postProblemSubmit(publishId, problemId); router.push(`/problem/solve/${publishId}/${problemId}/main-problem`); }; const handleClickStep = async () => { + trackEvent('problem_solve_step_button_click'); await postChildProblemSubmit(publishId, problemId); router.push(`/problem/solve/${publishId}/${problemId}/child-problem/${childProblemId}`); }; diff --git a/apps/service/src/app/problem/solve/[publishId]/[problemId]/child-problem/[childProblemId]/page.tsx b/apps/service/src/app/problem/solve/[publishId]/[problemId]/child-problem/[childProblemId]/page.tsx index 03f0c32e..c3e71cf9 100644 --- a/apps/service/src/app/problem/solve/[publishId]/[problemId]/child-problem/[childProblemId]/page.tsx +++ b/apps/service/src/app/problem/solve/[publishId]/[problemId]/child-problem/[childProblemId]/page.tsx @@ -15,7 +15,7 @@ import { TwoButtonModalTemplate, AnswerModalTemplate, } from '@components'; -import { useInvalidate, useModal } from '@hooks'; +import { useInvalidate, useModal, useTrackEvent } from '@hooks'; import { components } from '@schema'; import { useChildProblemContext } from '@/hooks/problem'; @@ -29,6 +29,7 @@ const Page = () => { childProblemId: string; }>(); const router = useRouter(); + const { trackEvent } = useTrackEvent(); const { childProblemLength, mainProblemImageUrl, onPrev, onNext } = useChildProblemContext(); const { invalidateAll } = useInvalidate(); @@ -72,6 +73,11 @@ const Page = () => { const isSolved = status === 'CORRECT' || status === 'RETRY_CORRECT'; const isSubmitted = status === 'CORRECT' || status === 'RETRY_CORRECT' || status === 'INCORRECT'; + const handleClickShowMainProblem = () => { + trackEvent('problem_child_solve_show_main_problem_modal_button_click'); + router.push(`/image-modal?imageUrl=${mainProblemImageUrl}`); + }; + const handleSubmitAnswer: SubmitHandler<{ answer: string }> = async ({ answer }) => { const { data } = await putChildProblemSubmit(publishId, childProblemId, answer); const resultData = data?.data; @@ -83,12 +89,49 @@ const Page = () => { } }; + const handleClickPrev = () => { + trackEvent('problem_child_solve_footer_prev_button_click', { + buttonLabel: prevButtonLabel, + }); + onPrev(); + }; + + const handleClickNext = () => { + trackEvent('problem_child_solve_footer_next_button_click', { + buttonLabel: nextButtonLabel, + }); + onNext(); + }; + + const handleClickFooterSkipButton = () => { + trackEvent('problem_child_solve_footer_skip_button_click', { + buttonLabel: nextButtonLabel, + }); + openSkipModal(); + }; + + const handleClickCloseCheckModal = () => { + trackEvent('problem_child_solve_check_modal_close_button_click'); + closeModal(); + }; + + const handleClickNextProblemButton = () => { + trackEvent('problem_child_solve_check_modal_next_problem_button_click'); + onNext(); + }; + const handleClickShowAnswer = () => { + trackEvent('problem_child_solve_check_modal_show_answer_button_click'); closeModal(); openAnswerModal(); }; + const handleClickCloseSkipModal = () => { + trackEvent('problem_child_solve_skip_modal_close_button_click'); + closeSkipModal(); + }; - const handleSkip = async () => { + const handleClickSkipButton = async () => { + trackEvent('problem_child_solve_modal_skip_button_click'); await putChildProblemSkip(publishId, childProblemId); invalidateAll(); onNext(); @@ -109,10 +152,7 @@ const Page = () => { />
- router.push(`/image-modal?imageUrl=${mainProblemImageUrl}`)}> + 메인 문제 다시보기
@@ -137,15 +177,15 @@ const Page = () => { openSkipModal()} + onClickPrev={handleClickPrev} + onClickNext={isSubmitted ? handleClickNext : handleClickFooterSkipButton} /> @@ -160,8 +200,8 @@ const Page = () => { text={`제출하지 않은 새끼 문제는\n오답 처리 돼요!`} topButtonText='다시 풀어보기' bottomButtonText='오답 처리 하고 넘어가기' - handleClickTopButton={closeSkipModal} - handleClickBottomButton={handleSkip} + handleClickTopButton={handleClickCloseSkipModal} + handleClickBottomButton={handleClickSkipButton} /> diff --git a/apps/service/src/app/problem/solve/[publishId]/[problemId]/main-problem/page.tsx b/apps/service/src/app/problem/solve/[publishId]/[problemId]/main-problem/page.tsx index 87a214f1..60501f9f 100644 --- a/apps/service/src/app/problem/solve/[publishId]/[problemId]/main-problem/page.tsx +++ b/apps/service/src/app/problem/solve/[publishId]/[problemId]/main-problem/page.tsx @@ -14,7 +14,7 @@ import { SmallButton, NavigationFooter, } from '@components'; -import { useInvalidate, useModal } from '@hooks'; +import { useInvalidate, useModal, useTrackEvent } from '@hooks'; import { ProblemStatus } from '@types'; import { useChildProblemContext } from '@/hooks/problem'; @@ -36,6 +36,7 @@ const statusColor: Record = { const Page = () => { const { publishId, problemId } = useParams<{ publishId: string; problemId: string }>(); const router = useRouter(); + const { trackEvent } = useTrackEvent(); const { childProblemLength } = useChildProblemContext(); const { invalidateAll } = useInvalidate(); @@ -80,6 +81,33 @@ const Page = () => { } }; + const handleClickStepSolve = () => { + trackEvent('problem_main_solve_step_solve_button_click'); + router.push(`/problem/solve/${publishId}/${problemId}`); + }; + + const handleClickPrev = () => { + trackEvent('problem_main_solve_footer_prev_button_click', { + buttonLabel: prevButtonLabel, + }); + router.back(); + }; + + const handleClickNext = () => { + trackEvent('problem_main_solve_footer_show_commentary_button_click'); + router.push(`/report/${publishId}/${problemId}/analysis`); + }; + + const handleClickSolveAgain = () => { + trackEvent('problem_main_solve_check_modal_solve_again_button_click'); + closeModal(); + }; + + const handleClickShowReport = () => { + trackEvent('problem_main_solve_check_modal_commentary_button_click'); + router.push(`/report/${publishId}/${problemId}/analysis`); + }; + return ( <> @@ -97,10 +125,7 @@ const Page = () => { {isDirect && (
- router.push(`/problem/solve/${publishId}/${problemId}`)}> + 단계별로 풀어보기
@@ -144,17 +169,15 @@ const Page = () => { router.back()} - onClickNext={ - isSubmitted ? () => router.push(`/report/${publishId}/${problemId}/analysis`) : undefined - } + onClickPrev={handleClickPrev} + onClickNext={isSubmitted ? handleClickNext : undefined} /> router.push(`/report/${publishId}/${problemId}/analysis`)} + onClose={handleClickSolveAgain} + handleClickButton={handleClickShowReport} /> diff --git a/apps/service/src/app/report/[publishId]/[problemId]/advanced/page.tsx b/apps/service/src/app/report/[publishId]/[problemId]/advanced/page.tsx index 64ed2c4b..a9b68e91 100644 --- a/apps/service/src/app/report/[publishId]/[problemId]/advanced/page.tsx +++ b/apps/service/src/app/report/[publishId]/[problemId]/advanced/page.tsx @@ -1,27 +1,45 @@ 'use client'; import { NavigationFooter, SmallButton, ProgressHeader } from '@components'; import { useParams, useRouter } from 'next/navigation'; +import { useTrackEvent } from '@hooks'; import { useReportContext } from '@/hooks/report'; const Page = () => { const { publishId, problemId } = useParams(); const router = useRouter(); + const { trackEvent } = useTrackEvent(); const { problemNumber, seniorTipImageUrl, prescription } = useReportContext(); const mainImageUrl = prescription?.mainProblem?.imageUrl; + const handleClickShowMainProblem = () => { + trackEvent('report_advanced_show_main_problem_button_click'); + router.push(`/image-modal?imageUrl=${mainImageUrl}`); + }; + + const handleClickPrev = () => { + trackEvent('report_advanced_prev_button_click', { + buttonLabel: '해설', + }); + router.push(`/report/${publishId}/${problemId}/analysis`); + }; + + const handleClickNext = () => { + trackEvent('report_advanced_next_button_click', { + buttonLabel: '포인팅', + }); + router.push(`/report/${publishId}/${problemId}/prescription`); + }; + return ( <>

한 걸음 더

- router.push(`/image-modal?imageUrl=${mainImageUrl}`)}> + 메인 문제 {problemNumber}번 다시 보기
@@ -35,8 +53,8 @@ const Page = () => { router.push(`/report/${publishId}/${problemId}/analysis`)} - onClickNext={() => router.push(`/report/${publishId}/${problemId}/prescription`)} + onClickPrev={handleClickPrev} + onClickNext={handleClickNext} />
diff --git a/apps/service/src/app/report/[publishId]/[problemId]/analysis/page.tsx b/apps/service/src/app/report/[publishId]/[problemId]/analysis/page.tsx index 27c5241b..4a5d781f 100644 --- a/apps/service/src/app/report/[publishId]/[problemId]/analysis/page.tsx +++ b/apps/service/src/app/report/[publishId]/[problemId]/analysis/page.tsx @@ -1,9 +1,9 @@ 'use client'; import { useState } from 'react'; -import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { IcRight, IcThumbtack } from '@svg'; import { NavigationFooter, ProgressHeader } from '@components'; +import { useTrackEvent } from '@hooks'; import { useReportContext } from '@/hooks/report'; import { TabMenu } from '@/components/report'; @@ -11,9 +11,30 @@ import { TabMenu } from '@/components/report'; const Page = () => { const router = useRouter(); const { publishId, problemId } = useParams(); + const { trackEvent } = useTrackEvent(); const { problemNumber, answer, mainAnalysisImageUrl, mainHandwritingExplanationImageUrl } = useReportContext(); const [selectedTab, setSelectedTab] = useState<'분석' | '손해설'>('분석'); + + const handleClickTab = (tab: '분석' | '손해설') => { + trackEvent('report_analysis_tab_click', { + tab, + }); + setSelectedTab(tab); + }; + + const handleClickReadingTip = () => { + trackEvent('report_analysis_reading_tip_click'); + router.push(`/report/${publishId}/${problemId}/reading-tip`); + }; + + const handleClickNext = () => { + trackEvent('report_analysis_next_button_click', { + buttonLabel: '한 걸음 더', + }); + router.push(`/report/${publishId}/${problemId}/advanced`); + }; + return ( <> @@ -30,7 +51,7 @@ const Page = () => { leftMenu='분석' rightMenu='손해설' selectedTab={selectedTab} - onTabChange={(tab) => setSelectedTab(tab)} + onTabChange={handleClickTab} /> { className={`w-full rounded-[1.6rem] object-contain ${selectedTab === '손해설' ? 'block' : 'hidden'}`} /> - - - + + + - router.push(`/report/${publishId}/${problemId}/advanced`)} - /> + ); diff --git a/apps/service/src/app/report/[publishId]/[problemId]/prescription/page.tsx b/apps/service/src/app/report/[publishId]/[problemId]/prescription/page.tsx index cd47b2a4..61f41ec5 100644 --- a/apps/service/src/app/report/[publishId]/[problemId]/prescription/page.tsx +++ b/apps/service/src/app/report/[publishId]/[problemId]/prescription/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { Divider, NavigationFooter, ProgressHeader } from '@components'; import { useParams, useRouter } from 'next/navigation'; +import { useTrackEvent } from '@hooks'; import { PrescriptionCard } from '@/components/report'; import { useReportContext } from '@/hooks/report'; @@ -8,11 +9,42 @@ import { useReportContext } from '@/hooks/report'; const Page = () => { const router = useRouter(); const { publishId, problemId } = useParams(); + const { trackEvent } = useTrackEvent(); const { problemNumber, prescription } = useReportContext(); const childProblems = prescription?.childProblem ?? []; const mainProblem = prescription?.mainProblem ?? {}; + const handleClickChildPrescription = (childProblemIndex: number) => { + trackEvent('report_prescription_child_prescription_click', { + problemNumber: `${problemNumber}-${childProblemIndex + 1}`, + }); + router.push( + `/report/${publishId}/${problemId}/prescription/detail?type=child&childNumber=${childProblemIndex + 1}` + ); + }; + + const handleClickMainPrescription = () => { + trackEvent('report_prescription_main_prescription_click', { + problemNumber: `${problemNumber}`, + }); + router.push(`/report/${publishId}/${problemId}/prescription/detail?type=main`); + }; + + const handleClickPrev = () => { + trackEvent('report_prescription_prev_button_click', { + buttonLabel: '한 걸음 더', + }); + router.push(`/report/${publishId}/${problemId}/advanced`); + }; + + const handleClickNext = () => { + trackEvent('report_prescription_next_button_click', { + buttonLabel: '리스트로', + }); + router.push(`/problem/list/${publishId}`); + }; + return ( <> @@ -26,11 +58,7 @@ const Page = () => { key={childProblemIndex} status={childProblem.submitStatus} title={`새끼 문항 ${problemNumber}-${childProblemIndex + 1}번`} - onClick={() => - router.push( - `/report/${publishId}/${problemId}/prescription/detail?type=child&childNumber=${childProblemIndex + 1}` - ) - } + onClick={() => handleClickChildPrescription(childProblemIndex)} /> ); })} @@ -39,17 +67,15 @@ const Page = () => { - router.push(`/report/${publishId}/${problemId}/prescription/detail?type=main`) - } + onClick={handleClickMainPrescription} /> router.push(`/report/${publishId}/${problemId}/advanced`)} - onClickNext={() => router.push(`/problem/list/${publishId}`)} + onClickPrev={handleClickPrev} + onClickNext={handleClickNext} /> diff --git a/apps/service/src/components/common/Header.tsx b/apps/service/src/components/common/Header.tsx index 91a33778..cbc5a84c 100644 --- a/apps/service/src/components/common/Header.tsx +++ b/apps/service/src/components/common/Header.tsx @@ -1,4 +1,5 @@ 'use client'; +import { useTrackEvent } from '@hooks'; import { IcHome, IcLeft } from '@svg'; import { useRouter } from 'next/navigation'; @@ -9,10 +10,17 @@ interface HeaderProps { const Header = ({ title, iconType = 'home' }: HeaderProps) => { const router = useRouter(); + const { trackEvent } = useTrackEvent(); + + const handleClickHome = () => { + trackEvent('header_home_button_click'); + router.push('/'); + }; + return (
-
- {iconType === 'home' && router.push('/')} />} +
+ {iconType === 'home' && } {iconType === 'back' && router.back()} />}
diff --git a/apps/service/src/components/common/ProgressHeader.tsx b/apps/service/src/components/common/ProgressHeader.tsx index ffaa0a53..a57f9f7d 100644 --- a/apps/service/src/components/common/ProgressHeader.tsx +++ b/apps/service/src/components/common/ProgressHeader.tsx @@ -1,6 +1,7 @@ 'use client'; import { ProgressBar } from '@components'; +import { useTrackEvent } from '@hooks'; import { IcList } from '@svg'; import { useParams, useRouter } from 'next/navigation'; @@ -11,11 +12,17 @@ interface ProgressHeaderProps { const ProgressHeader = ({ progress }: ProgressHeaderProps) => { const router = useRouter(); const { publishId } = useParams(); + const { trackEvent } = useTrackEvent(); + + const handleClickProblemList = () => { + trackEvent('progress_header_problem_list_button_click'); + router.push(`/problem/list/${publishId}`); + }; return (
- router.push(`/problem/list/${publishId}`)} /> +
{progress !== undefined && }
diff --git a/apps/service/src/components/home/ProblemCard/ProblemCard.tsx b/apps/service/src/components/home/ProblemCard/ProblemCard.tsx index e348e585..65291b8f 100644 --- a/apps/service/src/components/home/ProblemCard/ProblemCard.tsx +++ b/apps/service/src/components/home/ProblemCard/ProblemCard.tsx @@ -1,6 +1,7 @@ import { Button } from '@components'; +import { useTrackEvent } from '@hooks'; import { IcSolve } from '@svg'; -import Link from 'next/link'; +import { useRouter } from 'next/navigation'; interface Props { publishId: number; @@ -11,6 +12,16 @@ interface Props { } const ProblemCard = ({ publishId, dateString, title, image, solvedCount }: Props) => { + const router = useRouter(); + const { trackEvent } = useTrackEvent(); + + const handleClickProblem = () => { + trackEvent('home_carousel_problem_card_click', { + publishId, + }); + router.push(`/problem/list/${publishId}`); + }; + return (
@@ -35,12 +46,10 @@ const ProblemCard = ({ publishId, dateString, title, image, solvedCount }: Props {solvedCount}명이 문제를 풀었어요!

- - - +
); diff --git a/apps/service/src/components/problem/ProblemCalandar.tsx b/apps/service/src/components/problem/ProblemCalandar.tsx index 06d8adb2..04705296 100644 --- a/apps/service/src/components/problem/ProblemCalandar.tsx +++ b/apps/service/src/components/problem/ProblemCalandar.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { IcMinus, IcMinusSmall, IcNextBlack, IcPrevBlack } from '@svg'; import { components } from '@schema'; import { getProblemAll } from '@apis'; +import { useTrackEvent } from '@hooks'; import DayProblemCard from './DayProblemCard'; @@ -12,7 +13,7 @@ type AllProblemGetResponse = components['schemas']['AllProblemGetResponse']; const ProblemCalandar = () => { const [currentDay, setCurrentDay] = useState(dayjs()); const [selectedDay, setSelectedDay] = useState(null); - + const { trackEvent } = useTrackEvent(); const year = currentDay.year(); const month = currentDay.month() + 1; @@ -35,9 +36,18 @@ const ProblemCalandar = () => { return 'bg-white border border-[2px] border-lightgray300'; }; - const handleClickPrevMonth = () => setCurrentDay(currentDay.subtract(1, 'month')); - const handleClickNextMonth = () => setCurrentDay(currentDay.add(1, 'month')); - const handleClickCurrentMonth = () => setCurrentDay(dayjs().startOf('month')); + const handleClickPrevMonth = () => { + trackEvent('problem_calandar_prev_month'); + setCurrentDay(currentDay.subtract(1, 'month')); + }; + const handleClickNextMonth = () => { + trackEvent('problem_calandar_next_month'); + setCurrentDay(currentDay.add(1, 'month')); + }; + const handleClickCurrentMonth = () => { + trackEvent('problem_calandar_current_month'); + setCurrentDay(dayjs().startOf('month')); + }; const firstDayOfMonth = currentDay.startOf('month').day(); // 1일 요일, 0: Sunday ~ 6: Saturday const firstWeekdayOfMonth = firstDayOfMonth === 0 || firstDayOfMonth === 6 ? 1 : firstDayOfMonth; @@ -55,11 +65,19 @@ const ProblemCalandar = () => { }) .filter((day) => day !== null); + const handleClickDay = (day: number) => { + trackEvent('problem_calandar_day_click', { + today: dayjs().format('YYYY-MM-DD'), + day, + }); + setSelectedDay(day); + }; + return (
-

+

{`${month}월`} 진행도

@@ -84,7 +102,7 @@ const ProblemCalandar = () => {
setSelectedDay(day)}> + onClick={() => handleClickDay(day)}> {Object.keys(publishedDataArray[day]).length === 0 ? ( ) : ( diff --git a/apps/service/src/components/problem/ProblemStatusCard.tsx b/apps/service/src/components/problem/ProblemStatusCard.tsx index 8b6e2835..582a1520 100644 --- a/apps/service/src/components/problem/ProblemStatusCard.tsx +++ b/apps/service/src/components/problem/ProblemStatusCard.tsx @@ -1,6 +1,7 @@ 'use client'; import { Button, StatusIcon, StatusTag } from '@components'; +import { useTrackEvent } from '@hooks'; import { components } from '@schema'; import { IcDown, IcUp } from '@svg'; import { useRouter } from 'next/navigation'; @@ -21,12 +22,27 @@ const ProblemStatusCard = ({ }: ProblemStatusCardProps) => { const { problemId, status, childProblemStatuses } = problemData; const router = useRouter(); + const { trackEvent } = useTrackEvent(); const [isOpen, setIsOpen] = useState( !childProblemStatuses?.every((childStatus) => childStatus === 'NOT_STARTED') ); const isSolved = status === 'CORRECT' || status === 'RETRY_CORRECT' || status === 'INCORRECT'; + const handleClickSolveButton = () => { + trackEvent('problem_list_card_solve_button_click', { + problemId: problemId ?? '', + }); + router.push(`/problem/solve/${publishId}/${problemId}`); + }; + + const handleClickReportButton = () => { + trackEvent('problem_list_card_report_button_click', { + problemId: problemId ?? '', + }); + router.push(`/report/${publishId}/${problemId}/analysis`); + }; + return (
@@ -34,11 +50,9 @@ const ProblemStatusCard = ({

메인 문제 {mainProblemNumber}번

- {isOpen ? ( - setIsOpen((prev) => !prev)} /> - ) : ( - setIsOpen((prev) => !prev)} /> - )} +
setIsOpen((prev) => !prev)}> + {isOpen ? : } +
{isOpen && ( @@ -55,16 +69,10 @@ const ProblemStatusCard = ({ )}
- - {isSolved && ( - - )} + {isSolved && }
); diff --git a/apps/service/src/hooks/common/index.ts b/apps/service/src/hooks/common/index.ts index 6f25f49e..b50b29cc 100644 --- a/apps/service/src/hooks/common/index.ts +++ b/apps/service/src/hooks/common/index.ts @@ -1,4 +1,5 @@ import useModal from './useModal'; import useInvalidate from './useInvalidate'; +import useTrackEvent from './useTrackEvent'; -export { useModal, useInvalidate }; +export { useModal, useInvalidate, useTrackEvent }; diff --git a/apps/service/src/hooks/common/useTrackEvent.ts b/apps/service/src/hooks/common/useTrackEvent.ts new file mode 100644 index 00000000..b1af6465 --- /dev/null +++ b/apps/service/src/hooks/common/useTrackEvent.ts @@ -0,0 +1,18 @@ +'use client'; + +import { sendGAEvent } from '@next/third-parties/google'; +import { usePathname } from 'next/navigation'; + +const useTrackEvent = () => { + const pathname = usePathname(); + const trackEvent = (name: string, params?: Record) => { + sendGAEvent('event', name, { + pathname, + ...params, + }); + }; + + return { trackEvent }; +}; + +export default useTrackEvent; diff --git a/apps/service/tsconfig.json b/apps/service/tsconfig.json index b795b346..f96f0586 100644 --- a/apps/service/tsconfig.json +++ b/apps/service/tsconfig.json @@ -28,7 +28,7 @@ "@hooks": ["./src/hooks/common"], "@types": ["./src/types"], "@schema": ["./src/types/api/schema.d.ts"], - "@utils": ["./src/utils"], + "@utils": ["./src/utils/common"], "@/*": ["./src/*"] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b743e489..200d7188 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,9 @@ importers: apps/service: dependencies: + '@next/third-parties': + specifier: ^15.2.4 + version: 15.2.4(next@15.1.4(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@tanstack/react-query': specifier: ^5.66.0 version: 5.66.0(react@19.0.0) @@ -1402,6 +1405,12 @@ packages: cpu: [x64] os: [win32] + '@next/third-parties@15.2.4': + resolution: {integrity: sha512-a8GlPnMmPymxyLOiSnh5InUsG/hw7wjU3munGoHNB+oLCPruAeoplBa9Uf/xE83WMyutyK4cbi5Ixu4uyh96Mw==} + peerDependencies: + next: ^13.0.0 || ^14.0.0 || ^15.0.0 + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4038,6 +4047,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + third-party-capital@1.0.20: + resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -5483,6 +5495,12 @@ snapshots: '@next/swc-win32-x64-msvc@15.1.4': optional: true + '@next/third-parties@15.2.4(next@15.1.4(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + dependencies: + next: 15.1.4(@babel/core@7.26.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 + third-party-capital: 1.0.20 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8549,6 +8567,8 @@ snapshots: tapable@2.2.1: {} + third-party-capital@1.0.20: {} + through@2.3.8: {} tiny-invariant@1.3.3: {}