From 3fbaef0f8c8832c35b5a75efd86b2bc73b50b1c2 Mon Sep 17 00:00:00 2001 From: Vidhanvyrs Date: Wed, 2 Oct 2024 14:34:46 +0530 Subject: [PATCH] Added feature: full screen functionality --- README.md | 2 +- components/footer.tsx | 88 ++++++++++++++++++----------- components/talk-timer.tsx | 114 +++++++++++++++++++++++--------------- components/ui/button.tsx | 55 +++++++++--------- 4 files changed, 157 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 4ddc245..ceef5bf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Talk Timer -A simple timer for talks, with configurable time for warning and end of talk. +A simple timer for talks, with configurable time for warning and end of talk with full screen functionality added, use key F/f to make the timer full screen or exit full screen. This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). It uses [shadcn/ui](https://ui.shadcn.com/) for the UI components. diff --git a/components/footer.tsx b/components/footer.tsx index 11fcc94..e5a78b9 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -1,27 +1,36 @@ -import { Button } from '@/components/ui/button' +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, - DialogTrigger -} from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Pause, Play, Settings, TimerReset } from 'lucide-react' + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Pause, + Play, + Settings, + TimerReset, + Maximize2, + Minimize2, +} from "lucide-react"; type FooterProps = { - active: boolean - isRunning: boolean - talkTitle: string - yellowThreshold: number - redThreshold: number - toggleTimer: () => void - resetTimer: () => void - setTalkTitle: (title: string) => void - setYellowThreshold: (threshold: number) => void - setRedThreshold: (threshold: number) => void -} + active: boolean; + isRunning: boolean; + talkTitle: string; + yellowThreshold: number; + redThreshold: number; + toggleTimer: () => void; + resetTimer: () => void; + setTalkTitle: (title: string) => void; + setYellowThreshold: (threshold: number) => void; + setRedThreshold: (threshold: number) => void; + toggleFullscreen: () => void; + isFullscreen: boolean; +}; const Footer = ({ active, @@ -33,19 +42,21 @@ const Footer = ({ resetTimer, setTalkTitle, setYellowThreshold, - setRedThreshold + setRedThreshold, + toggleFullscreen, + isFullscreen, }: FooterProps) => { return ( - ) -} + ); +}; -export default Footer +export default Footer; diff --git a/components/talk-timer.tsx b/components/talk-timer.tsx index 2bd8748..d99255c 100644 --- a/components/talk-timer.tsx +++ b/components/talk-timer.tsx @@ -1,79 +1,103 @@ -'use client' +"use client"; -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from "react"; -import Footer from './footer' -import TimerDisplay from './timer-display' +import Footer from "./footer"; +import TimerDisplay from "./timer-display"; export function TalkTimer() { - const [isRunning, setIsRunning] = useState(false) - const [elapsedTime, setElapsedTime] = useState(0) - const [talkTitle, setTalkTitle] = useState('My Lightning Talk ⚡') - const [yellowThreshold, setYellowThreshold] = useState(90) // 1 and half minute - const [redThreshold, setRedThreshold] = useState(120) // 2 minutes - const [active, setActive] = useState(false) + const [isRunning, setIsRunning] = useState(false); + const [elapsedTime, setElapsedTime] = useState(0); + const [talkTitle, setTalkTitle] = useState("My Lightning Talk ⚡"); + const [yellowThreshold, setYellowThreshold] = useState(90); // 1 and half minute + const [redThreshold, setRedThreshold] = useState(120); // 2 minutes + const [active, setActive] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); const getBackgroundColor = useCallback(() => { if (elapsedTime < yellowThreshold) { - return 'bg-gradient-to-br from-green-300 to-green-600' + return "bg-gradient-to-br from-green-300 to-green-600"; } else if (elapsedTime < redThreshold) { - return 'bg-gradient-to-br from-yellow-400 to-yellow-600' + return "bg-gradient-to-br from-yellow-400 to-yellow-600"; } else { - return 'bg-gradient-to-br from-red-400 to-red-600' + return "bg-gradient-to-br from-red-400 to-red-600"; } - }, [elapsedTime, yellowThreshold, redThreshold]) + }, [elapsedTime, yellowThreshold, redThreshold]); useEffect(() => { - let interval: NodeJS.Timeout + let interval: NodeJS.Timeout; if (isRunning) { interval = setInterval(() => { - setElapsedTime((prevTime) => prevTime + 1) - }, 1000) + setElapsedTime((prevTime) => prevTime + 1); + }, 1000); } - return () => clearInterval(interval) - }, [isRunning]) + return () => clearInterval(interval); + }, [isRunning]); - const hideAfter = 3000 + const hideAfter = 3000; useEffect(() => { - let timeout: NodeJS.Timeout + let timeout: NodeJS.Timeout; const handleMouseMove = () => { - setActive(true) - clearTimeout(timeout) + setActive(true); + clearTimeout(timeout); timeout = setTimeout(() => { - setActive(false) - }, hideAfter) - } + setActive(false); + }, hideAfter); + }; - window.addEventListener('mousemove', handleMouseMove) - handleMouseMove() // Initial call to start the timeout + window.addEventListener("mousemove", handleMouseMove); + handleMouseMove(); // Initial call to start the timeout return () => { - window.removeEventListener('mousemove', handleMouseMove) - clearTimeout(timeout) - } - }, []) + window.removeEventListener("mousemove", handleMouseMove); + clearTimeout(timeout); + }; + }, []); useEffect(() => { if (elapsedTime > 0) { - document.title = `Elapsed - ${formatTime(elapsedTime)}` + document.title = `Elapsed - ${formatTime(elapsedTime)}`; + } + }); + + const toggleTimer = () => setIsRunning(!isRunning); + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + } else if (document.exitFullscreen) { + document.exitFullscreen(); } - }) + setIsFullscreen(!isFullscreen); + }; - const toggleTimer = () => setIsRunning(!isRunning) + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "f" || event.key === "F") { + toggleFullscreen(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [isFullscreen]); const resetTimer = () => { - setIsRunning(false) - setElapsedTime(0) - } + setIsRunning(false); + setElapsedTime(0); + }; const formatTime = (seconds: number) => { - const mins = Math.floor(seconds / 60) - const secs = seconds % 60 - return `${mins.toString().padStart(2, '0')}:${secs + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, "0")}:${secs .toString() - .padStart(2, '0')}` - } + .padStart(2, "0")}`; + }; return (
@@ -93,7 +117,9 @@ export function TalkTimer() { setTalkTitle={setTalkTitle} setYellowThreshold={setYellowThreshold} setRedThreshold={setRedThreshold} + toggleFullscreen={toggleFullscreen} + isFullscreen={isFullscreen} />
- ) + ); } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 123b28e..49fa894 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,57 +1,60 @@ -import * as React from 'react' -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: - 'bg-primary text-primary-foreground shadow hover:bg-primary/90', + "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: - 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: - 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: - 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline' + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + fullscreen: + "border border-white rounded-sm bg-transparent text-white hover:bg-gray-700", }, size: { - default: 'h-9 px-4 py-2', - sm: 'h-8 rounded-md px-3 text-xs', - lg: 'h-10 rounded-md px-8', - icon: 'h-9 w-9' - } + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + fullscreenIcon: "h-6 w-6", + }, }, defaultVariants: { - variant: 'default', - size: 'default' - } + variant: "default", + size: "default", + }, } -) +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button' + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -) -Button.displayName = 'Button' +); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants };