Skip to content

Added feature: full screen functionality #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
84 changes: 55 additions & 29 deletions components/footer.tsx
Original file line number Diff line number Diff line change
@@ -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'
} 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,
Expand All @@ -34,18 +43,20 @@ const Footer = ({
setTalkTitle,
setYellowThreshold,
setRedThreshold,
toggleFullscreen,
isFullscreen,
}: FooterProps) => {
return (
<footer
className={`absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 text-white px-5 py-4 flex justify-between items-center transition-opacity duration-300 ${
active ? 'opacity-100' : 'opacity-0'
active ? "opacity-100" : "opacity-100"
}`}
>
<div className="flex items-center space-x-4">
<Button
onClick={toggleTimer}
variant="outline"
title={isRunning ? 'Pause Timer' : 'Start Timer'}
title={isRunning ? "Pause Timer" : "Start Timer"}
>
{isRunning ? (
<Pause className="h-4 w-4" strokeWidth={3} />
Expand All @@ -62,12 +73,27 @@ const Footer = ({
{talkTitle}
</h2>
</div>

<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon" title="Settings">
<Settings className="h-4 w-4" strokeWidth={3} />
<div className="flex flex-row space-x-4">
<Button
onClick={toggleFullscreen}
variant="outline"
// size="fullscreenIcon"
title={isFullscreen ? "Minimize" : "Maximize"}
>
{isFullscreen ? (
<Minimize2 className="h-4 w-4" strokeWidth={3} />
) : (
<Maximize2 className="h-4 w-4" strokeWidth={3} />
)}
</Button>
</DialogTrigger>
<DialogTrigger asChild>
<Button variant="outline" size="icon" title="Settings">
<Settings className="h-4 w-4" strokeWidth={3} />
</Button>
</DialogTrigger>
</div>
<DialogContent className="">
<DialogHeader>
<DialogTitle>Timer Settings</DialogTitle>
Expand Down Expand Up @@ -113,7 +139,7 @@ const Footer = ({
</div>
<div className="text-xs text-center">
<p>
Built with{' '}
Built with{" "}
<a
href="https://nextjs.org"
target="_blank"
Expand All @@ -122,7 +148,7 @@ const Footer = ({
>
Next.js
</a>
,{' '}
,{" "}
<a
href="https://react.dev/"
target="_blank"
Expand All @@ -131,7 +157,7 @@ const Footer = ({
>
React
</a>
,{' '}
,{" "}
<a
href="https://tailwindcss.com"
target="_blank"
Expand All @@ -140,7 +166,7 @@ const Footer = ({
>
Tailwind
</a>
, and{' '}
, and{" "}
<a
href="https://ui.shadcn.com"
target="_blank"
Expand Down Expand Up @@ -173,7 +199,7 @@ const Footer = ({
</DialogContent>
</Dialog>
</footer>
)
}
);
};

export default Footer
export default Footer;
114 changes: 70 additions & 44 deletions components/talk-timer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative h-screen">
Expand All @@ -93,7 +117,9 @@ export function TalkTimer() {
setTalkTitle={setTalkTitle}
setYellowThreshold={setYellowThreshold}
setRedThreshold={setRedThreshold}
toggleFullscreen={toggleFullscreen}
isFullscreen={isFullscreen}
/>
</div>
)
);
}
47 changes: 25 additions & 22 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,51 @@
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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
Expand All @@ -54,4 +57,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = 'Button'

export { Button, buttonVariants }
export { Button, buttonVariants };