├── 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 |
4 |
--------------------------------------------------------------------------------
/src/assets/contestclock.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/calendar.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/src/assets/geeks.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/geeksForGeeks.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/leetcode.svg:
--------------------------------------------------------------------------------
1 |
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
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 |

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" && }
8 | { eventInfo.event.extendedProps?.platform === "codechef" &&
}
9 | {
10 | eventInfo.event.extendedProps?.platform === "leetcode" &&
}
11 | {
12 | eventInfo.event.extendedProps?.platform === "atcoder" &&
}
13 | {eventInfo.event.extendedProps.platform === "codeforces"&&
}
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 |
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 |

17 |
{new Date(eventInfo?.event?.extendedProps?.contestStartDate).toLocaleString('en-us',options)}
18 |
19 |
20 |
23 |
{new Date(eventInfo?.event?.extendedProps?.contestStartDate).toLocaleString('en-us',optionsTimer)}
24 |
25 |
26 |

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 | <>

34 |
Register Now>
35 | :<>
36 |

37 |
Contest Ended>
38 | }
39 |
40 |
41 |
42 |
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 |
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 |
47 |
49 |
50 |
51 |
52 | {/*
*/}
53 |
54 |
55 |
57 |
58 |
59 |
60 |
62 |
63 |
64 |
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 |
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 |
104 |
105 |
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
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" &&

}
25 | {item?.platform === "codeforces" &&

26 | } { item?.platform === "codechef" &&

}
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 |

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 |

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 |

65 |
{new Date(item?.contestStartDate).toLocaleString('en-us',optionsTimer)}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |

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 |
113 |
116 | - Event Tracker
117 | {/* { -
118 | Parent
119 |
123 |
} */}
124 | - Reminders
125 |
126 |
127 |
128 |
129 |
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 |

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 |