13 changed files with 16 additions and 764 deletions
@ -1,215 +0,0 @@ |
|||||||
import { useState, useEffect } from 'react'; |
|
||||||
import { |
|
||||||
getDailyLimit, |
|
||||||
getTimeUsedToday, |
|
||||||
setDailyLimit, |
|
||||||
resetDailyCounter |
|
||||||
} from '../../services/timeLimitService'; |
|
||||||
|
|
||||||
export function TimeLimitManager() { |
|
||||||
const [dailyLimit, setDailyLimitState] = useState<number | null>(null); |
|
||||||
const [timeUsed, setTimeUsed] = useState(getTimeUsedToday()); |
|
||||||
const [inputValue, setInputValue] = useState(''); |
|
||||||
const [isLoading, setIsLoading] = useState(true); |
|
||||||
const [isSaving, setIsSaving] = useState(false); |
|
||||||
const [showResetConfirm, setShowResetConfirm] = useState(false); |
|
||||||
const [error, setError] = useState<string | null>(null); |
|
||||||
|
|
||||||
// Fetch limit from server on mount
|
|
||||||
useEffect(() => { |
|
||||||
const fetchLimit = async () => { |
|
||||||
try { |
|
||||||
setIsLoading(true); |
|
||||||
const limit = await getDailyLimit(); |
|
||||||
setDailyLimitState(limit); |
|
||||||
setInputValue(limit.toString()); |
|
||||||
} catch (err: any) { |
|
||||||
setError(err.error?.message || 'Failed to load time limit'); |
|
||||||
} finally { |
|
||||||
setIsLoading(false); |
|
||||||
} |
|
||||||
}; |
|
||||||
fetchLimit(); |
|
||||||
}, []); |
|
||||||
|
|
||||||
// Update time used periodically
|
|
||||||
useEffect(() => { |
|
||||||
const interval = setInterval(() => { |
|
||||||
setTimeUsed(getTimeUsedToday()); |
|
||||||
}, 5000); |
|
||||||
|
|
||||||
return () => clearInterval(interval); |
|
||||||
}, []); |
|
||||||
|
|
||||||
const handleSaveLimit = async () => { |
|
||||||
const minutes = parseInt(inputValue, 10); |
|
||||||
if (isNaN(minutes) || minutes < 1) { |
|
||||||
alert('Please enter a valid number of minutes (minimum 1)'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
setIsSaving(true); |
|
||||||
setError(null); |
|
||||||
try { |
|
||||||
await setDailyLimit(minutes); |
|
||||||
setDailyLimitState(minutes); |
|
||||||
} catch (err: any) { |
|
||||||
setError(err.error?.message || 'Failed to save time limit'); |
|
||||||
} finally { |
|
||||||
setIsSaving(false); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
const handleResetCounter = () => { |
|
||||||
resetDailyCounter(); |
|
||||||
setTimeUsed(0); |
|
||||||
setShowResetConfirm(false); |
|
||||||
}; |
|
||||||
|
|
||||||
const formatTime = (minutes: number): string => { |
|
||||||
if (minutes < 1) { |
|
||||||
return `${Math.round(minutes * 60)} seconds`; |
|
||||||
} |
|
||||||
if (minutes < 60) { |
|
||||||
return `${Math.round(minutes)} minute${Math.round(minutes) !== 1 ? 's' : ''}`; |
|
||||||
} |
|
||||||
const hours = Math.floor(minutes / 60); |
|
||||||
const mins = Math.round(minutes % 60); |
|
||||||
if (mins === 0) { |
|
||||||
return `${hours} hour${hours !== 1 ? 's' : ''}`; |
|
||||||
} |
|
||||||
return `${hours} hour${hours !== 1 ? 's' : ''} ${mins} minute${mins !== 1 ? 's' : ''}`; |
|
||||||
}; |
|
||||||
|
|
||||||
const remainingTime = dailyLimit !== null ? Math.max(0, dailyLimit - timeUsed) : 0; |
|
||||||
|
|
||||||
if (isLoading) { |
|
||||||
return ( |
|
||||||
<div className="bg-card rounded-xl p-6 border border-border h-fit"> |
|
||||||
<div className="mb-6"> |
|
||||||
<h2 className="m-0 mb-2 text-xl font-semibold text-foreground">Daily Time Limit Settings</h2> |
|
||||||
<p className="m-0 text-sm text-muted-foreground">Loading...</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className="bg-card rounded-xl p-6 border border-border h-fit"> |
|
||||||
<div className="mb-6"> |
|
||||||
<h2 className="m-0 mb-2 text-xl font-semibold text-foreground">Daily Time Limit Settings</h2> |
|
||||||
<p className="m-0 text-sm text-muted-foreground"> |
|
||||||
Configure how much time users can spend watching videos each day |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
{error && ( |
|
||||||
<div className="px-4 py-3 rounded-md mb-4 text-sm bg-red-50 text-red-800 border border-red-200"> |
|
||||||
{error} |
|
||||||
</div> |
|
||||||
)} |
|
||||||
|
|
||||||
<div className="flex flex-col gap-6"> |
|
||||||
<div className="flex flex-col gap-3"> |
|
||||||
<label htmlFor="daily-limit-input" className="text-sm font-medium text-foreground"> |
|
||||||
Daily Limit (minutes) |
|
||||||
</label> |
|
||||||
<div className="flex gap-3 items-center md:flex-row flex-col md:items-center items-stretch"> |
|
||||||
<input |
|
||||||
id="daily-limit-input" |
|
||||||
type="number" |
|
||||||
min="1" |
|
||||||
step="1" |
|
||||||
value={inputValue} |
|
||||||
onChange={(e) => setInputValue(e.target.value)} |
|
||||||
onKeyPress={(e) => { |
|
||||||
if (e.key === 'Enter') { |
|
||||||
handleSaveLimit(); |
|
||||||
} |
|
||||||
}} |
|
||||||
className="flex-1 max-w-[200px] md:max-w-[200px] max-w-full px-3 py-2.5 border border-border rounded-md text-sm bg-muted text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" |
|
||||||
/> |
|
||||||
<button |
|
||||||
onClick={handleSaveLimit} |
|
||||||
disabled={isSaving || (dailyLimit !== null && inputValue === dailyLimit.toString())} |
|
||||||
className="px-5 py-2.5 bg-gradient-to-r from-primary to-secondary text-white border-none rounded-md text-sm font-medium cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed" |
|
||||||
> |
|
||||||
{isSaving ? 'Saving...' : 'Save'} |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
{dailyLimit !== null && ( |
|
||||||
<p className="m-0 text-[13px] text-muted-foreground"> |
|
||||||
Current limit: <strong className="font-semibold">{formatTime(dailyLimit)}</strong> per day |
|
||||||
</p> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
|
|
||||||
{dailyLimit !== null && ( |
|
||||||
<div className="p-5 bg-muted rounded-lg border border-border/50"> |
|
||||||
<h3 className="m-0 mb-4 text-base font-semibold text-foreground">Today's Usage</h3> |
|
||||||
<div className="flex flex-col gap-3"> |
|
||||||
<div className="w-full h-6 bg-border/30 rounded-xl overflow-hidden relative"> |
|
||||||
<div |
|
||||||
className="h-full bg-gradient-to-r from-primary to-secondary transition-all duration-300 ease-in-out rounded-xl" |
|
||||||
style={{ |
|
||||||
width: `${Math.min(100, (timeUsed / dailyLimit) * 100)}%` |
|
||||||
}} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
<div className="flex justify-between text-sm text-foreground md:flex-row flex-col md:gap-0 gap-2"> |
|
||||||
<span className="time-used"> |
|
||||||
Used: <strong className="text-primary font-semibold">{formatTime(timeUsed)}</strong> |
|
||||||
</span> |
|
||||||
<span className="time-remaining"> |
|
||||||
Remaining: <strong className="text-primary font-semibold">{formatTime(remainingTime)}</strong> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
{timeUsed > 0 && ( |
|
||||||
<div className="mt-4 pt-4 border-t border-border/50"> |
|
||||||
<button |
|
||||||
onClick={() => setShowResetConfirm(true)} |
|
||||||
className="px-4 py-2 bg-transparent text-primary border border-primary rounded-md text-sm font-medium cursor-pointer transition-colors hover:bg-primary hover:text-primary-foreground" |
|
||||||
> |
|
||||||
Reset Today's Counter |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
|
|
||||||
{showResetConfirm && ( |
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[1000] backdrop-blur-sm" |
|
||||||
onClick={() => setShowResetConfirm(false)} |
|
||||||
> |
|
||||||
<div
|
|
||||||
className="bg-card rounded-xl p-6 max-w-[400px] w-[90%] shadow-xl" |
|
||||||
onClick={(e) => e.stopPropagation()} |
|
||||||
> |
|
||||||
<h3 className="m-0 mb-3 text-lg font-semibold text-foreground">Reset Today's Counter?</h3> |
|
||||||
<p className="m-0 mb-5 text-sm text-muted-foreground leading-relaxed"> |
|
||||||
This will reset the time used today back to 0. Users will be able to watch videos again. |
|
||||||
</p> |
|
||||||
<div className="flex gap-3 justify-end md:flex-row flex-col"> |
|
||||||
<button |
|
||||||
onClick={handleResetCounter} |
|
||||||
className="px-5 py-2.5 bg-gradient-to-r from-primary to-secondary text-white border-none rounded-md text-sm font-medium cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-lg" |
|
||||||
> |
|
||||||
Reset Counter |
|
||||||
</button> |
|
||||||
<button |
|
||||||
onClick={() => setShowResetConfirm(false)} |
|
||||||
className="px-5 py-2.5 bg-transparent text-foreground border border-border rounded-md text-sm font-medium cursor-pointer transition-colors hover:bg-muted" |
|
||||||
> |
|
||||||
Cancel |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,155 +0,0 @@ |
|||||||
import { useState, useEffect, useCallback, useRef } from 'react'; |
|
||||||
import { |
|
||||||
getDailyLimit, |
|
||||||
getDailyLimitSync, |
|
||||||
getTimeUsedToday, |
|
||||||
getRemainingTimeToday, |
|
||||||
isLimitReached, |
|
||||||
addTimeSpent |
|
||||||
} from '../services/timeLimitService'; |
|
||||||
|
|
||||||
interface UseTimeLimitReturn { |
|
||||||
dailyLimit: number; |
|
||||||
timeUsed: number; |
|
||||||
remainingTime: number; |
|
||||||
limitReached: boolean; |
|
||||||
startTracking: () => void; |
|
||||||
stopTracking: () => void; |
|
||||||
isTracking: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Hook to manage video time limits |
|
||||||
* Tracks time spent watching videos and enforces daily limits |
|
||||||
*/ |
|
||||||
export function useTimeLimit(): UseTimeLimitReturn { |
|
||||||
const [dailyLimit, setDailyLimit] = useState(getDailyLimitSync()); |
|
||||||
const [timeUsed, setTimeUsed] = useState(getTimeUsedToday()); |
|
||||||
const [remainingTime, setRemainingTime] = useState(getRemainingTimeToday()); |
|
||||||
const [limitReached, setLimitReached] = useState(isLimitReached()); |
|
||||||
const [isTracking, setIsTracking] = useState(false); |
|
||||||
|
|
||||||
const trackingIntervalRef = useRef<NodeJS.Timeout | null>(null); |
|
||||||
const startTimeRef = useRef<number | null>(null); |
|
||||||
const lastUpdateRef = useRef<number>(Date.now()); |
|
||||||
|
|
||||||
// Update state from localStorage and cached server limit
|
|
||||||
const updateState = useCallback(() => { |
|
||||||
setDailyLimit(getDailyLimitSync()); |
|
||||||
setTimeUsed(getTimeUsedToday()); |
|
||||||
setRemainingTime(getRemainingTimeToday()); |
|
||||||
setLimitReached(isLimitReached()); |
|
||||||
}, []); |
|
||||||
|
|
||||||
// Fetch limit from server on mount
|
|
||||||
useEffect(() => { |
|
||||||
getDailyLimit().then(limit => { |
|
||||||
setDailyLimit(limit); |
|
||||||
// Immediately recalculate limitReached with the correct server limit
|
|
||||||
updateState(); |
|
||||||
}); |
|
||||||
}, [updateState]); |
|
||||||
|
|
||||||
// Start tracking time
|
|
||||||
const startTracking = useCallback(() => { |
|
||||||
if (limitReached) { |
|
||||||
return; // Don't start if limit already reached
|
|
||||||
} |
|
||||||
|
|
||||||
if (trackingIntervalRef.current) { |
|
||||||
return; // Already tracking
|
|
||||||
} |
|
||||||
|
|
||||||
startTimeRef.current = Date.now(); |
|
||||||
lastUpdateRef.current = Date.now(); |
|
||||||
setIsTracking(true); |
|
||||||
|
|
||||||
// Update every 5 seconds
|
|
||||||
trackingIntervalRef.current = setInterval(() => { |
|
||||||
if (startTimeRef.current) { |
|
||||||
const now = Date.now(); |
|
||||||
const secondsElapsed = (now - lastUpdateRef.current) / 1000; |
|
||||||
lastUpdateRef.current = now; |
|
||||||
|
|
||||||
// Add time spent
|
|
||||||
addTimeSpent(secondsElapsed); |
|
||||||
|
|
||||||
// Update state
|
|
||||||
updateState(); |
|
||||||
|
|
||||||
// Check if limit reached during tracking
|
|
||||||
if (isLimitReached()) { |
|
||||||
// Stop tracking if limit reached
|
|
||||||
if (trackingIntervalRef.current) { |
|
||||||
clearInterval(trackingIntervalRef.current); |
|
||||||
trackingIntervalRef.current = null; |
|
||||||
} |
|
||||||
if (startTimeRef.current && lastUpdateRef.current) { |
|
||||||
const secondsElapsed = (Date.now() - lastUpdateRef.current) / 1000; |
|
||||||
if (secondsElapsed > 0) { |
|
||||||
addTimeSpent(secondsElapsed); |
|
||||||
} |
|
||||||
startTimeRef.current = null; |
|
||||||
} |
|
||||||
setIsTracking(false); |
|
||||||
updateState(); |
|
||||||
} |
|
||||||
} |
|
||||||
}, 5000); |
|
||||||
}, [limitReached, updateState]); |
|
||||||
|
|
||||||
// Stop tracking time
|
|
||||||
const stopTracking = useCallback(() => { |
|
||||||
if (trackingIntervalRef.current) { |
|
||||||
clearInterval(trackingIntervalRef.current); |
|
||||||
trackingIntervalRef.current = null; |
|
||||||
} |
|
||||||
|
|
||||||
// Add any remaining time
|
|
||||||
if (startTimeRef.current && lastUpdateRef.current) { |
|
||||||
const secondsElapsed = (Date.now() - lastUpdateRef.current) / 1000; |
|
||||||
if (secondsElapsed > 0) { |
|
||||||
addTimeSpent(secondsElapsed); |
|
||||||
} |
|
||||||
startTimeRef.current = null; |
|
||||||
} |
|
||||||
|
|
||||||
setIsTracking(false); |
|
||||||
updateState(); |
|
||||||
}, [updateState]); |
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
useEffect(() => { |
|
||||||
return () => { |
|
||||||
if (trackingIntervalRef.current) { |
|
||||||
clearInterval(trackingIntervalRef.current); |
|
||||||
} |
|
||||||
// Save any remaining time before unmount
|
|
||||||
if (startTimeRef.current && lastUpdateRef.current) { |
|
||||||
const secondsElapsed = (Date.now() - lastUpdateRef.current) / 1000; |
|
||||||
if (secondsElapsed > 0) { |
|
||||||
addTimeSpent(secondsElapsed); |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
}, []); |
|
||||||
|
|
||||||
// Update state periodically to catch external changes
|
|
||||||
useEffect(() => { |
|
||||||
const interval = setInterval(() => { |
|
||||||
updateState(); |
|
||||||
}, 10000); // Check every 10 seconds
|
|
||||||
|
|
||||||
return () => clearInterval(interval); |
|
||||||
}, [updateState]); |
|
||||||
|
|
||||||
return { |
|
||||||
dailyLimit, |
|
||||||
timeUsed, |
|
||||||
remainingTime, |
|
||||||
limitReached, |
|
||||||
startTracking, |
|
||||||
stopTracking, |
|
||||||
isTracking |
|
||||||
}; |
|
||||||
} |
|
||||||
@ -1,210 +0,0 @@ |
|||||||
interface TimeLimitData { |
|
||||||
dailyLimit: number; // minutes (stored on server)
|
|
||||||
dailyTimeUsed: number; // minutes (stored per-device in localStorage)
|
|
||||||
lastResetDate: string; // ISO date string (YYYY-MM-DD)
|
|
||||||
} |
|
||||||
|
|
||||||
const STORAGE_KEY = 'video_time_limit'; |
|
||||||
const DEFAULT_DAILY_LIMIT = 1; // 1 minute for testing
|
|
||||||
|
|
||||||
// Cache for daily limit from server
|
|
||||||
let cachedDailyLimit: number | null = null; |
|
||||||
let limitCacheTime: number = 0; |
|
||||||
const LIMIT_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
||||||
|
|
||||||
/** |
|
||||||
* Get local date string in YYYY-MM-DD format (not UTC) |
|
||||||
* This ensures the daily reset happens at local midnight, not UTC midnight |
|
||||||
*/ |
|
||||||
function getLocalDateString(): string { |
|
||||||
const now = new Date(); |
|
||||||
const year = now.getFullYear(); |
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0'); |
|
||||||
const day = String(now.getDate()).padStart(2, '0'); |
|
||||||
return `${year}-${month}-${day}`; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Get time limit data from localStorage (for usage tracking only) |
|
||||||
*/ |
|
||||||
function getTimeLimitData(): Omit<TimeLimitData, 'dailyLimit'> { |
|
||||||
try { |
|
||||||
const stored = localStorage.getItem(STORAGE_KEY); |
|
||||||
if (stored) { |
|
||||||
const data = JSON.parse(stored); |
|
||||||
return { |
|
||||||
dailyTimeUsed: data.dailyTimeUsed || 0, |
|
||||||
lastResetDate: data.lastResetDate || getLocalDateString() |
|
||||||
}; |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
console.warn('Failed to parse time limit data from localStorage', e); |
|
||||||
} |
|
||||||
|
|
||||||
// Return default data
|
|
||||||
return { |
|
||||||
dailyTimeUsed: 0, |
|
||||||
lastResetDate: getLocalDateString() |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Save time limit data to localStorage (for usage tracking only) |
|
||||||
*/ |
|
||||||
function saveTimeLimitData(data: Omit<TimeLimitData, 'dailyLimit'>): void { |
|
||||||
try { |
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); |
|
||||||
} catch (e) { |
|
||||||
console.warn('Failed to save time limit data to localStorage', e); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Check if we need to reset daily counter (new day) |
|
||||||
*/ |
|
||||||
function shouldResetDaily(): boolean { |
|
||||||
const data = getTimeLimitData(); |
|
||||||
const today = getLocalDateString(); |
|
||||||
return data.lastResetDate !== today; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Reset daily counter if it's a new day |
|
||||||
*/ |
|
||||||
function resetIfNeeded(): void { |
|
||||||
if (shouldResetDaily()) { |
|
||||||
const data = getTimeLimitData(); |
|
||||||
data.dailyTimeUsed = 0; |
|
||||||
data.lastResetDate = getLocalDateString(); |
|
||||||
saveTimeLimitData(data); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Get current time limit configuration |
|
||||||
* Priority: Magic code settings > Server settings > Cached > Default |
|
||||||
*/ |
|
||||||
export async function getDailyLimit(): Promise<number> { |
|
||||||
// Check magic code settings first (highest priority)
|
|
||||||
try { |
|
||||||
const { getMagicCodeSettings } = await import('./magicCodeService'); |
|
||||||
const magicCodeSettings = getMagicCodeSettings(); |
|
||||||
if (magicCodeSettings && magicCodeSettings.dailyTimeLimit !== null && magicCodeSettings.dailyTimeLimit !== undefined) { |
|
||||||
return magicCodeSettings.dailyTimeLimit; |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.warn('Failed to check magic code settings:', error); |
|
||||||
} |
|
||||||
|
|
||||||
// Return cached value if still valid
|
|
||||||
const now = Date.now(); |
|
||||||
if (cachedDailyLimit !== null && (now - limitCacheTime) < LIMIT_CACHE_DURATION) { |
|
||||||
return cachedDailyLimit; |
|
||||||
} |
|
||||||
|
|
||||||
try { |
|
||||||
const { settingsApi } = await import('./apiClient'); |
|
||||||
const response = await settingsApi.getTimeLimit(); |
|
||||||
const limit = response.data.dailyLimit; |
|
||||||
cachedDailyLimit = limit; |
|
||||||
limitCacheTime = now; |
|
||||||
return limit; |
|
||||||
} catch (error) { |
|
||||||
console.warn('Failed to fetch daily limit from server, using cached/default:', error); |
|
||||||
// Return cached value or default
|
|
||||||
return cachedDailyLimit ?? DEFAULT_DAILY_LIMIT; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Set daily time limit (in minutes) on server |
|
||||||
*/ |
|
||||||
export async function setDailyLimit(minutes: number): Promise<void> { |
|
||||||
try { |
|
||||||
const { settingsApi } = await import('./apiClient'); |
|
||||||
await settingsApi.setTimeLimit(minutes); |
|
||||||
cachedDailyLimit = minutes; |
|
||||||
limitCacheTime = Date.now(); |
|
||||||
} catch (error) { |
|
||||||
console.error('Failed to set daily limit on server:', error); |
|
||||||
throw error; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Synchronous version for use in hooks (uses cached value or magic code settings) |
|
||||||
* Note: Magic code settings are checked synchronously from localStorage |
|
||||||
*/ |
|
||||||
export function getDailyLimitSync(): number { |
|
||||||
// Check magic code settings first (highest priority)
|
|
||||||
// We can access localStorage synchronously
|
|
||||||
try { |
|
||||||
const MAGIC_CODE_SETTINGS_KEY = 'magic_code_settings'; |
|
||||||
const stored = localStorage.getItem(MAGIC_CODE_SETTINGS_KEY); |
|
||||||
if (stored) { |
|
||||||
const magicCodeSettings = JSON.parse(stored); |
|
||||||
if (magicCodeSettings && magicCodeSettings.dailyTimeLimit !== null && magicCodeSettings.dailyTimeLimit !== undefined) { |
|
||||||
return magicCodeSettings.dailyTimeLimit; |
|
||||||
} |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
// Ignore errors in sync context
|
|
||||||
} |
|
||||||
|
|
||||||
return cachedDailyLimit ?? DEFAULT_DAILY_LIMIT; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Get time used today (in minutes) - per device |
|
||||||
*/ |
|
||||||
export function getTimeUsedToday(): number { |
|
||||||
resetIfNeeded(); |
|
||||||
const data = getTimeLimitData(); |
|
||||||
return data.dailyTimeUsed; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Get remaining time today (in minutes) |
|
||||||
* Note: Uses cached limit value |
|
||||||
*/ |
|
||||||
export function getRemainingTimeToday(): number { |
|
||||||
resetIfNeeded(); |
|
||||||
const limit = getDailyLimitSync(); |
|
||||||
const used = getTimeUsedToday(); |
|
||||||
return Math.max(0, limit - used); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Check if daily limit has been reached |
|
||||||
* Note: Uses cached limit value |
|
||||||
*/ |
|
||||||
export function isLimitReached(): boolean { |
|
||||||
resetIfNeeded(); |
|
||||||
return getRemainingTimeToday() <= 0; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Add time spent (in seconds) to the daily counter |
|
||||||
* Note: Uses cached limit value to cap the usage |
|
||||||
*/ |
|
||||||
export function addTimeSpent(seconds: number): void { |
|
||||||
resetIfNeeded(); |
|
||||||
const data = getTimeLimitData(); |
|
||||||
const minutesToAdd = seconds / 60; |
|
||||||
const limit = getDailyLimitSync(); |
|
||||||
data.dailyTimeUsed = Math.min( |
|
||||||
limit, |
|
||||||
data.dailyTimeUsed + minutesToAdd |
|
||||||
); |
|
||||||
saveTimeLimitData(data); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Reset daily counter (for testing/admin purposes) |
|
||||||
*/ |
|
||||||
export function resetDailyCounter(): void { |
|
||||||
const data = getTimeLimitData(); |
|
||||||
data.dailyTimeUsed = 0; |
|
||||||
data.lastResetDate = getLocalDateString(); |
|
||||||
saveTimeLimitData(data); |
|
||||||
} |
|
||||||
Loading…
Reference in new issue