Compare commits

...

1 Commits

Author SHA1 Message Date
Stephanie Gredell 88b400b464 remove time limit feature 1 month ago
  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'; @@ -3,10 +3,9 @@ import { Video } from '../../types/api';
interface VideoCardProps {
video: Video;
onClick: () => void;
disabled?: boolean;
}
export function VideoCard({ video, onClick, disabled = false }: VideoCardProps) {
export function VideoCard({ video, onClick }: VideoCardProps) {
const formatViews = (count: number): string => {
if (count >= 1000000) {
return `${(count / 1000000).toFixed(1)}M`;
@ -32,10 +31,8 @@ export function VideoCard({ video, onClick, disabled = false }: VideoCardProps) @@ -32,10 +31,8 @@ export function VideoCard({ video, onClick, disabled = false }: VideoCardProps)
return (
<div
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={disabled ? undefined : onClick}
className="cursor-pointer transition-all bg-card rounded-[20px] p-4 border border-border shadow-lg hover:-translate-y-1 hover:shadow-xl"
onClick={onClick}
>
<div className="relative w-full aspect-video overflow-hidden bg-muted rounded-xl group">
<img

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

@ -9,7 +9,6 @@ interface VideoGridProps { @@ -9,7 +9,6 @@ interface VideoGridProps {
page: number;
totalPages: number;
onPageChange: (page: number) => void;
disabled?: boolean;
}
export function VideoGrid({
@ -19,12 +18,11 @@ export function VideoGrid({ @@ -19,12 +18,11 @@ export function VideoGrid({
onVideoClick,
page,
totalPages,
onPageChange,
disabled = false
onPageChange
}: VideoGridProps) {
if (loading) {
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) => (
<div key={i} className="animate-pulse">
<div className="w-full aspect-video bg-muted rounded-2xl"></div>
@ -60,13 +58,12 @@ export function VideoGrid({ @@ -60,13 +58,12 @@ export function VideoGrid({
return (
<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 => (
<VideoCard
key={video.id}
video={video}
onClick={() => !disabled && onVideoClick(video.id)}
disabled={disabled}
onClick={() => onVideoClick(video.id)}
/>
))}
</div>

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

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
import { useEffect, useRef } from 'react';
import { useTimeLimit } from '../../hooks/useTimeLimit';
import { setCurrentVideo } from '../../services/connectionTracker';
interface VideoPlayerProps {
@ -10,9 +9,7 @@ interface VideoPlayerProps { @@ -10,9 +9,7 @@ interface VideoPlayerProps {
}
export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: VideoPlayerProps) {
const { limitReached, startTracking, stopTracking, remainingTime } = useTimeLimit();
const iframeRef = useRef<HTMLIFrameElement>(null);
const checkLimitIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// Set video info for connection tracking
@ -24,56 +21,21 @@ export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: Video @@ -24,56 +21,21 @@ export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: Video
// Handle Escape key
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
stopTracking();
setCurrentVideo(null);
onClose();
}
};
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 () => {
// Clear video info when player closes
setCurrentVideo(null);
document.body.style.overflow = 'unset';
window.removeEventListener('keydown', handleEscape);
stopTracking();
if (checkLimitIntervalRef.current) {
clearInterval(checkLimitIntervalRef.current);
}
};
}, [videoId, videoTitle, channelName, onClose, limitReached, startTracking, stopTracking]);
// 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]);
}, [videoId, videoTitle, channelName, onClose]);
const handleClose = () => {
stopTracking();
setCurrentVideo(null);
onClose();
};
@ -93,38 +55,18 @@ export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: Video @@ -93,38 +55,18 @@ export function VideoPlayer({ videoId, videoTitle, channelName, onClose }: Video
>
×
</button>
{limitReached ? (
<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">
<h2 className="text-[28px] md:text-[28px] text-2xl mb-4 text-[#ff6b6b]">Daily Time Limit Reached</h2>
<p className="text-lg md:text-lg text-base mb-6 opacity-90">
You've reached your daily video watching limit. Come back tomorrow!
</p>
<button
onClick={handleClose}
className="bg-[#4a90e2] text-white border-none py-3 px-6 rounded-md text-base cursor-pointer font-medium transition-colors hover:bg-[#357abd]"
>
Close
</button>
</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 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>
);

2
frontend/src/pages/AdminPage.tsx

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

39
frontend/src/pages/SettingsProfilesAdminPage.tsx

@ -75,13 +75,6 @@ export function SettingsProfilesAdminPage() { @@ -75,13 +75,6 @@ export function SettingsProfilesAdminPage() {
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) {
return (
@ -140,7 +133,6 @@ export function SettingsProfilesAdminPage() { @@ -140,7 +133,6 @@ export function SettingsProfilesAdminPage() {
<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">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">Status</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() { @@ -165,9 +157,6 @@ export function SettingsProfilesAdminPage() {
</button>
</div>
</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">
{profile.enabledApps && profile.enabledApps.length > 0 ? (
<div className="flex flex-wrap gap-1">
@ -259,7 +248,6 @@ function SettingsProfileFormModal({ @@ -259,7 +248,6 @@ function SettingsProfileFormModal({
}) {
const [name, setName] = useState(profile?.name || '');
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)
const defaultEnabledApps = APPS.filter(app => !app.disabled && app.id !== 'videos').map(app => app.id);
const [enabledApps, setEnabledApps] = useState<string[]>(profile?.enabledApps ?? defaultEnabledApps);
@ -283,21 +271,15 @@ function SettingsProfileFormModal({ @@ -283,21 +271,15 @@ function SettingsProfileFormModal({
return;
}
const limit = parseInt(dailyTimeLimit, 10);
if (isNaN(limit) || limit < 1) {
setError('Daily time limit must be at least 1 minute');
return;
}
try {
setLoading(true);
if (profile) {
// Update existing profile
await settingsProfilesApi.update(profile.id, { name, description });
await settingsProfilesApi.updateSettings(profile.id, { dailyTimeLimit: limit, enabledApps });
await settingsProfilesApi.updateSettings(profile.id, { enabledApps });
} else {
// Create new profile
await settingsProfilesApi.create({ name, description, dailyTimeLimit: limit, enabledApps });
await settingsProfilesApi.create({ name, description, enabledApps });
}
onSuccess();
} catch (err: any) {
@ -342,23 +324,6 @@ function SettingsProfileFormModal({ @@ -342,23 +324,6 @@ function SettingsProfileFormModal({
/>
</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">
<label className="block text-sm font-semibold text-foreground mb-2">
Enabled Apps

12
frontend/src/pages/VideoApp.tsx

@ -1,14 +1,12 @@ @@ -1,14 +1,12 @@
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useVideos } from '../hooks/useVideos';
import { useTimeLimit } from '../hooks/useTimeLimit';
import { VideoGrid } from '../components/VideoGrid/VideoGrid';
import { VideoPlayer } from '../components/VideoPlayer/VideoPlayer';
export function VideoApp() {
const [searchParams] = useSearchParams();
const [selectedVideo, setSelectedVideo] = useState<{ id: string; title: string; channelName: string } | null>(null);
const { limitReached } = useTimeLimit();
// Read from URL query params
const page = parseInt(searchParams.get('page') || '1', 10);
@ -34,9 +32,6 @@ export function VideoApp() { @@ -34,9 +32,6 @@ export function VideoApp() {
};
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
const video = videos.find(v => v.id === videoId);
if (video) {
@ -50,12 +45,6 @@ export function VideoApp() { @@ -50,12 +45,6 @@ export function VideoApp() {
return (
<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
videos={videos}
loading={loading}
@ -64,7 +53,6 @@ export function VideoApp() { @@ -64,7 +53,6 @@ export function VideoApp() {
page={page}
totalPages={meta.totalPages}
onPageChange={handlePageChange}
disabled={limitReached}
/>
{selectedVideo && (

12
frontend/src/pages/VideosAdminPage.tsx

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { ChannelManager } from '../components/ChannelManager/ChannelManager';
import { TimeLimitManager } from '../components/TimeLimitManager/TimeLimitManager';
import { videosApi } from '../services/apiClient';
export function VideosAdminPage() {
@ -37,7 +36,7 @@ export function VideosAdminPage() { @@ -37,7 +36,7 @@ export function VideosAdminPage() {
Back to Admin
</Link>
<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">
<button
onClick={handleRefreshVideos}
@ -59,13 +58,8 @@ export function VideosAdminPage() { @@ -59,13 +58,8 @@ export function VideosAdminPage() {
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6 max-w-[1600px] mx-auto">
<div className="flex flex-col">
<ChannelManager />
</div>
<div className="flex flex-col">
<TimeLimitManager />
</div>
<div className="p-6 max-w-[1600px] mx-auto">
<ChannelManager />
</div>
</div>
);

Loading…
Cancel
Save