├── .eslintrc.json ├── app ├── globals.css ├── favicon.ico ├── page.tsx └── layout.tsx ├── next.config.mjs ├── postcss.config.mjs ├── lib └── utils.ts ├── components.json ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json ├── components ├── ui │ ├── input.tsx │ └── button.tsx └── Countdown.tsx ├── README.md └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umarprogrammer19/NextJS-Countdown-Timer/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Countdown from "@/components/Countdown"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "01-countdown-timer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-slot": "^1.1.0", 13 | "class-variance-authority": "^0.7.0", 14 | "clsx": "^2.1.1", 15 | "lucide-react": "^0.438.0", 16 | "next": "14.2.7", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "tailwind-merge": "^2.5.2", 20 | "tailwindcss-animate": "^1.0.7" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "eslint": "^8", 27 | "eslint-config-next": "14.2.7", 28 | "postcss": "^8", 29 | "tailwindcss": "^3.4.1", 30 | "typescript": "^5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | backgroundImage: { 13 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 14 | 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' 15 | }, 16 | borderRadius: { 17 | lg: 'var(--radius)', 18 | md: 'calc(var(--radius) - 2px)', 19 | sm: 'calc(var(--radius) - 4px)' 20 | }, 21 | colors: { 22 | background: 'hsl(var(--background))', 23 | foreground: 'hsl(var(--foreground))', 24 | card: { 25 | DEFAULT: 'hsl(var(--card))', 26 | foreground: 'hsl(var(--card-foreground))' 27 | }, 28 | popover: { 29 | DEFAULT: 'hsl(var(--popover))', 30 | foreground: 'hsl(var(--popover-foreground))' 31 | }, 32 | primary: { 33 | DEFAULT: 'hsl(var(--primary))', 34 | foreground: 'hsl(var(--primary-foreground))' 35 | }, 36 | secondary: { 37 | DEFAULT: 'hsl(var(--secondary))', 38 | foreground: 'hsl(var(--secondary-foreground))' 39 | }, 40 | muted: { 41 | DEFAULT: 'hsl(var(--muted))', 42 | foreground: 'hsl(var(--muted-foreground))' 43 | }, 44 | accent: { 45 | DEFAULT: 'hsl(var(--accent))', 46 | foreground: 'hsl(var(--accent-foreground))' 47 | }, 48 | destructive: { 49 | DEFAULT: 'hsl(var(--destructive))', 50 | foreground: 'hsl(var(--destructive-foreground))' 51 | }, 52 | border: 'hsl(var(--border))', 53 | input: 'hsl(var(--input))', 54 | ring: 'hsl(var(--ring))', 55 | chart: { 56 | '1': 'hsl(var(--chart-1))', 57 | '2': 'hsl(var(--chart-2))', 58 | '3': 'hsl(var(--chart-3))', 59 | '4': 'hsl(var(--chart-4))', 60 | '5': 'hsl(var(--chart-5))' 61 | } 62 | } 63 | } 64 | }, 65 | plugins: [require("tailwindcss-animate")], 66 | }; 67 | export default config; 68 | -------------------------------------------------------------------------------- /components/Countdown.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Enables client-side rendering for this component 2 | 3 | import { useState, useRef, useEffect, ChangeEvent } from 'react'; // Importing necessary hooks from React 4 | import { Button } from '@/components/ui/button'; // Importing custom Button component 5 | import { Input } from '@/components/ui/input'; // Importing custom Input component 6 | 7 | function Countdown() { 8 | // State to store the duration input by the user (in seconds) 9 | const [duration, setDuration] = useState(""); 10 | 11 | // State to keep track of the time left in the countdown 12 | const [timeLeft, setTimeLeft] = useState(0); 13 | 14 | // State to check if the countdown timer is running 15 | const [isActive, setIsActive] = useState(false); 16 | 17 | // State to check if the countdown timer is paused 18 | const [isPaused, setIsPaused] = useState(false); 19 | 20 | // Reference to store the interval ID so we can clear it later 21 | const timerRef = useRef(null); 22 | 23 | // Function to set the countdown duration based on user input 24 | const handleSetDuration = (): void => { 25 | // Only proceed if duration is a valid number and greater than 0 26 | if (typeof duration === "number" && duration > 0) { 27 | setTimeLeft(duration); // Set the countdown time 28 | setIsActive(false); // Make sure the timer is not running 29 | setIsPaused(false); // Make sure the timer is not paused 30 | setDuration(""); // Clear the input field for better UX 31 | 32 | // Clear any existing timer to avoid multiple intervals running at once 33 | if (timerRef.current) { 34 | clearInterval(timerRef.current); 35 | timerRef.current = null; 36 | } 37 | } 38 | }; 39 | 40 | // Function to start the countdown timer 41 | const handleStart = (): void => { 42 | // Start the countdown only if there's time left 43 | if (timeLeft > 0) { 44 | setIsActive(true); // Activate the timer 45 | setIsPaused(false); // Ensure the timer is not paused 46 | } 47 | }; 48 | 49 | // Function to pause the countdown timer 50 | const handlePause = (): void => { 51 | // Only pause if the timer is currently active 52 | if (isActive) { 53 | setIsPaused(true); // Set the timer to paused 54 | setIsActive(false); // Deactivate the timer 55 | 56 | // Clear the interval to stop the countdown 57 | if (timerRef.current) { 58 | clearInterval(timerRef.current); 59 | timerRef.current = null; 60 | } 61 | } 62 | }; 63 | 64 | // Function to reset the countdown timer 65 | const handleReset = (): void => { 66 | setIsActive(false); // Deactivate the timer 67 | setIsPaused(false); // Make sure the timer is not paused 68 | setTimeLeft(typeof duration === "number" ? duration : 0); // Reset the time to the original duration 69 | 70 | // Clear any running interval 71 | if (timerRef.current) { 72 | clearInterval(timerRef.current); 73 | timerRef.current = null; 74 | } 75 | }; 76 | 77 | // Effect to handle the countdown logic when the timer is active 78 | useEffect(() => { 79 | // If the timer is active and not paused, start the countdown 80 | if (isActive && !isPaused) { 81 | timerRef.current = setInterval(() => { 82 | setTimeLeft((prevTime) => { 83 | if (prevTime <= 1) { 84 | clearInterval(timerRef.current!); // Stop the countdown at 0 85 | timerRef.current = null; 86 | } 87 | return prevTime - 1; // Decrease time by 1 second 88 | }); 89 | }, 1000); // Update every second 90 | } 91 | 92 | // Cleanup function to clear the interval when the component unmounts or the timer stops 93 | return () => { 94 | if (timerRef.current) { 95 | clearInterval(timerRef.current); 96 | timerRef.current = null; 97 | } 98 | }; 99 | }, [isActive, isPaused]); // Dependencies array ensures the effect runs when these states change 100 | 101 | // Helper function to format the remaining time into a mm:ss format 102 | const formatTime = (time: number): string => { 103 | const minutes = Math.floor(time / 60); // Calculate minutes 104 | const seconds = time % 60; // Calculate remaining seconds 105 | return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; // Format with leading zeros 106 | }; 107 | 108 | // Handler for changes in the duration input field 109 | const handleDurationChange = (event: ChangeEvent) => { 110 | const value = Number(event.target.value); // Convert input to number 111 | if (!isNaN(value)) { 112 | setDuration(value); // Set the duration if valid 113 | } else { 114 | setDuration(""); // Clear the input if invalid 115 | } 116 | }; 117 | 118 | return ( 119 |
120 | {/* Container for the countdown timer UI */} 121 |
122 | {/* Timer Title */} 123 |

124 | Countdown Timer 125 |

126 | 127 | {/* Input field for duration and Set button */} 128 |
129 | 137 | 147 |
148 | 149 | {/* Display the remaining time */} 150 |
151 | {formatTime(timeLeft)} 152 |
153 | 154 | {/* Buttons to control the countdown */} 155 |
156 | 166 | 176 | 186 |
187 |
188 |
189 | ); 190 | } 191 | 192 | export default Countdown; 193 | --------------------------------------------------------------------------------