/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useState, useMemo, useCallback } from 'react' import { useQuery } from '@tanstack/react-query' import { CalendarDays, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Sparkles, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { formatQuotaWithCurrency } from '@/lib/currency' import dayjs from '@/lib/dayjs' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider, } from '@/components/ui/tooltip' import { Dialog } from '@/components/dialog' import { Turnstile } from '@/components/turnstile' import { getCheckinStatus, performCheckin } from '../api' import type { CheckinRecord } from '../types' interface CheckinCalendarCardProps { checkinEnabled: boolean turnstileEnabled: boolean turnstileSiteKey: string } export function CheckinCalendarCard({ checkinEnabled, turnstileEnabled, turnstileSiteKey, }: CheckinCalendarCardProps) { const { t } = useTranslation() const [currentMonth, setCurrentMonth] = useState(() => { const now = new Date() return new Date(now.getFullYear(), now.getMonth(), 1) }) const [checkinLoading, setCheckinLoading] = useState(false) const [turnstileModalVisible, setTurnstileModalVisible] = useState(false) const [turnstileWidgetKey, setTurnstileWidgetKey] = useState(0) const [initialLoaded, setInitialLoaded] = useState(false) const [collapsed, setCollapsed] = useState(false) const currentMonthStr = useMemo(() => { const y = currentMonth.getFullYear() const m = String(currentMonth.getMonth() + 1).padStart(2, '0') return `${y}-${m}` }, [currentMonth]) // Fetch checkin status /* eslint-disable @tanstack/query/exhaustive-deps */ const { data: checkinData, isLoading, refetch, } = useQuery({ queryKey: ['checkin-status', currentMonthStr], queryFn: async () => { const res = await getCheckinStatus(currentMonthStr) if (res.success && res.data) { return res.data } throw new Error(res.message || t('Failed to fetch checkin status')) }, enabled: checkinEnabled, staleTime: 30000, }) /* eslint-enable @tanstack/query/exhaustive-deps */ const checkinRecordsMap = useMemo(() => { const map: Record = {} const records = checkinData?.stats?.records || [] records.forEach((record: CheckinRecord) => { map[record.checkin_date] = record.quota_awarded }) return map }, [checkinData?.stats?.records]) const monthlyQuota = useMemo(() => { const records = checkinData?.stats?.records || [] return records.reduce( (sum: number, record: CheckinRecord) => sum + (record.quota_awarded || 0), 0 ) }, [checkinData?.stats?.records]) const todayString = useMemo(() => { const d = new Date() return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` }, []) const checkedToday = checkinData?.stats?.checked_in_today === true const todayAward = checkinRecordsMap[todayString] useEffect(() => { if (initialLoaded) return if (isLoading) return if (!checkinData) return setCollapsed(checkedToday) setInitialLoaded(true) }, [checkinData, checkedToday, initialLoaded, isLoading]) const shouldTriggerTurnstile = useCallback( (message?: string) => { if (!turnstileEnabled) return false if (typeof message !== 'string') return true return message.includes('Turnstile') }, [turnstileEnabled] ) const doCheckin = useCallback( async (token?: string) => { setCheckinLoading(true) try { const res = await performCheckin(token) if (res.success && res.data) { toast.success( `${t('Check-in successful! Received')} ${formatQuotaWithCurrency(res.data.quota_awarded)}` ) refetch() setTurnstileModalVisible(false) } else { if (!token && shouldTriggerTurnstile(res.message)) { if (!turnstileSiteKey) { toast.error(t('Turnstile is enabled but site key is empty.')) return } setTurnstileModalVisible(true) return } if (token && shouldTriggerTurnstile(res.message)) { setTurnstileWidgetKey((v) => v + 1) } toast.error(res.message || t('Check-in failed')) } } catch (_error) { toast.error(t('Check-in failed')) } finally { setCheckinLoading(false) } }, [refetch, shouldTriggerTurnstile, t, turnstileSiteKey] ) const handlePrevMonth = () => { setCurrentMonth( new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1) ) } const handleNextMonth = () => { setCurrentMonth( new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1) ) } // Build calendar grid const calendarDays = useMemo(() => { const year = currentMonth.getFullYear() const month = currentMonth.getMonth() const firstDay = new Date(year, month, 1) const lastDay = new Date(year, month + 1, 0) const daysInMonth = lastDay.getDate() const startDayOfWeek = firstDay.getDay() // 0 = Sunday const days: Array<{ date: Date; isCurrentMonth: boolean }> = [] // Fill leading empty days for (let i = 0; i < startDayOfWeek; i++) { const d = new Date(year, month, -startDayOfWeek + i + 1) days.push({ date: d, isCurrentMonth: false }) } // Fill current month days for (let i = 1; i <= daysInMonth; i++) { days.push({ date: new Date(year, month, i), isCurrentMonth: true }) } // Fill trailing empty days to complete the grid const remaining = 7 - (days.length % 7) if (remaining < 7) { for (let i = 1; i <= remaining; i++) { days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false }) } } return days }, [currentMonth]) const weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] if (!checkinEnabled) { return null } if (isLoading) { return (
) } return ( { setTurnstileModalVisible(open) if (!open) { setTurnstileWidgetKey((v) => v + 1) } }} title={t('Security Check')} contentClassName='sm:max-w-md' contentHeight='auto' bodyClassName='space-y-4' >
{t('Please complete the security check to continue.')}
{ doCheckin(token) }} onExpire={() => { setTurnstileWidgetKey((v) => v + 1) }} />
{/* Header */}
{!collapsed ? ( <> {/* Stats */}
{checkinData?.stats?.total_checkins || 0}
{t('Total check-ins')}
{formatQuotaWithCurrency(monthlyQuota, { digitsLarge: 0 })}
{t('This month')}
{formatQuotaWithCurrency( checkinData?.stats?.total_quota || 0, { digitsLarge: 0, } )}
{t('Total earned')}
{/* Calendar */}
{/* Month navigation */}

{dayjs(currentMonth).format('YYYY-MM')}

{/* Calendar grid */}
{/* Week day headers */} {weekDays.map((day) => (
{day}
))} {/* Calendar days */} {calendarDays.map((dayObj, idx) => { const dateStr = `${dayObj.date.getFullYear()}-${String( dayObj.date.getMonth() + 1 ).padStart(2, '0')}-${String( dayObj.date.getDate() ).padStart(2, '0')}` const isToday = dateStr === todayString const quotaAwarded = checkinRecordsMap[dateStr] const isCheckedIn = quotaAwarded !== undefined const dayNum = dayObj.date.getDate() const dayButton = ( ) if (isCheckedIn && dayObj.isCurrentMonth) { return (
{t('Checked in')}
+{formatQuotaWithCurrency(quotaAwarded)}
) } return dayButton })}
{/* Footer hint */}
{t('You can only check in once per day')}
  • {t('Check in daily to receive random quota rewards')}
  • {t('Rewards will be added directly to your balance')}
  • {t('Do not repeat check-in; only once per day')}
) : null}
) }