Browse Source

Remove time limit feature

master
codegirl007 1 month ago committed by GitHub
parent
commit
a22310e032
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      frontend/src/components/VideoCard/VideoCard.tsx
  2. 11
      frontend/src/components/VideoGrid/VideoGrid.tsx
  3. 84
      frontend/src/components/VideoPlayer/VideoPlayer.tsx
  4. 2
      frontend/src/pages/AdminPage.tsx
  5. 39
      frontend/src/pages/SettingsProfilesAdminPage.tsx
  6. 12
      frontend/src/pages/VideoApp.tsx
  7. 12
      frontend/src/pages/VideosAdminPage.tsx

9
frontend/src/components/VideoCard/VideoCard.tsx

@ -3,10 +3,9 @@ import { Video } from '../../types/api';
interface VideoCardProps { interface VideoCardProps {
video: Video; video: Video;
onClick: () => void; onClick: () => void;
disabled?: boolean;
} }
export function VideoCard({ video, onClick, disabled = false }: VideoCardProps) { export function VideoCard({ video, onClick }: VideoCardProps) {
const formatViews = (count: number): string => { const formatViews = (count: number): string => {
if (count >= 1000000) { if (count >= 1000000) {
return `${(count / 1000000).toFixed(1)}M`; return `${(count / 1000000).toFixed(1)}M`;
@ -32,10 +31,8 @@ export function VideoCard({ video, onClick, disabled = false }: VideoCardProps)
return ( return (
<div <div
className={`cursor-pointer transition-all bg-card rounded-[20px] p-4 border border-border shadow-lg hover:-translate-y-1 hover:shadow-xl ${ className="cursor-pointer transition-all bg-card rounded-[20px] p-4 border border-border shadow-lg hover:-translate-y-1 hover:shadow-xl"
disabled ? 'opacity-50 cursor-not-allowed pointer-events-none' : '' onClick={onClick}
}`}
onClick={disabled ? undefined : onClick}
> >
<div className="relative w-full aspect-video overflow-hidden bg-muted rounded-xl group"> <div className="relative w-full aspect-video overflow-hidden bg-muted rounded-xl group">
<img <img

11
frontend/src/components/VideoGrid/VideoGrid.tsx

@ -9,7 +9,6 @@ interface VideoGridProps {
page: number; page: number;
totalPages: number; totalPages: number;
onPageChange: (page: number) => void; onPageChange: (page: number) => void;
disabled?: boolean;
} }
export function VideoGrid({ export function VideoGrid({
@ -19,12 +18,11 @@ export function VideoGrid({
onVideoClick, onVideoClick,
page, page,
totalPages, totalPages,
onPageChange, onPageChange
disabled = false
}: VideoGridProps) { }: VideoGridProps) {
if (loading) { if (loading) {
return ( return (
<div className={`grid grid-cols-[repeat(auto-fill,minmax(320px,1fr))] gap-6 py-8 px-6 max-w-[1600px] mx-auto ${disabled ? 'pointer-events-none' : ''}`}> <div className="grid grid-cols-[repeat(auto-fill,minmax(320px,1fr))] gap-6 py-8 px-6 max-w-[1600px] mx-auto">
{Array.from({ length: 12 }).map((_, i) => ( {Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="animate-pulse"> <div key={i} className="animate-pulse">
<div className="w-full aspect-video bg-muted rounded-2xl"></div> <div className="w-full aspect-video bg-muted rounded-2xl"></div>
@ -60,13 +58,12 @@ export function VideoGrid({
return ( return (
<div> <div>
<div className={`grid grid-cols-[repeat(auto-fill,minmax(320px,1fr))] gap-6 py-8 px-6 max-w-[1600px] mx-auto md:grid-cols-[repeat(auto-fill,minmax(320px,1fr))] md:gap-6 md:py-8 md:px-6 grid-cols-1 gap-4 p-4 ${disabled ? 'pointer-events-none' : ''}`}> <div className="grid grid-cols-[repeat(auto-fill,minmax(320px,1fr))] gap-6 py-8 px-6 max-w-[1600px] mx-auto md:grid-cols-[repeat(auto-fill,minmax(320px,1fr))] md:gap-6 md:py-8 md:px-6 grid-cols-1 gap-4 p-4">
{videos.map(video => ( {videos.map(video => (
<VideoCard <VideoCard
key={video.id} key={video.id}
video={video} video={video}
onClick={() => !disabled && onVideoClick(video.id)} onClick={() => onVideoClick(video.id)}
disabled={disabled}
/> />
))} ))}
</div> </div>

84
frontend/src/components/VideoPlayer/VideoPlayer.tsx

@ -1,5 +1,4 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useTimeLimit } from '../../hooks/useTimeLimit';
import { setCurrentVideo } from '../../services/connectionTracker'; import { setCurrentVideo } from '../../services/connectionTracker';
interface VideoPlayerProps { interface VideoPlayerProps {
@ -10,9 +9,7 @@ interface VideoPlayerProps {
} }
export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: VideoPlayerProps) { export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: VideoPlayerProps) {
const { limitReached, startTracking, stopTracking, remainingTime } = useTimeLimit();
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const checkLimitIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
// Set video info for connection tracking // Set video info for connection tracking
@ -24,56 +21,21 @@ export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: Video
// Handle Escape key // Handle Escape key
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
stopTracking();
setCurrentVideo(null); setCurrentVideo(null);
onClose(); onClose();
} }
}; };
window.addEventListener('keydown', handleEscape); window.addEventListener('keydown', handleEscape);
// Start tracking time when player opens
if (!limitReached) {
startTracking();
}
// Check limit periodically and stop video if reached
checkLimitIntervalRef.current = setInterval(() => {
if (limitReached && iframeRef.current) {
// Stop the video by removing autoplay and reloading with paused state
if (iframeRef.current.src.includes('autoplay=1')) {
iframeRef.current.src = iframeRef.current.src.replace('autoplay=1', 'autoplay=0');
}
stopTracking();
setCurrentVideo(null);
}
}, 1000);
return () => { return () => {
// Clear video info when player closes // Clear video info when player closes
setCurrentVideo(null); setCurrentVideo(null);
document.body.style.overflow = 'unset'; document.body.style.overflow = 'unset';
window.removeEventListener('keydown', handleEscape); window.removeEventListener('keydown', handleEscape);
stopTracking();
if (checkLimitIntervalRef.current) {
clearInterval(checkLimitIntervalRef.current);
}
}; };
}, [videoId, videoTitle, channelName, onClose, limitReached, startTracking, stopTracking]); }, [videoId, videoTitle, channelName, onClose]);
// Stop video immediately if limit reached
useEffect(() => {
if (limitReached && iframeRef.current) {
// Change iframe src to stop autoplay
const currentSrc = iframeRef.current.src;
if (currentSrc.includes('autoplay=1')) {
iframeRef.current.src = currentSrc.replace('autoplay=1', 'autoplay=0');
}
stopTracking();
}
}, [limitReached, stopTracking]);
const handleClose = () => { const handleClose = () => {
stopTracking();
setCurrentVideo(null); setCurrentVideo(null);
onClose(); onClose();
}; };
@ -93,38 +55,18 @@ export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: Video
> >
× ×
</button> </button>
{limitReached ? ( <div className="relative w-full pb-[56.25%]">
<div className="py-[60px] px-10 md:py-[60px] md:px-10 py-10 px-5 text-center text-white min-h-[400px] flex flex-col items-center justify-center"> <iframe
<h2 className="text-[28px] md:text-[28px] text-2xl mb-4 text-[#ff6b6b]">Daily Time Limit Reached</h2> ref={iframeRef}
<p className="text-lg md:text-lg text-base mb-6 opacity-90"> width="100%"
You've reached your daily video watching limit. Come back tomorrow! height="100%"
</p> src={`https://www.youtube.com/embed/${videoId}?autoplay=1&enablejsapi=1`}
<button allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
onClick={handleClose} allowFullScreen
className="bg-[#4a90e2] text-white border-none py-3 px-6 rounded-md text-base cursor-pointer font-medium transition-colors hover:bg-[#357abd]" title="YouTube video player"
> className="absolute top-0 left-0 w-full h-full border-none"
Close />
</button> </div>
</div>
) : (
<>
<div className="absolute top-2.5 md:top-2.5 top-[50px] left-2.5 md:left-2.5 left-2.5 bg-black/70 text-white py-2 px-3 rounded text-sm md:text-sm text-xs z-[1002] font-medium">
{Math.floor(remainingTime)} min remaining today
</div>
<div className="relative w-full pb-[56.25%]">
<iframe
ref={iframeRef}
width="100%"
height="100%"
src={`https://www.youtube.com/embed/${videoId}?autoplay=1&enablejsapi=1`}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="YouTube video player"
className="absolute top-0 left-0 w-full h-full border-none"
/>
</div>
</>
)}
</div> </div>
</div> </div>
); );

2
frontend/src/pages/AdminPage.tsx

@ -26,7 +26,7 @@ export function AdminPage() {
</div> </div>
<h2 className="text-xl font-bold mb-1">Video App</h2> <h2 className="text-xl font-bold mb-1">Video App</h2>
<p className="text-sm opacity-75"> <p className="text-sm opacity-75">
Manage YouTube channels and video time limits Manage YouTube channels
</p> </p>
</Link> </Link>

39
frontend/src/pages/SettingsProfilesAdminPage.tsx

@ -75,13 +75,6 @@ export function SettingsProfilesAdminPage() {
return new Date(dateString).toLocaleDateString(); return new Date(dateString).toLocaleDateString();
}; };
const formatTime = (minutes: number | null) => {
if (!minutes) return 'Not set';
if (minutes < 60) return `${minutes} min`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
};
if (loading) { if (loading) {
return ( return (
@ -140,7 +133,6 @@ export function SettingsProfilesAdminPage() {
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Name</th> <th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Name</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Magic Code</th> <th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Magic Code</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Time Limit</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Enabled Apps</th> <th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Enabled Apps</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Status</th> <th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Created</th> <th className="px-6 py-3 text-left text-xs font-semibold text-foreground uppercase">Created</th>
@ -165,9 +157,6 @@ export function SettingsProfilesAdminPage() {
</button> </button>
</div> </div>
</td> </td>
<td className="px-6 py-4 text-sm text-muted-foreground">
{formatTime(profile.dailyTimeLimit)}
</td>
<td className="px-6 py-4 text-sm text-muted-foreground"> <td className="px-6 py-4 text-sm text-muted-foreground">
{profile.enabledApps && profile.enabledApps.length > 0 ? ( {profile.enabledApps && profile.enabledApps.length > 0 ? (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
@ -259,7 +248,6 @@ function SettingsProfileFormModal({
}) { }) {
const [name, setName] = useState(profile?.name || ''); const [name, setName] = useState(profile?.name || '');
const [description, setDescription] = useState(profile?.description || ''); const [description, setDescription] = useState(profile?.description || '');
const [dailyTimeLimit, setDailyTimeLimit] = useState(profile?.dailyTimeLimit?.toString() || '30');
// Default enabled apps: speechsounds and tictactoe (videos is disabled by default) // Default enabled apps: speechsounds and tictactoe (videos is disabled by default)
const defaultEnabledApps = APPS.filter(app => !app.disabled && app.id !== 'videos').map(app => app.id); const defaultEnabledApps = APPS.filter(app => !app.disabled && app.id !== 'videos').map(app => app.id);
const [enabledApps, setEnabledApps] = useState<string[]>(profile?.enabledApps ?? defaultEnabledApps); const [enabledApps, setEnabledApps] = useState<string[]>(profile?.enabledApps ?? defaultEnabledApps);
@ -283,21 +271,15 @@ function SettingsProfileFormModal({
return; return;
} }
const limit = parseInt(dailyTimeLimit, 10);
if (isNaN(limit) || limit < 1) {
setError('Daily time limit must be at least 1 minute');
return;
}
try { try {
setLoading(true); setLoading(true);
if (profile) { if (profile) {
// Update existing profile // Update existing profile
await settingsProfilesApi.update(profile.id, { name, description }); await settingsProfilesApi.update(profile.id, { name, description });
await settingsProfilesApi.updateSettings(profile.id, { dailyTimeLimit: limit, enabledApps }); await settingsProfilesApi.updateSettings(profile.id, { enabledApps });
} else { } else {
// Create new profile // Create new profile
await settingsProfilesApi.create({ name, description, dailyTimeLimit: limit, enabledApps }); await settingsProfilesApi.create({ name, description, enabledApps });
} }
onSuccess(); onSuccess();
} catch (err: any) { } catch (err: any) {
@ -342,23 +324,6 @@ function SettingsProfileFormModal({
/> />
</div> </div>
<div className="mb-6">
<label className="block text-sm font-semibold text-foreground mb-2">
Daily Time Limit (minutes) *
</label>
<input
type="number"
min="1"
value={dailyTimeLimit}
onChange={(e) => setDailyTimeLimit(e.target.value)}
className="w-full px-4 py-2 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
required
/>
<p className="text-xs text-muted-foreground mt-1">
Maximum minutes per day children can watch videos
</p>
</div>
<div className="mb-6"> <div className="mb-6">
<label className="block text-sm font-semibold text-foreground mb-2"> <label className="block text-sm font-semibold text-foreground mb-2">
Enabled Apps Enabled Apps

12
frontend/src/pages/VideoApp.tsx

@ -1,14 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useVideos } from '../hooks/useVideos'; import { useVideos } from '../hooks/useVideos';
import { useTimeLimit } from '../hooks/useTimeLimit';
import { VideoGrid } from '../components/VideoGrid/VideoGrid'; import { VideoGrid } from '../components/VideoGrid/VideoGrid';
import { VideoPlayer } from '../components/VideoPlayer/VideoPlayer'; import { VideoPlayer } from '../components/VideoPlayer/VideoPlayer';
export function VideoApp() { export function VideoApp() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [selectedVideo, setSelectedVideo] = useState<{ id: string; title: string; channelName: string } | null>(null); const [selectedVideo, setSelectedVideo] = useState<{ id: string; title: string; channelName: string } | null>(null);
const { limitReached } = useTimeLimit();
// Read from URL query params // Read from URL query params
const page = parseInt(searchParams.get('page') || '1', 10); const page = parseInt(searchParams.get('page') || '1', 10);
@ -34,9 +32,6 @@ export function VideoApp() {
}; };
const handleVideoClick = (videoId: string) => { const handleVideoClick = (videoId: string) => {
if (limitReached) {
return; // Don't allow video to open if limit reached
}
// Find the video to get title and channel name // Find the video to get title and channel name
const video = videos.find(v => v.id === videoId); const video = videos.find(v => v.id === videoId);
if (video) { if (video) {
@ -50,12 +45,6 @@ export function VideoApp() {
return ( return (
<div> <div>
{limitReached && (
<div className="bg-[#ff6b6b] text-white py-3 px-5 text-center font-medium mb-5 rounded-md">
<p className="m-0">Daily time limit reached. Videos are disabled until tomorrow.</p>
</div>
)}
<VideoGrid <VideoGrid
videos={videos} videos={videos}
loading={loading} loading={loading}
@ -64,7 +53,6 @@ export function VideoApp() {
page={page} page={page}
totalPages={meta.totalPages} totalPages={meta.totalPages}
onPageChange={handlePageChange} onPageChange={handlePageChange}
disabled={limitReached}
/> />
{selectedVideo && ( {selectedVideo && (

12
frontend/src/pages/VideosAdminPage.tsx

@ -1,7 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ChannelManager } from '../components/ChannelManager/ChannelManager'; import { ChannelManager } from '../components/ChannelManager/ChannelManager';
import { TimeLimitManager } from '../components/TimeLimitManager/TimeLimitManager';
import { videosApi } from '../services/apiClient'; import { videosApi } from '../services/apiClient';
export function VideosAdminPage() { export function VideosAdminPage() {
@ -37,7 +36,7 @@ export function VideosAdminPage() {
Back to Admin Back to Admin
</Link> </Link>
<h1 className="m-0 mb-2 text-[28px] font-medium text-foreground">Video App Settings</h1> <h1 className="m-0 mb-2 text-[28px] font-medium text-foreground">Video App Settings</h1>
<p className="m-0 text-sm text-muted-foreground">Manage YouTube channels and video time limits</p> <p className="m-0 text-sm text-muted-foreground">Manage YouTube channels</p>
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={handleRefreshVideos} onClick={handleRefreshVideos}
@ -59,13 +58,8 @@ export function VideosAdminPage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6 max-w-[1600px] mx-auto"> <div className="p-6 max-w-[1600px] mx-auto">
<div className="flex flex-col"> <ChannelManager />
<ChannelManager />
</div>
<div className="flex flex-col">
<TimeLimitManager />
</div>
</div> </div>
</div> </div>
); );

Loading…
Cancel
Save