├── public ├── contestclock.png ├── contestclock.svg └── vite.svg ├── src ├── assets │ ├── leetcode-96.png │ ├── icons8-alarm-64.png │ ├── icons8-link-48.png │ ├── icons8-calendar-48.png │ ├── icons8-caution-48.png │ ├── icons8-google-48.png │ ├── icons8-leetcode-50.png │ ├── icons8-stopwatch-48.png │ ├── icons8-calendar-plus-48.png │ ├── icons8-empty-hourglass-48.png │ ├── contestclock.svg │ ├── calendar.svg │ ├── startTime.svg │ ├── code-forces.svg │ ├── g4gremind.svg │ ├── geeks.svg │ ├── geeksForGeeks.svg │ ├── bell.svg │ ├── leetcode.svg │ ├── react.svg │ ├── codeChef.svg │ └── chefLogo.svg ├── utils │ ├── constants.js │ ├── appStore.js │ ├── userSlice.js │ ├── registeredContestsSlice.js │ └── firebase.js ├── lib │ └── utils.js ├── main.jsx ├── components │ ├── Home.jsx │ ├── Popovers.jsx │ ├── CalendarUI.jsx │ ├── AddCalendarWrapper.jsx │ ├── AddCalendar.jsx │ ├── ui │ │ ├── popover.jsx │ │ ├── accordion.jsx │ │ ├── button.jsx │ │ ├── card.jsx │ │ └── calendar.jsx │ ├── CalendarWrapper.jsx │ ├── DayContests.jsx │ ├── ContestList.jsx │ ├── PopoverTriggerData.jsx │ ├── ContestCalendar.jsx │ ├── PopoverData.jsx │ ├── Footer.jsx │ ├── FilterContests.jsx │ ├── RegisteredContests.jsx │ ├── Shimmer │ │ └── ShimmerList.jsx │ ├── UpcomingWrapper.jsx │ ├── Body.jsx │ ├── UpcomingContests.jsx │ ├── Header.jsx │ └── CardsContest.jsx ├── App.jsx └── index.css ├── jsconfig.json ├── .gitignore ├── vite.config.js ├── components.json ├── eslint.config.js ├── index.html ├── README.md └── package.json /public/contestclock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/public/contestclock.png -------------------------------------------------------------------------------- /src/assets/leetcode-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/leetcode-96.png -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const BASE_URL = location.hostname === "localhost" ? "http://localhost:3000" : "/api" -------------------------------------------------------------------------------- /src/assets/icons8-alarm-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-alarm-64.png -------------------------------------------------------------------------------- /src/assets/icons8-link-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-link-48.png -------------------------------------------------------------------------------- /src/assets/icons8-calendar-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-calendar-48.png -------------------------------------------------------------------------------- /src/assets/icons8-caution-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-caution-48.png -------------------------------------------------------------------------------- /src/assets/icons8-google-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-google-48.png -------------------------------------------------------------------------------- /src/assets/icons8-leetcode-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-leetcode-50.png -------------------------------------------------------------------------------- /src/assets/icons8-stopwatch-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-stopwatch-48.png -------------------------------------------------------------------------------- /src/assets/icons8-calendar-plus-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-calendar-plus-48.png -------------------------------------------------------------------------------- /src/assets/icons8-empty-hourglass-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SreekarSBS/ContestClock-UI/HEAD/src/assets/icons8-empty-hourglass-48.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | 6 | 7 | createRoot(document.getElementById('root')).render( 8 | 9 | 10 | 11 | , 12 | ) 13 | -------------------------------------------------------------------------------- /src/utils/appStore.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import userReducer from "./userSlice" 3 | import registeredContestReducer from "./registeredContestsSlice" 4 | const appStore = configureStore({ 5 | reducer : { 6 | user : userReducer, 7 | registeredContests : registeredContestReducer 8 | } 9 | }) 10 | 11 | 12 | export default appStore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | stats.html 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/utils/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const userSlice = createSlice({ 4 | name : "user", 5 | initialState : null, 6 | reducers : { 7 | addUser : (state,action) => action.payload, 8 | removeUser : () => null 9 | } 10 | }) 11 | 12 | export const {addUser,removeUser} = userSlice.actions 13 | export default userSlice.reducer -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | import { visualizer } from "rollup-plugin-visualizer"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react() ,tailwindcss(),visualizer({ open: true })], 9 | build: { 10 | minify: "esbuild", 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import CalendarUI from './CalendarUI' 4 | import ContestCalendar from './ContestCalendar' 5 | import ContestList from './ContestList' 6 | 7 | 8 | 9 | 10 | const Home = () => { 11 | 12 | return ( 13 | 14 | 15 |
16 | 17 | 18 |
19 | 20 | ) 21 | } 22 | 23 | export default Home 24 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 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 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/utils/registeredContestsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const registeredContestSlice = createSlice({ 4 | name : "registeredContests", 5 | initialState : null, 6 | reducers : { 7 | addContest : (state,action) => action.payload, 8 | removeContest : (state,action) => { 9 | const filteredContests = state.filter(item => item._id != action.payload) 10 | return filteredContests 11 | }, 12 | clearContest :() => null 13 | } 14 | }) 15 | 16 | export const {addContest,removeContest,clearContest} = registeredContestSlice.actions; 17 | export default registeredContestSlice.reducer -------------------------------------------------------------------------------- /public/contestclock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/contestclock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/calendar.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Popovers.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Popover, 4 | PopoverContent, 5 | PopoverTrigger, 6 | } from "./ui/popover" 7 | 8 | import PopoverData from "./PopoverData"; 9 | import PopoverTriggerData from "./PopoverTriggerData"; 10 | 11 | const Popovers = ({eventInfo}) => 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | export default Popovers -------------------------------------------------------------------------------- /src/components/CalendarUI.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Calendar } from "./ui/calendar"; 3 | import ContestList from "./ContestList"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | const CalendarUI = () => { 7 | const [date, setDate] = useState(new Date()); 8 | const navigate = useNavigate() 9 | 10 | const handleClick = (selectedDate) => { 11 | setDate(selectedDate) 12 | navigate("/contests/"+selectedDate) 13 | } 14 | 15 | return ( 16 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export default CalendarUI; 30 | -------------------------------------------------------------------------------- /src/assets/startTime.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs['recommended-latest'], 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /src/utils/firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import { getAuth } from "firebase/auth"; 4 | // TODO: Add SDKs for Firebase products that you want to use 5 | // https://firebase.google.com/docs/web/setup#available-libraries 6 | 7 | // Your web app's Firebase configuration 8 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional 9 | const firebaseConfig = { 10 | apiKey: "AIzaSyBNDsqYa19R7cLAnU7VUw5ol6uQ6CUobmk", 11 | authDomain: "contesthub-8e38c.firebaseapp.com", 12 | projectId: "contesthub-8e38c", 13 | storageBucket: "contesthub-8e38c.firebasestorage.app", 14 | messagingSenderId: "146142469926", 15 | appId: "1:146142469926:web:a1d9820c335c5c4fe76be7", 16 | measurementId: "G-QZH2KBCZL1" 17 | }; 18 | 19 | // Initialize Firebase 20 | const app = initializeApp(firebaseConfig); 21 | export const auth = getAuth(app); -------------------------------------------------------------------------------- /src/assets/code-forces.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Route, Routes } from "react-router-dom" 2 | import Body from "./components/Body" 3 | import Home from "./components/Home" 4 | import DayContests from "./components/DayContests" 5 | import ContestCalendar from "./components/ContestCalendar" 6 | import { Provider } from "react-redux" 7 | import appStore from "./utils/appStore" 8 | import RegisteredContests from "./components/RegisteredContests" 9 | 10 | 11 | 12 | function App() { 13 | 14 | return ( 15 | <> 16 | 17 | 18 | 19 | }> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default App 33 | -------------------------------------------------------------------------------- /src/components/AddCalendarWrapper.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { AddToCalendarButton } from 'add-to-calendar-button-react' 3 | const AddCalendarWrapper = ({eventInfo,item,startDate,endDate,startTime,endTime}) => { 4 | const contestUrl = eventInfo?.event?.url || item?.contestUrl; 5 | 6 | return ( 7 | 8 | 26 | ) 27 | } 28 | 29 | export default AddCalendarWrapper 30 | -------------------------------------------------------------------------------- /src/assets/g4gremind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/geeks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/geeksForGeeks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | Contest Clock 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/components/AddCalendar.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { lazy, Suspense } from 'react'; 3 | const AddCalendarWrapper = lazy(() => import("./AddCalendarWrapper")); 4 | 5 | const AddCalendar = ({eventInfo,item}) => { 6 | const startObj = new Date(eventInfo?.event?.extendedProps?.contestStartDate || item?.contestStartDate) 7 | const endObj = new Date(eventInfo?.event?.extendedProps?.contestEndDate || item?.contestEndDate) 8 | const startDate = startObj.toISOString().split('T')[0]; 9 | const endDate = endObj.toISOString().split('T')[0] 10 | 11 | const toISTTime = (date) => 12 | new Intl.DateTimeFormat('en-GB', { 13 | hour: '2-digit', 14 | minute: '2-digit', 15 | hour12: false, 16 | timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, 17 | }).format(date) 18 | 19 | const startTime = toISTTime(startObj) 20 | const endTime = toISTTime(endObj) 21 | 22 | 23 | return ( 24 | Loading Calendar...}> 25 | 33 | 34 | ) 35 | } 36 | 37 | export default AddCalendar 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/leetcode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⏰ ContestClock 2 | 3 | > Your all-in-one competitive programming calendar app. Stay updated with upcoming contests across platforms like Codeforces, LeetCode, CodeChef, and more — all in one beautiful and responsive interface. 4 | 5 | --- 6 | 7 | ## 🚀 Features 8 | 9 | - 📅 Full calendar view with color-coded contest platforms 10 | - 🔔 Contest reminders & real-time updates 11 | - 💾 Save contests you're interested in 12 | - 🧑‍💻 Firebase authentication (Google login) 13 | - 🗓️ Add contests directly to your personal calendar with the built-in "Add to Calendar" feature. 14 | - 📊 Contest filtering by platform 15 | - 📌 Personalized dashboard with saved contests 16 | - 🎨 Responsive UI built with TailwindCSS and Ant Design 17 | - ⚙️ Backend with Express.js, MongoDB, and Firebase Admin SDK 18 | 19 | --- 20 | 21 | 22 | ## 🛠️ Tech Stack 23 | 24 | **Frontend** 25 | - React.js (with Vite) 26 | - TailwindCSS + Ant Design 27 | - Firebase Auth 28 | 29 | **Backend** 30 | - Node.js + Express.js 31 | - MongoDB (Mongoose) 32 | - Firebase Admin SDK (Token Verification) 33 | 34 | **Dev Tools** 35 | - Axios 36 | - FullCalendar.js 37 | - React-Toastify 38 | - Resend for notifications 39 | 40 | --- 41 | 42 | ## 🔧 Setup & Installation 43 | 44 | ### 1. Clone the repository 45 | 46 | ```bash 47 | git clone https://github.com/SreekarSBS/ContestClock-UI.git 48 | cd ContestClock 49 | ``` 50 | ### 2. Setup the Frontend 51 | ```bash 52 | npm run dev 53 | ``` 54 | ### Backend Microservice - [Contest Clock](https://github.com/SreekarSBS/ContestClock.git) 55 | -------------------------------------------------------------------------------- /src/components/ui/popover.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "../../lib/utils" 5 | 6 | function Popover({ 7 | ...props 8 | }) { 9 | return ; 10 | } 11 | 12 | function PopoverTrigger({ 13 | ...props 14 | }) { 15 | return ; 16 | } 17 | 18 | function PopoverContent({ 19 | className, 20 | align = "center", 21 | sideOffset = 4, 22 | ...props 23 | }) { 24 | return ( 25 | 26 | 35 | 36 | ); 37 | } 38 | 39 | function PopoverAnchor({ 40 | ...props 41 | }) { 42 | return ; 43 | } 44 | 45 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 46 | -------------------------------------------------------------------------------- /src/components/CalendarWrapper.jsx: -------------------------------------------------------------------------------- 1 | import FullCalendar from "@fullcalendar/react"; 2 | import dayGridPlugin from "@fullcalendar/daygrid"; 3 | import Popovers from "./Popovers"; 4 | import { useLocation } from "react-router-dom"; 5 | const CalendarWrapper = ({events,savedEvents,handleClick}) => { 6 | const location = useLocation() 7 | return ( 8 |
9 | {location.pathname === "/contest" &&
Scheduled Contests
} 10 | { 13 | //console.log("Selected Date Range:", info.startStr, "to", info.endStr); 14 | }} 15 | plugins={[dayGridPlugin]} 16 | initialView="dayGridMonth" 17 | events={location.pathname === "/" && events || location.pathname === "/contest" && savedEvents} 18 | eventContent={(eventInfo) => 19 | 20 | } 21 | dayCellContent={(arg) => { 22 | return ( 23 |
handleClick(arg)} 26 | style={{ cursor: "pointer" }} 27 | > 28 | {arg.dayNumberText} 29 |
30 | ); 31 | }} 32 | eventClick={(info) => { 33 | // console.log(info); 34 | 35 | info.jsEvent.preventDefault(); 36 | // window.open(info.event.url); 37 | }} 38 | /> 39 |
40 | ) 41 | } 42 | 43 | export default CalendarWrapper 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contesthub", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@fullcalendar/daygrid": "^6.1.18", 14 | "@fullcalendar/react": "^6.1.18", 15 | "@mui/material": "^7.2.0", 16 | "@radix-ui/react-accordion": "^1.2.11", 17 | "@radix-ui/react-popover": "^1.1.14", 18 | "@radix-ui/react-slot": "^1.2.3", 19 | "@reduxjs/toolkit": "^2.8.2", 20 | "@tailwindcss/vite": "^4.1.11", 21 | "add-to-calendar-button": "^2.9.1", 22 | "add-to-calendar-button-react": "^2.9.1", 23 | "antd": "^5.26.7", 24 | "axios": "^1.11.0", 25 | "cally": "^0.8.0", 26 | "class-variance-authority": "^0.7.1", 27 | "clsx": "^2.1.1", 28 | "daisyui": "^5.0.47", 29 | "date-fns": "^4.1.0", 30 | "firebase": "^12.0.0", 31 | "lucide-react": "^0.525.0", 32 | "mui": "^0.0.1", 33 | "react": "^19.1.0", 34 | "react-day-picker": "^9.8.1", 35 | "react-dom": "^19.1.0", 36 | "react-redux": "^9.2.0", 37 | "react-router-dom": "^7.7.1", 38 | "react-toastify": "^11.0.5", 39 | "tailwind-merge": "^3.3.1", 40 | "tailwindcss": "^4.1.11" 41 | }, 42 | "devDependencies": { 43 | "@eslint/js": "^9.30.1", 44 | "@types/react": "^19.1.8", 45 | "@types/react-dom": "^19.1.6", 46 | "@vitejs/plugin-react": "^4.6.0", 47 | "eslint": "^9.30.1", 48 | "eslint-plugin-react-hooks": "^5.2.0", 49 | "eslint-plugin-react-refresh": "^0.4.20", 50 | "globals": "^16.3.0", 51 | "rollup-plugin-visualizer": "^6.0.3", 52 | "tw-animate-css": "^1.3.6", 53 | "vite": "^7.0.4" 54 | }, 55 | "description": "Never miss a contest again", 56 | "main": "eslint.config.js", 57 | "author": "SreekarSBS", 58 | "license": "ISC" 59 | } 60 | -------------------------------------------------------------------------------- /src/components/ui/accordion.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDownIcon } from "lucide-react" 4 | 5 | import { cn } from "../../lib/utils" 6 | 7 | function Accordion({ 8 | ...props 9 | }) { 10 | return ; 11 | } 12 | 13 | function AccordionItem({ 14 | className, 15 | ...props 16 | }) { 17 | return ( 18 | 22 | ); 23 | } 24 | 25 | function AccordionTrigger({ 26 | className, 27 | children, 28 | ...props 29 | }) { 30 | return ( 31 | 32 | svg]:rotate-180", 36 | className 37 | )} 38 | {...props}> 39 | {children} 40 | 42 | 43 | 44 | ); 45 | } 46 | 47 | function AccordionContent({ 48 | className, 49 | children, 50 | ...props 51 | }) { 52 | return ( 53 | 57 |
{children}
58 |
59 | ); 60 | } 61 | 62 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 63 | -------------------------------------------------------------------------------- /src/components/DayContests.jsx: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from '../utils/constants' 2 | import axios from 'axios' 3 | import React, { useEffect, useState } from 'react' 4 | import { useParams } from 'react-router-dom' 5 | import CardsContest from './CardsContest' 6 | import Alert from '@mui/material/Alert'; 7 | 8 | 9 | 10 | 11 | const DayContests = () => { 12 | const [contests,setContests] = useState([]) 13 | useEffect(() => { 14 | fetchDayContests() 15 | },[]) 16 | 17 | const {date} = useParams() 18 | const newDate = new Date(date) 19 | const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; 20 | const formattedDate = newDate.toLocaleString('en-us',options) 21 | 22 | const fetchDayContests = async() => { 23 | try { 24 | const res = await axios.get(BASE_URL + "/contests/day/"+date,{ 25 | withCredentials : true 26 | }) 27 | 28 | setContests(res?.data?.data) 29 | 30 | }catch(err){ 31 | // console.log(err); 32 | } 33 | } 34 | let msg; 35 | if(newDate > new Date()) msg = "No Contests Scheduled on " + formattedDate; 36 | else if(newDate < new Date) msg = "No Contests were Conducted on " + formattedDate 37 | else msg = "No Contests for Today! Keep Practicing " 38 | 39 | 40 | 41 | 42 | return contests.length > 0 ? ( 43 |
44 |
{formattedDate}
45 | 46 | { 47 | contests?.map((item) => { 48 | return
    49 | 50 | 51 |
52 | }) 53 | } 54 | 55 |
56 | ) : 57 | <> 58 |
59 | 60 |

{msg}

61 |
62 |
63 | 64 | } 65 | 66 | export default DayContests 67 | -------------------------------------------------------------------------------- /src/components/ui/button.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva } from "class-variance-authority"; 4 | 5 | import { cn } from "../../lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary bg-blue-500 text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }) { 45 | const Comp = asChild ? Slot : "button" 46 | 47 | return ( 48 | 52 | ); 53 | } 54 | 55 | export { Button, buttonVariants } 56 | -------------------------------------------------------------------------------- /src/components/ContestList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import ShimmerList from './Shimmer/ShimmerList'; 3 | import axios from 'axios'; 4 | import { BASE_URL } from '../utils/constants'; 5 | 6 | import { useOutletContext } from 'react-router-dom'; 7 | // import { Link } from "react-router-dom"; 8 | 9 | 10 | 11 | import FilterContests from './FilterContests'; 12 | import UpcomingContests from './UpcomingContests'; 13 | 14 | 15 | 16 | 17 | const ContestList = () => { 18 | 19 | const [contests,setContests] = useState([]); 20 | 21 | 22 | const context = useOutletContext() 23 | const visibleContests = context[0]; 24 | 25 | 26 | 27 | useEffect(() => { 28 | fetchContests() 29 | },[visibleContests]) 30 | 31 | 32 | const fetchContests = async() => { 33 | try { 34 | 35 | const res = await axios.get(BASE_URL+`/contests/platform?platforms=${visibleContests.join(",")}&startDate=`+ new Date().toISOString() ,{ 36 | withCredentials : true 37 | }) 38 | // console.log(res); 39 | 40 | setContests(res?.data?.data) 41 | 42 | 43 | }catch(err){ 44 | // console.log(err); 45 | } 46 | } 47 | 48 | 49 | 50 | 51 | 52 | return ( 53 | 54 |
55 |
56 | Upcoming Contests 57 | 58 |
59 | 60 | 61 | {(!contests) && } 62 | {(contests?.length === 0) ?
63 | Looks Like there are no upcoming contests for the selected platforms. Please select other platforms or try again later. 64 |
65 | external-no-data-empty-state-flat-flat-juicy-fish 66 |
67 |
: 68 | } 69 |
70 | ) 71 | } 72 | 73 | export default ContestList 74 | -------------------------------------------------------------------------------- /src/components/ui/card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "../../lib/utils" 4 | 5 | function Card({ 6 | className, 7 | ...props 8 | }) { 9 | return ( 10 |
17 | ); 18 | } 19 | 20 | function CardHeader({ 21 | className, 22 | ...props 23 | }) { 24 | return ( 25 |
32 | ); 33 | } 34 | 35 | function CardTitle({ 36 | className, 37 | ...props 38 | }) { 39 | return ( 40 |
44 | ); 45 | } 46 | 47 | function CardDescription({ 48 | className, 49 | ...props 50 | }) { 51 | return ( 52 |
56 | ); 57 | } 58 | 59 | function CardAction({ 60 | className, 61 | ...props 62 | }) { 63 | return ( 64 |
71 | ); 72 | } 73 | 74 | function CardContent({ 75 | className, 76 | ...props 77 | }) { 78 | return (
); 79 | } 80 | 81 | function CardFooter({ 82 | className, 83 | ...props 84 | }) { 85 | return ( 86 |
90 | ); 91 | } 92 | 93 | export { 94 | Card, 95 | CardHeader, 96 | CardFooter, 97 | CardTitle, 98 | CardAction, 99 | CardDescription, 100 | CardContent, 101 | } 102 | -------------------------------------------------------------------------------- /src/components/PopoverTriggerData.jsx: -------------------------------------------------------------------------------- 1 | import LEETCODE_SVG_ICON from "../assets/leetcode.svg" 2 | 3 | const PopoverTriggerData = ({eventInfo}) => { 4 | return (<> 5 | {eventInfo.event.extendedProps?.platform === "geeksforgeeks" && 6 | 7 | } 8 | { eventInfo.event.extendedProps?.platform === "codechef" && } 9 | { 10 | eventInfo.event.extendedProps?.platform === "leetcode" && external-level-up-your-coding-skills-and-quickly-land-a-job-logo-shadow-tal-revivo } 11 | { 12 | eventInfo.event.extendedProps?.platform === "atcoder" && } 13 | {eventInfo.event.extendedProps.platform === "codeforces"&& Platform Logo} 14 | {eventInfo.event.extendedProps.contestName} 15 | 16 | ) 17 | } 18 | 19 | export default PopoverTriggerData 20 | -------------------------------------------------------------------------------- /src/components/ContestCalendar.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { lazy, Suspense, useState } from "react"; 3 | import { useNavigate, useOutletContext } from "react-router-dom"; 4 | import { BASE_URL } from "../utils/constants"; 5 | 6 | const CalendarWrapper = lazy(() => import("./CalendarWrapper")); 7 | import { Bounce, toast } from "react-toastify"; 8 | import { useSelector } from "react-redux"; 9 | import { 10 | Card, 11 | CardAction, 12 | CardContent, 13 | CardDescription, 14 | CardFooter, 15 | CardHeader, 16 | CardTitle, 17 | } from "../components/ui/card" 18 | import { Button } from "./ui/button"; 19 | import { GoogleAuthProvider, signInWithPopup } from "firebase/auth"; 20 | import { auth } from "../utils/firebase"; 21 | 22 | 23 | 24 | const ContestCalendar = () => { 25 | const navigate = useNavigate(); 26 | const [date, setDate] = useState(new Date()); 27 | const user = useSelector((store) => store.user); 28 | 29 | const context = useOutletContext() 30 | const events = context[2] 31 | let savedEvents = context[3] 32 | 33 | // console.log(date); 34 | const handleSignIn = async() => { 35 | const provider = new GoogleAuthProvider() 36 | signInWithPopup(auth, provider) 37 | .then((result) => { 38 | 39 | const credential = GoogleAuthProvider.credentialFromResult(result); 40 | const token = credential.accessToken; 41 | // console.log(token); 42 | toast.success('Logged In Successfully !', { 43 | position: "top-right", 44 | autoClose: 5000, 45 | hideProgressBar: false, 46 | closeOnClick: false, 47 | pauseOnHover: true, 48 | draggable: true, 49 | progress: undefined, 50 | theme: "dark", 51 | transition: Bounce, 52 | }); 53 | const user = result.user; 54 | // console.log(user); 55 | 56 | 57 | }).catch((error) => { 58 | 59 | 60 | const errorMessage = error.message; 61 | 62 | // console.log(errorMessage); 63 | 64 | }); 65 | } 66 | if(!user && location.pathname === "/contest") { 67 | savedEvents = []; 68 | 69 | return
70 | 71 | 72 | Login to ContestClock 73 | Sign In with Google 74 | 75 | Logo Icon 76 | 77 | 78 | 79 |
80 | 81 | 82 |
83 |
84 | 85 | To Track Your Saved Contests 86 | please sign in with your Google Account 87 | 88 |
89 |
90 | 91 | 92 | } 93 | 94 | const handleClick = (arg) => { 95 | setDate(arg.date); 96 | // console.log(arg.date); 97 | const formatted = arg?.date.toLocaleDateString('en-CA') 98 | navigate("/contests/" + formatted); 99 | }; 100 | return ( 101 | Loading Calendar...
}> 102 | 103 | 104 | ); 105 | }; 106 | 107 | export default ContestCalendar; 108 | -------------------------------------------------------------------------------- /src/components/PopoverData.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from "react-router-dom"; 3 | import AddCalendar from "./AddCalendar"; 4 | import CALENDAR_ICON from "../assets/icons8-calendar-48.png" 5 | import STOPWATCH_ICON from "../assets/icons8-stopwatch-48.png" 6 | import LINK_ICON from "../assets/icons8-link-48.png" 7 | import CAUTION_ICON from "../assets/icons8-caution-48.png" 8 | import CALENDAR_PLUS_ICON from "../assets/icons8-calendar-plus-48.png" 9 | const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}; 10 | const optionsTimer = { hour : 'numeric',minute : 'numeric'} 11 | 12 | const PopoverData = ({eventInfo}) => { 13 | return (<> 14 |
{eventInfo?.event?.title}
15 |
16 | calendar 17 | {new Date(eventInfo?.event?.extendedProps?.contestStartDate).toLocaleString('en-us',options)} 18 |
19 |
20 | 21 | 22 | 23 | {new Date(eventInfo?.event?.extendedProps?.contestStartDate).toLocaleString('en-us',optionsTimer)} 24 |
25 |
26 | stopwatch 27 | { Math.floor(Number(eventInfo?.event?.extendedProps?.contestDuration)/3600) } : {String(Math.floor(Number(eventInfo?.event?.extendedProps?.contestDuration)%3600/60)).padStart(2, "0")} hrs 28 |
29 |
30 | 31 | { 32 | new Date(eventInfo?.event?.extendedProps?.contestEndDate) >= new Date() ? 33 | <>external-link-web-flaticons-lineal-color-flat-icons-7 34 | Register Now 35 | :<> 36 | spam 37 | Contest Ended 38 | } 39 | 40 |
41 | 42 | calendar-plus 43 | 44 | ) 45 | } 46 | 47 | export default PopoverData 48 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 | 13 | 45 |
46 | ) 47 | } 48 | 49 | export default Footer 50 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/FilterContests.jsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'antd'; 2 | import React from 'react' 3 | import { useOutletContext } from 'react-router-dom'; 4 | import CODE_FORCES_ICON from "../assets/code-forces.svg" 5 | import GEEKS_LOGO from "../assets/geeks.svg" 6 | import CHEF_ICON from "../assets/chefLogo.svg" 7 | import LEETCODE_ICON from "../assets/leetcode-96.png" 8 | const FilterContests = () => { 9 | const context = useOutletContext() 10 | const visibleContests = context[0]; 11 | const setVisibleContests = context[1]; 12 | const handleFilterGeeksForGeeks = (checked) => { 13 | if(!checked) setVisibleContests((item) => item.filter(contest => contest !== 'geeksforgeeks' ) ) 14 | else setVisibleContests(prev => 15 | prev.includes('geeksforgeeks') ? prev : [...prev, 'geeksforgeeks'] 16 | ); 17 | } 18 | const handleFilterLeetcode = (checked) => { 19 | if(!checked) setVisibleContests((item) => item.filter(contest => contest !== 'leetcode' ) ) 20 | else setVisibleContests(prev => 21 | prev.includes('leetcode') ? prev : [...prev, 'leetcode'] 22 | ); 23 | } 24 | const handleFilterAtcoder = (checked) => { 25 | if(!checked) setVisibleContests((item) => item.filter(contest => contest !== 'atcoder' ) ) 26 | else setVisibleContests(prev => 27 | prev.includes('atcoder') ? prev : [...prev, 'atcoder'] 28 | ); 29 | } 30 | const handleFilterCodeChef = (checked) => { 31 | if(!checked) setVisibleContests((item) => item.filter(contest => contest !== 'codechef' ) ) 32 | else setVisibleContests(prev => 33 | prev.includes('codechef') ? prev : [...prev, 'codechef'] 34 | ); 35 | } 36 | const handleFilterCodeForces = (checked) => { 37 | if(!checked) setVisibleContests((item) => item.filter(contest => contest !== 'codeforces' ) ) 38 | else setVisibleContests(prev => 39 | prev.includes('codeforces') ? prev : [...prev, 'codeforces'] 40 | ); 41 | } 42 | return ( 43 |
44 | 45 | 46 | geeks-for-geeks-logo 47 | 49 | 50 | 51 | external-codeforces-programming-competitions-and-contests-programming-community-logo-shadow-tal-revivo 52 | {/* Codeforces */} 53 | 54 | 55 | 57 | 58 | 59 | logo-codechef 60 | 62 | 63 | 64 | external-level-up-your-coding-skills-and-quickly-land-a-job-logo-shadow-tal-revivo 65 | 66 | 68 | 69 | 70 | 71 | 73 | 74 |
75 | ) 76 | } 77 | 78 | export default FilterContests 79 | -------------------------------------------------------------------------------- /src/components/RegisteredContests.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useSelector } from 'react-redux' 3 | import { Bounce, toast } from 'react-toastify' 4 | import googleIcon from "../assets/icons8-google-48.png" 5 | import logoIcon from "../assets/contestclock.svg" 6 | import CardsContest from './CardsContest' 7 | import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth' 8 | import { auth } from '../utils/firebase' 9 | import { 10 | Card, 11 | CardAction, 12 | CardContent, 13 | CardDescription, 14 | CardFooter, 15 | CardHeader, 16 | CardTitle, 17 | } from "../components/ui/card" 18 | import { Button } from "./ui/button"; 19 | 20 | 21 | 22 | const RegisteredContests = () => { 23 | const user = useSelector(store => store.user) 24 | 25 | const savedContests = useSelector(store => store.registeredContests) 26 | 27 | const handleSignIn = async() => { 28 | const provider = new GoogleAuthProvider() 29 | signInWithPopup(auth, provider) 30 | .then((result) => { 31 | 32 | const credential = GoogleAuthProvider.credentialFromResult(result); 33 | const token = credential.accessToken; 34 | // console.log(token); 35 | toast.success('Logged In Successfully !', { 36 | position: "top-right", 37 | autoClose: 5000, 38 | hideProgressBar: false, 39 | closeOnClick: false, 40 | pauseOnHover: true, 41 | draggable: true, 42 | progress: undefined, 43 | theme: "dark", 44 | transition: Bounce, 45 | }); 46 | const user = result.user; 47 | // console.log(user); 48 | 49 | 50 | }).catch((error) => { 51 | 52 | 53 | const errorMessage = error.message; 54 | 55 | console.log(errorMessage); 56 | 57 | }); 58 | } 59 | if(!user ){ 60 | 61 | 62 | return
63 | 64 | 65 | Login to ContestClock 66 | Sign In with Google 67 | 68 | Logo Icon 69 | 70 | 71 | 72 |
73 | 74 | 75 |
76 |
77 | 78 | To Track Your Saved Contests 79 | please sign in with your Google Account 80 | 81 |
82 |
83 | 84 | 85 | } 86 | 87 | 88 | // Default values shown 89 | 90 | if(savedContests?.length === 0){ 91 | return (<> 92 |
93 |
Saved Contests
94 | 95 |
96 | 97 |
98 | 99 | 100 | No Saved Contests 101 | Looks like you haven't kept any reminders to contests yet. 102 | 103 | Logo Icon 104 | 105 | Logo Icon 106 | 107 | 108 | 109 | 110 |
111 | Please explore and Press 🔔 to be notified an hour before contest inception. 112 |
113 |
114 |
115 |
116 | 117 | ) 118 | } 119 | return ( 120 |
121 | 122 |
123 |
Saved Contests
124 | 125 |
126 | 127 | { savedContests?.map((item) => { 128 | 129 | return
    130 | 131 | 132 | 133 |
134 | 135 | } )} 136 |
137 | ) 138 | } 139 | 140 | export default RegisteredContests 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/components/Shimmer/ShimmerList.jsx: -------------------------------------------------------------------------------- 1 | const ShimmerList = () => { 2 | 3 | 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 |
11 | 12 |
item.contestName
13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 |
item.contestName
23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 |
item.contestName
33 | 34 | 35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 |
item.contestName
43 | 44 | 45 |
46 | 47 | 48 |
49 |
50 |
51 | 52 |
item.contestName
53 | 54 | 55 |
56 | 57 | 58 |
59 |
60 |
61 | 62 |
item.contestName
63 | 64 | 65 |
66 | 67 | 68 |
69 |
70 | 71 | 72 | 73 |
74 |
75 | ) 76 | }; 77 | 78 | export default ShimmerList; 79 | -------------------------------------------------------------------------------- /src/components/UpcomingWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AddCalendar from "./AddCalendar"; 3 | import { AccordionContent, 4 | AccordionItem, 5 | AccordionTrigger, 6 | } from "./ui/accordion" 7 | import GFG_ICON from "../assets/geeksForGeeks.svg" 8 | import CODE_FORCES_ICON from "../assets/code-forces.svg" 9 | import CODE_CHEF_ICON from "../assets/codeChef.svg" 10 | import LEETCODE_ICON from "../assets/leetcode-96.png" 11 | import ALARM_ICON from "../assets/icons8-alarm-64.png" 12 | import BELL_ICON from "../assets/bell.svg" 13 | import CALENDAR_ICON from "../assets/icons8-calendar-48.png" 14 | import START_TIME_ICON from "../assets/startTime.svg" 15 | import STOPWATCH_ICON from "../assets/icons8-stopwatch-48.png" 16 | 17 | import { Link } from 'react-router-dom'; 18 | const UpcomingWrapper = ({item,savedContests,index,handleDeleteContest,handleRemindClick,options,optionsTimer}) => { 19 | return ( 20 | 21 | 22 |
23 | 24 | {item?.platform === "geeksforgeeks" && geeks-for-geeks-logo } 25 | {item?.platform === "codeforces" && external-codeforces-programming-competitions-and-contests-programming-community-logo-shadow-tal-revivo 26 | } { item?.platform === "codechef" && code-chef-svg } 27 | { 28 | item?.platform === "leetcode" && } 29 | { 30 | item?.platform === "atcoder" && } 31 | 32 |
33 | 34 |
{item?.contestName}
35 |
36 | Register 37 | { savedContests?.some(contest => contest._id === item?._id) ?
handleDeleteContest(item?._id)} className='cursor-pointer hover:text-black hover:bg-gradient-to-bl from-green-600 to-emerald-500 p-1.5 m-2 md:p-2 border border-blue-400 rounded-2xl'> 38 | alarm 39 | 40 | 41 |
42 | : 43 |
handleRemindClick(item?._id)} className='cursor-pointer hover:text-black hover:bg-gradient-to-bl from-green-600 to-emerald-500 p-1.5 m-2 md:p-2 border border-blue-400 rounded-2xl'> 44 | 45 | reminder-bell 46 | 47 |
48 | } 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 | 61 | {new Date(item.contestStartDate).toLocaleString('en-us',options)} 62 |
63 |
64 | contest-start-time 65 | {new Date(item?.contestStartDate).toLocaleString('en-us',optionsTimer)} 66 |
67 |
68 |
69 | 70 | 71 | 72 |
73 | stopwatch 74 | 75 | { Math.floor(Number(item?.contestDuration)/3600) } : {String(Math.floor(Number(item?.contestDuration)%3600/60)).padStart(2, "0")} hrs 76 |
77 | 78 |
79 |
80 |
81 |
82 | 83 |
84 |
85 | ) 86 | } 87 | 88 | export default UpcomingWrapper 89 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | @plugin "daisyui"; 4 | @plugin "daisyui" { 5 | themes: all; 6 | } 7 | :root{ 8 | font-family: "Tomorrow", sans-serif; 9 | font-weight: 300; 10 | } 11 | 12 | .tomorrow-regular { 13 | font-family: "Tomorrow", sans-serif; 14 | font-weight: 300; 15 | font-style: normal; 16 | color : aquamarine 17 | } 18 | 19 | .tomorrow-thin { 20 | font-family: "Tomorrow", sans-serif; 21 | font-weight: 300; 22 | font-style: normal; 23 | background: #000; 24 | 25 | } 26 | 27 | .montserrat-logo { 28 | font-family: "Montserrat",; 29 | font-optical-sizing: auto; 30 | font-weight: 200; 31 | font-style: normal; 32 | font-size: 2.5rem; 33 | } 34 | .montserrat-item { 35 | font-family: "Montserrat",; 36 | font-optical-sizing: auto; 37 | font-weight: 300; 38 | font-style: normal; 39 | font-size: 2.5rem; 40 | } 41 | 42 | /* index.css */ 43 | .popover { 44 | position: absolute; 45 | z-index: 1060; 46 | display: block; 47 | max-width: 276px; 48 | font-family: "Montserrat"; 49 | font-size: 0.875rem; 50 | background-color: #fff; 51 | border: 1px solid rgba(0, 0, 0, 0.2); 52 | border-radius: 0.3rem; 53 | box-shadow: 0 .5rem 1rem rgba(0, 0, 0, 0.15); 54 | } 55 | 56 | .popover-header { 57 | padding: 0.5rem 0.75rem; 58 | font-weight: 600; 59 | background-color: #f7f7f7; 60 | border-bottom: 1px solid #ebebeb; 61 | border-top-left-radius: 0.3rem; 62 | border-top-right-radius: 0.3rem; 63 | } 64 | 65 | .popover-body { 66 | padding: 0.5rem 0.75rem; 67 | } 68 | 69 | 70 | @custom-variant dark (&:is(.dark *)); 71 | 72 | @theme inline { 73 | --radius-sm: calc(var(--radius) - 4px); 74 | --radius-md: calc(var(--radius) - 2px); 75 | --radius-lg: var(--radius); 76 | --radius-xl: calc(var(--radius) + 4px); 77 | --color-background: var(--background); 78 | --color-foreground: var(--foreground); 79 | --color-card: var(--card); 80 | --color-card-foreground: var(--card-foreground); 81 | --color-popover: var(--popover); 82 | --color-popover-foreground: var(--popover-foreground); 83 | --color-primary: var(--primary); 84 | --color-primary-foreground: var(--primary-foreground); 85 | --color-secondary: var(--secondary); 86 | --color-secondary-foreground: var(--secondary-foreground); 87 | --color-muted: var(--muted); 88 | --color-muted-foreground: var(--muted-foreground); 89 | --color-accent: var(--accent); 90 | --color-accent-foreground: var(--accent-foreground); 91 | --color-destructive: var(--destructive); 92 | --color-border: var(--border); 93 | --color-input: var(--input); 94 | --color-ring: var(--ring); 95 | --color-chart-1: var(--chart-1); 96 | --color-chart-2: var(--chart-2); 97 | --color-chart-3: var(--chart-3); 98 | --color-chart-4: var(--chart-4); 99 | --color-chart-5: var(--chart-5); 100 | --color-sidebar: var(--sidebar); 101 | --color-sidebar-foreground: var(--sidebar-foreground); 102 | --color-sidebar-primary: var(--sidebar-primary); 103 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 104 | --color-sidebar-accent: var(--sidebar-accent); 105 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 106 | --color-sidebar-border: var(--sidebar-border); 107 | --color-sidebar-ring: var(--sidebar-ring); 108 | } 109 | 110 | :root { 111 | --radius: 0.625rem; 112 | --background: black; 113 | --foreground: white; 114 | --card: oklch(1 0 0); 115 | --card-foreground: oklch(0.145 0 0); 116 | --popover: oklch(1 0 0); 117 | --popover-foreground: oklch(0.145 0 0); 118 | --primary: oklch(0.205 0 0); 119 | --primary-foreground: oklch(0.985 0 0); 120 | --secondary: oklch(0.97 0 0); 121 | --secondary-foreground: oklch(0.205 0 0); 122 | --muted: oklch(0.97 0 0); 123 | --muted-foreground: oklch(0.556 0 0); 124 | --accent: oklch(0.97 0 0); 125 | --accent-foreground: oklch(0.205 0 0); 126 | --destructive: oklch(0.577 0.245 27.325); 127 | --border: oklch(0.922 0 0); 128 | --input: oklch(0.922 0 0); 129 | --ring: oklch(0.708 0 0); 130 | --chart-1: oklch(0.646 0.222 41.116); 131 | --chart-2: oklch(0.6 0.118 184.704); 132 | --chart-3: oklch(0.398 0.07 227.392); 133 | --chart-4: oklch(0.828 0.189 84.429); 134 | --chart-5: oklch(0.769 0.188 70.08); 135 | --sidebar: oklch(0.985 0 0); 136 | --sidebar-foreground: oklch(0.145 0 0); 137 | --sidebar-primary: oklch(0.205 0 0); 138 | --sidebar-primary-foreground: oklch(0.985 0 0); 139 | --sidebar-accent: oklch(0.97 0 0); 140 | --sidebar-accent-foreground: oklch(0.205 0 0); 141 | --sidebar-border: oklch(0.922 0 0); 142 | --sidebar-ring: oklch(0.708 0 0); 143 | } 144 | 145 | .dark { 146 | --background: oklch(0.145 0 0); 147 | --foreground: oklch(0.985 0 0); 148 | --card: oklch(0.205 0 0); 149 | --card-foreground: oklch(0.985 0 0); 150 | --popover: oklch(0.205 0 0); 151 | --popover-foreground: oklch(0.985 0 0); 152 | --primary: oklch(0.922 0 0); 153 | --primary-foreground: oklch(0.205 0 0); 154 | --secondary: oklch(0.269 0 0); 155 | --secondary-foreground: oklch(0.985 0 0); 156 | --muted: oklch(0.269 0 0); 157 | --muted-foreground: oklch(0.708 0 0); 158 | --accent: oklch(0.269 0 0); 159 | --accent-foreground: oklch(0.985 0 0); 160 | --destructive: oklch(0.704 0.191 22.216); 161 | --border: oklch(1 0 0 / 10%); 162 | --input: oklch(1 0 0 / 15%); 163 | --ring: oklch(0.556 0 0); 164 | --chart-1: oklch(0.488 0.243 264.376); 165 | --chart-2: oklch(0.696 0.17 162.48); 166 | --chart-3: oklch(0.769 0.188 70.08); 167 | --chart-4: oklch(0.627 0.265 303.9); 168 | --chart-5: oklch(0.645 0.246 16.439); 169 | --sidebar: oklch(0.205 0 0); 170 | --sidebar-foreground: oklch(0.985 0 0); 171 | --sidebar-primary: oklch(0.488 0.243 264.376); 172 | --sidebar-primary-foreground: oklch(0.985 0 0); 173 | --sidebar-accent: oklch(0.269 0 0); 174 | --sidebar-accent-foreground: oklch(0.985 0 0); 175 | --sidebar-border: oklch(1 0 0 / 10%); 176 | --sidebar-ring: oklch(0.556 0 0); 177 | } 178 | 179 | @layer base { 180 | * { 181 | @apply border-border outline-ring/50; 182 | } 183 | body { 184 | @apply bg-background text-foreground; 185 | } 186 | } 187 | 188 | -------------------------------------------------------------------------------- /src/components/Body.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import Header from './Header' 3 | import { Outlet } from 'react-router-dom' 4 | import Footer from './Footer' 5 | import { onAuthStateChanged } from 'firebase/auth' 6 | import { auth } from '../utils/firebase' 7 | import { addUser } from '../utils/userSlice' 8 | import { useDispatch, useSelector } from 'react-redux' 9 | import axios from 'axios' 10 | import { BASE_URL } from '../utils/constants' 11 | import { ToastContainer } from 'react-toastify' 12 | import { addContest } from '../utils/registeredContestsSlice' 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | const Body = () => { 21 | 22 | const dispatch = useDispatch() 23 | // const savedContests = useSelector(store => store.registeredContests) 24 | const user = useSelector((store) => store.user) 25 | const [pictureURL,setPictureURL] = useState() 26 | const [visibleContests,setVisibleContests] = useState(['leetcode','codeforces','atcoder','codechef','geeksforgeeks']) 27 | 28 | // const [contests, setContests] = useState([]); 29 | const [events, setEvents] = useState([]); 30 | const [savedEvents, setSavedEvents] = useState([]); 31 | const fetchPutUser = async(token) => { 32 | try{ 33 | const userDocument =await axios.get(BASE_URL + "/user",{ 34 | withCredentials : true, 35 | headers : { 36 | 'Authorization': 'Bearer ' +token 37 | } 38 | }) 39 | setPictureURL(userDocument?.data?.data?.picture) 40 | 41 | 42 | }catch(err){ 43 | console.log(err); 44 | 45 | } 46 | } 47 | 48 | 49 | useEffect(() => { 50 | fetchContests(); 51 | }, [visibleContests]); 52 | 53 | const fetchContests = async () => { 54 | try { 55 | const res = await axios.get(BASE_URL + `/contests/platform?platforms=${visibleContests.join(",")}`, { 56 | withCredentials: true, 57 | }); 58 | // setContests(res?.data?.data); 59 | const formattedObject = res?.data?.data?. 60 | map((item) => { 61 | const { 62 | platform, 63 | contestType, 64 | contestEndDate, 65 | contestSlug, 66 | contestRegistrationStartDate, 67 | contestRegistrationEndDate, 68 | contestName, 69 | contestDuration, 70 | contestCode, 71 | contestStartDate 72 | } = item; 73 | return { 74 | title: item?.contestName, 75 | start: item?.contestStartDate, 76 | 77 | url: item?.contestUrl, 78 | extendedProps: { 79 | contestStartDate, 80 | platform, 81 | contestEndDate, 82 | contestType, 83 | contestSlug, 84 | contestRegistrationStartDate, 85 | contestRegistrationEndDate, 86 | contestName, 87 | contestDuration, 88 | contestCode 89 | }, 90 | }; 91 | }); 92 | setEvents(formattedObject); 93 | } catch (err) { 94 | console.log(err); 95 | } 96 | }; 97 | 98 | useEffect(() => { 99 | fetchRegisteredContests() 100 | },[user]) 101 | // const user = firebase.auth().currentUser; 102 | // if (user) { 103 | // const token = await user.getIdToken(); // This is the right way 104 | // // Pass this token in Authorization header 105 | // } 106 | const fetchRegisteredContests = async() => { 107 | try{ 108 | const res = await axios.get(BASE_URL + "/user/registeredContests",{ 109 | withCredentials : true, 110 | headers : { 111 | 'Authorization': 'Bearer ' + user?.token 112 | } 113 | }) 114 | 115 | dispatch(addContest(res?.data?.data.savedContests)) 116 | 117 | const formattedObject = res?.data?.data?.savedContests. 118 | map((item) => { 119 | const { 120 | platform, 121 | contestType, 122 | contestEndDate, 123 | contestSlug, 124 | contestRegistrationStartDate, 125 | contestRegistrationEndDate, 126 | contestName, 127 | contestDuration, 128 | contestCode, 129 | contestStartDate 130 | } = item; 131 | return { 132 | title: item?.contestName, 133 | start: item?.contestStartDate, 134 | 135 | url: item?.contestUrl, 136 | extendedProps: { 137 | contestStartDate, 138 | platform, 139 | contestEndDate, 140 | contestType, 141 | contestSlug, 142 | contestRegistrationStartDate, 143 | contestRegistrationEndDate, 144 | contestName, 145 | contestDuration, 146 | contestCode 147 | }, 148 | }; 149 | }); 150 | setSavedEvents(formattedObject); 151 | }catch(err){ 152 | // console.log(err); 153 | } 154 | } 155 | 156 | useEffect(() => { 157 | onAuthStateChanged(auth, async(user) => { 158 | if (user) { 159 | // console.log(user); 160 | // const idToken = await user.getIdToken(); 161 | fetchPutUser(user?.accessToken) 162 | const cleanedUser = { 163 | uid: user.uid, 164 | displayName: user.displayName, 165 | email: user.email, 166 | photoURL: user.photoURL, 167 | token : user?.accessToken 168 | }; 169 | dispatch(addUser(cleanedUser)) 170 | 171 | 172 | } else { 173 | 174 | // console.log("Sign In"); 175 | } 176 | }); 177 | }, [auth]); 178 | 179 | 180 | return ( 181 |
182 | 183 |
184 | 185 | 186 | 187 |
188 | 189 | 190 | 191 |
192 |
193 |
194 | ) 195 | } 196 | 197 | export default Body 198 | -------------------------------------------------------------------------------- /src/components/UpcomingContests.jsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | const UpcomingWrapper = lazy(() => import('./UpcomingWrapper')); 4 | import { 5 | Accordion, 6 | } from "./ui/accordion" 7 | import { addContest } from '../utils/registeredContestsSlice'; 8 | import { Bounce, toast } from 'react-toastify'; 9 | import axios from 'axios'; 10 | import { BASE_URL } from '../utils/constants'; 11 | import { useOutletContext } from 'react-router-dom'; 12 | 13 | 14 | 15 | 16 | const UpcomingContests = ({contests}) => { 17 | const dispatch = useDispatch() 18 | const context = useOutletContext(); 19 | const user = useSelector(store => store.user) 20 | const savedContests = useSelector(store => store.registeredContests) 21 | const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}; 22 | const optionsTimer = { hour : 'numeric',minute : 'numeric'} 23 | 24 | const setSavedEvents = context[4] 25 | const handleRemindClick = async(contestId) => { 26 | try{ 27 | if(!user) toast.error('Please Login to register Contests!', { 28 | position: "top-right", 29 | autoClose: 5000, 30 | hideProgressBar: false, 31 | closeOnClick: false, 32 | pauseOnHover: true, 33 | draggable: true, 34 | progress: undefined, 35 | theme: "dark", 36 | transition: Bounce, 37 | }); 38 | const res = await axios.post(BASE_URL + "/user/saveContests/" + contestId ,{},{ 39 | withCredentials : true, 40 | headers : { 41 | 'Authorization' : "Bearer " + user?.token 42 | } 43 | }) 44 | const formattedObject = res?.data?.data?.savedContests. 45 | map((item) => { 46 | const { 47 | platform, 48 | contestType, 49 | contestEndDate, 50 | contestSlug, 51 | contestRegistrationStartDate, 52 | contestRegistrationEndDate, 53 | contestName, 54 | contestDuration, 55 | contestCode, 56 | contestStartDate 57 | } = item; 58 | return { 59 | title: item?.contestName, 60 | start: item?.contestStartDate, 61 | 62 | url: item?.contestUrl, 63 | extendedProps: { 64 | contestStartDate, 65 | platform, 66 | contestEndDate, 67 | contestType, 68 | contestSlug, 69 | contestRegistrationStartDate, 70 | contestRegistrationEndDate, 71 | contestName, 72 | contestDuration, 73 | contestCode 74 | }, 75 | }; 76 | }); 77 | setSavedEvents(formattedObject); 78 | dispatch(addContest(res?.data?.data.savedContests)); 79 | // console.log(res?.data?.data.savedContests); 80 | toast.success('Reminder Set for an Hour before the contest !', { 81 | position: "top-right", 82 | autoClose: 5000, 83 | hideProgressBar: false, 84 | closeOnClick: false, 85 | pauseOnHover: true, 86 | draggable: true, 87 | progress: undefined, 88 | theme: "dark", 89 | transition: Bounce, 90 | }); 91 | } 92 | catch(err){ 93 | // console.log(err); 94 | } 95 | } 96 | 97 | const handleDeleteContest = async(contestId) => { 98 | 99 | try { 100 | const res = await axios.delete(BASE_URL +"/user/deleteContests/"+contestId,{ 101 | withCredentials : true, 102 | headers : { 103 | 'Authorization': 'Bearer ' + user?.token 104 | } 105 | }) 106 | const formattedObject = res?.data?.data?.savedContests. 107 | map((item) => { 108 | const { 109 | platform, 110 | contestType, 111 | contestEndDate, 112 | contestSlug, 113 | contestRegistrationStartDate, 114 | contestRegistrationEndDate, 115 | contestName, 116 | contestDuration, 117 | contestCode, 118 | contestStartDate 119 | } = item; 120 | return { 121 | title: item?.contestName, 122 | start: item?.contestStartDate, 123 | 124 | url: item?.contestUrl, 125 | extendedProps: { 126 | contestStartDate, 127 | platform, 128 | contestEndDate, 129 | contestType, 130 | contestSlug, 131 | contestRegistrationStartDate, 132 | contestRegistrationEndDate, 133 | contestName, 134 | contestDuration, 135 | contestCode 136 | }, 137 | }; 138 | }); 139 | setSavedEvents(formattedObject); 140 | dispatch(addContest(res?.data?.data?.savedContests)) 141 | // console.log(res?.data?.data?.savedContests); 142 | toast.success('Reminder removed Successfully !', { 143 | position: "top-right", 144 | autoClose: 5000, 145 | hideProgressBar: false, 146 | closeOnClick: false, 147 | pauseOnHover: true, 148 | draggable: true, 149 | progress: undefined, 150 | theme: "dark", 151 | transition: Bounce, 152 | }); 153 | }catch(err){ 154 | // console.log(err); 155 | 156 | } 157 | } 158 | 159 | return ( 160 |
161 | { 162 | contests?.map((item,index) => { 163 | return ( 164 | 165 | Loading...
}> 166 | 175 | 176 | 177 | ) 178 | 179 | }) 180 | } 181 |
182 | ) 183 | } 184 | 185 | export default UpcomingContests 186 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { auth } from "../utils/firebase"; 2 | import { GoogleAuthProvider, signInWithPopup, signOut } from "firebase/auth" 3 | import { Button } from "./ui/button"; 4 | import { removeUser } from "../utils/userSlice"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { Link } from "react-router-dom"; 7 | import { useEffect, useState } from "react"; 8 | import { clearContest } from "../utils/registeredContestsSlice"; 9 | import { Bounce, toast } from "react-toastify"; 10 | 11 | 12 | 13 | const Header = ({pictureURL}) => { 14 | 15 | const user = useSelector((store) =>store.user) 16 | const dispatch = useDispatch() 17 | const [isEventClicked, setIsEventClicked] = useState( 18 | () => JSON.parse(localStorage.getItem("isEventClicked")) || false 19 | ); 20 | const [isSavedClicked, setIsSavedClicked] = useState( 21 | () => JSON.parse(localStorage.getItem("isSavedClicked")) || false 22 | ); 23 | 24 | useEffect(() => { 25 | localStorage.setItem("isEventClicked", JSON.stringify(isEventClicked)); 26 | }, [isEventClicked]); 27 | 28 | useEffect(() => { 29 | localStorage.setItem("isSavedClicked", JSON.stringify(isSavedClicked)); 30 | }, [isSavedClicked]); 31 | 32 | const handleEventClick = () => { 33 | setIsEventClicked(!isEventClicked) 34 | setIsSavedClicked(isEventClicked) 35 | } 36 | 37 | const handleHomeClick = () => { 38 | setIsEventClicked(false) 39 | setIsSavedClicked(false) 40 | } 41 | 42 | const handleSavedClick = () => { 43 | setIsSavedClicked(!isSavedClicked) 44 | setIsEventClicked(isSavedClicked) 45 | } 46 | 47 | const handleSignIn = async() => { 48 | const provider = new GoogleAuthProvider() 49 | signInWithPopup(auth, provider) 50 | .then((result) => { 51 | 52 | const credential = GoogleAuthProvider.credentialFromResult(result); 53 | const token = credential.accessToken; 54 | // console.log(token); 55 | toast.success('Logged In Successfully !', { 56 | position: "top-right", 57 | autoClose: 5000, 58 | hideProgressBar: false, 59 | closeOnClick: false, 60 | pauseOnHover: true, 61 | draggable: true, 62 | progress: undefined, 63 | theme: "dark", 64 | transition: Bounce, 65 | }); 66 | const user = result.user; 67 | // console.log(user); 68 | 69 | 70 | }).catch((error) => { 71 | 72 | 73 | const errorMessage = error.message; 74 | 75 | // console.log(errorMessage); 76 | 77 | }); 78 | } 79 | 80 | const handleSignOut = async() => { 81 | signOut(auth).then(() => { 82 | toast.success('Logged Out Successfully !', { 83 | position: "top-right", 84 | autoClose: 5000, 85 | hideProgressBar: false, 86 | closeOnClick: false, 87 | pauseOnHover: true, 88 | draggable: true, 89 | progress: undefined, 90 | theme: "dark", 91 | transition: Bounce, 92 | }); 93 | dispatch(removeUser()) 94 | dispatch(clearContest()) 95 | }).catch((error) => { 96 | // An error happened. 97 | // console.log(error); 98 | 99 | }); 100 | 101 | } 102 | 103 | // console.log(user); 104 | 105 | 106 | 107 | return
108 |
109 |
110 |
111 | 112 |
113 |
    116 |
  • Event Tracker
  • 117 | {/* {
  • 118 | Parent 119 | 123 |
  • } */} 124 |
  • Reminders
  • 125 |
126 |
127 | 128 | 129 | 130 | 131 | 132 | ContestClock 133 | {/* ContestClock */} 134 | 135 | 136 |
137 |
138 |
    139 |
  • Event Tracker
  • 140 | {/*
  • 141 |
    142 | Parent 143 | 147 |
    148 |
  • */} 149 |
  • Saved Contests
  • 150 |
151 |
152 |
153 | { !user?
154 | 155 | 156 |
157 | : 158 |
159 | {user?.displayName} 160 |
161 |
162 | Tailwind CSS Navbar component 167 |
168 |
169 | 175 |
176 | 177 | } 178 |
179 |
180 | 181 | } 182 | 183 | export default Header 184 | 185 | 186 | -------------------------------------------------------------------------------- /src/components/ui/calendar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { 3 | ChevronDownIcon, 4 | ChevronLeftIcon, 5 | ChevronRightIcon, 6 | } from "lucide-react" 7 | import { DayPicker, getDefaultClassNames } from "react-day-picker"; 8 | 9 | import { cn } from "../../lib/utils" 10 | import { Button, buttonVariants } from "./button" 11 | 12 | 13 | function Calendar({ 14 | className, 15 | classNames, 16 | showOutsideDays = true, 17 | captionLayout = "label", 18 | buttonVariant = "ghost", 19 | formatters, 20 | components, 21 | ...props 22 | }) { 23 | const defaultClassNames = getDefaultClassNames() 24 | 25 | return ( 26 | svg]:rotate-180`, 31 | String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, 32 | className 33 | )} 34 | captionLayout={captionLayout} 35 | formatters={{ 36 | formatMonthDropdown: (date) => 37 | date.toLocaleString("default", { month: "short" }), 38 | ...formatters, 39 | }} 40 | classNames={{ 41 | root: cn("w-full sm:w-fit md:w-1/3 h-fit", defaultClassNames.root), 42 | months: cn("flex gap-4 flex-col md:flex-row relative", defaultClassNames.months), 43 | month: cn("flex flex-col w-full gap-4", defaultClassNames.month), 44 | nav: cn( 45 | "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between ", 46 | defaultClassNames.nav 47 | ), 48 | button_previous: cn( 49 | buttonVariants({ variant: buttonVariant }), 50 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 51 | defaultClassNames.button_previous 52 | ), 53 | button_next: cn( 54 | buttonVariants({ variant: buttonVariant }), 55 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", 56 | defaultClassNames.button_next 57 | ), 58 | month_caption: cn( 59 | "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", 60 | defaultClassNames.month_caption 61 | ), 62 | dropdowns: cn( 63 | "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", 64 | defaultClassNames.dropdowns 65 | ), 66 | dropdown_root: cn( 67 | "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", 68 | defaultClassNames.dropdown_root 69 | ), 70 | dropdown: cn("absolute bg-popover inset-0 opacity-0", defaultClassNames.dropdown), 71 | caption_label: cn("select-none font-medium", captionLayout === "label" 72 | ? "text-sm" 73 | : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", defaultClassNames.caption_label), 74 | table: "w-full border-collapse", 75 | weekdays: cn("flex", defaultClassNames.weekdays), 76 | weekday: cn( 77 | "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", 78 | defaultClassNames.weekday 79 | ), 80 | week: cn("flex w-full mt-2", defaultClassNames.week), 81 | week_number_header: cn("select-none w-(--cell-size)", defaultClassNames.week_number_header), 82 | week_number: cn( 83 | "text-[0.8rem] select-none text-muted-foreground", 84 | defaultClassNames.week_number 85 | ), 86 | day: cn( 87 | "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", 88 | defaultClassNames.day 89 | ), 90 | range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start), 91 | range_middle: cn("rounded-none", defaultClassNames.range_middle), 92 | range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), 93 | today: cn( 94 | "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", 95 | defaultClassNames.today 96 | ), 97 | outside: cn( 98 | "text-muted-foreground aria-selected:text-muted-foreground", 99 | defaultClassNames.outside 100 | ), 101 | disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled), 102 | hidden: cn("invisible", defaultClassNames.hidden), 103 | ...classNames, 104 | }} 105 | components={{ 106 | Root: ({ className, rootRef, ...props }) => { 107 | return (
); 108 | }, 109 | Chevron: ({ className, orientation, ...props }) => { 110 | if (orientation === "left") { 111 | return (); 112 | } 113 | 114 | if (orientation === "right") { 115 | return (); 116 | } 117 | 118 | return (); 119 | }, 120 | DayButton: CalendarDayButton, 121 | WeekNumber: ({ children, ...props }) => { 122 | return ( 123 | 124 |
126 | {children} 127 |
128 | 129 | ); 130 | }, 131 | ...components, 132 | }} 133 | {...props} /> 134 | ); 135 | } 136 | 137 | function CalendarDayButton({ 138 | className, 139 | day, 140 | modifiers, 141 | ...props 142 | }) { 143 | 144 | 145 | const defaultClassNames = getDefaultClassNames() 146 | 147 | const ref = React.useRef(null) 148 | React.useEffect(() => { 149 | if (modifiers.focused) ref.current?.focus() 150 | 151 | 152 | }, [modifiers.focused]) 153 | 154 | 155 | return ( 156 |
63 | 64 |
65 |
66 | calendar 67 | {new Date(item?.contestStartDate).toLocaleString('en-us',options)} 68 |
69 |
70 | 71 | 72 | 73 | {new Date(item?.contestStartDate).toLocaleString('en-us',optionsTimer)} 74 |
75 |
76 | stopwatch 77 | { Math.floor(Number(item?.contestDuration)/3600) } : {String(Math.floor(Number(item?.contestDuration)%3600/60)).padStart(2, "0")} hrs 78 | 79 |
80 | 81 | 82 | 83 | 84 | 85 | empty-hourglass 86 | { new Date(item?.contestStartDate) > new Date() && 87 | 88 | 89 | Starts {formatDistanceToNow(new Date(item?.contestStartDate), { addSuffix: true })} 90 | 91 | }{ new Date(item?.contestStartDate) < new Date() && new Date() < new Date(item?.contestEndDate) && 92 | 93 | 94 | Started {formatDistanceToNow(new Date(item?.contestStartDate), { addSuffix: true })} , 95 | Ends {formatDistanceToNow(new Date(item?.contestEndDate), { addSuffix: true })} 96 | 97 | 98 | }{ new Date() > new Date(item?.contestEndDate) && 99 | 100 | Ended {formatDistanceToNow(new Date(item?.contestEndDate), { addSuffix: true })} 101 | 102 | 103 | } 104 | 105 |
106 | 107 | Loading Calendar...
}> 108 | 109 | 110 | 111 | { 112 | new Date(item?.contestEndDate) >= new Date() ? 113 | 114 | 115 | Register Now 116 | : 117 | spam 118 | Contest Ended 119 | } 120 | 121 |
122 |
123 | 124 | 125 | ) 126 | } 127 | 128 | export default CardsContest 129 | --------------------------------------------------------------------------------