Compare commits
12 Commits
remove-tim
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
9260d38749 | 2 weeks ago |
|
|
7774791bd8 | 3 weeks ago |
|
|
fa59e065ac | 3 weeks ago |
|
|
f1cc9509e9 | 3 weeks ago |
|
|
a601e6a788 | 3 weeks ago |
|
|
503da2ac9d | 3 weeks ago |
|
|
ce84848a25 | 3 weeks ago |
|
|
eeced22ea4 | 3 weeks ago |
|
|
e035cb472d | 4 weeks ago |
|
|
98f00da62d | 1 month ago |
|
|
5753f2c87e | 1 month ago |
|
|
a22310e032 | 1 month ago |
28 changed files with 752 additions and 983 deletions
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
@ -1,215 +0,0 @@
@@ -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 @@
@@ -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 |
||||
}; |
||||
} |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
import { useMemo, useState } from 'react'; |
||||
|
||||
type GameIframeAppProps = { |
||||
iframeUrl: string; |
||||
badgeText?: string; |
||||
title?: string; |
||||
description?: string; |
||||
footerNote?: string; |
||||
}; |
||||
|
||||
const DEFAULT_CONFIG = { |
||||
badgeText: 'Arcade Corner', |
||||
title: 'Embedded Game View', |
||||
description: 'Enjoy a game inside Kiddos while keeping all of our safety tools and controls handy.', |
||||
footerNote: 'Content below is provided by an external site.' |
||||
}; |
||||
|
||||
export function GameIframeApp({ |
||||
iframeUrl, |
||||
badgeText, |
||||
title, |
||||
description, |
||||
footerNote |
||||
}: GameIframeAppProps) { |
||||
const [loading, setLoading] = useState(true); |
||||
|
||||
const config = useMemo( |
||||
() => ({ |
||||
iframeUrl, |
||||
badgeText: badgeText || DEFAULT_CONFIG.badgeText, |
||||
title: title || DEFAULT_CONFIG.title, |
||||
description: description || DEFAULT_CONFIG.description, |
||||
footerNote: footerNote || DEFAULT_CONFIG.footerNote |
||||
}), |
||||
[iframeUrl, badgeText, title, description, footerNote] |
||||
); |
||||
|
||||
if (!config.iframeUrl) { |
||||
return ( |
||||
<div className="min-h-screen flex items-center justify-center bg-background text-muted-foreground"> |
||||
Missing iframeUrl for embedded game. |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<div className="min-h-screen w-full bg-background flex flex-col"> |
||||
|
||||
<div className="flex-1 w-full flex flex-col"> |
||||
<div className="flex-1 w-full bg-card border border-border overflow-hidden flex flex-col"> |
||||
{loading && ( |
||||
<div className="p-4 text-center text-muted-foreground text-sm bg-muted"> |
||||
Loading embedded game... |
||||
</div> |
||||
)} |
||||
<iframe |
||||
src={config.iframeUrl} |
||||
title="Embedded Game" |
||||
className="w-full h-full flex-1 border-0" |
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" |
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups" |
||||
allowFullScreen |
||||
onLoad={() => setLoading(false)} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,185 @@
@@ -0,0 +1,185 @@
|
||||
import { useState } from 'react'; |
||||
import { useNavigate, Link } from 'react-router-dom'; |
||||
import { useAuth } from '../hooks/useAuth'; |
||||
|
||||
export function RegisterPage() { |
||||
const [username, setUsername] = useState(''); |
||||
const [password, setPassword] = useState(''); |
||||
const [confirmPassword, setConfirmPassword] = useState(''); |
||||
const [dateOfBirth, setDateOfBirth] = useState(''); |
||||
const [error, setError] = useState<string | null>(null); |
||||
const [loading, setLoading] = useState(false); |
||||
|
||||
const { login } = useAuth(); |
||||
const navigate = useNavigate(); |
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => { |
||||
e.preventDefault(); |
||||
setError(null); |
||||
|
||||
// Validation
|
||||
if (username.length < 3) { |
||||
setError('Username must be at least 3 characters long'); |
||||
return; |
||||
} |
||||
|
||||
if (password.length < 8) { |
||||
setError('Password must be at least 8 characters long'); |
||||
return; |
||||
} |
||||
|
||||
if (password !== confirmPassword) { |
||||
setError('Passwords do not match'); |
||||
return; |
||||
} |
||||
|
||||
if (!dateOfBirth) { |
||||
setError('Date of birth is required'); |
||||
return; |
||||
} |
||||
|
||||
// Validate age on frontend as well
|
||||
const birthDate = new Date(dateOfBirth); |
||||
const today = new Date(); |
||||
const age = today.getFullYear() - birthDate.getFullYear(); |
||||
const monthDiff = today.getMonth() - birthDate.getMonth(); |
||||
const dayDiff = today.getDate() - birthDate.getDate(); |
||||
|
||||
let actualAge = age; |
||||
if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { |
||||
actualAge--; |
||||
} |
||||
|
||||
if (actualAge < 18) { |
||||
setError('You must be at least 18 years old to register'); |
||||
return; |
||||
} |
||||
|
||||
setLoading(true); |
||||
|
||||
try { |
||||
const { authApi } = await import('../services/apiClient'); |
||||
await authApi.register(username, password, dateOfBirth); |
||||
|
||||
// Registration endpoint returns tokens and user data, same as login
|
||||
// Use login function to set user and token in auth context
|
||||
// This ensures consistent state management
|
||||
await login(username, password); |
||||
|
||||
// Navigate to home page
|
||||
navigate('/'); |
||||
} catch (err: any) { |
||||
setError(err.error?.message || 'Registration failed. Please try again.'); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<div className="flex items-start justify-center bg-background px-4 pt-12 pb-8"> |
||||
<div className="w-full max-w-md bg-card rounded-3xl shadow-lg overflow-hidden border border-border"> |
||||
<div className="px-8 pt-8 pb-6 text-center border-b border-border"> |
||||
<h1 className="text-2xl font-bold text-foreground mb-2">Create Account</h1> |
||||
<p className="text-sm text-muted-foreground">Sign up to get started</p> |
||||
</div> |
||||
|
||||
<form onSubmit={handleSubmit} className="px-8 py-8"> |
||||
{error && ( |
||||
<div className="mb-6 p-3 bg-destructive/10 text-destructive border border-destructive/20 rounded-xl text-sm"> |
||||
{error} |
||||
</div> |
||||
)} |
||||
|
||||
<div className="mb-5"> |
||||
<label htmlFor="username" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Username |
||||
</label> |
||||
<input |
||||
id="username" |
||||
type="text" |
||||
value={username} |
||||
onChange={(e) => setUsername(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
minLength={3} |
||||
maxLength={50} |
||||
autoFocus |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
<p className="mt-1 text-xs text-muted-foreground">Must be at least 3 characters</p> |
||||
</div> |
||||
|
||||
<div className="mb-5"> |
||||
<label htmlFor="password" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Password |
||||
</label> |
||||
<input |
||||
id="password" |
||||
type="password" |
||||
value={password} |
||||
onChange={(e) => setPassword(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
minLength={8} |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
<p className="mt-1 text-xs text-muted-foreground">Must be at least 8 characters</p> |
||||
</div> |
||||
|
||||
<div className="mb-5"> |
||||
<label htmlFor="confirmPassword" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Confirm Password |
||||
</label> |
||||
<input |
||||
id="confirmPassword" |
||||
type="password" |
||||
value={confirmPassword} |
||||
onChange={(e) => setConfirmPassword(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
minLength={8} |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
</div> |
||||
|
||||
<div className="mb-6"> |
||||
<label htmlFor="dateOfBirth" className="block mb-2 text-sm font-semibold text-foreground"> |
||||
Date of Birth |
||||
</label> |
||||
<input |
||||
id="dateOfBirth" |
||||
type="date" |
||||
value={dateOfBirth} |
||||
onChange={(e) => setDateOfBirth(e.target.value)} |
||||
disabled={loading} |
||||
required |
||||
max={new Date(new Date().setFullYear(new Date().getFullYear() - 18)).toISOString().split('T')[0]} |
||||
className="w-full px-4 py-3 border border-border rounded-xl bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50" |
||||
/> |
||||
<p className="mt-1 text-xs text-muted-foreground">You must be at least 18 years old to register</p> |
||||
</div> |
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full px-4 py-3 bg-primary text-primary-foreground rounded-xl font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-md mb-4" |
||||
> |
||||
{loading ? 'Creating account...' : 'Create Account'} |
||||
</button> |
||||
|
||||
<div className="text-center"> |
||||
<p className="text-sm text-muted-foreground"> |
||||
Have an account?{' '} |
||||
<Link
|
||||
to="/login"
|
||||
className="text-primary hover:underline font-semibold" |
||||
> |
||||
Sign in here |
||||
</Link> |
||||
</p> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
@ -1,210 +0,0 @@
@@ -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