├── .dockerignore
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report_template.md
│ ├── config.yml
│ ├── custom_issue_template.md
│ └── feature_request_template.md
├── .gitignore
├── About.html
├── Code_of_conduct.md
├── Contributing.md
├── Dockerfile
├── History.html
├── Home.css
├── Home.html
├── LICENSE
├── Project Structure.md
├── README.md
├── Settings.html
├── Theme.html
├── Timer.html
├── World_Clock.html
├── backend
├── .env
├── .gitignore
├── Dockerfile
├── config
│ ├── database.js
│ └── passport.js
├── controllers
│ ├── authController.js
│ ├── othercontrollers.js
│ ├── subscribeController.js
│ └── todoController.js
├── index.js
├── middlewares
│ └── auth.middleware.js
├── models
│ ├── Newsletters.js
│ ├── Todo.js
│ └── User.js
├── package-lock.json
├── package.json
├── passport.js
├── routes
│ ├── authRoutes.js
│ ├── googleauth.js
│ └── todoRoutes.js
└── vercel.json
├── components.json
├── eslint.config.js
├── folder_structure.txt
├── footer.css
├── footer.html
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── postcss.config.js
├── preloader.js
├── preloaderStyle.css
├── public
├── bg.jpg
├── vite.svg
└── wallpaperflare.com_wallpaper.jpg
├── readme1.md
├── src
├── App.css
├── App.jsx
├── assets
│ ├── afternoonBackground.png
│ ├── afternoonBackground.webp
│ ├── clock.png
│ ├── clock.webp
│ ├── clock_img.png
│ ├── clock_img.webp
│ ├── counter-app-video-demo2 (1) (2).mp4
│ ├── dark.jpg
│ ├── dark.webp
│ ├── darklogo1.png
│ ├── darklogo1.webp
│ ├── image_with_black_background.png
│ ├── image_with_black_background.webp
│ ├── login.png
│ ├── login.webp
│ ├── logo.png
│ ├── logo.webp
│ ├── logo1.png
│ ├── logo1.webp
│ ├── morningBackground.png
│ ├── morningBackground.webp
│ ├── nightBackground.png
│ ├── nightBackground.webp
│ ├── react.svg
│ ├── signup.png
│ ├── signup.webp
│ ├── timer1.png
│ └── timer1.webp
├── components
│ ├── About.jsx
│ ├── Auth
│ │ ├── LoginForm.jsx
│ │ ├── LoginImage.jsx
│ │ ├── PasswordRecovery.jsx
│ │ ├── Review.jsx
│ │ ├── SignupForm.jsx
│ │ ├── SignupImage.jsx
│ │ └── Template.jsx
│ ├── CumulativeTimeChart.jsx
│ ├── Design.jsx
│ ├── Footer.jsx
│ ├── GoogleTranslate.jsx
│ ├── LapAnalysis.jsx
│ ├── LapBarChart.jsx
│ ├── LapPieChart.jsx
│ ├── LapVisualization.jsx
│ ├── ParticlesComponent.jsx
│ ├── SpotifyPlayer.css
│ ├── SpotifyPlayer.jsx
│ ├── SwitchTab.jsx
│ ├── audio
│ │ └── alert-85101.mp3
│ ├── controls.jsx
│ ├── css
│ │ ├── Footer.css
│ │ └── navbar.css
│ ├── laplist.jsx
│ ├── navbar.jsx
│ ├── pop-up.jsx
│ ├── timer.jsx
│ └── ui
│ │ ├── button.jsx
│ │ ├── card.jsx
│ │ └── scroll-area.jsx
├── index.css
├── lib
│ └── utils.js
├── main.jsx
├── output.css
├── pages
│ ├── AutoCounterPage.jsx
│ ├── Contributors.css
│ ├── Contributors.jsx
│ ├── Counter.jsx
│ ├── Error404.jsx
│ ├── Feedback.css
│ ├── Feedback.jsx
│ ├── PrivacyPolicy.css
│ ├── PrivacyPolicy.jsx
│ ├── SignUpPage.css
│ ├── SignUpPage.jsx
│ ├── TermsPage.jsx
│ ├── TimerPage.css
│ ├── TimerPage.jsx
│ ├── Todo.jsx
│ ├── WorkTracker.jsx
│ └── WorldClockPage.jsx
├── reducer
│ └── index.js
├── services
│ ├── apiConnector.js
│ ├── apis.js
│ └── operations
│ │ └── authAPI.js
├── slices
│ ├── authSlice.js
│ └── profileSlice.js
└── validations
│ └── validation.js
├── tailwind.config.js
├── vercel.json
└── vite.config.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | yarn-error.log
4 |
5 | dist
6 | build
7 |
8 | .env
9 | .env.local
10 | .env.*.local
11 |
12 | *.log
13 |
14 | Dockerfile
15 | .dockerignore
16 |
17 | .git
18 | .gitignore
19 |
20 | .vscode
21 | .idea
22 | *.swp
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🐞 Bug Report"
3 | about: "Report a bug in the Counter App."
4 | title: "[Bug]: "
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | # 🐞 Bug Report
11 |
12 | ### 📋 Description
13 |
14 |
15 | ### ⚙️ Steps to Reproduce
16 | Steps to reproduce the behavior:
17 | 1.
18 | 2.
19 | 3.
20 |
21 | ### ✔️ Expected Behavior
22 |
23 |
24 | ### ❌ Actual Behavior
25 |
26 |
27 | ### 📷 Screenshots
28 |
29 |
30 | ### 🖥️ Environment (if applicable):
31 | - **OS**: (e.g., Windows, macOS, Linux)
32 | - **Browser**: (e.g., Chrome, Safari, Firefox)
33 | - **Version**: (if relevant)
34 |
35 | ### 🔧 Additional Information
36 |
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | contact_links:
3 | - name: Guidelines
4 | url: https://github.com/param-code/counter-app/blob/main/Contributing.md
5 | about: Before opening a new issue, please make sure to read CONTRIBUTING.md
6 | - name: Questions or need suggestions?
7 | url: https://github.com/param-code/counter-app/discussions/new?category=q-a
8 | about: You can create a Q&A discussion and ask for clarifications
9 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom_issue_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "💡 Custom Issue"
3 | about: "Create a custom issue related to the Counter App."
4 | title: "[Custom]: "
5 | labels: enhancement, discussion
6 | assignees: ''
7 |
8 | ---
9 |
10 | # 💡 Custom Issue
11 |
12 | ### 📝 Description
13 |
14 |
15 | ### 🔍 Steps to Reproduce (if applicable)
16 | 1.
17 | 2.
18 | 3.
19 |
20 | ### 🎯 Expected Outcome
21 |
22 |
23 | ### 📸 Screenshots or Additional Information
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "🚀 Feature Request"
3 | about: "Suggest a new feature or enhancement for the Counter App."
4 | title: "[Feature]: "
5 | labels: enhancement, feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 | # 🚀 Feature Request
11 |
12 | ### 📝 Feature Description
13 |
14 |
15 | ### 🔧 How Will This Enhance the App?
16 |
17 |
18 | ### 🎯 Use Cases
19 |
20 |
21 | ### 📦 Suggested Implementation
22 |
23 |
24 | ### 📸 Additional Context
25 |
26 |
--------------------------------------------------------------------------------
/.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 |
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 |
--------------------------------------------------------------------------------
/About.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/About.html
--------------------------------------------------------------------------------
/Code_of_conduct.md:
--------------------------------------------------------------------------------
1 | # ✨Contributor Covenant Code of Conduct✨
2 |
3 | ---
4 |
5 | ## 🌟Our Pledge
6 | ---
7 | We as members, contributors, and leaders pledge to make participation in our
8 | community a harassment-free experience for everyone, regardless of age, body
9 | size, visible or invisible disability, ethnicity, sex characteristics, gender
10 | identity and expression, level of experience, education, socio-economic status,
11 | nationality, personal appearance, race, religion, or sexual identity
12 | and orientation.
13 |
14 | We pledge to act and interact in ways that contribute to an open, welcoming,
15 | diverse, inclusive, and healthy community.
16 |
17 | ---
18 | ## 🚦Our Standards
19 |
20 | Examples of behavior that contributes to a positive environment for our
21 | community include:
22 |
23 | * 💖Demonstrating empathy and kindness toward other people
24 | * 🤝Being respectful of differing opinions, viewpoints, and experiences
25 | * 📝Giving and gracefully accepting constructive feedback
26 | * 🌱Accepting responsibility and apologizing to those affected by our mistakes,
27 | and learning from the experience
28 | * 🎯Focusing on what is best not just for us as individuals, but for the
29 | overall community
30 |
31 | Examples of unacceptable behavior include:
32 |
33 | * 🚫The use of sexualized language or imagery, and sexual attention or
34 | advances of any kind
35 | * 🛑Trolling, insulting or derogatory comments, and personal or political attacks
36 | * ⚠️Public or private harassment
37 | * 🔒Publishing others' private information, such as a physical or email
38 | address, without their explicit permission
39 | * ❌Other conduct which could reasonably be considered inappropriate in a
40 | professional setting
41 |
42 | ---
43 |
44 | ## 👨⚖️Enforcement Responsibilities
45 |
46 | Community leaders are responsible for clarifying and enforcing our standards of
47 | acceptable behavior and will take appropriate and fair corrective action in
48 | response to any behavior that they deem inappropriate, threatening, offensive,
49 | or harmful.
50 |
51 | Community leaders have the right and responsibility to remove, edit, or reject
52 | comments, commits, code, wiki edits, issues, and other contributions that are
53 | not aligned to this Code of Conduct, and will communicate reasons for moderation
54 | decisions when appropriate.
55 |
56 | ---
57 | ## 🌐Scope
58 |
59 | This Code of Conduct applies within all community spaces, and also applies when
60 | an individual is officially representing the community in public spaces.
61 | Examples of representing our community include using an official e-mail address,
62 | posting via an official social media account, or acting as an appointed
63 | representative at an online or offline event.
64 |
65 | ---
66 | ## 📋Pull Request Guidelines
67 |
68 | Contributions, including pull requests, must align with this Code of Conduct.
69 | All contributors are expected to submit work that fosters a positive and inclusive community.
70 | Inappropriate behavior, language, or content in pull requests may result in the rejection of
71 | the contribution and further enforcement actions as outlined in the guidelines.
72 |
73 | ---
74 | ## 🛡️Enforcement
75 |
76 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
77 | reported to the community leaders responsible for enforcement at
78 | .
79 | All complaints will be reviewed and investigated promptly and fairly.
80 |
81 | All community leaders are obligated to respect the privacy and security of the
82 | reporter of any incident.
83 |
84 | ---
85 | ## 📖Enforcement Guidelines
86 |
87 | Community leaders will follow these Community Impact Guidelines in determining
88 | the consequences for any action they deem in violation of this Code of Conduct:
89 |
90 | ### 1. ✏️Correction
91 |
92 | **Community Impact**: Use of inappropriate language or other behavior deemed
93 | unprofessional or unwelcome in the community.
94 |
95 | **Consequence**: A private, written warning from community leaders, providing
96 | clarity around the nature of the violation and an explanation of why the
97 | behavior was inappropriate. A public apology may be requested.
98 |
99 | ### 2. ⚠️Warning
100 |
101 | **Community Impact**: A violation through a single incident or series
102 | of actions.
103 |
104 | **Consequence**: A warning with consequences for continued behavior. No
105 | interaction with the people involved, including unsolicited interaction with
106 | those enforcing the Code of Conduct, for a specified period of time. This
107 | includes avoiding interactions in community spaces as well as external channels
108 | like social media. Violating these terms may lead to a temporary or
109 | permanent ban.
110 |
111 | ### 3. ⏳Temporary Ban
112 |
113 | **Community Impact**: A serious violation of community standards, including
114 | sustained inappropriate behavior.
115 |
116 | **Consequence**: A temporary ban from any sort of interaction or public
117 | communication with the community for a specified period of time. No public or
118 | private interaction with the people involved, including unsolicited interaction
119 | with those enforcing the Code of Conduct, is allowed during this period.
120 | Violating these terms may lead to a permanent ban.
121 |
122 | ### 4. 🚫Permanent Ban
123 |
124 | **Community Impact**: Demonstrating a pattern of violation of community
125 | standards, including sustained inappropriate behavior, harassment of an
126 | individual, or aggression toward or disparagement of classes of individuals.
127 |
128 | **Consequence**: A permanent ban from any sort of public interaction within
129 | the community.
130 |
131 | ---
132 | ## 📜Attribution
133 |
134 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
135 | version 2.0, available at
136 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
137 |
138 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
139 | enforcement ladder](https://github.com/mozilla/diversity).
140 |
141 | [homepage]: https://www.contributor-covenant.org
142 |
143 | For answers to common questions about this code of conduct, see the FAQ at
144 | https://www.contributor-covenant.org/faq. Translations are available at
145 | https://www.contributor-covenant.org/translations.
146 |
147 | ---
148 | # 🌟Conclusion
149 | By adhering to this Code of Conduct, we create a respectful, inclusive, and empowering environment for everyone involved in our community. Together, we can build a space where all members feel safe, supported, and encouraged to contribute their best. We invite every member to take responsibility for upholding these standards, ensuring that our community remains open and welcoming to all.
150 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | #step 1
2 | FROM node:14-alpine AS build
3 |
4 |
5 | WORKDIR /usr/src/app
6 |
7 | COPY package*.json ./
8 |
9 |
10 | RUN npm install
11 |
12 |
13 | COPY . .
14 | #step 2
15 |
16 | RUN npm run build
17 |
18 | FROM nginx:alpine
19 |
20 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html
21 |
22 | EXPOSE 80
23 |
24 | CMD ["nginx", "-g", "daemon off;"]
25 |
--------------------------------------------------------------------------------
/History.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/History.html
--------------------------------------------------------------------------------
/Home.css:
--------------------------------------------------------------------------------
1 | footer {
2 | background-color: #05365f;
3 | color: #f6f0f0;
4 | text-align: center;
5 | position: fixed;
6 | bottom: 0;
7 | width: 100%;
8 | padding: 10px 0;
9 | }
10 |
11 | .footer-content {
12 | justify-content: space-between;
13 | align-items: center;
14 | max-width: 1200px;
15 | margin: 0 auto;
16 | padding: 0 20px;
17 | }
18 |
19 | .footer-nav {
20 | list-style-type: none;
21 | display:flexbox;
22 | gap: 60px;
23 | margin: 20;
24 | padding:0;
25 | }
26 | .footer-nav a {
27 | color: #ccc;
28 | text-decoration: none;
29 | font-weight: 500;
30 | gap: 20px;
31 | }
32 |
33 | .footer-nav a:hover {
34 | text-decoration: underline;
35 | gap:30px;
36 | }
37 |
38 | .footer-copy {
39 | font-size: 0.9em;
40 | color: #ccc;
41 | }
42 |
43 | body {
44 | margin:0;
45 | min-height: 100vh;
46 | }
47 | html {
48 | scroll-behavior: smooth;
49 | }
50 | /* Footer content styling */
51 | .footer-content {
52 | background-color: #333; /* Background color of footer */
53 | color: #fff; /* Text color */
54 | padding: 20px;
55 | text-align: center;
56 | }
57 |
58 | /* Footer navigation bar styling */
59 | .footer-nav {
60 | list-style: none; /* Removes bullets from the list */
61 | padding: 0; /* Removes padding */
62 | margin: 0; /* Removes margin */
63 | display: flex; /* Flexbox for horizontal layout */
64 | justify-content: center; /* Centers the navigation links */
65 | }
66 |
67 | /* Footer nav links */
68 | .footer-nav li {
69 | margin: 0 15px; /* Space between links */
70 | }
71 |
72 | .footer-nav a {
73 | color: #fff; /* Link color */
74 | text-decoration: none; /* Removes underlines */
75 | font-size: 16px;
76 | font-weight: bold;
77 | padding: 5px 10px;
78 | border-radius: 5px;
79 | transition: background-color 0.3s ease; /* Adds a transition effect for hover */
80 | }
81 |
82 | /* Hover effect for navigation links */
83 | .footer-nav a:hover {
84 | background-color: #555; /* Change background on hover */
85 | color: #ffcc00; /* Change text color on hover */
86 | }
87 |
88 | /* Footer copyright text styling */
89 | .footer-copy {
90 | margin-top: 20px;
91 | font-size: 14px;
92 | color: #ccc; /* Lighter text color for copyright info */
93 | }
94 |
95 | /* Section styling */
96 | section {
97 | padding: 60px 20px; /* Adds space inside the section */
98 | margin: 40px 0; /* Adds space between sections */
99 | background-color: #f9f9f9; /* Background color for the section */
100 | border-radius: 10px; /* Rounded corners for the section */
101 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
102 | }
103 |
104 | /* Section titles (h1) styling */
105 | section h1 {
106 | font-size: 28px;
107 | color: #333;
108 | text-align: center; /* Centers the title */
109 | margin-bottom: 20px;
110 | }
111 |
112 | /* Section paragraph (p) styling */
113 | section p {
114 | font-size: 16px;
115 | color: #666;
116 | text-align: center; /* Centers the text */
117 | line-height: 1.6;
118 | }
119 |
120 | /* Media query for smaller screens */
121 | @media (max-width: 768px) {
122 | .footer-nav {
123 | flex-direction: column; /* Stacks the navigation links vertically */
124 | }
125 |
126 | .footer-nav li {
127 | margin: 10px 0; /* Adds space between stacked links */
128 | }
129 |
130 | section {
131 | padding: 40px 10px; /* Reduce padding for smaller screens */
132 | }
133 |
134 | section h1 {
135 | font-size: 24px; /* Adjust title size */
136 | }
137 |
138 | section p {
139 | font-size: 14px; /* Adjust paragraph size */
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Counter Timer
7 |
8 |
9 |
15 |
16 |
17 |
18 |
74 |
75 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Paramveer Singh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Project Structure.md:
--------------------------------------------------------------------------------
1 | ## Project Structure 📂
2 | ```
3 | counter-app
4 | │
5 | ├──.github/ISSUE_TEMPLATE
6 | │ ├──bug_report_template.md
7 | │ ├──config.yml
8 | │ ├──custom_issue_template.md
9 | │ └──feature_request_template.md
10 | │
11 | ├── backend
12 | │ ├──config
13 | │ │ ├──database.js
14 | │ │ └──passport.js
15 | │ ├──controllers
16 | │ │ ├─authController.js
17 | │ │ ├─othercontrollers.js
18 | │ │ ├─subscribeController.js
19 | │ │ └──todoController.js
20 | │ │
21 | │ ├── middlewares
22 | │ │ └──auth.middleware.js
23 | │ │
24 | │ ├──models
25 | │ │ ├─Newsletters.js
26 | │ │ ├─Todo.js
27 | │ │ └──User.js
28 | │ │
29 | │ ├──routes
30 | │ │ ├─authRoutes.js
31 | │ │ └──todoRoutes.js
32 | │ │
33 | │ ├──.env
34 | │ ├──.gitignore
35 | │ ├──Dockerfile
36 | │ ├──index.js
37 | │ ├──package-lock.json
38 | │ ├──package.json
39 | │ └──vercel.json
40 | │
41 | ├──counter-app
42 | ├──public
43 | │ ├──bg.jpg
44 | │ ├──vite.svg
45 | │ └──wallpaperflare.com_wallpaper.jpg
46 | ├──src
47 | │ ├──assets
48 | │ ├──components
49 | │ ├──lib
50 | │ ├──pages
51 | │ ├──reducer
52 | │ ├──services
53 | │ ├──slices
54 | │ ├──validations
55 | │ ├──App.css
56 | │ ├──App.jsx
57 | │ ├──index.css
58 | │ ├──main.jsx
59 | │ └──output.css
60 | │
61 | ├──.dockerignore
62 | ├──.gitignore
63 | ├──About.html
64 | ├──Code_of_conduct.md
65 | ├──Contributing.md
66 | ├──Dockerfile
67 | ├──History.html
68 | ├──Home.css
69 | ├──Home.html
70 | ├──LICENSE
71 | ├──README.md
72 | ├──Settings.html
73 | ├──Theme.html
74 | ├──Timer.html
75 | ├──World_Clock.html
76 | ├──components.json
77 | ├──eslint.config.js
78 | ├──folder_structure.txt
79 | ├──footer.css
80 | ├──footer.html
81 | ├──index.html
82 | ├──jsconfig.json
83 | ├──package-lock.json
84 | ├──package.json├──
85 | ├──postcss.config.js
86 | ├──preloader.js
87 | ├──preloaderStyle.css
88 | ├──readme1.md
89 | ├──tailwind.config.js
90 | ├──vercel.json
91 | └──vite.config.js
92 | ```
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ⏱️ Counter App - Stopwatch Application
3 |
4 | Welcome to the **Counter App**, a simple stopwatch application built using **React** and **Vite**. This app allows users to measure time intervals with features for starting, pausing, and resetting the stopwatch.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ### 🌐 Backend is available at https://counter-app-backend.vercel.app/
14 |
15 | ## 📈 GitHub Repository Stats
16 | | 🌟 **Stars** | 🍴 **Forks** | 🐛 **Issues** | 🔔 **Open PRs** | 🔕 **Closed PRs** | 🛠️ **Languages** | ✅ **Contributors** |
17 | |--------------|--------------|---------------|-----------------|------------------|------------------|------------------|
18 | |  |  |  |  |  |  |  |
19 |
20 | ## 📋 Table of Contents
21 |
22 | - [Introduction](#introduction)
23 | - [Features](#features)
24 | - [Installation](#installation)
25 | - [Usage](#usage)
26 | - [How It Works](#how-it-works)
27 | - [Functionality Overview](#functionality-overview)
28 | - [Video Demo](#video-demo)
29 | - [Contributing](#contributing)
30 | - [License](#license)
31 | - [Our Valuable Contributors](#our-valuable-contributors)
32 |
33 | ## 🛠️ Features
34 |
35 | - 🚀 Start, pause, and reset stopwatch functionality
36 | - ⏱️ Real-time time display
37 | - 💻 Easy-to-use and responsive user interface
38 | - ⚡ Fast performance with Vite for development
39 | - 🌐 Deployed version available (optional: include link)
40 |
41 | ## 🧰 Installation
42 |
43 | Follow these steps to get the Counter App up and running on your local machine.
44 |
45 | 1. **Clone the repository**
46 | ```bash
47 | git clone https://github.com/yourusername/counter-app.git
48 | ```
49 | 2. **Navigate to the project directory**
50 | ```bash
51 | cd counter-app
52 | ```
53 | 3. **Install dependencies**
54 | ```bash
55 | npm install
56 | ```
57 | 4. **Start the development server**
58 | ```bash
59 | npm run dev
60 | ```
61 |
62 | Vite will launch the app in your default browser at `http://localhost:5173`.
63 |
64 | ## 🖥️ Usage
65 |
66 | 1. **Start/Stop**: Click the 'Start' button to begin timing, and the same button will turn into 'Stop' when the timer is running.
67 | 2. **Pause/Resume**: Pause the stopwatch by pressing the 'Stop' button and resume with 'Start'.
68 | 3. **Reset**: Reset the timer to zero by clicking the 'Reset' button.
69 |
70 | ## ⚙️ How It Works
71 |
72 | The Counter App uses React's state management to control the stopwatch functionality. When the 'Start' button is pressed, the app starts counting time using `setInterval()`, and when paused or stopped, it clears the interval. Time is displayed in real-time by continuously updating the state.
73 |
74 | ### 🔍 Functionality Overview:
75 | - **Start Timer**: Initializes the timer using a `setInterval` function, updating every 100 milliseconds.
76 | - **Pause Timer**: Stops the interval without resetting the elapsed time.
77 | - **Reset Timer**: Clears the interval and resets the state to the initial value of `00:00:00`.
78 |
79 | ## 🎥 Video Demo
80 |
81 | Check out the app in action by watching this demo:
82 |
83 |
84 |
85 | Your browser does not support the video tag.
86 |
87 |
88 |
89 | ## 🤝 Contributing
90 |
91 | Contributions are welcome! If you would like to make any changes, feel free to fork the repository and submit a pull request.
92 |
93 | 1. Fork the repository
94 | 2. Clone the forked repository `git clone https://github.com/yourusername/counter-app.git`
95 | 3. Create a new branch for your feature: `git checkout -b feature-branch`
96 | 4. Commit your changes: `git commit -m 'Add new feature'`
97 | 5. Push to the branch: `git push origin feature-branch`
98 | 6. Submit a pull request
99 |
100 | For more detailed guidelines on contributing, please refer to the [Contributing.md](Contributing.md) file.
101 |
102 | ## 📜 License
103 |
104 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
105 |
106 | ## 🙌 Our Valuable Contributors
107 |
108 | [](https://github.com/param-code/counter-app/graphs/contributors)
109 |
110 | 
111 |
112 |
113 |
114 | ## ⭐ Stargazers ❤️
115 |
116 |
117 |
118 | [](https://github.com/param-code/counter-app/stargazers)
119 |
120 |
121 |
122 |
123 | ## 🍴 Forkers ❤️
124 |
125 | [](https://github.com/param-code/counter-app/network/members)
126 |
127 |
128 |
129 |
130 | 
131 |
132 |
133 |
--------------------------------------------------------------------------------
/Settings.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/Settings.html
--------------------------------------------------------------------------------
/Theme.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 | wb_sunny
14 | brightness_2
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Timer.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/Timer.html
--------------------------------------------------------------------------------
/World_Clock.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/World_Clock.html
--------------------------------------------------------------------------------
/backend/.env:
--------------------------------------------------------------------------------
1 | MONGODB_URL=mongodb://127.0.0.1:27017/counter_app
2 | JWT_SECRET="MYSECRET"
3 | PORT=4000
4 | GOOGLE_CLIENT_ID=your_client_id
5 | GOOGLE_CLIENT_SECRET=your_client_secret
6 |
7 | #your email
8 | SMTP_EMAIL=your_email_id_for nodemailer_which_have_passkey
9 | # To create a passkey on the phone or computer you’re on:
10 |
11 | # 1. Go to https://myaccount.google.com/signinoptions/passkeys.
12 | # 2. Tap Create a passkey and then Continue.(You'll be required to unlock your device.)
13 | # 3. A 16 character passkey is generated which you can use in below.
14 |
15 | SMTP_PASSWORD=your_passkey
16 |
17 | #your mail id where you want to accept the feedback mail
18 | MAIL=your_mail_where_you_accept_feedback_mails
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | #.env
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18
2 |
3 | WORKDIR /app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install --production
8 |
9 | COPY . .
10 |
11 | EXPOSE 4000
12 |
13 | CMD ["node", "index.js"]
14 |
--------------------------------------------------------------------------------
/backend/config/database.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | const mongoose = require("mongoose");
4 | require("dotenv").config();
5 |
6 | const database_name = "counter_app";
7 |
8 | exports.dbConnect = () => {
9 | mongoose
10 | .connect(process.env.MONGODB_URL, {
11 | useNewUrlParser: true,
12 | useUnifiedTopology: true,
13 | })
14 | .then(() => {
15 | console.log("Db connect successfully");
16 | })
17 | .catch((err) => {
18 | console.log("Error in db connection");
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/backend/config/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require("passport");
2 | const GoogleStrategy = require("passport-google-oauth20").Strategy;
3 | const User = require("../models/User");
4 |
5 | passport.use(
6 | new GoogleStrategy(
7 | {
8 | clientID: process.env.GOOGLE_CLIENT_ID,
9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
10 | callbackURL: "http://localhost:5000/auth/google/callback",
11 | },
12 | async (accessToken, refreshToken, profile, done) => {
13 | try {
14 | let user = await User.findOne({ googleId: profile.id });
15 | if (!user) {
16 | user = await User.create({
17 | googleId: profile.id,
18 | name: profile.displayName,
19 | email: profile.emails[0].value,
20 | avatar: profile.photos[0].value,
21 | });
22 | }
23 | done(null, user);
24 | } catch (err) {
25 | done(err, null);
26 | }
27 | }
28 | )
29 | );
30 |
31 | passport.serializeUser((user, done) => done(null, user.id));
32 | passport.deserializeUser((id, done) => {
33 | User.findById(id, (err, user) => done(err, user));
34 | });
35 |
--------------------------------------------------------------------------------
/backend/controllers/authController.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcrypt");
2 | const jwt = require("jsonwebtoken");
3 | const User = require("../models/User");
4 |
5 | async function signup(req, res) {
6 | try {
7 | const { firstName, lastName, email, password, confirmPassword } = req.body;
8 |
9 | if (!firstName || !lastName || !email || !password || !confirmPassword) {
10 | return res.status(403).send({
11 | success: false,
12 | message: "All Fields are required",
13 | });
14 | }
15 |
16 | if (!email.includes("@gmail.com")) {
17 | return res.status(400).send({
18 | success: false,
19 | message: "Invalid Email!",
20 | });
21 | }
22 |
23 | if (password !== confirmPassword) {
24 | return res.status(400).json({
25 | success: false,
26 | message:
27 | "Password and Confirm Password do not match. Please try again.",
28 | });
29 | }
30 |
31 | const existingUser = await User.findOne({ email });
32 | if (existingUser) {
33 | return res.status(400).json({
34 | success: false,
35 | message: "User already exists. Please login to continue.",
36 | });
37 | }
38 |
39 | const hashedPassword = await bcrypt.hash(password, 10);
40 |
41 | const user = await User.create({
42 | firstName,
43 | lastName,
44 | email,
45 | password: hashedPassword,
46 | image: `https://api.dicebear.com/5.x/initials/svg?seed=${firstName} ${lastName}`,
47 | });
48 |
49 | return res.status(200).json({
50 | success: true,
51 | user,
52 | message: "User registered successfully!",
53 | });
54 | } catch (error) {
55 | console.log(error.message);
56 | return res.status(500).json({
57 | success: false,
58 | message: "User cannot be registered, Please try again!",
59 | });
60 | }
61 | }
62 |
63 | async function login(req, res) {
64 | try {
65 | const { email, password } = req.body;
66 |
67 | if (!email || !password) {
68 | return res.status(400).json({
69 | success: false,
70 | message: `All fields are required!`,
71 | });
72 | }
73 |
74 | const user = await User.findOne({ email });
75 |
76 | if (!user) {
77 | return res.status(401).json({
78 | success: false,
79 | message: `User is not Registered with Us. Please SignUp to Continue.`,
80 | });
81 | }
82 |
83 | if (await bcrypt.compare(password, user.password)) {
84 | const token = jwt.sign(
85 | {
86 | email: user.email,
87 | id: user._id,
88 | },
89 | process.env.JWT_SECRET,
90 | {
91 | expiresIn: "24h",
92 | }
93 | );
94 |
95 | user._doc.token = token;
96 | user._doc.password = undefined;
97 |
98 | const options = {
99 | expires: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
100 | httpOnly: true,
101 | };
102 |
103 | res.cookie("token", token, options).status(200).json({
104 | success: true,
105 | token,
106 | user,
107 | message: `User Login Successfully!`,
108 | });
109 | } else {
110 | return res.status(401).json({
111 | success: false,
112 | message: `Password is incorrect`,
113 | });
114 | }
115 | } catch (error) {
116 | console.error(error);
117 | return res.status(500).json({
118 | success: false,
119 | message: `Login Failure, Please Try Again!`,
120 | });
121 | }
122 | }
123 |
124 | async function logout(req, res) {
125 | try {
126 | console.log("req.user:", req.user);
127 | await User.findByIdAndUpdate(
128 | req.user._id,
129 | {
130 | $unset: {
131 | token: 1,
132 | },
133 | },
134 | {
135 | new: true,
136 | }
137 | );
138 |
139 | const options = {
140 | httpOnly: true,
141 | secure: true,
142 | };
143 |
144 | res
145 | .status(200)
146 | .clearCookie("token", options)
147 | .json({ message: "User logged out successfully" });
148 | } catch (error) {
149 | res.status(500).json({ error: "An error occurred during logout" });
150 | }
151 | }
152 |
153 | function googleCallback(req, res) {
154 | res.redirect("/profile");
155 | }
156 |
157 | module.exports = {
158 | signup,
159 | login,
160 | logout,
161 | googleCallback,
162 | };
163 |
--------------------------------------------------------------------------------
/backend/controllers/othercontrollers.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer');
2 |
3 | exports.sendFeedbackEmail = async (req, res) => {
4 | const { name, email, message, rating } = req.body; // Capture rating from the request
5 | console.log("Sending email with the following details:", { name, email, message, rating });
6 | // Ensure all required fields are provided
7 | if (!name || !email || !message || rating === undefined) {
8 | console.log("Sending email with the following details 2:", { name, email, message, rating });
9 | return res.status(400).json({ success: false, message: 'All fields including rating are required' });
10 | }
11 |
12 | try {
13 | console.log("Sending email with the following details 3:", { name, email, message, rating });
14 | const transporter = nodemailer.createTransport({
15 | service: 'gmail',
16 | auth: {
17 | user: process.env.SMTP_EMAIL,
18 | pass: process.env.SMTP_PASSWORD,
19 | },
20 | });
21 |
22 | // Generate stars for rating
23 | const totalStars = 5;
24 | const filledStars = '★'.repeat(rating); // Filled stars based on rating
25 | const emptyStars = '☆'.repeat(totalStars - rating); // Empty stars for the rest
26 | const starDisplay = `${filledStars}${emptyStars}
`;
27 |
28 | const mailOptions = {
29 | from: `"${name}" <${email}>`, // Display sender's name and email
30 | to: process.env.MAIL, // Make sure RESPONSE_MAIL is set in the .env
31 | subject: `Feedback from ${name}`,
32 | text: message,
33 | html: `
34 | You have received a new message from the Feedback form:
35 | Contact Details:
36 |
37 | Name: ${name}
38 | Email: ${email}
39 |
40 | Message:
41 | ${message}
42 | Rating:
43 | ${starDisplay}
44 | `, // HTML formatted message body with rating stars
45 | };
46 |
47 | // Send the email
48 | await transporter.sendMail(mailOptions);
49 |
50 | // Respond with success if email is sent
51 | return res.status(200).json({ success: true, message: 'Feedback sent successfully!' });
52 | } catch (error) {
53 | console.error('Error sending feedback email:', error);
54 | return res.status(500).json({ success: false, message: 'Error sending feedback email' });
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/backend/controllers/subscribeController.js:
--------------------------------------------------------------------------------
1 |
2 | const nodemailer = require('nodemailer');
3 | const Newsletters = require('../models/Newsletters');
4 | const transporter = nodemailer.createTransport({
5 | service: 'gmail', // Or your preferred email service
6 | auth: {
7 | user: process.env.SMTP_EMAIL, // Your email address
8 | pass: process.env.SMTP_PASSWORD, // Your email password or app-specific password
9 | },
10 | });
11 | exports.subscribe = async (req, res) => {
12 | const { email} = req.body;
13 | try {
14 |
15 | const existingSubscription = await Newsletters.findOne({ email });
16 | if (existingSubscription) {
17 | return res.status(400).json({ message: 'This email is already subscribed.' });
18 | }
19 |
20 | // Step 2: Save the new subscription to the database
21 | const newSubscription = new Newsletters({ email });
22 | await newSubscription.save();
23 |
24 | const mailOptions = {
25 | from: process.env.SMTP_EMAIL,
26 | to: email,
27 | subject: 'Subscription Confirmation',
28 | html: `
29 | Thank you for subscribing!
30 | You have successfully subscribed to our platform.
31 | Stay tuned for updates and exclusive offers!
32 |
34 | Explore More
35 |
36 | Best Regards, Hiring Portal
37 | `,
38 | };
39 |
40 | await transporter.sendMail(mailOptions);
41 |
42 | res.status(200).json({ message: 'Subscription successful. Confirmation email sent.' });
43 | } catch (error) {
44 | console.error('Subscription error:', error);
45 | res.status(500).json({ message: 'Subscription failed.' });
46 | }
47 | }
--------------------------------------------------------------------------------
/backend/controllers/todoController.js:
--------------------------------------------------------------------------------
1 | const Todo = require("../models/Todo");
2 |
3 | async function addTodo(req, res) {
4 | try {
5 | const { task, userId } = req.body;
6 |
7 | console.log(req.body);
8 | if (!task || !userId) {
9 | return res.status(403).send({
10 | success: false,
11 | message: "All Fields are required",
12 | });
13 | }
14 | const newTodo = await Todo.create({
15 | task,
16 | userId,
17 | });
18 |
19 | return res.status(200).json({
20 | success: true,
21 | newTodo,
22 | message: "Todo added successfully!",
23 | });
24 | } catch (error) {
25 | console.log(error.message);
26 | return res.status(500).json({
27 | success: false,
28 | message: "Cannot add todo",
29 | });
30 | }
31 | }
32 |
33 | async function getTodos(req, res) {
34 | try {
35 | const { userId } = req.params;
36 |
37 | // Find todos that match the provided userId
38 | const todos = await Todo.find({ userId });
39 |
40 | if (!todos || todos.length === 0) {
41 | return res.status(404).json({
42 | success: false,
43 | message: "No todos found for this user",
44 | });
45 | }
46 |
47 | return res.status(200).json({
48 | success: true,
49 | todos,
50 | });
51 | } catch (error) {
52 | console.log(error.message);
53 | return res.status(500).json({
54 | success: false,
55 | message: "Error fetching todos",
56 | });
57 | }
58 | }
59 |
60 | async function deleteTodo(req, res) {
61 | try {
62 | const todoId = req.params.id;
63 |
64 | const deletedTodo = await Todo.findByIdAndDelete(todoId);
65 |
66 | if (!deletedTodo) {
67 | return res.status(404).json({ message: "Todo not found" });
68 | }
69 |
70 | return res.status(200).json({ message: "Todo deleted successfully" });
71 | } catch (error) {
72 | console.error(error);
73 | return res
74 | .status(500)
75 | .json({ message: "An error occurred while deleting the todo" });
76 | }
77 | }
78 |
79 | module.exports = {
80 | addTodo,
81 | getTodos,
82 | deleteTodo,
83 | };
84 |
--------------------------------------------------------------------------------
/backend/index.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | // Importing necessary modules and packages
4 | const express = require("express");
5 | const mongoose = require("mongoose");
6 | const app = express();
7 | const database = require("./config/database");
8 | const passport = require("passport");
9 | const session = require("express-session");
10 | require("./passport"); // Import passport configuration
11 | const cookieParser = require("cookie-parser");
12 | const cors = require("cors");
13 | const { sendFeedbackEmail } = require("./controllers/othercontrollers");
14 | const { subscribe } = require("./controllers/subscribeController");
15 | // Loading environment variables from .env file
16 | require("dotenv").config();
17 | const authRoutes = require("./routes/authRoutes");
18 | const todoRoutes = require("./routes/todoRoutes");
19 | const googleauth = require("./routes/googleauth");
20 |
21 | // Visitor Schema
22 | const visitorSchema = new mongoose.Schema({
23 | count: { type: Number, default: 0 }
24 | });
25 |
26 | const Visitor = mongoose.model("Visitor", visitorSchema);
27 |
28 | // Setting up port number
29 | const PORT = process.env.PORT || 4000;
30 |
31 | // Connecting to database
32 | database.dbConnect();
33 |
34 | // Middlewares
35 | app.use(express.json());
36 | app.use(express.urlencoded({ extended: true }));
37 | app.use(cookieParser());
38 | app.use(
39 | cors({
40 | origin: ["http://localhost:5173", "http://localhost:3000"],
41 | credentials: true,
42 | })
43 | );
44 |
45 | app.use(
46 | session({
47 | secret: process.env.JWT_SECRET,
48 | resave: false,
49 | saveUninitialized: true,
50 | })
51 | );
52 |
53 | require("./config/passport");
54 | app.use(passport.initialize());
55 | app.use(passport.session());
56 |
57 | // Setting up routes
58 | app.use("/api/v1/auth", authRoutes);
59 | app.use("/api/v1/todo", todoRoutes);
60 | app.use("/auth", googleauth)
61 |
62 | // Testing the server
63 | app.get("/", (req, res) => {
64 | return res.json({
65 | success: true,
66 | message: "Your server is up and running ...",
67 | });
68 | });
69 |
70 | app.post("/contact", sendFeedbackEmail);
71 | app.post("/subscribe", subscribe);
72 |
73 | // Listening to the server
74 | app.listen(PORT, () => {
75 | console.log(`App is listening at ${PORT}`);
76 | });
77 |
78 | // Endpoint to get visitor count
79 | // Import Node's `crypto` module to create a unique ID for each visitor
80 | const crypto = require("crypto");
81 |
82 | // Create a temporary set to store recent visitor IDs
83 | const recentVisitors = new Set();
84 |
85 | app.get("/api/visitor-count", async (req, res) => {
86 | // Generate a unique ID based on the visitor's IP and browser information
87 | const visitorId = crypto
88 | .createHash("md5")
89 | .update(req.ip + req.headers["user-agent"])
90 | .digest("hex");
91 |
92 | // If the visitor has already been counted recently, skip incrementing
93 | if (recentVisitors.has(visitorId)) {
94 | const visitor = await Visitor.findOne();
95 | return res.json({ count: visitor.count });
96 | }
97 |
98 | // Add the visitor to the recentVisitors set
99 | recentVisitors.add(visitorId);
100 |
101 | // Remove the visitor from the cache after 30 seconds to allow counting later if they revisit
102 | setTimeout(() => {
103 | recentVisitors.delete(visitorId);
104 | }, 3000); // 3 seconds
105 |
106 | // Increment visitor count
107 | let visitor = await Visitor.findOne();
108 | if (!visitor) {
109 | visitor = new Visitor({ count: 1 });
110 | } else {
111 | visitor.count += 1;
112 | }
113 | await visitor.save();
114 |
115 | // Send the updated count to the client
116 | res.json({ count: visitor.count });
117 | });
118 |
119 | // End of code.
120 |
--------------------------------------------------------------------------------
/backend/middlewares/auth.middleware.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const User = require("../models/User.js");
3 |
4 | const verifyJWT = async (req, res, next) => {
5 | try {
6 | const token =
7 | req.cookies?.token || req.header("Authorization")?.replace("Bearer ", "");
8 |
9 | if (!token) {
10 | return res.status(401).json({ message: "Unauthorized request" });
11 | }
12 |
13 | const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
14 |
15 | const user = await User.findById(decodedToken?.id);
16 |
17 | if (!user) {
18 | return res.status(401).json({ message: "Invalid Access Token" });
19 | }
20 |
21 | req.user = user;
22 | next();
23 | } catch (error) {
24 | return res
25 | .status(401)
26 | .json({ message: error?.message || "Invalid access token" });
27 | }
28 | };
29 |
30 | module.exports = { verifyJWT };
31 |
--------------------------------------------------------------------------------
/backend/models/Newsletters.js:
--------------------------------------------------------------------------------
1 | // models/Subscription.js
2 | const mongoose = require('mongoose');
3 |
4 | const Newsletter = new mongoose.Schema({
5 | email: {
6 | type: String,
7 | required: true,
8 | unique: true, // Ensures no duplicate emails
9 | match: /.+\@.+\..+/ // Basic regex to validate email format
10 | },
11 | subscribedAt: {
12 | type: Date,
13 | default: Date.now
14 | }
15 | });
16 |
17 | module.exports = mongoose.model('Subscription', Newsletter);
18 |
--------------------------------------------------------------------------------
/backend/models/Todo.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const todoSchema = new mongoose.Schema({
4 | task: {
5 | type: String,
6 | required: true,
7 | trim: true,
8 | },
9 | userId: {
10 | type: mongoose.Schema.Types.ObjectId,
11 | ref: "User",
12 | required: true,
13 | },
14 | });
15 |
16 | module.exports = mongoose.model("Todo", todoSchema);
17 |
--------------------------------------------------------------------------------
/backend/models/User.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | // Import the Mongoose library
4 | const mongoose = require("mongoose");
5 |
6 | // Define the user schema using the Mongoose Schema constructor
7 | const userSchema = new mongoose.Schema(
8 | {
9 | // Define the name field with type String, required, and trimmed
10 | firstName: {
11 | type: String,
12 | required: true,
13 | trim: true,
14 | },
15 | lastName: {
16 | type: String,
17 | required: true,
18 | trim: true,
19 | },
20 | // Define the email field with type String, required, and trimmed
21 | email: {
22 | type: String,
23 | required: true,
24 | trim: true,
25 | },
26 |
27 | // Define the password field with type String and required
28 | password: {
29 | type: String,
30 | required: true,
31 | },
32 | token: {
33 | type: String,
34 | },
35 | image: {
36 | type: String,
37 | required: true,
38 | },
39 | googleId: {
40 | type: String,
41 | unique: true,
42 | sparse: true, // This allows either Google login or email/password login
43 | }
44 |
45 | },
46 | { timestamps: true }
47 | );
48 |
49 | // Export the Mongoose model for the user schema, using the name "user"
50 | module.exports = mongoose.model("user", userSchema);
51 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "start": "node index.js",
7 | "dev": "nodemon index.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "backend": "file:",
14 | "bcrypt": "^5.1.1",
15 | "cookie-parser": "^1.4.6",
16 | "cors": "^2.8.5",
17 | "dotenv": "^16.4.5",
18 | "express": "^4.21.1",
19 | "express-session": "^1.18.1",
20 | "jsonwebtoken": "^9.0.2",
21 | "mongoose": "^8.7.0",
22 | "nodemailer": "^6.9.15",
23 | "nodemon": "^3.1.7",
24 | "passport": "^0.7.0",
25 | "passport-google-oauth20": "^2.0.0"
26 | },
27 | "devDependencies": {},
28 | "description": ""
29 | }
30 |
--------------------------------------------------------------------------------
/backend/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require("passport");
2 | const GoogleStrategy = require("passport-google-oauth20").Strategy;
3 | const User = require("./models/User"); // Import your User model
4 |
5 | passport.use(
6 | new GoogleStrategy(
7 | {
8 | clientID: process.env.GOOGLE_CLIENT_ID,
9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
10 | callbackURL: "/auth/google/callback",
11 | },
12 | async (accessToken, refreshToken, profile, done) => {
13 | try {
14 | // Check if user already exists
15 | let user = await User.findOne({ googleId: profile.id });
16 | if (user) {
17 | return done(null, user);
18 | }
19 |
20 | // If user doesn't exist, create a new one
21 | user = new User({
22 | firstName: profile.name.givenName,
23 | lastName: profile.name.familyName,
24 | email: profile.emails[0].value,
25 | googleId: profile.id,
26 | image: profile.photos[0].value,
27 | });
28 | await user.save();
29 | done(null, user);
30 | } catch (error) {
31 | done(error, false);
32 | }
33 | }
34 | )
35 | );
36 |
37 | // Serialize user information into session
38 | passport.serializeUser((user, done) => done(null, user.id));
39 |
40 | // Deserialize user information from session
41 | passport.deserializeUser(async (id, done) => {
42 | const user = await User.findById(id);
43 | done(null, user);
44 | });
45 |
--------------------------------------------------------------------------------
/backend/routes/authRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const passport = require("passport");
3 | const {
4 | signup,
5 | login,
6 | logout,
7 | googleCallback,
8 | } = require("../controllers/authController");
9 | const { verifyJWT } = require("../middlewares/auth.middleware");
10 |
11 | const router = express.Router();
12 |
13 | router.post("/signup", signup);
14 | router.post("/login", login);
15 | router.get("/logout", verifyJWT, logout);
16 | router.get(
17 | "/google",
18 | passport.authenticate("google", { scope: ["profile", "email"] })
19 | );
20 | router.get(
21 | "/google/callback",
22 | passport.authenticate("google", { failureRedirect: "/login" }),
23 | googleCallback
24 | );
25 |
26 | module.exports = router;
27 |
--------------------------------------------------------------------------------
/backend/routes/googleauth.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const passport = require("passport");
3 |
4 | const router = express.Router();
5 |
6 | // Route to initiate Google authentication
7 | router.get(
8 | "/google",
9 | passport.authenticate("google", { scope: ["profile", "email"] })
10 | );
11 |
12 | // Google OAuth callback route
13 | router.get(
14 | "/google/callback",
15 | passport.authenticate("google", { failureRedirect: "/" }),
16 | (req, res) => {
17 | // Redirect to a page after successful login (e.g., dashboard)
18 | res.redirect("/dashboard");
19 | }
20 | );
21 |
22 | // Logout route
23 | router.get("/logout", (req, res) => {
24 | req.logout(err => {
25 | if (err) {
26 | console.error(err);
27 | return res.redirect("/"); // Redirect to home on error
28 | }
29 | res.redirect("/"); // Redirect to home after logout
30 | });
31 | });
32 |
33 | module.exports = router;
34 |
--------------------------------------------------------------------------------
/backend/routes/todoRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { addTodo, getTodos, deleteTodo } = require("../controllers/todoController");
3 | const { verifyJWT } = require("../middlewares/auth.middleware");
4 |
5 | const router = express.Router();
6 |
7 | router.post("/add", verifyJWT, addTodo);
8 | router.get("/:userId", verifyJWT, getTodos);
9 | router.delete("/:id", verifyJWT, deleteTodo);
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/backend/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "index.js",
6 | "use": "@vercel/node",
7 | "config": { "includeFiles": ["dist/**"] }
8 | }
9 | ],
10 | "routes": [
11 | {
12 | "src": "/(.*)",
13 | "dest": "index.js"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": false,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
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 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import react from 'eslint-plugin-react'
4 | import reactHooks from 'eslint-plugin-react-hooks'
5 | import reactRefresh from 'eslint-plugin-react-refresh'
6 |
7 | export default [
8 | { ignores: ['dist'] },
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | languageOptions: {
12 | ecmaVersion: 2020,
13 | globals: globals.browser,
14 | parserOptions: {
15 | ecmaVersion: 'latest',
16 | ecmaFeatures: { jsx: true },
17 | sourceType: 'module',
18 | },
19 | },
20 | settings: { react: { version: '18.3' } },
21 | plugins: {
22 | react,
23 | 'react-hooks': reactHooks,
24 | 'react-refresh': reactRefresh,
25 | },
26 | rules: {
27 | ...js.configs.recommended.rules,
28 | ...react.configs.recommended.rules,
29 | ...react.configs['jsx-runtime'].rules,
30 | ...reactHooks.configs.recommended.rules,
31 | 'react/jsx-no-target-blank': 'off',
32 | 'react/prop-types':'off',
33 | 'react-refresh/only-export-components': [
34 | 'warn',
35 | { allowConstantExport: true },
36 | ],
37 | },
38 | },
39 | ]
40 |
--------------------------------------------------------------------------------
/folder_structure.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/folder_structure.txt
--------------------------------------------------------------------------------
/footer.css:
--------------------------------------------------------------------------------
1 | footer {
2 | background-color: #05365f;
3 | color: #f6f0f0;
4 | text-align: center;
5 | position: fixed;
6 | bottom: 0;
7 | width: 100%;
8 | padding: 10px 0;
9 | }
10 |
11 | .footer-content {
12 | justify-content: space-between;
13 | align-items: center;
14 | max-width: 1200px;
15 | margin: 0 auto;
16 | padding: 0 20px;
17 | }
18 |
19 | .footer-nav {
20 | list-style-type: none;
21 | display:flexbox;
22 | gap: 60px;
23 | margin: 20;
24 | padding:0;
25 | }
26 | .footer-nav a {
27 | color: #ccc;
28 | text-decoration: none;
29 | font-weight: 500;
30 | gap: 20px;
31 | }
32 |
33 | .footer-nav a:hover {
34 | text-decoration: underline;
35 | gap:30px;
36 | }
37 |
38 | .footer-copy {
39 | font-size: 0.9em;
40 | color: #ccc;
41 | }
42 |
43 | body {
44 | margin:0;
45 | min-height: 100vh;
46 | }
47 | html {
48 | scroll-behavior: smooth;
49 | }
50 | /* Footer content styling */
51 | .footer-content {
52 | background-color: #333; /* Background color of footer */
53 | color: #fff; /* Text color */
54 | padding: 20px;
55 | text-align: center;
56 | }
57 |
58 | /* Footer navigation bar styling */
59 | .footer-nav {
60 | list-style: none; /* Removes bullets from the list */
61 | padding: 0; /* Removes padding */
62 | margin: 0; /* Removes margin */
63 | display: flex; /* Flexbox for horizontal layout */
64 | justify-content: center; /* Centers the navigation links */
65 | }
66 |
67 | /* Footer nav links */
68 | .footer-nav li {
69 | margin: 0 15px; /* Space between links */
70 | }
71 |
72 | .footer-nav a {
73 | color: #fff; /* Link color */
74 | text-decoration: none; /* Removes underlines */
75 | font-size: 16px;
76 | font-weight: bold;
77 | padding: 5px 10px;
78 | border-radius: 5px;
79 | transition: background-color 0.3s ease; /* Adds a transition effect for hover */
80 | }
81 |
82 | /* Hover effect for navigation links */
83 | .footer-nav a:hover {
84 | background-color: #555; /* Change background on hover */
85 | color: #ffcc00; /* Change text color on hover */
86 | }
87 |
88 | /* Footer copyright text styling */
89 | .footer-copy {
90 | margin-top: 20px;
91 | font-size: 14px;
92 | color: #ccc; /* Lighter text color for copyright info */
93 | }
94 |
95 | /* Section styling */
96 | section {
97 | padding: 60px 20px; /* Adds space inside the section */
98 | margin: 40px 0; /* Adds space between sections */
99 | background-color: #f9f9f9; /* Background color for the section */
100 | border-radius: 10px; /* Rounded corners for the section */
101 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */
102 | }
103 |
104 | /* Section titles (h1) styling */
105 | section h1 {
106 | font-size: 28px;
107 | color: #333;
108 | text-align: center; /* Centers the title */
109 | margin-bottom: 20px;
110 | }
111 |
112 | /* Section paragraph (p) styling */
113 | section p {
114 | font-size: 16px;
115 | color: #666;
116 | text-align: center; /* Centers the text */
117 | line-height: 1.6;
118 | }
119 |
120 | /* Media query for smaller screens */
121 | @media (max-width: 768px) {
122 | .footer-nav {
123 | flex-direction: column; /* Stacks the navigation links vertically */
124 | }
125 |
126 | .footer-nav li {
127 | margin: 10px 0; /* Adds space between stacked links */
128 | }
129 |
130 | section {
131 | padding: 40px 10px; /* Reduce padding for smaller screens */
132 | }
133 |
134 | section h1 {
135 | font-size: 24px; /* Adjust title size */
136 | }
137 |
138 | section p {
139 | font-size: 14px; /* Adjust paragraph size */
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/footer.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/footer.html
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Counter App
10 |
11 |
12 |
13 | ↑
14 |
15 |
16 |
43 |
44 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // ...
4 | "baseUrl": ".",
5 | "paths": {
6 | "@/*": [
7 | "./src/*"
8 | ]
9 | }
10 | // ...
11 | }
12 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "counter-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview",
11 | "server": "cd backend && npm run dev",
12 | "dev": "concurrently -n \"client,server\" -c \"bgBlue,bgYellow\" \"npm start\" \"npm run server\"",
13 | "predeploy": "npm run build",
14 | "deploy": "gh-pages -d build"
15 | },
16 | "dependencies": {
17 | "@chakra-ui/react": "^2.10.1",
18 | "@fortawesome/fontawesome-svg-core": "^6.6.0",
19 | "@fortawesome/free-brands-svg-icons": "^6.6.0",
20 | "@fortawesome/free-solid-svg-icons": "^6.6.0",
21 | "@fortawesome/react-fontawesome": "^0.2.2",
22 | "@radix-ui/react-scroll-area": "^1.1.0",
23 | "@radix-ui/react-slot": "^1.1.0",
24 | "@react-oauth/google": "^0.12.1",
25 | "@reduxjs/toolkit": "^2.2.8",
26 | "@tsparticles/react": "^3.0.0",
27 | "@tsparticles/slim": "^3.5.0",
28 | "axios": "^1.7.7",
29 | "bcrypt": "^5.1.1",
30 | "class-variance-authority": "^0.7.0",
31 | "clsx": "^2.1.1",
32 | "concurrently": "^9.0.1",
33 | "counter-project": "file:",
34 | "dotenv": "^16.4.5",
35 | "express-session": "^1.18.1",
36 | "framer-motion": "^11.11.8",
37 | "jsonwebtoken": "^9.0.2",
38 | "lucide-react": "^0.447.0",
39 | "moment": "^2.30.1",
40 | "moment-timezone": "^0.5.45",
41 | "mongoose": "^8.7.2",
42 | "next-themes": "^0.3.0",
43 | "passport": "^0.7.0",
44 | "passport-google-oauth20": "^2.0.0",
45 | "react": "^18.3.1",
46 | "react-clock": "^5.0.0",
47 | "react-dom": "^18.3.1",
48 | "react-hot-toast": "^2.4.1",
49 | "react-icons": "^5.3.0",
50 | "react-redux": "^9.1.2",
51 | "react-router-dom": "^6.26.2",
52 | "react-toastify": "^10.0.6",
53 | "recharts": "^2.13.0",
54 | "styled-components": "^6.1.13",
55 | "tailwind-merge": "^2.5.2",
56 | "tailwindcss-animate": "^1.0.7",
57 | "tsparticles": "^3.5.0",
58 | "yup": "^1.4.0"
59 | },
60 | "devDependencies": {
61 | "@eslint/js": "^9.9.0",
62 | "@shadcn/ui": "^0.0.4",
63 | "@tailwindcss/aspect-ratio": "^0.4.2",
64 | "@tailwindcss/forms": "^0.5.9",
65 | "@tailwindcss/typography": "^0.5.15",
66 | "@types/react": "^18.3.3",
67 | "@types/react-dom": "^18.3.0",
68 | "@vitejs/plugin-react": "^4.3.1",
69 | "autoprefixer": "^10.4.20",
70 | "eslint": "^9.9.0",
71 | "eslint-plugin-react": "^7.35.0",
72 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
73 | "eslint-plugin-react-refresh": "^0.4.9",
74 | "globals": "^15.9.0",
75 | "postcss": "^8.4.45",
76 | "tailwindcss": "^3.4.10",
77 | "vite": "^5.4.8",
78 | "vite-plugin-babel": "^1.2.0"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/preloader.js:
--------------------------------------------------------------------------------
1 | // /preloader js styling
2 |
3 | let preloader = document.querySelector("#preloader");
4 |
5 | window.addEventListener("load",function(e){
6 |
7 | preloader.style.display = "none";
8 |
9 | });
--------------------------------------------------------------------------------
/public/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/public/bg.jpg
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/wallpaperflare.com_wallpaper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/public/wallpaperflare.com_wallpaper.jpg
--------------------------------------------------------------------------------
/readme1.md:
--------------------------------------------------------------------------------
1 | ## Important Guidelines ⚡
2 |
3 | 1. Contributors should only work on issues that have been assigned to them.
4 | 2. Each pull request should be associated with one issue only.
5 | 3. No minor text edits should be submitted unless necessary.
6 | 4. Unethical behavior, tampering with files, or harassment will result in disqualification.
7 | 5. Follow the community guidelines while contributing to ensure a healthy collaborative environment.
8 | 6. No Issue Repetitions are allowed.
9 | 7. Check the issues before you raise an issue.
10 | 8. No Plagiarism of Codes.
11 |
12 | ## Community Guidelines 🤝
13 |
14 | Please follow these guidelines while contributing:
15 |
16 | - Be respectful and considerate towards others.
17 | - Use inclusive language and foster a welcoming environment.
18 | - Avoid personal attacks, harassment, or discrimination.
19 | - Keep discussions focused on constructive topics.
20 |
21 | - ## Code Reviews ✅
22 |
23 | - Be open to feedback from other contributors.
24 | - Participate in code reviews to help improve the project.
25 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | /* Custom scrollbar styles for Chrome, Edge, Safari */
2 | ::-webkit-scrollbar {
3 | width: 12px; /* width for vertical scrollbar */
4 | height: 12px; /* height for horizontal scrollbar */
5 | }
6 |
7 | ::-webkit-scrollbar-track {
8 | background: #f1f1f1; /* Track background */
9 | border-radius: 10px; /* Roundness of the track */
10 | }
11 |
12 | ::-webkit-scrollbar-thumb {
13 | background-color: #a6f1f3; /* Scrollbar thumb color */
14 | border-radius: 20px; /* Increase roundness for thumb */
15 | border: 3px solid #f1f1f1; /* Space around thumb to create a more rounded appearance */
16 | }
17 |
18 | /* Hover effect */
19 | ::-webkit-scrollbar-thumb:hover {
20 | background-color: #555; /* Darken on hover */
21 | }
22 |
23 | /* Firefox */
24 | * {
25 | scrollbar-width: thin;
26 | scrollbar-color: #8ca0bb #f1f1f1;
27 | }
28 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
3 | import WorldClockPage from "./pages/WorldClockPage";
4 | // import SignUpPage from "./pages/SignUpPage";
5 | import TimerPage from "./pages/TimerPage";
6 | import AutoCounterPage from "./pages/AutoCounterPage";
7 | import Design from "./components/Design";
8 | import Counter from "./pages/Counter";
9 | import Contributors from "./pages/Contributors";
10 | import Feedback from "./pages/Feedback";
11 | import PrivacyPolicy from "./pages/PrivacyPolicy";
12 | import Footer from "./components/Footer.jsx";
13 | import SpotifyPlayer from "./components/SpotifyPlayer";
14 | import About from "./components/About";
15 | import './App.css';
16 |
17 | // import AboutPage from './pages/AboutPage'; // Import About page
18 | // import HistoryPage from './pages/HistoryPage'; // Import History page
19 | // import SettingsPage from './pages/SettingsPage'; // Import Settings page
20 | // import ThemesPage from './pages/ThemesPage'; // Import Themes page
21 | // import Footer from './Footer'; // Import the Footer component
22 | import Template from "./components/Auth/Template";
23 | import WorkTracker from "./pages/WorkTracker";
24 | import TermsPage from "./pages/TermsPage";
25 | import Error404 from "./pages/Error404";
26 | import PasswordRecovery from "./components/Auth/PasswordRecovery";
27 | import Todo from "./pages/Todo";
28 |
29 | const App = () => {
30 | return (
31 |
32 |
33 | {/* Particles design will be displayed globally */}
34 |
35 |
36 |
37 | } />
38 | } />
39 | {/* } /> */}
40 | } />
41 | } />
42 | } />
43 | } />
44 | } />
45 | } />
46 | } />
47 | } />
48 | } />
49 | } />
50 | } />
51 | {/* } /> */}
52 | } />
53 | } />
54 | } />
55 | } />
56 | } />
57 | }/>
58 | }
61 | />{" "}
62 | {/* Add PasswordRecovery route */}
63 | } />{" "}
64 | {/* Add NotFoundPage route */}
65 | } />
66 | } />
67 | } />
68 | } />
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | // Define the new sections at the bottom of App.jsx
77 |
78 | /*const AboutPage = () => {
79 | return (
80 |
81 |
About Page
82 |
This is the About section of the Counter App, explaining its features and purpose.
83 |
84 | );
85 | };*/
86 |
87 | /*const HistoryPage = () => {
88 | return (
89 |
90 |
History Page
91 |
This is the History section of the Counter App where you can review your past counter sessions.
92 |
93 | );
94 | };*/
95 |
96 | /*const SettingsPage = () => {
97 | return (
98 |
99 |
Settings Page
100 |
This is the Settings section of the Counter App where you can configure app settings like notifications and time zones.
101 |
102 | );
103 | };*/
104 |
105 | /*const ThemesPage = () => {
106 | return (
107 |
108 |
Themes Page
109 |
This is the Themes section of the Counter App where you can switch between light and dark modes.
110 |
111 | );
112 | };*/
113 |
114 | // Define the NotFoundPage component to display when no routes match
115 | const NotFoundPage = () => {
116 | return (
117 |
118 |
404 - Page Not Found
119 |
Oops! The page you're looking for doesn't exist.
120 |
121 | );
122 | };
123 |
124 | } />;
125 |
126 | } />;
127 |
128 | export default App;
129 |
--------------------------------------------------------------------------------
/src/assets/afternoonBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/afternoonBackground.png
--------------------------------------------------------------------------------
/src/assets/afternoonBackground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/afternoonBackground.webp
--------------------------------------------------------------------------------
/src/assets/clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/clock.png
--------------------------------------------------------------------------------
/src/assets/clock.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/clock.webp
--------------------------------------------------------------------------------
/src/assets/clock_img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/clock_img.png
--------------------------------------------------------------------------------
/src/assets/clock_img.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/clock_img.webp
--------------------------------------------------------------------------------
/src/assets/counter-app-video-demo2 (1) (2).mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/counter-app-video-demo2 (1) (2).mp4
--------------------------------------------------------------------------------
/src/assets/dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/dark.jpg
--------------------------------------------------------------------------------
/src/assets/dark.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/dark.webp
--------------------------------------------------------------------------------
/src/assets/darklogo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/darklogo1.png
--------------------------------------------------------------------------------
/src/assets/darklogo1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/darklogo1.webp
--------------------------------------------------------------------------------
/src/assets/image_with_black_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/image_with_black_background.png
--------------------------------------------------------------------------------
/src/assets/image_with_black_background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/image_with_black_background.webp
--------------------------------------------------------------------------------
/src/assets/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/login.png
--------------------------------------------------------------------------------
/src/assets/login.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/login.webp
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/logo.webp
--------------------------------------------------------------------------------
/src/assets/logo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/logo1.png
--------------------------------------------------------------------------------
/src/assets/logo1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/logo1.webp
--------------------------------------------------------------------------------
/src/assets/morningBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/morningBackground.png
--------------------------------------------------------------------------------
/src/assets/morningBackground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/morningBackground.webp
--------------------------------------------------------------------------------
/src/assets/nightBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/nightBackground.png
--------------------------------------------------------------------------------
/src/assets/nightBackground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/nightBackground.webp
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/signup.png
--------------------------------------------------------------------------------
/src/assets/signup.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/signup.webp
--------------------------------------------------------------------------------
/src/assets/timer1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/timer1.png
--------------------------------------------------------------------------------
/src/assets/timer1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/assets/timer1.webp
--------------------------------------------------------------------------------
/src/components/Auth/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai";
3 | import { useDispatch } from "react-redux";
4 | import { useNavigate } from "react-router-dom";
5 | import { loginValidation } from "@/validations/validation";
6 | import { login } from "../../services/operations/authAPI";
7 |
8 | function LoginForm() {
9 | const navigate = useNavigate();
10 | const dispatch = useDispatch();
11 | const [email, setEmail] = useState("");
12 | const [password, setPassword] = useState("");
13 | const [errors, setErrors] = useState({});
14 | const [showPassword, setShowPassword] = useState(false);
15 |
16 | const handleOnSubmit = async (e) => {
17 | e.preventDefault();
18 |
19 | try {
20 | await loginValidation.validate(
21 | { email, password },
22 | { abortEarly: false }
23 | );
24 | setErrors({});
25 | } catch (error) {
26 | const newErrors = {};
27 | error.inner.forEach((err) => {
28 | newErrors[err.path] = err.message;
29 | });
30 | setErrors(newErrors);
31 | return;
32 | }
33 |
34 | dispatch(login(email, password, navigate));
35 | };
36 |
37 | const handlePasswordRecovery = () => {
38 | navigate("/password-recovery");
39 | };
40 |
41 | const togglePasswordVisibility = useCallback(() => {
42 | setShowPassword((prev) => !prev);
43 | }, []);
44 |
45 | return (
46 |
131 | );
132 | }
133 |
134 | export default LoginForm;
135 |
--------------------------------------------------------------------------------
/src/components/Auth/LoginImage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { motion } from "framer-motion";
3 |
4 | const LoginImage = () => {
5 | const [transform, setTransform] = useState({ rotateX: 0, rotateY: 0 });
6 |
7 | const handleMouseMove = (e) => {
8 | const rect = e.currentTarget.getBoundingClientRect();
9 | const xPos = (e.clientX - rect.left) / rect.width - 0.5;
10 | const yPos = (e.clientY - rect.top) / rect.height - 0.5;
11 |
12 | setTransform({
13 | rotateX: yPos * 20,
14 | rotateY: -xPos * 20,
15 | });
16 | };
17 |
18 | const handleMouseLeave = () => {
19 | setTransform({ rotateX: 0, rotateY: 0 });
20 | };
21 |
22 | return (
23 |
33 |
51 |
52 | );
53 | };
54 |
55 | export default LoginImage;
56 |
--------------------------------------------------------------------------------
/src/components/Auth/PasswordRecovery.jsx:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import { useState } from "react";
4 | import { useNavigate } from "react-router-dom";
5 | import { useDispatch } from "react-redux";
6 | import { passwordRecovery } from "../../services/operations/authAPI"; // Correct import
7 | import { FaEye, FaEyeSlash } from "react-icons/fa"; // Import icons
8 |
9 | const PasswordRecovery = () => {
10 | const navigate = useNavigate();
11 | const dispatch = useDispatch();
12 | const [email, setEmail] = useState("");
13 | const [message, setMessage] = useState("");
14 |
15 | const handleOnSubmit = async (e) => {
16 | e.preventDefault();
17 | try {
18 | await dispatch(passwordRecovery(email));
19 | setMessage(
20 | "Password recovery instructions have been sent to your email."
21 | );
22 | } catch (error) {
23 | setMessage("Error sending password recovery instructions.");
24 | }
25 | };
26 |
27 | // Tile-style glassmorphism effect
28 | const glassTileStyle = {
29 | background: "rgba(255, 255, 255, 0.1)", // More transparent glassy effect
30 | borderRadius: "20px", // Increased rounded corners for tile effect
31 | boxShadow: "0 8px 30px rgba(0, 0, 0, 0.2)", // Tile-like shadow
32 | backdropFilter: "blur(30px)", // Increased blur for glass effect
33 | border: "1px solid rgba(255, 255, 255, 0.3)", // Optional: border to enhance tile
34 | padding: "2rem", // Padding inside the form
35 | maxWidth: "420px", // Fixed max width to simulate a tile size
36 | width: "100%",
37 | margin: "0 auto", // Center the form horizontally
38 | };
39 |
40 | return (
41 |
42 |
43 | Password Recovery
44 |
45 |
46 |
95 |
96 | );
97 | };
98 |
99 | export default PasswordRecovery;
100 |
--------------------------------------------------------------------------------
/src/components/Auth/Review.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | const Review = () => {
4 | const [rating, setRating] = useState(0);
5 | const [hover, setHover] = useState(0);
6 | const [comment, setComment] = useState('');
7 | const [visible, setVisible] = useState(false);
8 |
9 | // Show the component after 15 seconds
10 | useEffect(() => {
11 | const timer = setTimeout(() => {
12 | setVisible(true);
13 | }, 15000);
14 | return () => clearTimeout(timer);
15 | }, []);
16 |
17 | // Function to handle comment change
18 | const handleCommentChange = (e) => {
19 | setComment(e.target.value);
20 | };
21 |
22 | // Function to handle form submit
23 | const handleSubmit = (e) => {
24 | e.preventDefault();
25 |
26 | // Check if a rating is selected and a comment is provided
27 | if (rating === 0 || comment.trim() === '') {
28 | alert('Please provide both a rating and a comment before submitting.');
29 | return;
30 | }
31 | alert(`We value your feedback. Thank you for rating us!`);
32 |
33 | // Clear the comment and rating after submission
34 | setComment('');
35 | setRating(0);
36 |
37 | // Hide the review component after submission
38 | setVisible(false);
39 | };
40 |
41 | return (
42 | <>
43 | {visible && (
44 |
45 | {/* Background overlay */}
46 |
47 |
48 | {/* Popup content */}
49 |
50 | {/* Close button */}
51 |
setVisible(false)}
54 | aria-label="Close"
55 | >
56 | ✕
57 |
58 |
59 |
60 |
RATE US!
61 |
Please take a second to review our services!
62 |
63 | {/* Star Rating */}
64 |
65 | {[...Array(5)].map((_, index) => {
66 | const starValue = index + 1;
67 | return (
68 | setRating(starValue)}
74 | onMouseEnter={() => setHover(starValue)}
75 | onMouseLeave={() => setHover(0)}
76 | style={{
77 | marginRight: index < 4 ? '2px' : '0',
78 | padding: '0',
79 | }}
80 | >
81 | ★
82 |
83 | );
84 | })}
85 |
86 |
87 | {/* Comment Box */}
88 |
105 |
106 |
107 | )}
108 | >
109 | );
110 | };
111 |
112 | export default Review;
113 |
--------------------------------------------------------------------------------
/src/components/Auth/SignupImage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { motion } from "framer-motion";
3 |
4 | const SignupImage = () => {
5 | const [transform, setTransform] = useState({ rotateX: 0, rotateY: 0 });
6 |
7 | const handleMouseMove = (e) => {
8 | const rect = e.currentTarget.getBoundingClientRect();
9 | const xPos = (e.clientX - rect.left) / rect.width - 0.5;
10 | const yPos = (e.clientY - rect.top) / rect.height - 0.5;
11 |
12 | setTransform({
13 | rotateX: yPos * 20,
14 | rotateY: -xPos * 20,
15 | });
16 | };
17 |
18 | const handleMouseLeave = () => {
19 | setTransform({ rotateX: 0, rotateY: 0 });
20 | };
21 |
22 | return (
23 |
33 |
51 |
52 | );
53 | };
54 |
55 | export default SignupImage;
56 |
--------------------------------------------------------------------------------
/src/components/Auth/Template.jsx:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import { useSelector } from "react-redux";
4 | import LoginForm from "./LoginForm";
5 | import SignupForm from "./SignupForm";
6 | import { FaArrowCircleLeft } from "react-icons/fa";
7 | import { useNavigate } from "react-router-dom";
8 | import SignupImage from "./SignupImage";
9 | import LoginImage from "./LoginImage";
10 |
11 | function Template({ formType }) {
12 | const { loading } = useSelector((state) => state.auth);
13 | const navigate = useNavigate();
14 |
15 | return (
16 |
17 |
18 | {loading ? (
19 |
20 | ) : (
21 |
22 |
navigate("/")}
25 | >
26 | navigate("/")}
29 | />
30 |
31 |
32 | {/* Conditionally reverse flex direction for login */}
33 |
38 |
39 |
40 | {formType === "signup" ? "Signup" : "Login"}
41 |
42 |
43 |
44 | {formType === "signup"
45 | ? "Welcome to our platform"
46 | : "Welcome back"}
47 |
48 |
49 | {formType === "signup" ?
:
}
50 |
51 |
52 | {formType === "signup" ? : }
53 |
54 |
55 |
56 |
57 | )}
58 |
59 |
60 | );
61 | }
62 |
63 | export default Template;
64 |
--------------------------------------------------------------------------------
/src/components/CumulativeTimeChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | LineChart,
4 | Line,
5 | XAxis,
6 | YAxis,
7 | Tooltip,
8 | CartesianGrid,
9 | ResponsiveContainer,
10 | } from 'recharts';
11 |
12 | const CumulativeTimeChart = ({ laps, formatTime }) => {
13 | // Check if laps is an array and not empty
14 | if (!Array.isArray(laps) || laps.length === 0) {
15 | return No lap data available.
; // Handle empty laps
16 | }
17 |
18 | // Calculate cumulative times
19 | const cumulativeData = laps.reduce((acc, lap, index) => {
20 | const cumulativeTime = (acc[index - 1]?.cumulativeTime || 0) + lap; // Add current lap to the previous cumulative time
21 | acc.push({
22 | lapNumber: index + 1,
23 | cumulativeTime,
24 | });
25 | return acc;
26 | }, []);
27 |
28 | return (
29 |
30 |
Progress Graph
31 |
32 |
33 |
34 |
35 | formatTime(value)} />
36 | [formatTime(value), 'Cumulative Time']} />
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default CumulativeTimeChart;
45 |
--------------------------------------------------------------------------------
/src/components/Design.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import Particles from "@tsparticles/react";
3 | import { loadSlim } from "@tsparticles/slim"; // Loading only necessary features
4 |
5 | const Design = ({ theme }) => {
6 | const [init, setInit] = useState(false);
7 |
8 | useEffect(() => {
9 | // Initializing the particles engine only once
10 | const initParticlesEngine = async (engine) => {
11 | await loadSlim(engine);
12 | setInit(true);
13 | };
14 |
15 | initParticlesEngine(Particles.engine);
16 | }, []);
17 |
18 | const particlesLoaded = (container) => {
19 | console.log(container);
20 | };
21 |
22 | return (
23 | <>
24 | {init && (
25 |
98 | )}
99 | >
100 | );
101 | };
102 |
103 | export default Design;
104 |
--------------------------------------------------------------------------------
/src/components/GoogleTranslate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import styled from "styled-components";
3 |
4 | const TranslateContainer = styled.div`
5 | /* added z-index here*/
6 | position:relative;
7 | z-index:1000;
8 | .goog-te-combo {
9 | display: inline-block;
10 | background-color: #e0f2ff;
11 | border: 2px solid #0056b3;
12 | border-radius: 0.5rem;
13 | padding: 0.5rem 1rem;
14 | font-size: 0.875rem;
15 | transition: all 0.3s ease;
16 | outline: none;
17 | color: #000;
18 | font-weight: 500;
19 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
20 | }
21 |
22 | .goog-te-combo:hover {
23 | background-color: #b3e0ff;
24 | border-color: #004494;
25 | box-shadow: 0 6px 8px rgba(0, 0, 0, 0.25);
26 | }
27 |
28 | .goog-logo-link {
29 | display: none !important;
30 | }
31 |
32 | .goog-te-gadget {
33 | color: transparent !important;
34 | }
35 |
36 | .goog-te-gadget > span > a {
37 | display: none !important;
38 | }
39 |
40 | .goog-te-gadget .goog-te-combo {
41 | color: #0056b3 !important;
42 | }
43 |
44 | #google_translate_element .goog-te-gadget-simple .goog-te-menu-value span:first-child {
45 | display: none;
46 | }
47 |
48 | #google_translate_element .goog-te-gadget-simple .goog-te-menu-value:before {
49 | content: 'Translate';
50 | color: #0056b3;
51 | }
52 |
53 | .goog-te-banner-frame {
54 | display: none !important;
55 | }
56 |
57 | .goog-te-menu-frame {
58 | max-height: 400px !important;
59 | overflow-y: auto !important;
60 | background-color: #fff;
61 | border: 1px solid #cce5ff;
62 | border-radius: 0.5rem;
63 | }
64 |
65 | .skiptranslate > iframe {
66 | height: 0 !important;
67 | border-style: none;
68 | box-shadow: none;
69 | }
70 | `;
71 |
72 | const GoogleTranslate = () => {
73 | useEffect(() => {
74 | window.googleTranslateInit = () => {
75 | if (!window.google?.translate?.TranslateElement) {
76 | setTimeout(window.googleTranslateInit, 100);
77 | } else {
78 | new window.google.translate.TranslateElement(
79 | {
80 | pageLanguage: "en",
81 | includedLanguages:
82 | "en,hi,pa,sa,mr,ur,bn,es,ja,ko,zh-CN,es,nl,fr,de,it,ta,te",
83 | layout: window.google.translate.TranslateElement.InlineLayout.HORIZONTAL,
84 | defaultLanguage: "en",
85 | autoDisplay: false,
86 | },
87 | "google_element"
88 | );
89 | }
90 | };
91 |
92 | const loadGoogleTranslateScript = () => {
93 | if (!document.getElementById("google_translate_script")) {
94 | const script = document.createElement("script");
95 | script.type = "text/javascript";
96 | script.src =
97 | "https://translate.google.com/translate_a/element.js?cb=googleTranslateInit";
98 | script.id = "google_translate_script";
99 | script.onerror = () => console.error("Error loading Google Translate script");
100 | document.body.appendChild(script);
101 | }
102 | };
103 |
104 | loadGoogleTranslateScript();
105 |
106 | if (window.google && window.google.translate) {
107 | window.googleTranslateInit();
108 | }
109 |
110 | return () => {
111 | // Cleanup logic if necessary
112 | };
113 | }, []);
114 |
115 | return (
116 |
117 | {/* Google Translate Element will be inserted here */}
118 |
119 | );
120 | };
121 |
122 | export default GoogleTranslate;
123 |
--------------------------------------------------------------------------------
/src/components/LapAnalysis.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LapAnalysis = ({ laps, formatTime }) => {
4 | if (laps.length === 0) return null;
5 |
6 | // Use the laps array directly, as it already contains lap times
7 | const lapTimes = laps;
8 |
9 | // Calculate average, fastest, and slowest lap times
10 | const averageLapTime = lapTimes.reduce((sum, time) => sum + time, 0) / lapTimes.length;
11 | const fastestLap = Math.min(...lapTimes);
12 | const slowestLap = Math.max(...lapTimes);
13 |
14 | return (
15 |
16 |
17 |
Average Lap Time
18 |
{formatTime(Math.round(averageLapTime))}
19 |
20 |
21 |
Fastest Lap
22 |
{formatTime(fastestLap)}
23 |
24 |
25 |
Slowest Lap
26 |
{formatTime(slowestLap)}
27 |
28 |
29 | );
30 | };
31 |
32 | export default LapAnalysis;
33 |
--------------------------------------------------------------------------------
/src/components/LapBarChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BarChart,
4 | Bar,
5 | XAxis,
6 | YAxis,
7 | Tooltip,
8 | CartesianGrid,
9 | ResponsiveContainer,
10 | Cell
11 | } from 'recharts';
12 |
13 | const BarChartLapTimes = ({ laps, formatTime }) => {
14 | // Function to get color based on lap index
15 | const getLapColor = (index) => {
16 | const colors = ['#82ca9d', '#ff7f50', '#6495ed', '#da70d6', '#ffd700', '#6a5acd'];
17 | return colors[index % colors.length]; // Cycle through colors
18 | };
19 |
20 | // Prepare the data for the chart
21 | const formattedData = laps.map((lap, index) => ({
22 | lapNumber: index + 1,
23 | lapTime: lap,
24 | color: getLapColor(index), // Add color property
25 | }));
26 |
27 | return (
28 |
29 |
Bar Graph
30 |
31 |
32 |
33 |
34 |
35 | [formatTime(value), 'Lap Time']} />
36 |
37 | {formattedData.map((entry, index) => (
38 | | // Use dynamic color
39 | ))}
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default BarChartLapTimes;
48 |
--------------------------------------------------------------------------------
/src/components/LapPieChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts';
3 |
4 | const LapTimePieChart = ({ laps, formatTime }) => {
5 | // Calculate total lap time
6 | const totalLapTime = laps.reduce((acc, lap) => acc + lap, 0);
7 |
8 | // Create data for the pie chart
9 | const data = laps.map((lap, index) => ({
10 | name: `Lap ${index + 1}`,
11 | value: lap,
12 | }));
13 | const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#FF5555'];
14 |
15 | return (
16 |
17 |
Pie Chart
18 |
19 | `${name}: ${formatTime(value)}`}
25 | outerRadius={80}
26 | fill="#8884d8"
27 | dataKey="value"
28 | >
29 | {data.map((entry, index) => (
30 | |
31 | ))}
32 |
33 | [formatTime(value), 'Lap Time']} />
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default LapTimePieChart;
41 |
--------------------------------------------------------------------------------
/src/components/LapVisualization.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LapVisualization = ({ laps, formatTime }) => {
4 | const maxLap = Math.max(...laps);
5 |
6 | return (
7 |
8 |
Lap Visualization
9 |
10 | {laps.map((lap, index) => (
11 |
12 |
#{index + 1}
13 |
19 |
{formatTime(lap)}
20 |
21 | ))}
22 |
23 |
24 | );
25 | };
26 |
27 | export default LapVisualization;
28 |
--------------------------------------------------------------------------------
/src/components/ParticlesComponent.jsx:
--------------------------------------------------------------------------------
1 | // ParticlesComponent.js
2 | import React from "react";
3 | import Particles from "@tsparticles/react";
4 | import { loadSlim } from "@tsparticles/slim";
5 |
6 | const ParticlesComponent = React.memo(({ particleOptions, particlesLoaded }) => {
7 | return (
8 |
13 | );
14 | });
15 |
16 | export default ParticlesComponent;
17 |
--------------------------------------------------------------------------------
/src/components/SpotifyPlayer.css:
--------------------------------------------------------------------------------
1 | .spotify-player-container {
2 | position: fixed;
3 | right: 0;
4 | top: 50%;
5 | transform: translateY(-50%);
6 | z-index: 1000;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: flex-end;
10 | }
11 |
12 | .spotify-icon {
13 | background: black;
14 | color: white;
15 | padding: 10px;
16 | border-radius: 50%;
17 | cursor: pointer;
18 | margin-bottom: 10px;
19 | }
20 |
21 | .spotify-icon img {
22 | width: 40px;
23 | height: 40px;
24 | }
25 |
26 | .spotify-iframe {
27 | border-radius: 12px;
28 | width: 300px;
29 | height: 380px;
30 | transition: transform 0.3s ease;
31 | transform: translateX(100%);
32 | overflow: hidden;
33 | }
34 |
35 | .spotify-iframe.visible {
36 | transform: translateX(0);
37 | }
38 |
39 | @media (max-width: 900px) {
40 | .spotify-player-container{
41 | top: 35%;
42 | }
43 | .spotify-iframe {
44 | width: 250px;
45 | height: 300px;
46 | }
47 | }
48 |
49 | @media (max-width: 480px) {
50 | .spotify-iframe {
51 | width: 100%;
52 | height: 200px;
53 | max-width: 300px;
54 | }
55 |
56 | .spotify-icon {
57 | padding: 8px;
58 | }
59 |
60 | .spotify-icon img {
61 | width: 30px;
62 | height: 30px;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/SpotifyPlayer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import './SpotifyPlayer.css';
3 |
4 | const SpotifyPlayer = () => {
5 | const [isHovered, setIsHovered] = useState(false);
6 |
7 | return (
8 |
9 | {/* Spotify icon with hover events */}
10 |
setIsHovered(true)}
13 | onMouseLeave={() => setIsHovered(false)}
14 | >
15 |
19 |
20 |
21 | {/* Spotify player iframe with hover events */}
22 |
31 |
32 | );
33 | };
34 |
35 | export default SpotifyPlayer;
36 |
--------------------------------------------------------------------------------
/src/components/SwitchTab.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useNavigate } from "react-router-dom"; // Import useNavigate
3 |
4 | const SwitchTab = () => {
5 | const navigate = useNavigate(); // Initialize the navigate function
6 |
7 | const handleSwitch = () => {
8 | navigate("/"); // Navigate to the AutoCounter page
9 | };
10 |
11 | return (
12 |
13 | Switch to AutoCounter Tab
14 |
15 | );
16 | };
17 |
18 | export default SwitchTab;
19 |
--------------------------------------------------------------------------------
/src/components/audio/alert-85101.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/param-code/counter-app/9e06090d67aa9c3c943cae95cf6c777bd52d45ec/src/components/audio/alert-85101.mp3
--------------------------------------------------------------------------------
/src/components/css/Footer.css:
--------------------------------------------------------------------------------
1 | .clock-icon {
2 | width: 96px; /* Adjusting based on w-24 */
3 | height: 100px; /* Adjusting based on h-15 */
4 | margin-bottom: -8px; /* For -mb-2 */
5 | transition: transform 0.3s ease, box-shadow 0.3s ease;
6 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Initial shadow */
7 | }
8 |
9 | .clock-icon:hover {
10 | transform: translateY(-10px) rotateX(15deg) rotateY(15deg); /* 3D effect */
11 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* Increased shadow for depth */
12 | }
13 | ul li a {
14 | position: relative;
15 | text-decoration: none;
16 | color: #gray-400 ; /* Default text color */
17 | transition: color 0.3s ease; /* Smooth color transition */
18 | }
19 |
20 | ul li a::after {
21 | content: '';
22 | position: absolute;
23 | left: 0;
24 | bottom: -3px; /* Distance from the text */
25 | width: 0;
26 | height: 2px; /* Thickness of the underline */
27 | background-color: white; /* Color of the underline */
28 | transition: width 0.3s ease; /* Smooth width transition */
29 | }
30 |
31 | ul li a:hover::after {
32 | width: 100%; /* Expand underline on hover */
33 | }
34 |
35 | ul li a:hover {
36 | color: white; /* Change text color on hover */
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/css/navbar.css:
--------------------------------------------------------------------------------
1 | .butt{
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 | .page{
7 | transition: all 0.2s ease-in-out;
8 | }
9 | .page:hover{
10 | transform: scaleX(1.1);
11 | }
12 | .pages{
13 | transition: all 0.2s ease-in-out;
14 | }
15 | .pages:hover{
16 | /* transform: scale(1.1); */
17 | animation: jump-shaking 0.83s infinite;
18 | }
19 |
20 | /* span.rise-shake {
21 | animation: jump-shaking 0.83s infinite;
22 | } */
23 |
24 | @keyframes jump-shaking {
25 | 0% { transform: translateX(0) }
26 | 25% { transform: translateY(-5px) }
27 | 35% { transform: translateY(-5px) rotate(17deg) }
28 | 55% { transform: translateY(-5px) rotate(-17deg) }
29 | 65% { transform: translateY(-5px) rotate(17deg) }
30 | 75% { transform: translateY(-5px) rotate(-17deg) }
31 | 100% { transform: translateY(0) rotate(0) }
32 | }
--------------------------------------------------------------------------------
/src/components/laplist.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { memo, useEffect } from 'react'
4 | import { ScrollArea } from './ui/scroll-area'
5 | import { motion, AnimatePresence } from 'framer-motion'
6 |
7 | const LapList = ({ laps, formatTime, lapsEndRef }) => {
8 | // Auto-scroll to the latest lap
9 | useEffect(() => {
10 | if (lapsEndRef?.current) {
11 | lapsEndRef.current.scrollIntoView({ behavior: 'smooth' })
12 | }
13 | }, [laps, lapsEndRef])
14 |
15 | return (
16 |
17 |
Laps
18 |
19 |
20 |
21 | {laps.map((lap, index) => (
22 |
30 | Lap {index + 1}
31 | {formatTime(lap)}
32 |
33 | ))}
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default LapList
42 |
--------------------------------------------------------------------------------
/src/components/pop-up.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 |
4 | const PopupNotification = ({ message, onClose }) => {
5 | return (
6 |
7 |
{message}
8 |
12 | Got It !
13 |
14 |
15 | );
16 | };
17 |
18 | export default PopupNotification;
19 |
--------------------------------------------------------------------------------
/src/components/timer.jsx:
--------------------------------------------------------------------------------
1 |
2 | const Timer = ({ formattedTime }) => {
3 | return (
4 |
5 |
6 | {formattedTime}
7 |
8 |
9 | )
10 | }
11 |
12 | export default Timer
13 |
--------------------------------------------------------------------------------
/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 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
37 | const Comp = asChild ? Slot : "button"
38 | return (
39 | ( )
43 | );
44 | })
45 | Button.displayName = "Button"
46 |
47 | export { Button, buttonVariants }
48 |
--------------------------------------------------------------------------------
/src/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef((props, ref) => {
6 | const { className, ...rest } = props;
7 | return (
8 |
13 | );
14 | });
15 | Card.displayName = "Card";
16 |
17 | const CardHeader = React.forwardRef((props, ref) => {
18 | const { className, ...rest } = props;
19 | return (
20 |
25 | );
26 | });
27 | CardHeader.displayName = "CardHeader";
28 |
29 | const CardTitle = React.forwardRef((props, ref) => {
30 | const { className, ...rest } = props;
31 | return (
32 |
37 | );
38 | });
39 | CardTitle.displayName = "CardTitle";
40 |
41 | const CardDescription = React.forwardRef((props, ref) => {
42 | const { className, ...rest } = props;
43 | return (
44 |
49 | );
50 | });
51 | CardDescription.displayName = "CardDescription";
52 |
53 | const CardContent = React.forwardRef((props, ref) => {
54 | const { className, ...rest } = props;
55 | return (
56 |
57 | );
58 | });
59 | CardContent.displayName = "CardContent";
60 |
61 | const CardFooter = React.forwardRef((props, ref) => {
62 | const { className, ...rest } = props;
63 | return (
64 |
69 | );
70 | });
71 | CardFooter.displayName = "CardFooter";
72 |
73 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
74 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
7 |
11 |
12 | {children}
13 |
14 |
15 |
16 |
17 | ))
18 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
19 |
20 | const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
21 |
33 |
34 |
35 | ))
36 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
37 |
38 | export { ScrollArea, ScrollBar }
39 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | /* body {
6 | background-image: url('/bg.jpg');
7 | background-size: cover;
8 | background-position: center;
9 | background-repeat: no-repeat;
10 | } */
11 |
12 |
13 | .typewriter {
14 | overflow: hidden;
15 | border-right: .15em solid orange;
16 | white-space: nowrap;
17 | margin: 0 auto;
18 | letter-spacing:.15em;
19 | animation:
20 | typing 3.5s steps(40, end),
21 | blink-caret .75s step-end infinite;
22 | }
23 |
24 | @keyframes typing {
25 | from { width: 0 }
26 | to { width: 100% }
27 | }
28 |
29 | @keyframes blink-caret {
30 | from, to { border-color: transparent }
31 | 50% { border-color: orange; }
32 | }
33 |
34 | @layer base {
35 | :root {
36 | --background: 0 0% 100%;
37 | --foreground: 0 0% 3.9%;
38 | --card: 0 0% 100%;
39 | --card-foreground: 0 0% 3.9%;
40 | --popover: 0 0% 100%;
41 | --popover-foreground: 0 0% 3.9%;
42 | --primary: 0 0% 9%;
43 | --primary-foreground: 0 0% 98%;
44 | --secondary: 0 0% 96.1%;
45 | --secondary-foreground: 0 0% 9%;
46 | --muted: 0 0% 96.1%;
47 | --muted-foreground: 0 0% 45.1%;
48 | --accent: 0 0% 96.1%;
49 | --accent-foreground: 0 0% 9%;
50 | --destructive: 0 84.2% 60.2%;
51 | --destructive-foreground: 0 0% 98%;
52 | --border: 0 0% 89.8%;
53 | --input: 0 0% 89.8%;
54 | --ring: 0 0% 3.9%;
55 | --chart-1: 12 76% 61%;
56 | --chart-2: 173 58% 39%;
57 | --chart-3: 197 37% 24%;
58 | --chart-4: 43 74% 66%;
59 | --chart-5: 27 87% 67%;
60 | --radius: 0.5rem;
61 | }
62 | .dark {
63 | --background: 0 0% 3.9%;
64 | --foreground: 0 0% 98%;
65 | --card: 0 0% 3.9%;
66 | --card-foreground: 0 0% 98%;
67 | --popover: 0 0% 3.9%;
68 | --popover-foreground: 0 0% 98%;
69 | --primary: 0 0% 98%;
70 | --primary-foreground: 0 0% 9%;
71 | --secondary: 0 0% 14.9%;
72 | --secondary-foreground: 0 0% 98%;
73 | --muted: 0 0% 14.9%;
74 | --muted-foreground: 0 0% 63.9%;
75 | --accent: 0 0% 14.9%;
76 | --accent-foreground: 0 0% 98%;
77 | --destructive: 0 62.8% 30.6%;
78 | --destructive-foreground: 0 0% 98%;
79 | --border: 0 0% 14.9%;
80 | --input: 0 0% 14.9%;
81 | --ring: 0 0% 83.1%;
82 | --chart-1: 220 70% 50%;
83 | --chart-2: 160 60% 45%;
84 | --chart-3: 30 80% 55%;
85 | --chart-4: 280 65% 60%;
86 | --chart-5: 340 75% 55%;
87 | }
88 | }
89 |
90 | @layer base {
91 | * {
92 | @apply border-border;
93 | }
94 | body {
95 | @apply bg-background text-foreground;
96 | }
97 | }
98 | @media screen and (max-width: 778px) {
99 | .container {
100 | flex-direction: column;
101 | align-items: center; /* Center the items vertically */
102 | text-align: center; /* Center align text */
103 | }
104 |
105 | .googletranslate {
106 | width: 100%;
107 | margin-top: 8px;
108 | display: flex;
109 | justify-content: center;
110 | }
111 |
112 | .social-icons {
113 | justify-content: center;
114 | margin-top: 8px;
115 | }
116 |
117 | .text-sm {
118 | margin: 0;
119 | }
120 | }
--------------------------------------------------------------------------------
/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 App from './App.jsx'
4 | import './index.css'
5 |
6 | import { Provider } from "react-redux";
7 | import rootReducer from "./reducer";
8 | import { configureStore } from "@reduxjs/toolkit";
9 | import { Toaster } from 'react-hot-toast';
10 | import Review from './components/Auth/Review.jsx';
11 |
12 | const store = configureStore({
13 | reducer: rootReducer,
14 | });
15 |
16 |
17 | createRoot(document.getElementById("root")).render(
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
27 |
--------------------------------------------------------------------------------
/src/pages/Contributors.css:
--------------------------------------------------------------------------------
1 | .contributors-container {
2 | width: 100%;
3 | height: 100%;
4 | padding-top: 2rem;
5 | overflow: hidden;
6 | transition: background 0.3s ease-in-out; /* Add transition for smooth background change */
7 | }
8 |
9 | .github-icon {
10 | margin-right: 0.5rem;
11 | vertical-align: middle;
12 | fill: white; /* Adjust color as needed */
13 | }
14 |
15 | .contributors-title {
16 | margin-top: 0rem;
17 | text-align: center;
18 | color: #0D9488;
19 | font-size: 2.5rem;
20 | font-weight: bold;
21 | margin-bottom: 2rem;
22 | text-transform: uppercase;
23 | }
24 |
25 | .contributors-grid {
26 | display: flex;
27 | flex-wrap: wrap;
28 | justify-content: center;
29 | gap: 2rem;
30 | margin-bottom: 4rem;
31 | }
32 |
33 | .contributor-card {
34 | position: relative;
35 | width: 100%;
36 | max-width: 25%;
37 | display: flex;
38 | flex-direction: column;
39 | align-items: center;
40 | background-color: #0D9488;
41 | border: 1px solid #004d46;
42 | border-radius: 0.5rem;
43 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
44 | padding: 1rem;
45 | overflow: hidden;
46 | transition: transform 0.4s ease, box-shadow 0.3s ease;
47 | }
48 |
49 | .contributor-card:hover {
50 | transform: scale(1.05);
51 | box-shadow: 0 4px 6px rgba(13, 148, 136, 0.752);
52 | }
53 |
54 | .contributor-card:hover p {
55 | text-shadow: 1px 1px 2px rgb(13, 148, 136), 0 0 0.2em rgb(0, 191, 255), 0 0 0.8em rgb(135, 206, 235);
56 | color: rgb(0, 0, 0);
57 | font-weight: 500;
58 | }
59 |
60 | .contributor-card:hover h2 {
61 | text-shadow: 1px 1px 2px rgba(237, 9, 176, 0.926), 0 0 0.2em rgb(0, 191, 255), 0 0 0.8em rgb(135, 206, 235);
62 | color: white;
63 | font-size: 1.04rem;
64 | font-weight: 600;
65 | text-decoration: wavy;
66 | }
67 |
68 | .contributor-card:hover .contributor-avatar {
69 | border: 3.5px solid #89e6f0;
70 | width: 5.2rem;
71 | height: 5.2rem;
72 | box-shadow: -2px 4px 10px 1px rgba(1, 41, 218, 0.75);
73 | }
74 |
75 | .contributor-card::before {
76 | content: "";
77 | position: absolute;
78 | top: 0;
79 | left: 0;
80 | width: 100%;
81 | height: 100%;
82 | background: linear-gradient(152deg, #01988c 50%, #004d46 50%);
83 | transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
84 | transform: translate(-100%, -100%);
85 | opacity: 0;
86 | z-index: -1;
87 | }
88 |
89 | .contributor-card:hover::before {
90 | transform: translate(0, 0);
91 | opacity: 1;
92 | }
93 |
94 | .contributor-link {
95 | display: block;
96 | }
97 |
98 | .contributor-avatar {
99 | width: 5rem;
100 | height: 5rem;
101 | border-radius: 50%;
102 | object-fit: cover;
103 | margin-bottom: 1rem;
104 | border: 2px solid #00b4d8;
105 | transition: border 0.4s ease-in-out, height 0.4s ease-in-out, width 0.4s ease-in-out, box-shadow 0.3s ease-in-out;
106 | }
107 |
108 | .contributor-name {
109 | font-size: 1rem;
110 | font-weight: 600;
111 | color: #f3f4f6;
112 | margin-bottom: 0.5rem;
113 | transition: text-shadow 0.4s ease-in-out, font-size 0.5s ease-in-out, text-decoration 0.4s ease-in-out;
114 | }
115 |
116 | .contributor-contributions {
117 | color: #d1d5db;
118 | transition: text-shadow 0.4s ease-in-out;
119 | }
120 |
121 | @media (max-width: 1200px) {
122 | .contributor-card {
123 | max-width: 33.333%;
124 | }
125 | }
126 |
127 | @media (max-width: 992px) {
128 | .contributor-card {
129 | max-width: 50%;
130 | }
131 | }
132 |
133 | @media (max-width: 768px) {
134 | .contributor-card {
135 | max-width: 97%;
136 | }
137 | }
138 |
139 | /* Dark Mode Support */
140 | .contributors-container.dark-mode {
141 | background: linear-gradient(to right, #333, #555); /* Change background for dark mode */
142 | }
143 |
144 | /* Dark mode body background */
145 | body.dark {
146 | background-color: #121212; /* Dark background for the body */
147 | }
148 |
149 | .pagination {
150 | display: flex;
151 | justify-content: center;
152 | align-items: center;
153 | margin-top: 2rem;
154 | gap: 1rem;
155 | }
156 |
157 | .pagination-button {
158 | padding: 0.5rem 1rem;
159 | background-color: #0D9488;
160 | color: white;
161 | margin: 5px;
162 | border: none;
163 | border-radius: 0.25rem;
164 | cursor: pointer;
165 | transition: background-color 0.3s ease;
166 | }
167 |
168 | .pagination-button:hover {
169 | background-color: #005f87;
170 | }
171 |
172 | .pagination-button:disabled {
173 | background-color: #a09e9e;
174 | cursor: not-allowed;
175 | }
176 |
177 | .page-info {
178 | font-size: 1rem;
179 | color: #333;
180 |
181 | }
182 |
--------------------------------------------------------------------------------
/src/pages/Contributors.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axios from "axios";
3 | import "./Contributors.css";
4 | import Navbar from "../components/navbar";
5 |
6 | function Contributors() {
7 | const [contributors, setContributors] = useState([]);
8 | const [loading, setLoading] = useState(true);
9 | const [error, setError] = useState(null);
10 | const [theme, setTheme] = useState("light");
11 |
12 | const [currentPage, setCurrentPage] = useState(1);
13 | const contributorsPerPage = 9;
14 |
15 | useEffect(() => {
16 | const storedTheme = localStorage.getItem("theme");
17 | if (storedTheme) {
18 | setTheme(storedTheme);
19 | }
20 | }, []);
21 |
22 | useEffect(() => {
23 | document.body.className = theme;
24 | localStorage.setItem("theme", theme);
25 | }, [theme]);
26 |
27 | useEffect(() => {
28 | async function fetchContributors() {
29 | let allContributors = [];
30 | let page = 1;
31 |
32 | try {
33 | while (true) {
34 | const response = await axios.get(
35 | `https://api.github.com/repos/param-code/counter-app/contributors`,
36 | {
37 | params: {
38 | per_page: 100,
39 | page,
40 | },
41 | }
42 | );
43 | const data = response.data;
44 | if (data.length === 0) {
45 | break;
46 | }
47 | allContributors = [...allContributors, ...data];
48 | page++;
49 | }
50 | setContributors(allContributors);
51 | } catch (error) {
52 | console.error("Error fetching contributors:", error.message);
53 | setError("Failed to load contributors. Please try again later.");
54 | } finally {
55 | setLoading(false);
56 | }
57 | }
58 | fetchContributors();
59 | }, []);
60 |
61 | const indexOfLastContributor = currentPage * contributorsPerPage;
62 | const indexOfFirstContributor = indexOfLastContributor - contributorsPerPage;
63 | const currentContributors = contributors.slice(
64 | indexOfFirstContributor,
65 | indexOfLastContributor
66 | );
67 |
68 | const totalPages = Math.ceil(contributors.length / contributorsPerPage);
69 |
70 | const handleNextPage = () => {
71 | if (currentPage < totalPages) {
72 | setCurrentPage((prevPage) => prevPage + 1);
73 | }
74 | };
75 |
76 | const handlePrevPage = () => {
77 | if (currentPage > 1) {
78 | setCurrentPage((prevPage) => prevPage - 1);
79 | }
80 | };
81 |
82 | return (
83 |
88 |
setTheme((prev) => (prev === "dark" ? "light" : "dark"))}
91 | />
92 | Our Contributors
93 |
94 | {loading ? (
95 |
Loading contributors...
96 | ) : error ? (
97 |
{error}
98 | ) : currentContributors.length > 0 ? (
99 | currentContributors.map((contributor) => (
100 |
101 |
107 |
112 |
113 |
{contributor.login}
114 |
115 | Contributions: {contributor.contributions}
116 |
117 |
118 | ))
119 | ) : (
120 |
No contributors found.
121 | )}
122 |
123 |
124 |
125 |
130 | Previous
131 |
132 |
133 | Page {currentPage} of {totalPages}
134 |
135 |
140 | Next
141 |
142 |
143 |
144 | );
145 | }
146 |
147 | export default Contributors;
148 |
--------------------------------------------------------------------------------
/src/pages/Counter.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import Navbar from "../components/navbar";
3 |
4 | function Counter() {
5 | const [minutes, setMinutes] = useState(0);
6 | const [seconds, setSeconds] = useState(0);
7 | const [isRunning, setIsRunning] = useState(false);
8 | const [theme, setTheme] = useState("light"); // State for theme
9 |
10 | // Function to toggle theme
11 | const toggleTheme = () => {
12 | setTheme((prev) => (prev === "dark" ? "light" : "dark"));
13 | };
14 |
15 | const handleMinutesChange = (e) => {
16 | setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value))));
17 | };
18 |
19 | const handleSecondsChange = (e) => {
20 | setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value))));
21 | };
22 |
23 | const startTimer = () => {
24 | if (minutes === 0 && seconds === 0) {
25 | return;
26 | }
27 | setIsRunning(true);
28 | };
29 |
30 | const pauseTimer = () => {
31 | setIsRunning(false);
32 | };
33 |
34 | const resetTimer = () => {
35 | setMinutes(0);
36 | setSeconds(0);
37 | setIsRunning(false);
38 | };
39 |
40 | useEffect(() => {
41 | let intervalId;
42 | if (isRunning) {
43 | intervalId = setInterval(() => {
44 | if (seconds === 0) {
45 | if (minutes === 0) {
46 | clearInterval(intervalId);
47 | setIsRunning(false);
48 | return;
49 | }
50 | setMinutes((prev) => prev - 1);
51 | setSeconds(59);
52 | } else {
53 | setSeconds((prev) => prev - 1);
54 | }
55 | }, 1000);
56 | }
57 | return () => clearInterval(intervalId);
58 | }, [isRunning, minutes, seconds]);
59 |
60 | const formatTime = () => {
61 | const minutesStr = String(minutes).padStart(2, "0");
62 | const secondsStr = String(seconds).padStart(2, "0");
63 | return `${minutesStr}:${secondsStr}`;
64 | };
65 |
66 | return (
67 |
72 | {/* Navbar with toggleTheme function passed as a prop */}
73 |
74 |
75 |
76 |
77 | Countdown Timer
78 |
79 |
80 | {/* Input Fields */}
81 |
114 |
115 | {/* Timer Display */}
116 |
117 | {formatTime()}
118 |
119 |
120 | {/* Control Buttons */}
121 |
122 | {/* Start Button */}
123 |
132 | {isRunning ? "Resume" : "Start"}
133 |
134 |
135 | {/* Pause Button */}
136 | {isRunning && (
137 |
141 | Pause
142 |
143 | )}
144 |
145 | {/* Reset Button */}
146 |
150 | Reset
151 |
152 |
153 |
154 |
155 |
156 | );
157 | }
158 |
159 | export default Counter;
160 |
--------------------------------------------------------------------------------
/src/pages/Error404.jsx:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import React from "react";
4 | import { Link } from "react-router-dom";
5 |
6 | const Error404 = () => {
7 | return (
8 |
9 |
10 |
11 |
19 |
20 |
21 |
22 | Looks like you're lost
23 |
24 |
25 |
26 | The page you are looking for is not available!
27 |
28 |
29 |
32 | Home
33 |
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default Error404;
42 |
--------------------------------------------------------------------------------
/src/pages/Feedback.css:
--------------------------------------------------------------------------------
1 | .feedback-form-container {
2 | background: linear-gradient(135deg, #2e2e3a 0%, #1e1e2f 100%);
3 | border-radius: 20px;
4 | padding: 60px;
5 | width: 650px;
6 | margin: 50px auto;
7 | color: #f5f4ef;
8 | text-align: center;
9 | box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5);
10 | transition: transform 0.3s ease;
11 | }
12 |
13 | .feedback-form-container:hover {
14 | transform: scale(1.02);
15 | }
16 |
17 | .heading {
18 | margin-bottom: 20px;
19 | font-size: 42px;
20 | font-family: 'Poppins', sans-serif;
21 | color: #17e6e6;
22 | text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
23 | }
24 |
25 | .name,
26 | .email,
27 | .exp {
28 | margin-top: 20px;
29 | width: 100%;
30 | padding: 15px;
31 | background-color: #3a3a4d;
32 | border: 2px solid #17e6e6;
33 | border-radius: 10px;
34 | color: #ffffff;
35 | font-size: 16px;
36 | box-sizing: border-box;
37 | transition: border-color 0.3s, box-shadow 0.3s;
38 | }
39 |
40 | .name:focus,
41 | .email:focus,
42 | .exp:focus {
43 | outline: none;
44 | border-color: #58a7db;
45 | box-shadow: 0 0 5px rgba(24, 144, 255, 0.5);
46 | }
47 |
48 | textarea {
49 | height: 120px;
50 | resize: none;
51 | }
52 |
53 | .stars {
54 | margin-top: 20px;
55 | display: flex;
56 | justify-content: center;
57 | margin: 10px 0;
58 | }
59 |
60 | .star,
61 | .star-filled {
62 | font-size: 40px;
63 | cursor: pointer;
64 | transition: color 0.3s, transform 0.2s;
65 | }
66 |
67 | .star {
68 | color: #a3a3a3;
69 | }
70 |
71 | .star-filled {
72 | color: #f39c12;
73 | }
74 |
75 | .star:hover,
76 | .star-filled:hover {
77 | transform: scale(1.2);
78 | }
79 |
80 | .post-button {
81 | margin-top: 30px;
82 | background: linear-gradient(135deg, #17e6e6, #15b2b2);
83 | width: 220px;
84 | color: #2e2e2e;
85 | border: none;
86 | padding: 15px;
87 | border-radius: 8px;
88 | font-size: 18px;
89 | cursor: pointer;
90 | transition: background 0.3s, transform 0.2s;
91 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
92 | }
93 |
94 | .post-button:hover {
95 | background: linear-gradient(135deg, #15b2b2, #0e8f8f);
96 | transform: translateY(-2px);
97 | }
98 |
99 | .popup-overlay {
100 | position: fixed;
101 | top: 0;
102 | left: 0;
103 | width: 100%;
104 | height: 100%;
105 | background-color: rgba(0, 0, 0, 0.7);
106 | display: flex;
107 | justify-content: center;
108 | align-items: center;
109 | }
110 |
111 | .popup {
112 | background-color: #ffffff;
113 | padding: 30px;
114 | border-radius: 15px;
115 | text-align: center;
116 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
117 | animation: fadeIn 0.5s ease;
118 | }
119 |
120 | .popup h3 {
121 | margin-bottom: 15px;
122 | color: #333;
123 | font-family: 'Poppins', sans-serif;
124 | }
125 |
126 | .popup p {
127 | margin-bottom: 20px;
128 | color: #555;
129 | }
130 |
131 | .close-popup-button {
132 | background-color: #17e6e6;
133 | color: #2e2e2e;
134 | border: none;
135 | padding: 12px 24px;
136 | border-radius: 5px;
137 | cursor: pointer;
138 | transition: background-color 0.3s, transform 0.2s;
139 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
140 | }
141 |
142 | .close-popup-button:hover {
143 | background-color: #15b2b2;
144 | transform: scale(1.05);
145 | }
146 |
147 | @keyframes fadeIn {
148 | from {
149 | opacity: 0;
150 | transform: translateY(-20px);
151 | }
152 | to {
153 | opacity: 1;
154 | transform: translateY(0);
155 | }
156 | }
157 |
158 | @media (max-width: 768px) {
159 | .feedback-form-container {
160 | width: 90%;
161 | padding: 30px;
162 | box-sizing: border-box;
163 | }
164 |
165 | .heading {
166 | font-size: 36px;
167 | }
168 |
169 | .post-button {
170 | width: 100%;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/pages/Feedback.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./Feedback.css";
3 | import { feedbackValidation } from "@/validations/validation";
4 |
5 | const Feedback = () => {
6 | const [name, setName] = useState("");
7 | const [email, setEmail] = useState("");
8 | const [rating, setRating] = useState(0);
9 | const [hoverRating, setHoverRating] = useState(0);
10 | const [feedback, setFeedback] = useState("");
11 | const [isSubmitted, setIsSubmitted] = useState(false);
12 | const [message, setMessage] = useState("");
13 | const [messageType, setMessageType] = useState("");
14 | const [errors, setErrors] = useState({});
15 |
16 | const handleRating = (rate) => {
17 | setRating(rate);
18 | };
19 |
20 | const handleSubmit = async (e) => {
21 | e.preventDefault();
22 |
23 | try {
24 | await feedbackValidation.validate(
25 | { name, email, feedback },
26 | { abortEarly: false }
27 | );
28 | setErrors({});
29 | } catch (error) {
30 | const newErrors = {};
31 | error.inner.forEach((err) => {
32 | newErrors[err.path] = err.message;
33 | });
34 | setErrors(newErrors);
35 | return;
36 | }
37 |
38 | if (name && email && rating && feedback) {
39 | try {
40 | const response = await fetch("http://localhost:5000/contact", {
41 | method: "POST",
42 | headers: {
43 | "Content-Type": "application/json",
44 | },
45 | body: JSON.stringify({
46 | name,
47 | email,
48 | rating,
49 | message: feedback,
50 | }),
51 | });
52 |
53 | const result = await response.json();
54 |
55 | if (response.ok) {
56 | setMessage("Your feedback has been successfully submitted.");
57 | setMessageType("success");
58 | setIsSubmitted(true);
59 | resetForm();
60 | } else {
61 | setMessage(result.message || "An error occurred. Please try again.");
62 | setMessageType("error");
63 | }
64 | } catch (error) {
65 | setMessage("Failed to submit feedback. Please try again later.");
66 | setMessageType("error");
67 | }
68 | } else {
69 | setMessage("Please fill out all fields.");
70 | setMessageType("error");
71 | }
72 | };
73 |
74 | const resetForm = () => {
75 | setTimeout(() => {
76 | setName("");
77 | setEmail("");
78 | setRating(0);
79 | setFeedback("");
80 | setHoverRating(0);
81 | setIsSubmitted(false);
82 | setMessage("");
83 | setMessageType("");
84 | }, 10000); // Reset form and message after 10 seconds
85 | };
86 |
87 | const closePopup = () => {
88 | setIsSubmitted(false);
89 | };
90 |
91 | return (
92 |
93 |
94 | Feedback Form
95 |
96 | {/* Name Input */}
97 | setName(e.target.value)}
103 | required
104 | />
105 | {errors.name && {errors.name}
}
106 | {/* Email Input */}
107 | setEmail(e.target.value)}
113 | required
114 | />
115 | {errors.email && {errors.email}
}
116 | {/* Feedback Textarea */}
117 | setFeedback(e.target.value)}
121 | placeholder="Describe your experience.."
122 | required
123 | />
124 | {errors.feedback && (
125 | {errors.feedback}
126 | )}
127 | {/* Star Rating */}
128 |
129 | {[1, 2, 3, 4, 5].map((star) => (
130 | handleRating(star)}
136 | onMouseEnter={() => setHoverRating(star)}
137 | onMouseLeave={() => setHoverRating(0)}
138 | >
139 | ★
140 |
141 | ))}
142 |
143 |
144 | {/* Submit Button */}
145 |
146 | Submit
147 |
148 |
149 |
150 | {/* Success or Error Message */}
151 | {message && (
152 |
159 | {message}
160 |
161 | )}
162 |
163 | {/* Success Pop-up */}
164 | {isSubmitted && (
165 |
166 |
167 |
Thank You!
168 |
{message}
169 |
170 | Close
171 |
172 |
173 |
174 | )}
175 |
176 | );
177 | };
178 |
179 | export default Feedback;
180 |
--------------------------------------------------------------------------------
/src/pages/PrivacyPolicy.css:
--------------------------------------------------------------------------------
1 | /* PrivacyPolicy.css */
2 | .privacy-container {
3 | max-width: 990px;
4 | margin: 40px auto;
5 | padding: 40px;
6 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
7 | background-color: #001f3f; /* Navy blue background */
8 | color: white; /* White text for better contrast */
9 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
10 | border-radius: 15px;
11 | line-height: 1.8;
12 | transition: transform 0.3s, box-shadow 0.3s;
13 | }
14 |
15 | .privacy-section {
16 | border: 1px solid #d8dee4;
17 | border-radius: 10px;
18 | padding: 10px;
19 | margin-bottom: 20px;
20 | background-color: none; /* Light gray background for sections */
21 | transition: background-color 0.3s, color 0.3s; /* Added color transition */
22 | color: black; /* Default text color */
23 | }
24 |
25 | .privacy-section:hover {
26 | background-color: #001f3f; /* Change background to navy blue on hover */
27 | color: white; /* Change text color to white on hover */
28 | }
29 |
30 | .privacy-section p {
31 | color: inherit; /* Ensure p elements inherit the color of the section */
32 | }
33 |
34 | .privacy-container:hover {
35 | transform: scale(1.02);
36 | box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
37 | }
38 |
39 | .privacy-container h1 {
40 | font-size: 2.5em;
41 | color: #fff;
42 | text-align: center;
43 | margin-bottom: 35px;
44 | font-weight: 700;
45 | }
46 |
47 | .privacy-container h2 {
48 | font-size: 1.8em;
49 | color: #f1f1f1;
50 | margin-top: 30px;
51 | padding-bottom: 10px;
52 | border-bottom: 2px solid #d8dee4;
53 | transition: color 0.3s;
54 | }
55 |
56 | .privacy-container h2:hover {
57 | color: #ffcc00;
58 | }
59 | .privacy-container p:hover {
60 | color: #ffcc00;
61 | }
62 |
63 | .privacy-container p {
64 | font-size: 1.15em;
65 | color: #f1f1f1;
66 | margin: 20px 0;
67 | }
68 |
69 | .privacy-container section {
70 | margin-bottom: 30px;
71 | }
72 |
73 | .privacy-container section:last-child {
74 | margin-bottom: 0;
75 | }
76 |
77 | .privacy-container a {
78 | color: #ffcc00;
79 | text-decoration: none;
80 | font-weight: 500;
81 | transition: color 0.3s;
82 | }
83 |
84 | .privacy-container a:hover {
85 | color: #ffdd57;
86 | text-decoration: underline;
87 | }
88 |
89 | html, body, #root {
90 | background-color: #f3f6f9;
91 | margin: 0;
92 | padding: 0;
93 | }
94 |
--------------------------------------------------------------------------------
/src/pages/PrivacyPolicy.jsx:
--------------------------------------------------------------------------------
1 | // PrivacyPolicy.js
2 | import React from 'react';
3 | import "./PrivacyPolicy.css";
4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5 | import { faShieldAlt, faLock, faCookie, faUsers, faPaperPlane } from '@fortawesome/free-solid-svg-icons';
6 |
7 | const PrivacyPolicy = () => {
8 | return (
9 |
10 |
Privacy and Policy
11 |
12 |
13 | 1. Data Collection
14 |
15 | We collect various types of information, including personal data, such as your name,
16 | email address, and other contact details, as well as non-personal data, like browsing
17 | behavior and device information. This data is gathered through forms, cookies, and
18 | other technologies.
19 |
20 |
21 |
22 |
23 | 2. Use of Information
24 |
25 | The information we collect is used to provide and improve our services, personalize user
26 | experiences, respond to your inquiries, and send you updates about our services. We may
27 | also use data for research and analytics purposes.
28 |
29 |
30 |
31 |
32 | 3. Data Security
33 |
34 | We take data security seriously and implement industry-standard measures to protect
35 | your information from unauthorized access, disclosure, or loss. However, no online
36 | service can be 100% secure, and we cannot guarantee absolute security.
37 |
38 |
39 |
40 |
41 | 4. Cookies
42 |
43 | Cookies are small files stored on your device that help us enhance your browsing
44 | experience. We use cookies to understand user behavior, track preferences, and
45 | optimize site performance. You can disable cookies in your browser settings, but
46 | some features of the site may not function properly.
47 |
48 |
49 |
50 |
51 | 5. Third-Party Services
52 |
53 | We may use third-party services for hosting, analytics, and advertising. These
54 | third parties have their own privacy policies, and we are not responsible for
55 | their practices. However, we do our best to work with reputable companies that
56 | share our commitment to privacy.
57 |
58 |
59 |
60 |
61 | 6. Your Rights
62 |
63 | You have the right to access, correct, or delete your personal information. If
64 | you wish to exercise any of these rights, please contact us at the provided
65 | contact details.
66 |
67 |
68 |
69 |
70 | 7. Policy Updates
71 |
72 | This privacy policy may be updated periodically to reflect changes in our practices.
73 | We encourage you to review this page regularly for the latest information on our
74 | privacy practices.
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | export default PrivacyPolicy;
82 |
--------------------------------------------------------------------------------
/src/pages/SignUpPage.css:
--------------------------------------------------------------------------------
1 | /* General Styles */
2 | body {
3 | margin: 0;
4 | font-family: 'Arial', sans-serif;
5 | background: linear-gradient(to right, #4facfe, #00f2fe); /* Gradient background */
6 | color: #333;
7 | transition: background 0.3s ease-in-out;
8 | }
9 |
10 | /* Form Container with Glass Effect */
11 | .form-container {
12 | background: rgba(255, 255, 255, 0.1); /* Light white background with transparency */
13 | border-radius: 10px; /* Rounded corners */
14 | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); /* Shadow for depth */
15 | backdrop-filter: blur(15px); /* Increase blur effect for glass look */
16 | border: 1px solid rgba(255, 255, 255, 0.3); /* Optional: border to enhance the glass effect */
17 | padding: 2rem; /* Padding inside the form */
18 | }
19 |
20 | /* Input Styles */
21 | input[type="text"],
22 | input[type="email"],
23 | input[type="password"] {
24 | background-color: rgba(255, 255, 255, 0.2); /* Slightly opaque for input fields */
25 | border: 1px solid rgba(200, 200, 200, 0.5); /* Light border for inputs */
26 | border-radius: 5px; /* Rounded input corners */
27 | padding: 10px; /* Padding inside inputs */
28 | transition: box-shadow 0.3s ease; /* Transition for input focus */
29 | }
30 |
31 | input[type="text"]:focus,
32 | input[type="email"]:focus,
33 | input[type="password"]:focus {
34 | outline: none; /* Remove default outline */
35 | box-shadow: 0 0 10px rgba(0, 0, 255, 0.5); /* Focus effect */
36 | }
37 |
38 | /* Button Styles */
39 | button {
40 | transition: transform 0.2s ease, box-shadow 0.2s ease; /* Transition for buttons */
41 | }
42 |
43 | /* Responsive Styles */
44 | @media (max-width: 600px) {
45 | .form-container {
46 | width: 90%; /* Responsive width */
47 | }
48 | }
49 |
50 | /* Additional styles for the glass effect on the whole page */
51 | h1 {
52 | color: rgba(255, 255, 255, 0.9); /* Adjust heading color for better visibility */
53 | }
54 |
55 | .text-center {
56 | color: rgba(255, 255, 255, 0.8); /* Adjust text color for better visibility */
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/SignUpPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import toast from 'react-hot-toast';
3 | import { useNavigate } from 'react-router-dom';
4 | import { FaEye, FaEyeSlash } from 'react-icons/fa'; // Import icons
5 |
6 | const SignUpPage = () => {
7 | const [currState, setCurrState] = useState('Sign Up');
8 | const [username, setUsername] = useState('');
9 | const [email, setEmail] = useState('');
10 | const [password, setPassword] = useState('');
11 | const [showPassword, setShowPassword] = useState(false); // State for toggling password visibility
12 | const navigate = useNavigate();
13 |
14 | const handleSubmit = (e) => {
15 | e.preventDefault();
16 | if (currState === 'Sign Up') {
17 | console.log('Sign Up Data:', { username, email, password });
18 | toast.success('Signup Successfully!');
19 | } else {
20 | console.log('Sign In Data:', { email, password });
21 | toast.success('Login Successfully!');
22 | }
23 | navigate('/');
24 | };
25 |
26 | // Tile-style glassmorphism effect
27 | const glassTileStyle = {
28 | background: 'rgba(255, 255, 255, 0.1)', // More transparent glassy effect
29 | borderRadius: '20px', // Increased rounded corners for tile effect
30 | boxShadow: '0 8px 30px rgba(0, 0, 0, 0.2)', // Tile-like shadow
31 | backdropFilter: 'blur(30px)', // Increased blur for glass effect
32 | border: '1px solid rgba(255, 255, 255, 0.3)', // Optional: border to enhance tile
33 | padding: '2rem', // Padding inside the form
34 | maxWidth: '420px', // Fixed max width to simulate a tile size
35 | width: '100%',
36 | margin: '0 auto', // Center the form horizontally
37 | };
38 |
39 | return (
40 |
41 |
{currState}
42 |
43 |
44 | {/* Back Button for Sign In */}
45 | {currState === 'Sign In' && (
46 |
47 | {
49 | e.preventDefault(); // Prevent form submission
50 | setCurrState('Sign Up'); // Switch to Sign Up
51 | }}
52 | className="bg-blue-600 text-white py-1 px-2 rounded text-sm hover:bg-blue-700"
53 | >
54 | {'<<'} Back
55 |
56 |
57 | )}
58 |
59 | {currState === 'Sign Up' && (
60 |
61 |
62 | Username
63 |
64 | setUsername(e.target.value)}
69 | className="mt-1 block w-full p-2 border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:border-blue-500"
70 | required
71 | />
72 |
73 | )}
74 |
75 |
76 |
77 | Email
78 |
79 | setEmail(e.target.value)}
84 | className="mt-1 block w-full p-2 border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:border-blue-500"
85 | required
86 | />
87 |
88 |
89 |
90 |
91 | Password
92 |
93 | setPassword(e.target.value)}
98 | className="mt-1 block w-full p-2 border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:border-blue-500"
99 | />
100 | {/* Icon for toggling password visibility */}
101 | setShowPassword(!showPassword)}
103 | className="absolute right-3 top-9 cursor-pointer text-gray-600 dark:text-gray-400"
104 | >
105 | {showPassword ? : }
106 |
107 |
108 |
109 |
113 | {currState === 'Sign Up' ? 'Create Account' : 'Sign In'}
114 |
115 |
116 |
117 | {currState === 'Sign Up' ? (
118 |
119 | Already have an account?{' '}
120 | setCurrState('Sign In')}>
121 | Sign In Here
122 |
123 |
124 | ) : (
125 |
126 | Don't have an account?{' '}
127 | setCurrState('Sign Up')}>
128 | Create an Account
129 |
130 |
131 | )}
132 | {currState === 'Sign Up' && (
133 |
navigate('/')}>
134 | Home
135 |
136 | )}
137 |
138 |
139 |
140 | );
141 | };
142 |
143 | export default SignUpPage;
144 | ``
145 |
--------------------------------------------------------------------------------
/src/pages/TermsPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Navbar from "../components/navbar";
3 |
4 | function TermsAndConditions() {
5 | const [theme, setTheme] = useState("light");
6 |
7 | const toggleTheme = () => {
8 | setTheme((prev) => (prev === "dark" ? "light" : "dark"));
9 | };
10 |
11 | return (
12 |
13 |
14 |
15 | Terms and Conditions
16 |
17 | Welcome to our platform. Please read these terms and conditions carefully before using our services.
18 |
19 |
20 |
21 |
1. Acceptance of Terms
22 |
23 | By accessing or using our website, you agree to comply with these terms and all applicable laws.
24 |
25 |
26 |
27 |
28 |
2. Privacy Policy
29 |
30 | We respect your privacy and are committed to protecting your personal information. Please refer to our Privacy Policy for details.
31 |
32 |
33 |
34 |
35 |
3. User Obligations
36 |
37 | You agree not to use the website for any unlawful purposes and to comply with all applicable laws and regulations.
38 |
39 |
40 |
41 |
42 |
4. Intellectual Property
43 |
44 | All content on this site, including text, graphics, logos, and software, is the property of our platform and protected by copyright laws.
45 |
46 |
47 |
48 |
49 |
5. Limitation of Liability
50 |
51 | We are not liable for any damages or losses arising from your use of the service, including, but not limited to, direct, indirect, or consequential damages.
52 |
53 |
54 |
55 |
56 |
6. Termination
57 |
58 | We reserve the right to suspend or terminate your access to the website if you breach any of these terms.
59 |
60 |
61 |
62 |
63 |
7. Changes to the Terms
64 |
65 | We may update these terms from time to time, and it is your responsibility to review them regularly.
66 |
67 |
68 |
69 |
70 | If you have any questions, feel free to contact us.
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | export default TermsAndConditions;
78 |
--------------------------------------------------------------------------------
/src/pages/TimerPage.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: 'Arial', sans-serif;
4 | background: linear-gradient(to right, #4facfe, #00f2fe);
5 | color: #333;
6 | transition: background 0.3s ease-in-out;
7 | }
8 |
9 | /* Dark Mode Support */
10 | body.dark-mode {
11 | background: linear-gradient(to right, #333, #555);
12 | color: #f0f0f0;
13 | }
14 |
15 | /* Container */
16 | .timer-page {
17 | display: flex;
18 | flex-direction: column;
19 | align-items: center;
20 | justify-content: center;
21 | height: 100vh;
22 | padding: 20px;
23 | transition: background 0.3s ease-in-out;
24 | }
25 |
26 | /* Timer Display */
27 | .timer-display {
28 | font-size: 48px;
29 | font-weight: bold;
30 | color: #ffffff;
31 | background-color: rgba(0, 0, 0, 0.5);
32 | padding: 20px;
33 | border-radius: 10px;
34 | margin: 20px 0;
35 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
36 | transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
37 | }
38 |
39 | .timer-display:hover {
40 | transform: scale(1.05); /* Slight scaling on hover */
41 | box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
42 | }
43 |
44 | /* Input Field */
45 | input[type="number"] {
46 | width: 100%;
47 | max-width: 300px;
48 | padding: 10px;
49 | margin-bottom: 2px;
50 | border: none;
51 | border-radius: 5px;
52 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
53 | transition: box-shadow 0.3s ease;
54 | }
55 |
56 | input[type="number"]:focus {
57 | outline: none;
58 | box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
59 | transform: scale(1.05);
60 | }
61 |
62 | /* Start Div Container */
63 | .start-div {
64 | display: flex;
65 | flex-direction: column;
66 | align-items: center;
67 | justify-content: center;
68 | margin-top: 20px;
69 | padding: 20px;
70 | background-color: rgba(255, 255, 255, 0.9);
71 | border-radius: 10px;
72 | box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
73 | transition: background 0.3s ease, box-shadow 0.3s ease;
74 | }
75 |
76 | .start-div:hover {
77 | box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
78 | }
79 |
80 | /* Button Container */
81 | .button-container {
82 | display: flex;
83 | justify-content: space-between;
84 | margin-top: 20px;
85 | }
86 |
87 | /* Common Button Styles */
88 | button {
89 | background-color: #6200ea;
90 | color: white;
91 | padding: 15px 25px;
92 | border: none;
93 | border-radius: 5px;
94 | font-size: 16px;
95 | cursor: pointer;
96 | flex: 1;
97 | margin: 0 5px;
98 | transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease;
99 | }
100 |
101 | button:focus {
102 | outline: none;
103 | box-shadow: 0 0 10px rgba(255, 255, 255, 0.7);
104 | transform: scale(1.05);
105 | }
106 |
107 | /* Individual Button Styles */
108 | .start-button {
109 | background-color: #4caf50;
110 | }
111 |
112 | .start-button:hover,
113 | .start-button:focus {
114 | background-color: #388e3c;
115 | }
116 |
117 | .pause-button {
118 | background-color: #ff9800;
119 | }
120 |
121 | .pause-button:hover,
122 | .pause-button:focus {
123 | background-color: #f57c00;
124 | }
125 |
126 | .reset-button {
127 | background-color: #f44336;
128 | }
129 |
130 | .reset-button:hover,
131 | .reset-button:focus {
132 | background-color: #d32f2f;
133 | }
134 |
135 | button:active {
136 | transform: scale(0.98); /* Button presses feel more tactile */
137 | }
138 |
139 | /* Preset Buttons */
140 | .preset-buttons {
141 | display: flex;
142 | justify-content: space-between;
143 | width: 100%;
144 | max-width: 400px;
145 | margin-top: 15px;
146 | }
147 |
148 | .preset-buttons button {
149 | flex: 1;
150 | margin: 0 5px;
151 | }
152 |
153 | /* Animations for Elements */
154 | @keyframes pulse {
155 | 0% {
156 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
157 | }
158 | 50% {
159 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
160 | }
161 | 100% {
162 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
163 | }
164 | }
165 |
166 | button:hover {
167 | animation: pulse 1s infinite; /* Pulse effect on hover */
168 | }
169 | @keyframes fall{
170 | from{
171 | top: -90px;
172 | opacity: 0;
173 | }
174 | to{
175 | top: 50px;
176 | opacity: 1;
177 | }
178 | }
179 | @keyframes hide {
180 | to {
181 | opacity: 0;
182 | }
183 | }
184 |
185 | @keyframes hide {
186 | 0% {
187 | opacity: 1;
188 | }
189 | 100% {
190 | opacity: 0;
191 | }
192 | }
193 |
194 | .popup {
195 | position: fixed;
196 | top: -90px;
197 | left: 50%;
198 | transform: translateX(-50%);
199 | background-color: rgba(14, 72, 129, 0.7);
200 | color: white;
201 | padding: 15px 20px;
202 | border-radius: 5px;
203 | animation: fall 0.5s ease forwards, hide 5s 2s forwards;
204 | }
205 |
206 |
207 |
208 | /* Responsive Styles */
209 | @media (max-width: 600px) {
210 | .timer-display {
211 | font-size: 36px;
212 | }
213 |
214 | button {
215 | padding: 10px 15px;
216 | font-size: 14px;
217 | }
218 |
219 | input[type="number"] {
220 | max-width: 100%;
221 | }
222 | }
223 |
224 |
--------------------------------------------------------------------------------
/src/pages/TimerPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, useRef } from "react";
2 | import Navbar from "../components/navbar";
3 | import SwitchTab from "../components/SwitchTab";
4 | import Particles, { initParticlesEngine } from "@tsparticles/react";
5 | import { loadSlim } from "@tsparticles/slim"; // Slim version for smaller bundle
6 | import "./TimerPage.css";
7 |
8 | const TimerPage = () => {
9 | const [secondsLeft, setSecondsLeft] = useState(0);
10 | const [isRunning, setIsRunning] = useState(false);
11 | const [isPaused, setIsPaused] = useState(false);
12 | const [customTime, setCustomTime] = useState(0);
13 | //for pop-ups
14 | const [message, setMessage] = useState("");
15 | const timerRef = useRef(null);
16 | const [theme, setTheme] = useState("light");
17 | const [init, setInit] = useState(false);
18 |
19 | // Load particle engine only once
20 | useEffect(() => {
21 | initParticlesEngine(async (engine) => {
22 | await loadSlim(engine);
23 | }).then(() => {
24 | setInit(true);
25 | });
26 | }, []);
27 |
28 | // Load theme from local storage
29 | useEffect(() => {
30 | const storedTheme = localStorage.getItem("theme");
31 | if (storedTheme) {
32 | setTheme(storedTheme);
33 | }
34 | }, []);
35 |
36 | // Apply theme changes to body and local storage
37 | useEffect(() => {
38 | document.body.className = theme;
39 | localStorage.setItem("theme", theme);
40 | }, [theme]);
41 |
42 | useEffect(() => {
43 | if (isRunning && !isPaused && secondsLeft > 0) {
44 | timerRef.current = setInterval(() => {
45 | setSecondsLeft((prev) => prev - 1);
46 | }, 1000);
47 | } else if (secondsLeft === 0 && isRunning) {
48 | clearInterval(timerRef.current);
49 | alert("Time's up!");
50 | setIsRunning(false);
51 | }
52 |
53 | return () => clearInterval(timerRef.current);
54 | }, [isRunning, isPaused, secondsLeft]);
55 |
56 | const showMessage = (msg) => {
57 | setMessage(msg);
58 | setTimeout(() => setMessage(""), 3000); // Message disappears after 2 seconds
59 | };
60 |
61 | const handleStart = () => {
62 | if (customTime > 0) {
63 | setSecondsLeft(customTime);
64 | setIsRunning(true);
65 | setIsPaused(false);
66 | showMessage("Timer started!");
67 | }
68 | };
69 |
70 | const handlePause = () => {
71 | setIsPaused(!isPaused);
72 | clearInterval(timerRef.current);
73 | showMessage(isPaused ? "Timer Resumed!" : "Timer Paused!");
74 | };
75 |
76 | const handleReset = () => {
77 | setIsRunning(false);
78 | setSecondsLeft(0);
79 | clearInterval(timerRef.current);
80 | showMessage("Timer reset!");
81 | };
82 |
83 | const handlePreset = (time) => {
84 | setCustomTime(time);
85 | setSecondsLeft(time);
86 | setIsRunning(true);
87 | };
88 |
89 | const formatTime = (seconds) => {
90 | const minutes = Math.floor(seconds / 60);
91 | const remainderSeconds = seconds % 60;
92 | return `${minutes}:${remainderSeconds < 10 ? "0" : ""}${remainderSeconds}`;
93 | };
94 |
95 | const particlesLoaded = (container) => {
96 | console.log(container);
97 | };
98 |
99 | return (
100 |
105 | {init && (
106 |
152 | )}
153 |
156 | setTheme((prev) => (prev === "dark" ? "light" : "dark"))
157 | }
158 | />
159 |
160 |
161 |
Timer
162 |
{formatTime(secondsLeft)}
163 |
164 | {message && (
165 |
172 | {message}
173 |
174 | )}
175 |
176 |
setCustomTime(Math.max(0, e.target.value))}
181 | className="text-black"
182 | />
183 |
184 |
185 |
186 | Start
187 |
188 |
189 | {isPaused ? "Resume" : "Pause"}
190 |
191 |
192 | Reset
193 |
194 |
195 |
196 | handlePreset(60)}>1 Min
197 | handlePreset(300)}>5 Mins
198 | handlePreset(600)}>10 Mins
199 |
200 |
{/* Include the SwitchTab component here */}
201 |
202 |
203 |
204 | );
205 | };
206 |
207 | export default TimerPage;
208 |
--------------------------------------------------------------------------------
/src/pages/Todo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useSelector } from "react-redux";
3 | import { useNavigate } from "react-router-dom";
4 | import axios from "axios";
5 |
6 | // axios.defaults.baseURL = "http://localhost:4000/api/v1/auth";
7 | axios.defaults.baseURL = "https://counter-app-backend.vercel.app/api/v1/auth";
8 |
9 | function Todo() {
10 | const { user } = useSelector((state) => state.profile);
11 | const navigate = useNavigate();
12 | const [todos, setTodos] = useState([]);
13 | const [newTodo, setNewTodo] = useState("");
14 |
15 | useEffect(() => {
16 | if (!user) {
17 | navigate("/login");
18 | } else {
19 | fetchTodos();
20 | }
21 | }, [user, navigate]);
22 |
23 | const fetchTodos = async () => {
24 | try {
25 | const response = await axios.get(`/todos/${user._id}`, {
26 | headers: {
27 | Authorization: `Bearer ${user.token}`,
28 | },
29 | });
30 | setTodos(response.data.todos || []);
31 | } catch (error) {
32 | console.error("Error fetching todos:", error);
33 | setTodos([]);
34 | }
35 | };
36 |
37 | const addTodo = async (e) => {
38 | e.preventDefault();
39 | try {
40 | const response = await axios.post(
41 | `/addTodo`,
42 | { userId: user._id, task: newTodo },
43 | {
44 | headers: {
45 | Authorization: `Bearer ${user.token}`,
46 | },
47 | }
48 | );
49 | setTodos([...todos, response.data.newTodo]);
50 | setNewTodo("");
51 | } catch (error) {
52 | console.error("Error adding todo:", error);
53 | }
54 | };
55 |
56 | const deleteTodo = async (todoId) => {
57 | try {
58 | await axios.delete(`/todos/${todoId}`, {
59 | headers: {
60 | Authorization: `Bearer ${user.token}`,
61 | },
62 | });
63 | setTodos(todos.filter((todo) => todo._id !== todoId));
64 | } catch (error) {
65 | console.error("Error deleting todo:", error);
66 | }
67 | };
68 |
69 | return (
70 |
71 |
72 |
73 | Your Todos
74 |
75 |
76 |
77 | setNewTodo(e.target.value)}
82 | className="p-3 rounded-lg bg-gray-100 border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-300 shadow-sm"
83 | />
84 |
85 | Add Todo
86 |
87 |
88 |
89 |
90 | {todos.length === 0 ? (
91 |
92 | No todos found. Add a task to get started!
93 |
94 | ) : (
95 | todos.map((todo) => (
96 |
100 | {todo.task}
101 | deleteTodo(todo._id)}
103 | className="text-red-600 hover:text-red-500 transition duration-300"
104 | >
105 | Delete
106 |
107 |
108 | ))
109 | )}
110 |
111 |
112 |
113 | );
114 | }
115 |
116 | export default Todo;
117 |
--------------------------------------------------------------------------------
/src/pages/WorldClockPage.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from "react";
2 | import Clock from "react-clock";
3 | import "react-clock/dist/Clock.css";
4 | import moment from "moment-timezone";
5 | import Navbar from "../components/navbar";
6 | import Particles, { initParticlesEngine } from "@tsparticles/react";
7 | import { loadSlim } from "@tsparticles/slim"; // Slim version for smaller bundle
8 | // import morningImage from "../assets/morningBackground.png";
9 | import morningImage from "../assets/morningBackground.webp";
10 | import nightImage from "../assets/nightBackground.png";
11 | import afternoonImage from "../assets/afternoonBackground.png"; // Fix: Renamed to match usage
12 |
13 | const WorldClockPage = () => {
14 | const [selectedCountry, setSelectedCountry] = useState("UTC");
15 | const [currentTime, setCurrentTime] = useState(new Date());
16 | const [theme, setTheme] = useState("light"); // Updated theme management
17 | const [init, setInit] = useState(false); // Track if particles are initialized
18 | const [backgroundImage, setBackgroundImage] = useState(morningImage); // Static based on time logic removed
19 |
20 | const countryTimezones = {
21 | "United States": "America/New_York",
22 | "United Kingdom": "Europe/London",
23 | "Japan": "Asia/Tokyo",
24 | "Australia": "Australia/Sydney",
25 | "New Zealand": "Pacific/Auckland",
26 | "France": "Europe/Paris",
27 | "United Arab Emirates": "Asia/Dubai",
28 | "Canada": "America/Toronto",
29 | "India": "Asia/Kolkata",
30 | "China": "Asia/Shanghai",
31 | "Brazil": "America/Sao_Paulo",
32 | "South Africa": "Africa/Johannesburg",
33 | "Russia": "Europe/Moscow",
34 | "Germany": "Europe/Berlin",
35 | "Mexico": "America/Mexico_City",
36 | "Italy": "Europe/Rome",
37 | "Argentina": "America/Argentina/Buenos_Aires",
38 | "Indonesia": "Asia/Jakarta",
39 | };
40 |
41 | const countries = Object.keys(countryTimezones);
42 |
43 | useEffect(() => {
44 | // Initialize the particles engine
45 | initParticlesEngine(async (engine) => {
46 | await loadSlim(engine);
47 | }).then(() => {
48 | setInit(true);
49 | });
50 | }, []);
51 |
52 | useEffect(() => {
53 | const timezone = countryTimezones[selectedCountry] || "UTC";
54 |
55 | const updateTime = () => {
56 | const now = moment();
57 | const time = now.tz(timezone);
58 | const newDate = new Date(
59 | time.year(),
60 | time.month(),
61 | time.date(),
62 | time.hour(),
63 | time.minute(),
64 | time.second()
65 | );
66 | setCurrentTime(newDate);
67 | };
68 |
69 | updateTime();
70 |
71 | const timer = setInterval(updateTime, 1000);
72 | return () => clearInterval(timer);
73 | }, [selectedCountry]);
74 |
75 | // Updated theme management similar to TimerPage
76 | useEffect(() => {
77 | const storedTheme = localStorage.getItem("theme");
78 | if (storedTheme) {
79 | setTheme(storedTheme);
80 | }
81 | }, []);
82 |
83 | useEffect(() => {
84 | document.body.className = theme;
85 | localStorage.setItem("theme", theme);
86 | }, [theme]);
87 |
88 | const toggleTheme = () => {
89 | setTheme((prevTheme) => (prevTheme === "dark" ? "light" : "dark"));
90 | };
91 |
92 | return (
93 |
97 | {init && (
98 |
143 | )}
144 |
145 |
146 |
147 |
148 | World Clock
149 |
150 |
151 |
setSelectedCountry(e.target.value)}
154 | className="w-full mb-6 p-2 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-800 dark:text-white"
155 | >
156 | UTC (Default)
157 | {countries.map((country) => (
158 |
159 | {country}
160 |
161 | ))}
162 |
163 |
164 |
169 |
170 |
171 |
172 | {selectedCountry}: {moment(currentTime).format("YYYY-MM-DD HH:mm:ss")}
173 |
174 |
175 |
176 |
177 |
178 | );
179 | };
180 |
181 | export default WorldClockPage;
182 |
--------------------------------------------------------------------------------
/src/reducer/index.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import { combineReducers } from "@reduxjs/toolkit";
4 | import authReducer from "../slices/authSlice";
5 | import profileReducer from "../slices/profileSlice";
6 |
7 | const rootReducer = combineReducers({
8 | // add all reducer
9 | auth: authReducer,
10 | profile: profileReducer,
11 | });
12 |
13 | export default rootReducer;
14 |
--------------------------------------------------------------------------------
/src/services/apiConnector.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import axios from "axios";
4 |
5 | export const axiosInstance = axios.create({});
6 | export const apiConnector = (method, url, bodyData, headers, params) => {
7 | return axiosInstance({
8 | method: `${method}`,
9 | url: `${url}`,
10 | data: bodyData ? bodyData : null,
11 | headers: headers ? headers : null,
12 | params: params ? params : null,
13 | });
14 | };
15 |
--------------------------------------------------------------------------------
/src/services/apis.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | const BASE_URL = "https://counter-app-backend.vercel.app/api/v1";
4 | // const BASE_URL = "http://localhost:4000/api/v1";
5 |
6 |
7 | // AUTH ENDPOINTS - for login and signup
8 | export const endpoints = {
9 | SIGNUP_API: BASE_URL + "/auth/signup",
10 | LOGIN_API: BASE_URL + "/auth/login",
11 | };
12 |
--------------------------------------------------------------------------------
/src/services/operations/authAPI.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import { toast } from "react-hot-toast";
4 | import { setLoading, setToken } from "../../slices/authSlice";
5 | import { setUser } from "../../slices/profileSlice";
6 | import { apiConnector } from "../apiConnector";
7 | import { endpoints } from "../apis";
8 |
9 | const { SIGNUP_API, LOGIN_API, PASSWORD_RECOVERY_API } = endpoints;
10 | const DEFAULT_AVATAR_URL = "https://api.dicebear.com/5.x/initials/svg?seed=";
11 |
12 | const startLoadingWithToast = (dispatch) => {
13 | const toastId = toast.loading("Loading...");
14 | dispatch(setLoading(true));
15 | return toastId;
16 | };
17 |
18 | const stopLoadingWithToast = (dispatch, toastId) => {
19 | dispatch(setLoading(false));
20 | toast.dismiss(toastId);
21 | };
22 |
23 | const handleApiError = (error, defaultMessage) => {
24 | console.log(error);
25 | toast.error(error.response?.data?.message || defaultMessage);
26 | };
27 |
28 | export function signUp(firstName, lastName, email, password, confirmPassword, navigate) {
29 | return async (dispatch) => {
30 | const toastId = startLoadingWithToast(dispatch);
31 | try {
32 | const response = await apiConnector("POST", SIGNUP_API, {
33 | firstName,
34 | lastName,
35 | email,
36 | password,
37 | confirmPassword,
38 | });
39 |
40 | console.log("SIGNUP API RESPONSE:", response);
41 |
42 | if (!response.data.success) {
43 | throw new Error(response.data.message);
44 | }
45 | toast.success("Signup Successful");
46 | navigate("/login");
47 | } catch (error) {
48 | handleApiError(error, "Signup Failed");
49 | navigate("/signup");
50 | }
51 | stopLoadingWithToast(dispatch, toastId);
52 | };
53 | }
54 |
55 | export function login(email, password, navigate) {
56 | return async (dispatch) => {
57 | const toastId = startLoadingWithToast(dispatch);
58 | try {
59 | const response = await apiConnector("POST", LOGIN_API, { email, password });
60 | console.log("LOGIN API RESPONSE:", response);
61 |
62 | if (!response.data.success) {
63 | throw new Error(response.data.message);
64 | }
65 |
66 | toast.success("Login Successful");
67 | dispatch(setToken(response.data.token));
68 |
69 | const userImage = response.data?.user?.image
70 | ? response.data.user.image
71 | : `${DEFAULT_AVATAR_URL}${response.data.user.firstName}${response.data.user.lastName}`;
72 |
73 | dispatch(setUser({ ...response.data.user, image: userImage }));
74 | localStorage.setItem("token", JSON.stringify(response.data.token));
75 | localStorage.setItem("user", JSON.stringify(response.data.user));
76 | navigate("/");
77 | } catch (error) {
78 | handleApiError(error, "Login Failed");
79 | }
80 | stopLoadingWithToast(dispatch, toastId);
81 | };
82 | }
83 |
84 | export function logout(navigate) {
85 | return (dispatch) => {
86 | dispatch(setToken(null));
87 | dispatch(setUser(null));
88 | localStorage.removeItem("token");
89 | localStorage.removeItem("user");
90 | toast.success("Logged Out");
91 | navigate("/");
92 | };
93 | }
94 |
95 | export function passwordRecovery(email) {
96 | return async (dispatch) => {
97 | const toastId = startLoadingWithToast(dispatch);
98 | try {
99 | const response = await apiConnector("POST", PASSWORD_RECOVERY_API, { email });
100 | console.log("PASSWORD RECOVERY API RESPONSE:", response);
101 |
102 | if (!response.data.success) {
103 | throw new Error(response.data.message);
104 | }
105 |
106 | toast.success("Password recovery instructions have been sent to your email.");
107 | } catch (error) {
108 | handleApiError(error, "Error sending password recovery instructions.");
109 | }
110 | stopLoadingWithToast(dispatch, toastId);
111 | };
112 | }
113 |
--------------------------------------------------------------------------------
/src/slices/authSlice.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import { createSlice } from "@reduxjs/toolkit";
4 |
5 | const initialState = {
6 | loading: false,
7 | token: localStorage.getItem("token")
8 | ? localStorage.getItem("token")
9 | : null,
10 | };
11 |
12 | const authSlice = createSlice({
13 | name: "auth",
14 | initialState: initialState,
15 | reducers: {
16 | setLoading(state, value) {
17 | state.loading = value.payload;
18 | },
19 | setToken(state, value) {
20 | state.token = value.payload;
21 | },
22 | },
23 | });
24 |
25 | export const { setLoading, setToken } = authSlice.actions;
26 |
27 | export default authSlice.reducer;
28 |
--------------------------------------------------------------------------------
/src/slices/profileSlice.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import { createSlice } from "@reduxjs/toolkit";
4 |
5 | const initialState = {
6 | user: localStorage.getItem("user")
7 | ? JSON.parse(localStorage.getItem("user"))
8 | : null,
9 | loading: false,
10 | };
11 |
12 | const profileSlice = createSlice({
13 | name: "profile",
14 | initialState: initialState,
15 | reducers: {
16 | setUser(state, value) {
17 | state.user = value.payload;
18 | },
19 | setLoading(state, value) {
20 | state.loading = value.payload;
21 | },
22 | },
23 | });
24 |
25 | export const { setUser, setLoading } = profileSlice.actions;
26 |
27 | export default profileSlice.reducer;
28 |
--------------------------------------------------------------------------------
/src/validations/validation.js:
--------------------------------------------------------------------------------
1 | import * as yup from "yup";
2 |
3 | export const loginValidation = yup.object().shape({
4 | email: yup
5 | .string()
6 | .email("Email format invalid")
7 | .required("Email is required"),
8 | password: yup.string().required("Password is required"),
9 | });
10 |
11 | export const registerValidation = yup.object().shape({
12 | firstName: yup.string().required("First name is required"),
13 | lastName: yup.string().required("Last name is required"),
14 | email: yup
15 | .string()
16 | .email("Email format invalid")
17 | .required("Email is required"),
18 | password: yup
19 | .string()
20 | .min(6, "Minimum length should be 6")
21 | .required("Password is required"),
22 | confirmPassword: yup
23 | .string()
24 | .oneOf([yup.ref("password"), null], "Passwords must match")
25 | .required("Confirm password is required"),
26 | });
27 |
28 | export const feedbackValidation = yup.object().shape({
29 | name: yup.string().required("Name is required"),
30 | email: yup
31 | .string()
32 | .email("Email format invalid")
33 | .required("Email is required"),
34 | feedback: yup.string().required("Feedback is required"),
35 | });
36 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: 'class',
4 | content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
5 | safelist: [
6 | 'animate-pulse',
7 | 'animate-shimmer',
8 | 'animate-spin-slow',
9 | 'animate-fade-in',
10 | 'animate-bounce-in'
11 | ],
12 | theme: {
13 | extend: {
14 | borderRadius: {
15 | lg: 'var(--radius)',
16 | md: 'calc(var(--radius) - 2px)',
17 | sm: 'calc(var(--radius) - 4px)'
18 | },
19 | colors: {
20 | background: 'hsl(var(--background))',
21 | foreground: 'hsl(var(--foreground))',
22 | card: {
23 | DEFAULT: 'hsl(var(--card))',
24 | foreground: 'hsl(var(--card-foreground))'
25 | },
26 | popover: {
27 | DEFAULT: 'hsl(var(--popover))',
28 | foreground: 'hsl(var(--popover-foreground))'
29 | },
30 | primary: {
31 | DEFAULT: 'hsl(var(--primary))',
32 | foreground: 'hsl(var(--primary-foreground))'
33 | },
34 | secondary: {
35 | DEFAULT: 'hsl(var(--secondary))',
36 | foreground: 'hsl(var(--secondary-foreground))'
37 | },
38 | muted: {
39 | DEFAULT: 'hsl(var(--muted))',
40 | foreground: 'hsl(var(--muted-foreground))'
41 | },
42 | accent: {
43 | DEFAULT: 'hsl(var(--accent))',
44 | foreground: 'hsl(var(--accent-foreground))'
45 | },
46 | destructive: {
47 | DEFAULT: 'hsl(var(--destructive))',
48 | foreground: 'hsl(var(--destructive-foreground))'
49 | },
50 | border: 'hsl(var(--border))',
51 | input: 'hsl(var(--input))',
52 | ring: 'hsl(var(--ring))',
53 | chart: {
54 | 1: 'hsl(var(--chart-1))',
55 | 2: 'hsl(var(--chart-2))',
56 | 3: 'hsl(var(--chart-3))',
57 | 4: 'hsl(var(--chart-4))',
58 | 5: 'hsl(var(--chart-5))'
59 | }
60 | },
61 | animation: {
62 | 'spin-slow': 'spin 3s linear infinite',
63 | 'shimmer': 'shimmer 2s linear infinite',
64 | 'pulse': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
65 | 'fade-in': 'fadeIn 0.5s ease-out',
66 | 'bounce-in': 'bounceIn 0.5s ease-out'
67 | },
68 | keyframes: {
69 | shimmer: {
70 | '0%': { transform: 'translateX(-100%)' },
71 | '100%': { transform: 'translateX(100%)' }
72 | },
73 | pulse: {
74 | '0%, 100%': { opacity: 1 },
75 | '50%': { opacity: 0.5 }
76 | },
77 | fadeIn: {
78 | '0%': { opacity: 0 },
79 | '100%': { opacity: 1 }
80 | },
81 | bounceIn: {
82 | '0%': { transform: 'scale(0.9)', opacity: 0 },
83 | '50%': { transform: 'scale(1.03)', opacity: 1 },
84 | '100%': { transform: 'scale(1)', opacity: 1 }
85 | }
86 | },
87 | spacing: {
88 | '128': '32rem',
89 | '144': '36rem',
90 | },
91 | fontFamily: {
92 | sans: ['Inter var', 'sans-serif'],
93 | },
94 | typography: (theme) => ({
95 | DEFAULT: {
96 | css: {
97 | color: theme('colors.gray.700'),
98 | a: {
99 | color: theme('colors.blue.500'),
100 | '&:hover': {
101 | color: theme('colors.blue.700'),
102 | },
103 | },
104 | },
105 | },
106 | }),
107 | }
108 | },
109 | plugins: [
110 | // require('tailwindcss-animate'),
111 | // require('@tailwindcss/typography'),
112 | // require('@tailwindcss/forms'),
113 | // require('@tailwindcss/aspect-ratio'),
114 | ],
115 | }
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "src": "/[^.]+",
5 | "dest": "/"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import path from "path"
3 | import react from '@vitejs/plugin-react'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | resolve: {
9 | alias: {
10 | "@": path.resolve(__dirname, "./src"),
11 | },
12 | },
13 | })
14 |
--------------------------------------------------------------------------------