19 changed files with 1108 additions and 25 deletions
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
import { Response } from 'express'; |
||||
import { AuthRequest } from '../types/index.js'; |
||||
import { getSetting, setSetting } from '../config/database.js'; |
||||
|
||||
export async function getTimeLimit(req: AuthRequest, res: Response) { |
||||
try { |
||||
const limit = await getSetting('daily_time_limit_minutes'); |
||||
const defaultLimit = 1; // Default 1 minute for testing
|
||||
|
||||
res.json({ |
||||
success: true, |
||||
data: { |
||||
dailyLimit: limit ? parseInt(limit, 10) : defaultLimit |
||||
} |
||||
}); |
||||
} catch (error: any) { |
||||
console.error('Get time limit error:', error); |
||||
res.status(500).json({ |
||||
success: false, |
||||
error: { |
||||
code: 'GET_TIME_LIMIT_ERROR', |
||||
message: 'Error fetching time limit' |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export async function setTimeLimit(req: AuthRequest, res: Response) { |
||||
try { |
||||
const { dailyLimit } = req.body; |
||||
|
||||
if (!dailyLimit || typeof dailyLimit !== 'number' || dailyLimit < 1) { |
||||
return res.status(400).json({ |
||||
success: false, |
||||
error: { |
||||
code: 'INVALID_LIMIT', |
||||
message: 'Daily limit must be a number greater than 0' |
||||
} |
||||
}); |
||||
} |
||||
|
||||
await setSetting('daily_time_limit_minutes', dailyLimit.toString()); |
||||
|
||||
res.json({ |
||||
success: true, |
||||
data: { |
||||
dailyLimit |
||||
} |
||||
}); |
||||
} catch (error: any) { |
||||
console.error('Set time limit error:', error); |
||||
res.status(500).json({ |
||||
success: false, |
||||
error: { |
||||
code: 'SET_TIME_LIMIT_ERROR', |
||||
message: 'Error setting time limit' |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import { Router } from 'express'; |
||||
import { getTimeLimit, setTimeLimit } from '../controllers/settings.controller.js'; |
||||
import { authMiddleware } from '../middleware/auth.js'; |
||||
|
||||
const router = Router(); |
||||
|
||||
// Protected routes - only admins can get/set time limits
|
||||
router.get('/time-limit', authMiddleware, getTimeLimit); |
||||
router.put('/time-limit', authMiddleware, setTimeLimit); |
||||
|
||||
export default router; |
||||
@ -0,0 +1,268 @@
@@ -0,0 +1,268 @@
|
||||
.time-limit-manager { |
||||
background: var(--color-surface); |
||||
border-radius: 12px; |
||||
padding: 24px; |
||||
border: 1px solid rgba(212, 222, 239, 0.8); |
||||
height: fit-content; |
||||
} |
||||
|
||||
.time-limit-header { |
||||
margin-bottom: 24px; |
||||
} |
||||
|
||||
.time-limit-header h2 { |
||||
margin: 0 0 8px 0; |
||||
font-size: 20px; |
||||
font-weight: 600; |
||||
color: var(--color-text); |
||||
} |
||||
|
||||
.time-limit-header p { |
||||
margin: 0; |
||||
font-size: 14px; |
||||
color: var(--color-muted); |
||||
} |
||||
|
||||
.time-limit-section { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 24px; |
||||
} |
||||
|
||||
.time-limit-setting { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 12px; |
||||
} |
||||
|
||||
.time-limit-setting label { |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
color: var(--color-text); |
||||
} |
||||
|
||||
.time-limit-input-group { |
||||
display: flex; |
||||
gap: 12px; |
||||
align-items: center; |
||||
} |
||||
|
||||
.time-limit-input { |
||||
flex: 1; |
||||
max-width: 200px; |
||||
padding: 10px 12px; |
||||
border: 1px solid rgba(212, 222, 239, 0.8); |
||||
border-radius: 6px; |
||||
font-size: 14px; |
||||
background: var(--color-surface-muted); |
||||
color: var(--color-text); |
||||
transition: border-color 0.2s; |
||||
} |
||||
|
||||
.time-limit-input:focus { |
||||
outline: none; |
||||
border-color: var(--color-primary); |
||||
} |
||||
|
||||
.time-limit-save-btn { |
||||
padding: 10px 20px; |
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary)); |
||||
color: white; |
||||
border: none; |
||||
border-radius: 6px; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
cursor: pointer; |
||||
transition: transform 0.2s, box-shadow 0.2s; |
||||
} |
||||
|
||||
.time-limit-save-btn:hover:not(:disabled) { |
||||
transform: translateY(-2px); |
||||
box-shadow: 0 4px 12px rgba(47, 128, 237, 0.3); |
||||
} |
||||
|
||||
.time-limit-save-btn:disabled { |
||||
opacity: 0.5; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.time-limit-hint { |
||||
margin: 0; |
||||
font-size: 13px; |
||||
color: var(--color-muted); |
||||
} |
||||
|
||||
.time-limit-status { |
||||
padding: 20px; |
||||
background: var(--color-surface-muted); |
||||
border-radius: 8px; |
||||
border: 1px solid rgba(212, 222, 239, 0.5); |
||||
} |
||||
|
||||
.time-limit-status h3 { |
||||
margin: 0 0 16px 0; |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
color: var(--color-text); |
||||
} |
||||
|
||||
.time-limit-progress { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 12px; |
||||
} |
||||
|
||||
.time-limit-progress-bar { |
||||
width: 100%; |
||||
height: 24px; |
||||
background: rgba(212, 222, 239, 0.3); |
||||
border-radius: 12px; |
||||
overflow: hidden; |
||||
position: relative; |
||||
} |
||||
|
||||
.time-limit-progress-fill { |
||||
height: 100%; |
||||
background: linear-gradient(90deg, #4a90e2, #357abd); |
||||
transition: width 0.3s ease; |
||||
border-radius: 12px; |
||||
} |
||||
|
||||
.time-limit-stats { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
font-size: 14px; |
||||
color: var(--color-text); |
||||
} |
||||
|
||||
.time-used strong { |
||||
color: var(--color-primary); |
||||
} |
||||
|
||||
.time-remaining strong { |
||||
color: #4a90e2; |
||||
} |
||||
|
||||
.time-limit-actions { |
||||
margin-top: 16px; |
||||
padding-top: 16px; |
||||
border-top: 1px solid rgba(212, 222, 239, 0.5); |
||||
} |
||||
|
||||
.time-limit-reset-btn { |
||||
padding: 8px 16px; |
||||
background: transparent; |
||||
color: var(--color-primary); |
||||
border: 1px solid var(--color-primary); |
||||
border-radius: 6px; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
cursor: pointer; |
||||
transition: background-color 0.2s, color 0.2s; |
||||
} |
||||
|
||||
.time-limit-reset-btn:hover { |
||||
background: var(--color-primary); |
||||
color: white; |
||||
} |
||||
|
||||
.time-limit-confirm-overlay { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background: rgba(0, 0, 0, 0.5); |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
z-index: 1000; |
||||
backdrop-filter: blur(4px); |
||||
} |
||||
|
||||
.time-limit-confirm-modal { |
||||
background: var(--color-surface); |
||||
border-radius: 12px; |
||||
padding: 24px; |
||||
max-width: 400px; |
||||
width: 90%; |
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); |
||||
} |
||||
|
||||
.time-limit-confirm-modal h3 { |
||||
margin: 0 0 12px 0; |
||||
font-size: 18px; |
||||
font-weight: 600; |
||||
color: var(--color-text); |
||||
} |
||||
|
||||
.time-limit-confirm-modal p { |
||||
margin: 0 0 20px 0; |
||||
font-size: 14px; |
||||
color: var(--color-muted); |
||||
line-height: 1.5; |
||||
} |
||||
|
||||
.time-limit-confirm-actions { |
||||
display: flex; |
||||
gap: 12px; |
||||
justify-content: flex-end; |
||||
} |
||||
|
||||
.time-limit-confirm-btn { |
||||
padding: 10px 20px; |
||||
border: none; |
||||
border-radius: 6px; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
cursor: pointer; |
||||
transition: transform 0.2s, box-shadow 0.2s; |
||||
} |
||||
|
||||
.time-limit-confirm-btn.confirm { |
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary)); |
||||
color: white; |
||||
} |
||||
|
||||
.time-limit-confirm-btn.confirm:hover { |
||||
transform: translateY(-2px); |
||||
box-shadow: 0 4px 12px rgba(47, 128, 237, 0.3); |
||||
} |
||||
|
||||
.time-limit-confirm-btn.cancel { |
||||
background: transparent; |
||||
color: var(--color-text); |
||||
border: 1px solid rgba(212, 222, 239, 0.8); |
||||
} |
||||
|
||||
.time-limit-confirm-btn.cancel:hover { |
||||
background: var(--color-surface-muted); |
||||
} |
||||
|
||||
@media (max-width: 768px) { |
||||
.time-limit-manager { |
||||
padding: 16px; |
||||
} |
||||
|
||||
.time-limit-input-group { |
||||
flex-direction: column; |
||||
align-items: stretch; |
||||
} |
||||
|
||||
.time-limit-input { |
||||
max-width: 100%; |
||||
} |
||||
|
||||
.time-limit-stats { |
||||
flex-direction: column; |
||||
gap: 8px; |
||||
} |
||||
|
||||
.time-limit-confirm-modal { |
||||
padding: 20px; |
||||
} |
||||
|
||||
.time-limit-confirm-actions { |
||||
flex-direction: column; |
||||
} |
||||
} |
||||
@ -0,0 +1,208 @@
@@ -0,0 +1,208 @@
|
||||
import { useState, useEffect } from 'react'; |
||||
import { |
||||
getDailyLimit, |
||||
getTimeUsedToday, |
||||
setDailyLimit, |
||||
resetDailyCounter |
||||
} from '../../services/timeLimitService'; |
||||
import './TimeLimitManager.css'; |
||||
|
||||
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="time-limit-manager"> |
||||
<div className="time-limit-header"> |
||||
<h2>Daily Time Limit Settings</h2> |
||||
<p>Loading...</p> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className="time-limit-manager"> |
||||
<div className="time-limit-header"> |
||||
<h2>Daily Time Limit Settings</h2> |
||||
<p>Configure how much time users can spend watching videos each day</p> |
||||
</div> |
||||
|
||||
{error && ( |
||||
<div className="alert alert-error" style={{ marginBottom: '16px' }}> |
||||
{error} |
||||
</div> |
||||
)} |
||||
|
||||
<div className="time-limit-section"> |
||||
<div className="time-limit-setting"> |
||||
<label htmlFor="daily-limit-input"> |
||||
Daily Limit (minutes) |
||||
</label> |
||||
<div className="time-limit-input-group"> |
||||
<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="time-limit-input" |
||||
/> |
||||
<button |
||||
onClick={handleSaveLimit} |
||||
disabled={isSaving || (dailyLimit !== null && inputValue === dailyLimit.toString())} |
||||
className="time-limit-save-btn" |
||||
> |
||||
{isSaving ? 'Saving...' : 'Save'} |
||||
</button> |
||||
</div> |
||||
{dailyLimit !== null && ( |
||||
<p className="time-limit-hint"> |
||||
Current limit: <strong>{formatTime(dailyLimit)}</strong> per day |
||||
</p> |
||||
)} |
||||
</div> |
||||
|
||||
{dailyLimit !== null && ( |
||||
<div className="time-limit-status"> |
||||
<h3>Today's Usage</h3> |
||||
<div className="time-limit-progress"> |
||||
<div className="time-limit-progress-bar"> |
||||
<div |
||||
className="time-limit-progress-fill" |
||||
style={{ |
||||
width: `${Math.min(100, (timeUsed / dailyLimit) * 100)}%` |
||||
}} |
||||
/> |
||||
</div> |
||||
<div className="time-limit-stats"> |
||||
<span className="time-used"> |
||||
Used: <strong>{formatTime(timeUsed)}</strong> |
||||
</span> |
||||
<span className="time-remaining"> |
||||
Remaining: <strong>{formatTime(remainingTime)}</strong> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
{timeUsed > 0 && ( |
||||
<div className="time-limit-actions"> |
||||
<button |
||||
onClick={() => setShowResetConfirm(true)} |
||||
className="time-limit-reset-btn" |
||||
> |
||||
Reset Today's Counter |
||||
</button> |
||||
</div> |
||||
)} |
||||
</div> |
||||
)} |
||||
</div> |
||||
|
||||
{showResetConfirm && ( |
||||
<div className="time-limit-confirm-overlay" onClick={() => setShowResetConfirm(false)}> |
||||
<div className="time-limit-confirm-modal" onClick={(e) => e.stopPropagation()}> |
||||
<h3>Reset Today's Counter?</h3> |
||||
<p> |
||||
This will reset the time used today back to 0. Users will be able to watch videos again. |
||||
</p> |
||||
<div className="time-limit-confirm-actions"> |
||||
<button |
||||
onClick={handleResetCounter} |
||||
className="time-limit-confirm-btn confirm" |
||||
> |
||||
Reset Counter |
||||
</button> |
||||
<button |
||||
onClick={() => setShowResetConfirm(false)} |
||||
className="time-limit-confirm-btn cancel" |
||||
> |
||||
Cancel |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
@ -0,0 +1,153 @@
@@ -0,0 +1,153 @@
|
||||
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()); |
||||
|
||||
// Fetch limit from server on mount
|
||||
useEffect(() => { |
||||
getDailyLimit().then(limit => { |
||||
setDailyLimit(limit); |
||||
}); |
||||
}, []); |
||||
|
||||
// Update state from localStorage and cached server limit
|
||||
const updateState = useCallback(() => { |
||||
setDailyLimit(getDailyLimitSync()); |
||||
setTimeUsed(getTimeUsedToday()); |
||||
setRemainingTime(getRemainingTimeToday()); |
||||
setLimitReached(isLimitReached()); |
||||
}, []); |
||||
|
||||
// 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 |
||||
}; |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
.time-limit-banner { |
||||
background-color: #ff6b6b; |
||||
color: white; |
||||
padding: 12px 20px; |
||||
text-align: center; |
||||
font-weight: 500; |
||||
margin-bottom: 20px; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.time-limit-banner p { |
||||
margin: 0; |
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
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 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 || new Date().toISOString().split('T')[0] |
||||
}; |
||||
} |
||||
} catch (e) { |
||||
console.warn('Failed to parse time limit data from localStorage', e); |
||||
} |
||||
|
||||
// Return default data
|
||||
return { |
||||
dailyTimeUsed: 0, |
||||
lastResetDate: new Date().toISOString().split('T')[0] |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 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 = new Date().toISOString().split('T')[0]; |
||||
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 = new Date().toISOString().split('T')[0]; |
||||
saveTimeLimitData(data); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get current time limit configuration from server |
||||
* Falls back to cached value or default if server unavailable |
||||
*/ |
||||
export async function getDailyLimit(): Promise<number> { |
||||
// 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(); |
||||
cachedDailyLimit = response.data.dailyLimit; |
||||
limitCacheTime = now; |
||||
return cachedDailyLimit; |
||||
} 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) |
||||
*/ |
||||
export function getDailyLimitSync(): number { |
||||
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 = new Date().toISOString().split('T')[0]; |
||||
saveTimeLimitData(data); |
||||
} |
||||
Loading…
Reference in new issue