├── .github
└── workflows
│ ├── firebase-hosting-merge.yml
│ ├── firebase-hosting-pull-request.yml
│ └── jekyll-gh-pages.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── firebase.json
├── gh
└── images
│ ├── social-media-preview.png
│ └── swift-type-logo.jpg
├── package-lock.json
├── package.json
├── public
├── favicon.png
├── favicon.svg
├── index.html
├── logo192.png
├── logo192bak.png
├── logo512.png
├── logo512bak.png
├── manifest.json
└── robots.txt
├── server.cjs
├── src
├── App.js
├── App.test.js
├── components
│ ├── 404.js
│ ├── Forgot.js
│ ├── Hamburger.js
│ ├── Header.js
│ ├── Home.js
│ ├── LoadingSpinner.js
│ ├── Login.js
│ ├── Modal.js
│ ├── Profile.js
│ ├── ProtectedRoute.js
│ ├── Settings.js
│ ├── Signup.js
│ └── Verify.js
├── firebase.js
├── index.js
├── logo.svg
├── reportWebVitals.js
├── setupTests.js
└── static
│ ├── data
│ └── thresholds.json
│ ├── icons
│ └── favicon-64x64.png
│ ├── images
│ ├── cheetah.svg
│ ├── eagle.svg
│ ├── falcon.svg
│ ├── flash.svg
│ ├── hausemaster.svg
│ ├── horse.svg
│ ├── lion.svg
│ ├── sea_turtle.svg
│ └── sloth.svg
│ ├── scripts
│ └── flying-focus.js
│ └── styles
│ └── styles.scss
└── webpack.config.cjs
/.github/workflows/firebase-hosting-merge.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on merge
5 | 'on':
6 | push:
7 | branches:
8 | - main
9 | jobs:
10 | build_and_deploy:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - run: npm ci && npm run build
15 | env:
16 | REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }}
17 | REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }}
18 | REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }}
19 | REACT_APP_FIREBASE_STORAGE_BUCKET: ${{ secrets.REACT_APP_FIREBASE_STORAGE_BUCKET }}
20 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.REACT_APP_FIREBASE_MESSAGING_SENDER_ID }}
21 | REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }}
22 | REACT_APP_FIREBASE_MEASUREMENT_ID: ${{ secrets.REACT_APP_FIREBASE_MEASUREMENT_ID }}
23 | REACT_APP_FIREBASE_COLLECTION_NAME: ${{ secrets.REACT_APP_FIREBASE_COLLECTION_NAME }}
24 | REACT_APP_USERNAME_KEY: ${{ secrets.REACT_APP_USERNAME_KEY }}
25 | REACT_APP_PROFILE_PHOTO_URL_KEY: ${{ secrets.REACT_APP_PROFILE_PHOTO_URL_KEY }}
26 | REACT_APP_TOTAL_RACES_TAKEN_KEY: ${{ secrets.REACT_APP_TOTAL_RACES_TAKEN_KEY }}
27 | REACT_APP_TOTAL_AVG_ACCURACY_KEY: ${{ secrets.REACT_APP_TOTAL_AVG_ACCURACY_KEY }}
28 | REACT_APP_TOTAL_AVG_WPM_KEY: ${{ secrets.REACT_APP_TOTAL_AVG_WPM_KEY }}
29 | REACT_APP_EMAIL_KEY: ${{ secrets.REACT_APP_EMAIL_KEY }}
30 | REACT_APP_CREATED_AT_KEY: ${{ secrets.REACT_APP_CREATED_AT_KEY }}
31 | REACT_APP_DEFAULT_PROFILE_PHOTO_URL: ${{ secrets.REACT_APP_DEFAULT_PROFILE_PHOTO_URL }}
32 | - uses: FirebaseExtended/action-hosting-deploy@v0
33 | with:
34 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
35 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_SWIFT_TYPE_7007D }}'
36 | channelId: live
37 | projectId: swift-type-7007d
38 |
--------------------------------------------------------------------------------
/.github/workflows/firebase-hosting-pull-request.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: Deploy to Firebase Hosting on PR
5 | 'on': pull_request
6 | jobs:
7 | build_and_preview:
8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - run: npm ci && npm run build
13 | - uses: FirebaseExtended/action-hosting-deploy@v0
14 | with:
15 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
16 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_SWIFT_TYPE_7007D }}'
17 | projectId: swift-type-7007d
18 |
--------------------------------------------------------------------------------
/.github/workflows/jekyll-gh-pages.yml:
--------------------------------------------------------------------------------
1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages
2 | name: Deploy Jekyll with GitHub Pages dependencies preinstalled
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["legacy-branch"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Build job
26 | build:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v3
31 | with:
32 | ref: legacy-branch # Specify the branch name
33 | - name: Setup Pages
34 | uses: actions/configure-pages@v3
35 | - name: Build with Jekyll
36 | uses: actions/jekyll-build-pages@v1
37 | with:
38 | source: ./
39 | destination: ./_site
40 | - name: Upload artifact
41 | uses: actions/upload-pages-artifact@v2
42 |
43 | # Deployment job
44 | deploy:
45 | environment:
46 | name: github-pages
47 | url: ${{ steps.deployment.outputs.page_url }}
48 | runs-on: ubuntu-latest
49 | needs: build
50 | steps:
51 | - name: Deploy to GitHub Pages
52 | id: deployment
53 | uses: actions/deploy-pages@v2
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules/
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage/
10 |
11 | # production
12 | /build/
13 | .firebase
14 | .firebaserc
15 | firebase-debug.log
16 |
17 | # misc
18 | .DS_Store
19 | .env
20 | .envlocal
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | - Using welcoming and inclusive language
12 | - Being respectful of differing viewpoints and experiences
13 | - Gracefully accepting constructive criticism
14 | - Focusing on what is best for the community
15 | - Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | - Trolling, insulting/derogatory comments, and personal or political attacks
21 | - Public or private harassment
22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | - Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at !(Github)[https://github.com/HauseMasterZ/swift-type]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
44 |
45 | [homepage]: https://www.contributor-covenant.org
46 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Swift Type
2 |
3 | Welcome to Swift Type! I appreciate your interest in contributing to our project. By contributing, you can help make this project better and more valuable for the community. Whether you're a developer, designer, tester, or just someone with good ideas, I welcome your contributions.
4 |
5 | Please take a moment to review this document for important information on how to contribute effectively.
6 |
7 | ## Table of Contents
8 |
9 | 1. [Getting Started](#getting-started)
10 | 2. [Code of Conduct](#code-of-conduct)
11 | 3. [How to Contribute](#how-to-contribute)
12 | 4. [Pull Request Guidelines](#pull-request-guidelines)
13 | 5. [Reporting Issues](#reporting-issues)
14 | 6. [License](#license)
15 |
16 | ## Getting Started
17 |
18 | To get started with contributing, you'll need to:
19 |
20 | 1. Fork the repository to your GitHub account.
21 | 2. Clone the forked repository to your local machine.
22 | 3. Create a new branch for your work: `git checkout -b feature/your-feature-name`.
23 |
24 | Now, you are ready to start making changes.
25 |
26 | ## Code of Conduct
27 | Be cool and follow the norm.
28 | I have a [Code of Conduct](CODE_OF_CONDUCT.md) that I expect all contributors to adhere to. Please read it to understand the expectations for behavior within our community.
29 |
30 | ## How to Contribute
31 |
32 | You can contribute to the project in various ways:
33 |
34 | - Writing code (new features, bug fixes, improvements)
35 | - Improving documentation
36 | - Reporting and verifying issues
37 | - Reviewing and commenting on pull requests
38 | - Providing feedback and suggestions
39 |
40 | ## Pull Request Guidelines
41 |
42 | Before submitting a pull request (PR), please ensure the following:
43 |
44 | 1. Your code is clean and follows the project's coding style.
45 | 2. You have written unit tests, where applicable, and they pass.
46 | 3. The commit message is descriptive and follows the [commit message guidelines](https://www.conventionalcommits.org/).
47 | 4. You have updated the project documentation, if necessary.
48 |
49 | ## Reporting Issues
50 |
51 | If you encounter a bug, have a feature request, or have questions about the project, please open an issue in our [issue tracker](https://github.com/HauseMasterZ/swift-type/issues). When reporting issues, please provide as much detail as possible, including:
52 |
53 | - A clear and concise description of the issue or request.
54 | - Steps to reproduce the issue.
55 | - Expected behavior vs. actual behavior.
56 | - Screenshots or error messages, if applicable.
57 |
58 | ## License
59 |
60 | By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE) of this project.
61 |
62 | Thank you for your contribution!
63 |
64 | -- HauseMaster
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 HauseMaster
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ## About
39 | Visit the site here: SwiftType .
40 |
41 |
42 | SwiftType is a minimalistic typing test site that allows you to test your typing speed and accuracy. It features a simple and minimal interface, with the ability to toggle dark and light theme. Uses [Quotable API](https://github.com/lukePeavey/quotable) to fetch random quotes.
43 |
44 |
45 | ## Contributing
46 | Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for more info.
47 |
48 | ## Features
49 | - Blazingly fast
50 | - Custom fonts
51 | - Dark/Light Theme
52 | - Calculates Current WPM, Gross WPM, Net WPM, Raw WPM, accuracy, errors, time taken
53 | - Shortcut for fast restart and repeat
54 | - Input field auto clear
55 | - Result highlighting
56 | - Word highlighting (on | off)
57 | - Smooth caret (on | off)
58 | - Control + Backspace delete
59 | - Account support with tracking of scores
60 |
61 | ## Building and Testing
62 | To build and test the site locally, follow these steps:
63 |
64 | 1. Clone the repository to your local machine.
65 | 2. Install the necessary dependencies by running `npm install`
66 | 3. Create a new file in the root directory of the project called `.env.local`
67 | 4. Add the necessary environment variables to the file in the format `VARIABLE_NAME=value`
68 | 5. To build the project, run the command `npm run build`.
69 | 6. To start the development server, run the command `npm run server` or `npm start` to use Create React App Deployment.
70 | 7. Open your web browser and navigate to `http://localhost:3000` to view the site.
71 | 8. To run the tests, open a new terminal window and navigate to the project directory.
72 | 9. Run the command `npm test` to run the test suite.
73 |
74 | Make sure to add the `.envlocal` file to your `.gitignore` file to prevent it from being tracked by Git.
75 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 |
6 | | Version | Supported |
7 | | ------- | ------------------ |
8 | | 2.0.x | :white_check_mark: |
9 | | < 2.0 | :x: |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | If you discover a security vulnerability within our project, please create a new issue in our [issue tracker](https://github.com/HauseMasterZ/swift-type/issues).
14 |
15 | Please provide the following details in your report:
16 |
17 | - A clear description of the type of vulnerability.
18 | - Steps to reproduce the issue.
19 | - The potential impact of the vulnerability.
20 | - Any known solutions or patches.
21 |
22 | I are committed to providing a secure environment for our users and will address any issues as quickly as possible. I appreciate your help in making our project a safer place for everyone.
23 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/gh/images/social-media-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/gh/images/social-media-preview.png
--------------------------------------------------------------------------------
/gh/images/swift-type-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/gh/images/swift-type-logo.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swift-type-react",
3 | "version": "2.0.1",
4 | "private": true,
5 | "description": "Swift Type app",
6 | "author": "HauseMaster",
7 | "dependencies": {
8 | "@chase439/flying-focus": "^1.6.0",
9 | "@cypress/request": "^3.0.1",
10 | "@jridgewell/sourcemap-codec": "^1.4.15",
11 | "@npmcli/fs": "^3.1.0",
12 | "@testing-library/jest-dom": "^6.1.4",
13 | "@testing-library/react": "^14.0.0",
14 | "@testing-library/user-event": "^14.5.1",
15 | "axios": "^1.7.4",
16 | "babel-preset-react-app": "^10.0.1",
17 | "css-select": "^5.1.0",
18 | "firebase": "^10.14.1",
19 | "nth-check": "^2.1.1",
20 | "postcss": "^8.4.31",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0",
23 | "react-router-dom": "^6.16.0",
24 | "react-scripts": "^5.0.1",
25 | "react-toastify": "^9.1.3",
26 | "resolve-url-loader": "^5.0.0",
27 | "rollup-plugin-terser": "^7.0.2",
28 | "sass": "^1.69.5",
29 | "serialize-javascript": "^6.0.2",
30 | "svgo": "^3.0.2",
31 | "swift-type-react": "file:",
32 | "tough-cookie": "^4.1.3",
33 | "undici": "^7.3.0",
34 | "uuid": "^9.0.1",
35 | "web-vitals": "^3.5.0",
36 | "webfontloader": "^1.6.28",
37 | "workbox-background-sync": "^6.6.0"
38 | },
39 | "scripts": {
40 | "start": "react-scripts start",
41 | "build": "CI=false react-scripts build",
42 | "test": "react-scripts test",
43 | "eject": "react-scripts eject",
44 | "server": "node server.cjs"
45 | },
46 | "eslintConfig": {
47 | "extends": [
48 | "react-app",
49 | "react-app/jest"
50 | ]
51 | },
52 | "browserslist": {
53 | "production": [
54 | ">0.2%",
55 | "not dead",
56 | "not op_mini all"
57 | ],
58 | "development": [
59 | "last 1 chrome version",
60 | "last 1 firefox version",
61 | "last 1 safari version"
62 | ]
63 | },
64 | "devDependencies": {
65 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
66 | "@babel/plugin-transform-class-properties": "^7.22.5",
67 | "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11",
68 | "@babel/plugin-transform-numeric-separator": "^7.22.11",
69 | "@babel/plugin-transform-optional-chaining": "^7.23.0",
70 | "@babel/plugin-transform-private-methods": "^7.22.5",
71 | "@babel/plugin-transform-private-property-in-object": "^7.22.11",
72 | "@rollup/plugin-terser": "^0.4.4",
73 | "sass-loader": "^13.3.2"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/public/favicon.png
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
3 |
16 |
36 |
53 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Swift Type ~ HauseMasterZ
13 |
15 |
16 |
17 |
18 |
22 |
23 |
32 |
33 |
34 |
35 | You need to enable JavaScript to run this app.
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/public/logo192.png
--------------------------------------------------------------------------------
/public/logo192bak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/public/logo192bak.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/public/logo512.png
--------------------------------------------------------------------------------
/public/logo512bak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/public/logo512bak.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/server.cjs:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const rateLimit = require('express-rate-limit');
4 | const app = express();
5 | const port = 3000;
6 |
7 | // Set up rate limiting
8 | const limiter = rateLimit({
9 | windowMs: 15 * 60 * 1000, // 15 minutes
10 | max: 100 // limit each IP to 100 requests per windowMs
11 | });
12 |
13 | // Apply rate limiting to all requests
14 | app.use(limiter);
15 |
16 | // Serve static files from the 'public' directory
17 | app.use(express.static(path.join(__dirname, 'src/')));
18 |
19 | // Define a route for the root URL
20 | app.get('/', (req, res) => {
21 | res.sendFile(__dirname + './public/index.html');
22 | });
23 |
24 | app.listen(port, () => {
25 | console.log(`Server is running on http://localhost:${port}`);
26 | });
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Routes, Navigate } from 'react-router-dom';
3 | import Home from './components/Home';
4 | import Login from './components/Login';
5 | import Signup from './components/Signup';
6 | import Forgot from './components/Forgot';
7 | import NotFound from './components/404';
8 | import Verify from './components/Verify';
9 | import ProtectedRoute from './components/ProtectedRoute';
10 | import Profile from './components/Profile';
11 | import Settings from './components/Settings';
12 |
13 | function App() {
14 | return (
15 |
16 | } />
17 | } />
18 | } />
19 | } />
20 | } />
21 | } />
22 | } />
23 | } />
24 | } />
25 | } />
26 |
27 | );
28 | }
29 |
30 | export default App;
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/components/404.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Header from "./Header";
3 | import "../static/styles/styles.scss";
4 | import HamburgerMenu from "./Hamburger";
5 | function NotFound() {
6 | return (
7 |
8 |
9 |
10 |
11 |
404 Not Found
12 |
The page you are looking for does not exist.
13 |
14 |
15 | );
16 | }
17 |
18 | export default NotFound;
19 |
--------------------------------------------------------------------------------
/src/components/Forgot.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { Link } from "react-router-dom";
3 | import { collection } from "firebase/firestore";
4 | import { useNavigate } from "react-router-dom";
5 | import { auth, db } from "../firebase";
6 | import { query, where, getDocs } from "firebase/firestore";
7 | import { sendPasswordResetEmail } from "firebase/auth";
8 | import Header from "./Header";
9 | import "../static/styles/styles.scss";
10 | import HamburgerMenu from "./Hamburger";
11 | import LoadingSpinner from "./LoadingSpinner";
12 | function Forgot() {
13 | const navigate = useNavigate();
14 | const [isLoading, setIsLoading] = useState(false);
15 | const [isSent, setIsSent] = useState(false);
16 | const [email, setEmail] = useState("");
17 | const emailRef = useRef();
18 | const handleSubmit = async (event) => {
19 | event.preventDefault();
20 | setIsLoading(true);
21 | try {
22 | const usernameQuery = query(
23 | collection(db, process.env.REACT_APP_FIREBASE_COLLECTION_NAME),
24 | where(process.env.REACT_APP_USERNAME_KEY, "==", email)
25 | );
26 | const emailQuery = query(
27 | collection(db, process.env.REACT_APP_FIREBASE_COLLECTION_NAME),
28 | where(process.env.REACT_APP_EMAIL_KEY, "==", email)
29 | );
30 | const [, emailDoc] = await Promise.all([
31 | getDocs(usernameQuery),
32 | getDocs(emailQuery),
33 | ]);
34 | if (emailDoc.size === 0) {
35 | alert("Email doesnt exist");
36 | return;
37 | }
38 |
39 | await sendPasswordResetEmail(auth, email);
40 | setIsSent(true);
41 | } catch (error) {
42 | console.error(error);
43 | } finally {
44 | setIsLoading(false);
45 | }
46 | };
47 |
48 | useEffect(() => {
49 | const unsubscribe = auth.onAuthStateChanged((user) => {
50 | if (user) {
51 | navigate("/");
52 | } else {
53 | return;
54 | }
55 | });
56 |
57 | return () => unsubscribe();
58 | }, [navigate]);
59 |
60 | return (
61 |
62 |
63 |
64 | {isLoading ?
: ""}
65 | {!isSent ? (
66 |
67 |
83 |
84 |
85 | Back to Login
86 |
87 |
88 |
89 | ) : (
90 |
91 |
92 |
93 | Password reset link has been sent to your email.
94 |
95 |
96 | Login
97 |
98 |
99 |
100 | )}
101 |
102 | );
103 | }
104 |
105 | export default Forgot;
106 |
--------------------------------------------------------------------------------
/src/components/Hamburger.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | const HamburgerMenu = ({ user, handleLogoutClick, ...props }) => {
5 | const [isDropDownMenuOpen, setIsDropDownMenuOpen] = useState(false);
6 | const dropdownRef = useRef(null);
7 | const hamburgerMenuRef = useRef(null);
8 | useEffect(() => {
9 | const handleClickOutside = (event) => {
10 | if (
11 | dropdownRef.current &&
12 | !dropdownRef.current.contains(event.target) &&
13 | !hamburgerMenuRef.current.contains(event.target)
14 | ) {
15 | setIsDropDownMenuOpen(false);
16 | }
17 | };
18 |
19 | window.addEventListener("click", handleClickOutside);
20 |
21 | return () => {
22 | window.removeEventListener("click", handleClickOutside);
23 | };
24 | }, [dropdownRef]);
25 | return (
26 |
27 |
{
30 | setIsDropDownMenuOpen(!isDropDownMenuOpen);
31 | }}
32 | ref={hamburgerMenuRef}
33 | >
34 |
35 |
36 |
37 |
38 | {isDropDownMenuOpen ? (
39 |
40 | {user && props.profileData ? (
41 | <>
42 |
47 |
52 | {props.profileData.username}
53 |
54 |
55 | Account Settings
56 |
57 |
58 | Logout
59 |
60 | >
61 | ) : (
62 | <>
63 | {Object.keys(props).map(
64 | (key) =>
65 | props[key] && (
66 |
67 | {props[key]}
68 |
69 | )
70 | )}
71 | >
72 | )}
73 |
74 | ) : null}
75 |
76 | );
77 | };
78 |
79 | export default HamburgerMenu;
80 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { Link } from "react-router-dom";
3 | import "../static/styles/styles.scss";
4 |
5 | const Header = ({ toBeFocusedRef, smoothCursorBlockRef }) => {
6 | const [isDarkMode, setIsDarkMode] = useState(false);
7 | const darkLightToggleElementRef = React.useRef(null);
8 |
9 | function handleDarkLightToggleClick() {
10 | darkLightToggleElementRef.current.classList.toggle("active");
11 | document.body.classList.toggle("dark");
12 | if (!isDarkMode) {
13 | document.body.style.backgroundColor = "#18191A";
14 | smoothCursorBlockRef.current.style.backgroundColor = "#18191A";
15 | localStorage.setItem("theme", "dark");
16 | } else {
17 | document.body.style.backgroundColor = "#E4E9F7";
18 | smoothCursorBlockRef.current.style.backgroundColor = "#E4E9F7";
19 | localStorage.setItem("theme", "light");
20 | }
21 | setIsDarkMode(!isDarkMode);
22 | if (toBeFocusedRef && toBeFocusedRef.current) {
23 | toBeFocusedRef.current.focus();
24 | }
25 | }
26 |
27 | useEffect(() => {
28 | const preferredTheme = localStorage.getItem("theme");
29 | if (preferredTheme === "dark") {
30 | setIsDarkMode(true);
31 | document.body.classList.add("dark");
32 | darkLightToggleElementRef.current.classList.add("active");
33 | } else {
34 | setIsDarkMode(false);
35 | document.body.classList.remove("dark");
36 | darkLightToggleElementRef.current.classList.remove("active");
37 | }
38 | }, []);
39 |
40 | return (
41 |
42 |
43 |
44 | Swift Type ~ HauseMaster
45 |
46 |
47 |
52 |
53 |
54 |
55 |
64 |
65 | );
66 | };
67 |
68 | export default Header;
69 |
--------------------------------------------------------------------------------
/src/components/LoadingSpinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const LoadingSpinner = () => {
4 | return (
5 |
15 | );
16 | };
17 |
18 | export default LoadingSpinner;
19 |
--------------------------------------------------------------------------------
/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { auth } from "../firebase";
3 | import { Link } from "react-router-dom";
4 | import { useNavigate } from "react-router-dom";
5 | import { signInWithEmailAndPassword, signOut } from "firebase/auth";
6 | import { toast } from "react-toastify";
7 | import "react-toastify/dist/ReactToastify.css";
8 | import "../static/styles/styles.scss";
9 | import Header from "./Header";
10 | import HamburgerMenu from "./Hamburger";
11 | import LoadingSpinner from "./LoadingSpinner";
12 | function Login() {
13 | const [email, setEmail] = useState("");
14 | const [password, setPassword] = useState("");
15 | const [isLoading, setIsLoading] = useState(false);
16 | const emailRef = useRef();
17 | const navigate = useNavigate();
18 |
19 | const handleUsernameChange = (event) => {
20 | setEmail(event.target.value);
21 | };
22 |
23 | const handlePasswordChange = (event) => {
24 | setPassword(event.target.value);
25 | };
26 |
27 | const handleSubmit = async (event) => {
28 | event.preventDefault();
29 | setIsLoading(true);
30 | try {
31 | const userCredential = await signInWithEmailAndPassword(
32 | auth,
33 | email,
34 | password
35 | );
36 | const user = userCredential.user;
37 | if (!user.emailVerified) {
38 | alert("Please verify your email before logging in");
39 | await signOut(auth);
40 | setIsLoading(false);
41 | return;
42 | }
43 | toast.success("Login successful");
44 | setIsLoading(false);
45 | navigate("/");
46 | } catch (error) {
47 | console.error(error);
48 | alert(error.message);
49 | setIsLoading(false);
50 | }
51 | };
52 |
53 | useEffect(() => {
54 | let unsubscribe = null;
55 | try {
56 | unsubscribe = auth.onAuthStateChanged((user) => {
57 | if (user && user.emailVerified) {
58 | navigate("/");
59 | return;
60 | } else {
61 | return;
62 | }
63 | });
64 | } catch (error) {
65 | console.error(error);
66 | }
67 | return () => {
68 | if (unsubscribe) unsubscribe();
69 | };
70 | }, []);
71 |
72 | return (
73 |
74 |
75 |
76 | {isLoading ?
: ""}
77 |
78 |
105 |
106 | Forgot Password?
107 |
108 |
109 | Don't have an account? Create one
110 |
111 |
112 |
113 | );
114 | }
115 |
116 | export default Login;
117 |
--------------------------------------------------------------------------------
/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "../static/styles/styles.scss";
3 |
4 | const Modal = ({ isOpen, onClose, onApply, modalInputRef }) => {
5 | const [inputValue, setInputValue] = useState("");
6 |
7 | const handleApply = () => {
8 | onApply(inputValue);
9 | setInputValue("");
10 | onClose();
11 | };
12 |
13 | const handleCancel = () => {
14 | setInputValue("");
15 | onClose();
16 | };
17 |
18 | if (!isOpen) return null;
19 |
20 | return (
21 |
22 |
23 |
24 |
e.stopPropagation()}>
25 |
26 | Enter Custom Text
27 | setInputValue(e.target.value)}
34 | onKeyDown={(e) => {
35 | if (e.key === "Enter") {
36 | handleApply();
37 | }
38 | }}
39 | />
40 |
41 |
42 | Apply
43 |
44 |
45 | Cancel
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default Modal;
55 |
--------------------------------------------------------------------------------
/src/components/Profile.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import { auth, db, storage } from "../firebase";
3 | import { doc, getDoc, updateDoc } from "firebase/firestore";
4 | import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
5 | import { useLocation } from "react-router-dom";
6 | import "../static/styles/styles.scss";
7 | import Header from "./Header";
8 | import HamburgerMenu from "./Hamburger";
9 | import LoadingSpinner from "./LoadingSpinner";
10 | function Profile(props) {
11 | const [user, setUser] = useState(null);
12 | const [isLoading, setIsLoading] = useState(false);
13 | const [email, setEmail] = useState("");
14 | const [username, setUsername] = useState("");
15 | const [profilePhotoUrl, setProfilePhotoUrl] = useState("");
16 | const [totalRacesTaken, setTotalRacesTaken] = useState(0);
17 | const [totalAvgAccuracy, setTotalAvgAccuracy] = useState(0);
18 | const [totalAverageWpm, setTotalAverageWpm] = useState(0);
19 | const MAX_FILE_SIZE = 6 * 1024 * 1024; // 6MB
20 | const location = useLocation();
21 | const fileInputRef = useRef(null);
22 | const profileData = location.state?.profileData;
23 | useEffect(() => {
24 | if (profileData) {
25 | setUsername(profileData.username);
26 | setProfilePhotoUrl(profileData.profilePhotoUrl);
27 | setTotalRacesTaken(profileData.totalRacesTaken);
28 | setTotalAvgAccuracy(profileData.totalAvgAccuracy);
29 | setTotalAverageWpm(profileData.totalAvgWpm);
30 | }
31 | }, [profileData]);
32 |
33 | function handleProfileAvatarClick() {
34 | fileInputRef.current.accept = "image/*"; // Only allow image files
35 | fileInputRef.current.click();
36 | }
37 |
38 | const handleFileInputChange = async (event) => {
39 | setIsLoading(true);
40 | const file = event.target.files[0]; // Get the selected file
41 | if (file.size > MAX_FILE_SIZE) {
42 | console.error("File size exceeds the limit of 6MB");
43 | setIsLoading(false);
44 | return;
45 | }
46 | const storageRef = ref(storage, `avatars/${file.name}`); // Create a reference to the storage location
47 |
48 | try {
49 | await uploadBytes(storageRef, file);
50 |
51 | const downloadURL = await getDownloadURL(storageRef);
52 | setProfilePhotoUrl(downloadURL);
53 | } catch (error) {
54 | console.error("Error uploading file:", error);
55 | setIsLoading(false);
56 | }
57 | };
58 |
59 | useEffect(() => {
60 | if (!user) {
61 | return;
62 | }
63 | if (
64 | profilePhotoUrl !== "" &&
65 | profilePhotoUrl !== process.env.REACT_APP_DEFAULT_PROFILE_PHOTO_URL
66 | ) {
67 | const userRef = doc(
68 | db,
69 | process.env.REACT_APP_FIREBASE_COLLECTION_NAME,
70 | user.uid
71 | );
72 | updateDoc(userRef, {
73 | [process.env.REACT_APP_PROFILE_PHOTO_URL_KEY]: profilePhotoUrl,
74 | }).catch((error) => {
75 | console.error(
76 | "Error updating profile photo URL in the database:",
77 | error
78 | );
79 | });
80 | setIsLoading(false);
81 | }
82 | }, [profilePhotoUrl]);
83 |
84 | // useEffect(() => {
85 | // const unsubscribe = auth.onAuthStateChanged((user) => {
86 | // if (user) {
87 | // setUser(user);
88 | // setEmail(user.email);
89 |
90 | // const userRef = doc(db, process.env.REACT_APP_FIREBASE_COLLECTION_NAME, user.uid);
91 | // getDoc(userRef).then((doc) => {
92 | // if (doc.exists()) {
93 | // const data = doc.data();
94 | // setUsername(data[process.env.REACT_APP_USERNAME_KEY]);
95 | // setProfilePhotoUrl(data[process.env.REACT_APP_PROFILE_PHOTO_URL_KEY]);
96 | // setTotalRacesTaken(data[process.env.REACT_APP_TOTAL_RACES_TAKEN_KEY]);
97 | // setTotalAvgAccuracy(data[process.env.REACT_APP_TOTAL_AVG_ACCURACY_KEY]);
98 | // setTotalAverageWpm(data[process.env.REACT_APP_TOTAL_AVG_WPM_KEY]);
99 | // } else {
100 | // console.log('No such document!');
101 | // }
102 | // }).catch((error) => {
103 | // console.log('Error getting document:', error);
104 | // });
105 | // } else {
106 | // setUser(null);
107 | // setUsername('');
108 | // setEmail('');
109 | // setProfilePhotoUrl('');
110 | // setTotalRacesTaken(0);
111 | // setTotalAvgAccuracy(0);
112 | // navigate('/');
113 | // return;
114 | // }
115 | // });
116 |
117 | // // Clean up the observer when the component unmounts
118 | // return () => unsubscribe();
119 | // }, []);
120 |
121 | return (
122 |
123 |
124 |
125 | {isLoading ?
: ""}
126 |
127 |
128 |
134 |
{username}
135 |
{email}
136 |
137 |
143 |
144 |
145 |
146 |
Total Races Taken
147 | {totalRacesTaken}
148 |
149 |
150 |
Total Average Accuracy
151 | {totalAvgAccuracy}%
152 |
153 |
154 |
Total Average WPM
155 | {totalAverageWpm} WPM
156 |
157 |
158 |
159 | );
160 | }
161 |
162 | export default Profile;
163 |
--------------------------------------------------------------------------------
/src/components/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Navigate } from "react-router-dom";
3 | import { auth } from "../firebase";
4 |
5 | function ProtectedRoute({ component: Component, ...rest }) {
6 | const user = auth.currentUser;
7 |
8 | return user ? : ;
9 | }
10 |
11 | export default ProtectedRoute;
12 |
--------------------------------------------------------------------------------
/src/components/Settings.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { doc, deleteDoc } from "firebase/firestore";
3 | import { auth, db } from "../firebase";
4 | import { useNavigate } from "react-router-dom";
5 | import { toast } from "react-toastify";
6 | import "react-toastify/dist/ReactToastify.css";
7 | import Header from "./Header";
8 | import "../static/styles/styles.scss";
9 | import HamburgerMenu from "./Hamburger";
10 | import LoadingSpinner from "./LoadingSpinner";
11 | function Settings() {
12 | const [isLoading, setIsLoading] = useState(false);
13 | const [showModal, setShowModal] = useState(false);
14 | const navigate = useNavigate();
15 |
16 | const handleDeleteAccountClick = () => {
17 | setShowModal(true);
18 | };
19 |
20 | const handleNoClick = () => {
21 | setShowModal(false);
22 | };
23 |
24 | const handleYesClick = async () => {
25 | setIsLoading(true);
26 | const user = auth.currentUser;
27 | const userId = user.uid;
28 | const userRef = doc(
29 | db,
30 | process.env.REACT_APP_FIREBASE_COLLECTION_NAME,
31 | userId
32 | );
33 | try {
34 | await deleteDoc(userRef);
35 | try {
36 | await user.delete();
37 | toast.warn("Account deleted");
38 | setIsLoading(false);
39 | navigate("/");
40 | } catch (error) {
41 | console.error("Error deleting user data:", error);
42 | setIsLoading(false);
43 | alert("Error deleting user, requires recent login");
44 | }
45 | } catch (error) {
46 | console.log(error);
47 | alert("Error deleting user data please try again later");
48 | setIsLoading(false);
49 | }
50 | };
51 |
52 | return (
53 |
54 |
55 |
56 | {isLoading ?
: ""}
57 |
58 |
63 | Delete Account
64 |
65 |
66 | {showModal && (
67 |
68 |
69 |
ARE YOU SURE?
70 |
71 |
76 | Yes
77 |
78 |
79 | No
80 |
81 |
82 |
83 |
84 | )}
85 |
86 | );
87 | }
88 |
89 | export default Settings;
90 |
--------------------------------------------------------------------------------
/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { Link } from "react-router-dom";
3 | import {
4 | createUserWithEmailAndPassword,
5 | sendEmailVerification,
6 | } from "firebase/auth";
7 | import { doc, setDoc } from "firebase/firestore";
8 | import Header from "./Header";
9 | import { useNavigate } from "react-router-dom";
10 | import { auth, db } from "../firebase";
11 | import { onEnd } from "../static/scripts/flying-focus";
12 | import "../static/styles/styles.scss";
13 | import HamburgerMenu from "./Hamburger";
14 | import LoadingSpinner from "./LoadingSpinner";
15 |
16 | function Signup() {
17 | const [isLoading, setIsLoading] = useState(false);
18 | const usernameRef = useRef();
19 | const emailRef = useRef();
20 | const passwordRef = useRef();
21 | const confirmPasswordRef = useRef();
22 | const navigate = useNavigate();
23 |
24 | const handleSubmit = async (event) => {
25 | event.preventDefault();
26 | setIsLoading(true);
27 | const username = usernameRef.current.value;
28 | const email = emailRef.current.value;
29 | const password = passwordRef.current.value;
30 | const confirmPassword = confirmPasswordRef.current.value;
31 |
32 | if (password !== confirmPassword) {
33 | alert("Passwords do not match");
34 | setIsLoading(false);
35 | return;
36 | }
37 |
38 | try {
39 | const userCredential = await createUserWithEmailAndPassword(
40 | auth,
41 | email,
42 | password
43 | );
44 | const user = userCredential.user;
45 | const userDocRef = doc(
46 | db,
47 | process.env.REACT_APP_FIREBASE_COLLECTION_NAME,
48 | user.uid
49 | );
50 | await setDoc(userDocRef, {});
51 | await setDoc(userDocRef, {
52 | [process.env.REACT_APP_CREATED_AT_KEY]: new Date(),
53 | [process.env.REACT_APP_PROFILE_PHOTO_URL_KEY]:
54 | process.env.REACT_APP_DEFAULT_PROFILE_PHOTO_URL,
55 | [process.env.REACT_APP_TOTAL_RACES_TAKEN_KEY]: 0,
56 | [process.env.REACT_APP_TOTAL_AVG_WPM_KEY]: 0,
57 | [process.env.REACT_APP_TOTAL_AVG_ACCURACY_KEY]: 0,
58 | [process.env.REACT_APP_USERNAME_KEY]: username,
59 | [process.env.REACT_APP_EMAIL_KEY]: email,
60 | });
61 | onEnd();
62 | await sendEmailVerification(user);
63 | setIsLoading(false);
64 | navigate("/verify");
65 | } catch (error) {
66 | console.error(error);
67 | alert(error.message);
68 | setIsLoading(false);
69 | }
70 | };
71 |
72 | return (
73 |
74 |
75 |
76 | {isLoading ?
: ""}
77 |
78 |
79 |
125 |
126 | Already have an account? Login
127 |
128 |
129 |
130 | );
131 | }
132 |
133 | export default Signup;
134 |
--------------------------------------------------------------------------------
/src/components/Verify.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import Header from "./Header";
4 | import "../static/styles/styles.scss";
5 |
6 | function Verify() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Verification link has been sent to your email.
14 |
15 |
16 | Login
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default Verify;
25 |
--------------------------------------------------------------------------------
/src/firebase.js:
--------------------------------------------------------------------------------
1 | import { initializeApp } from 'firebase/app';
2 | import { getAuth } from 'firebase/auth';
3 | import { getFirestore } from 'firebase/firestore';
4 | import { getStorage } from 'firebase/storage';
5 |
6 | const firebaseConfig = {
7 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
8 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
9 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
10 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
11 | messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
12 | appId: process.env.REACT_APP_FIREBASE_APP_ID,
13 | measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
14 | };
15 |
16 | let app, auth, db, storage;
17 |
18 | try {
19 | app = initializeApp(firebaseConfig);
20 | auth = getAuth(app);
21 | db = getFirestore(app);
22 | storage = getStorage(app);
23 | } catch (error) {
24 | console.error('Error initializing Firebase:', error);
25 | }
26 |
27 | export { app, auth, db, storage };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 |
5 | import App from './App';
6 | import reportWebVitals from './reportWebVitals';
7 |
8 | const root = ReactDOM.createRoot(document.getElementById('root'));
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
21 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/static/data/thresholds.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-04/schema#",
3 | "title": "User",
4 | "description": "A list of users",
5 | "type": "array",
6 | "items": {
7 | "title": "User",
8 | "description": "A user",
9 | "type": "object",
10 | "properties": {
11 | "id": {
12 | "description": "The unique identifier for a user",
13 | "type": "integer"
14 | },
15 | "name": {
16 | "description": "Name of the user",
17 | "type": "string"
18 | },
19 | "email": {
20 | "description": "Email of the user",
21 | "type": "string"
22 | },
23 | "password": {
24 | "description": "Password of the user",
25 | "type": "string"
26 | },
27 | "created_at": {
28 | "description": "Date of creation",
29 | "type": "string"
30 | },
31 | "updated_at": {
32 | "description": "Date of last update",
33 | "type": "string"
34 | }
35 | },
36 | "required": [
37 | "id",
38 | "name",
39 | "email",
40 | "password",
41 | "created_at",
42 | "updated_at"
43 | ]
44 | },
45 | "thresholds": [
46 | {
47 | "threshold": 0,
48 | "imgSrc": [
49 | {
50 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/sloth.svg",
51 | "alt": "Sloth"
52 | },
53 | {
54 | "1": "../static/images/sloth.svg",
55 | "alt": "Sloth"
56 | }
57 | ],
58 | "title": "Sloth-paced Typist 🐌🦥",
59 | "speed": "Math.random() * (10 - 5) + 5",
60 | "stars": "⭐",
61 | "backgroundColor": "#C69061"
62 | },
63 | {
64 | "threshold": 20,
65 | "imgSrc": [
66 | {
67 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/sea_turtle.svg",
68 | "alt": "Turtle"
69 | },
70 | {
71 | "1": "../static/images/sea_turtle.svg",
72 | "alt": "Turtle"
73 | }
74 | ],
75 | "title": "Turtle-paced Typist 🐢",
76 | "speed": "Math.random() * (50 - 10) + 10",
77 | "stars": "⭐",
78 | "backgroundColor": "rgb(151, 54, 193)"
79 | },
80 | {
81 | "threshold": 40,
82 | "imgSrc": [
83 | {
84 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/horse.svg",
85 | "alt": "Horse"
86 | },
87 | {
88 | "1": "../static/images/horse.svg",
89 | "alt": "Horse"
90 | }
91 | ],
92 | "title": "Horse-speed Typist 🐎",
93 | "speed": "Math.random() * (90 - 50) + 50",
94 | "stars": "⭐⭐",
95 | "backgroundColor": "#BFAA87"
96 | },
97 | {
98 | "threshold": 60,
99 | "imgSrc": [
100 | {
101 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/lion.svg",
102 | "alt": "Lion"
103 | },
104 | {
105 | "1": "../static/images/lion.svg",
106 | "alt": "Lion"
107 | }
108 | ],
109 | "title": "Lion-fingered Typist 🦁",
110 | "speed": "Math.random() * (120 - 90) + 90",
111 | "stars": "⭐⭐",
112 | "backgroundColor": "#DD8547"
113 | },
114 | {
115 | "threshold": 80,
116 | "imgSrc": [
117 | {
118 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/cheetah.svg",
119 | "alt": "Cheetah"
120 | },
121 | {
122 | "1": "../static/images/cheetah.svg",
123 | "alt": "Cheetah"
124 | }
125 | ],
126 | "title": "Cheetah-swift Typist 🐆",
127 | "speed": "Math.random() * (180 - 120) + 120",
128 | "stars": "⭐⭐⭐",
129 | "backgroundColor": "#DC864B"
130 | },
131 | {
132 | "threshold": 100,
133 | "imgSrc": [
134 | {
135 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/eagle.svg",
136 | "alt": "Eagle"
137 | },
138 | {
139 | "1": "../static/images/eagle.svg",
140 | "alt": "Eagle"
141 | }
142 | ],
143 | "title": "Eagle-eyed Typist 🕊️",
144 | "speed": "Math.random() * (300 - 180) + 180",
145 | "stars": "⭐⭐⭐⭐",
146 | "backgroundColor": "#AB7D5A"
147 | },
148 | {
149 | "threshold": 120,
150 | "imgSrc": [
151 | {
152 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/falcon.svg",
153 | "alt": "Falcon"
154 | },
155 | {
156 | "1": "../static/images/falcon.svg",
157 | "alt": "Falcon"
158 | }
159 | ],
160 | "title": "Falcon-keyed Typist 🦅",
161 | "speed": "Math.random() * (400 - 300) + 300",
162 | "stars": "⭐⭐⭐⭐",
163 | "backgroundColor": "rgb(72, 59, 197)"
164 | },
165 | {
166 | "threshold": 140,
167 | "imgSrc": [
168 | {
169 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/hausemaster.svg",
170 | "alt": "HauseMaster"
171 | },
172 | {
173 | "1": "../static/images/hausemaster.svg",
174 | "alt": "HauseMaster"
175 | }
176 | ],
177 | "title": "Supersonic Typist 🚀 AKA HauseMaster",
178 | "speed": "Math.random() * (1000 - 300) + 300",
179 | "stars": "⭐⭐⭐⭐⭐",
180 | "backgroundColor": "#D21404"
181 | },
182 | {
183 | "threshold": 160,
184 | "imgSrc": [
185 | {
186 | "0": "https://raw.githubusercontent.com/HauseMasterZ/swift-type/main/src/static/images/flash.svg",
187 | "alt": "Flash"
188 | },
189 | {
190 | "1": "../static/images/flash.svg",
191 | "alt": "Flash"
192 | }
193 | ],
194 | "title": "Lightning-Fast Typist ⚡️",
195 | "speed": "300000",
196 | "stars": "⭐⭐⭐⭐⭐",
197 | "backgroundColor": "rgb(230, 230, 0)"
198 | }
199 | ]
200 | }
--------------------------------------------------------------------------------
/src/static/icons/favicon-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HauseMasterZ/swift-type/3e686accbd5c5f67136c64ed92256913908d8a32/src/static/icons/favicon-64x64.png
--------------------------------------------------------------------------------
/src/static/images/cheetah.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/images/eagle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/images/falcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/images/flash.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.15, written by Peter Selinger 2001-2017
9 |
10 |
12 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/static/images/horse.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/images/lion.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/images/sea_turtle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/images/sloth.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/static/scripts/flying-focus.js:
--------------------------------------------------------------------------------
1 | const DURATION = 500;
2 | // const win = window;
3 | const doc = document;
4 | const docElem = doc.documentElement;
5 | let ringElem = null;
6 | let movingId = 0;
7 | let prevFocused = null;
8 | let keyDownTime = 0;
9 | const body = doc.body;
10 | docElem.addEventListener('keydown', function (event) {
11 | // Show animation only upon Tab or Arrow keys press.
12 | // var key = event.key;
13 | // if (key !== 'Tab' && key !== 'ArrowUp' && key !== 'ArrowDown' && key !== 'ArrowLeft' && key !== 'ArrowRight') {
14 | // return;
15 | // }
16 | keyDownTime = Date.now();
17 | }, false);
18 |
19 | docElem.addEventListener('focus', (event) => {
20 | const target = event.target;
21 | if (target.id === 'flying-focus') {
22 | return;
23 | }
24 |
25 | const isFirstFocus = !ringElem;
26 | if (isFirstFocus) {
27 | initialize();
28 | }
29 |
30 | const offset = offsetOf(target);
31 | ringElem.style.left = `${offset.left}px`;
32 | ringElem.style.top = `${offset.top}px`;
33 | ringElem.style.width = `${target.offsetWidth}px`;
34 | ringElem.style.height = `${target.offsetHeight}px`;
35 |
36 | if (isFirstFocus || !isJustPressed()) {
37 | return;
38 | }
39 |
40 | onEnd();
41 | target.classList.add('flying-focus_target');
42 | ringElem.classList.add('flying-focus_visible');
43 | prevFocused = target;
44 |
45 | }, true);
46 |
47 | docElem.addEventListener('blur', () => {
48 | onEnd();
49 | }, false);
50 |
51 | function initialize() {
52 | ringElem = doc.createElement('div');
53 | ringElem.id = 'flying-focus';
54 | ringElem.style.transitionDuration = `${DURATION / 1000}s`;
55 | body.appendChild(ringElem);
56 | }
57 |
58 | function onEnd() {
59 | movingId = 0;
60 | if (prevFocused) {
61 | ringElem.classList.remove('flying-focus_visible');
62 | prevFocused.classList.remove('flying-focus_target');
63 | prevFocused = null;
64 | }
65 | }
66 |
67 | function isJustPressed() {
68 | return Date.now() - keyDownTime < 42;
69 | }
70 |
71 | function offsetOf(elem) {
72 | const rect = elem.getBoundingClientRect();
73 | const clientLeft = docElem.clientLeft || body.clientLeft;
74 | const clientTop = docElem.clientTop || body.clientTop;
75 | const scrollLeft = docElem.scrollLeft || body.scrollLeft;
76 | const scrollTop = docElem.scrollTop || body.scrollTop;
77 | const left = rect.left + scrollLeft - clientLeft;
78 | const top = rect.top + scrollTop - clientTop;
79 | return {
80 | top: top || 0,
81 | left: left || 0,
82 | };
83 | }
84 |
85 | export { onEnd };
--------------------------------------------------------------------------------
/src/static/styles/styles.scss:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600&display=swap");
2 | @import url("https://fonts.googleapis.com/css?family=Roboto&display=swap");
3 | @import url("https://fonts.googleapis.com/css2?family=Play&display=swap");
4 | @import url("https://fonts.googleapis.com/css?family=Ubuntu&display=swap");
5 | @import url("https://fonts.googleapis.com/css2?family=Anton&family=Arimo&family=Assistant&family=Dancing+Script&family=EB+Garamond&family=Lato&family=Montserrat&family=Nunito:ital@1&family=Pacifico&family=Poppins&display=swap");
6 | @import url("https://fonts.googleapis.com/css2?family=Oswald:wght@400;700&display=swap");
7 | @import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css");
8 | @import url("https://unpkg.com/boxicons@2.1.1/css/boxicons.min.css");
9 |
10 | :root {
11 | --body-color: #e4e9f7;
12 | --text-color: black;
13 | --text-color-rgb: 0, 0, 0;
14 | --input-background-color: #fff;
15 | }
16 |
17 | .no-style {
18 | text-decoration: none;
19 | color: inherit;
20 | }
21 |
22 | .hidden {
23 | display: none;
24 | }
25 |
26 | body {
27 | overflow-x: hidden;
28 | font-family: "Open Sans", sans-serif, Arial;
29 | background-color: var(--body-color);
30 | color: var(--text-color);
31 | transition: background-color 1s ease;
32 | }
33 |
34 | body.dark {
35 | --body-color: #18191a;
36 | --text-color: white;
37 | --text-color-rgb: 75, 75, 75;
38 | --input-background-color: #333333;
39 | color: var(--text-color);
40 | }
41 |
42 | ::selection {
43 | background-color: #ff6347;
44 | color: white;
45 | }
46 |
47 | ::-webkit-scrollbar {
48 | display: none;
49 | }
50 |
51 | .correct {
52 | color: #07c944;
53 | }
54 |
55 | .incorrect {
56 | color: red;
57 | }
58 |
59 | h1 {
60 | font-size: 24px;
61 | margin-bottom: 20px;
62 | }
63 |
64 | #title span:first-child {
65 | color: #60d3fe;
66 | }
67 |
68 | #title span:last-child {
69 | color: #bc8dfd;
70 | }
71 |
72 | .instruction {
73 | font-weight: bold;
74 | color: #3191d6;
75 | }
76 |
77 | #accuracyDisplay {
78 | transition: color 1s ease;
79 | }
80 |
81 | #errorsDisplay {
82 | transition: color 1s ease;
83 | }
84 |
85 | .dark-light {
86 | left: 0px;
87 | transform: translate(-40%, 0%);
88 | }
89 |
90 | .dark-light i {
91 | position: absolute;
92 | color: var(--text-color);
93 | font-size: 22px;
94 | cursor: pointer;
95 | transition: all 0.3s ease;
96 | }
97 |
98 | .dark-light i.sun {
99 | opacity: 1;
100 | }
101 |
102 | .dark-light i.moon {
103 | opacity: 0;
104 | }
105 |
106 | .dark-light.active i.sun {
107 | opacity: 0;
108 | }
109 |
110 | .dark-light.active i.moon {
111 | opacity: 1;
112 | }
113 |
114 | .github {
115 | right: 0px;
116 | transform: translate(33%, 0%);
117 | margin: 0px;
118 | padding: 0px;
119 | }
120 |
121 | .github i {
122 | margin: 0px;
123 | padding: 0px;
124 | position: absolute;
125 | color: var(--text-color);
126 | font-size: 22px;
127 | cursor: pointer;
128 | transition: all 0.3s ease;
129 | }
130 |
131 | .container {
132 | max-width: min(700px, 98vw);
133 | margin: 0 auto;
134 | text-align: center;
135 | padding: 0;
136 | padding-top: 2rem;
137 | }
138 |
139 | #quote {
140 | font-size: 20px;
141 | margin-bottom: 20px;
142 | max-width: 100%;
143 | display: flex;
144 | flex-direction: row;
145 | flex-wrap: wrap;
146 | text-align: center;
147 | justify-content: center;
148 | }
149 |
150 | .word {
151 | position: relative;
152 | display: inline;
153 | font-variant-ligatures: none;
154 | margin-right: 5px;
155 | /* Adjust this value to control the spacing between words */
156 | }
157 |
158 | .letter {
159 | display: inline;
160 | font-variant-ligatures: none;
161 | margin-right: 0px;
162 | /* Adjust this value to control the spacing between letters */
163 | }
164 |
165 | @keyframes font-size-highlight {
166 | to {
167 | font-size: 36px;
168 | }
169 | }
170 |
171 | @keyframes font-size-category {
172 | to {
173 | font-size: 24px;
174 | }
175 | }
176 |
177 | #inputBox {
178 | width: 100%;
179 | height: 40px;
180 | margin-bottom: 20px;
181 | padding: 5px;
182 | box-sizing: border-box;
183 | background-color: var(--input-background-color);
184 | border: 1px solid var(--text-color);
185 | color: var(--text-color);
186 | transition: background-color 1s ease;
187 | }
188 |
189 | .hamburger-menu {
190 | position: absolute;
191 | top: 0;
192 | right: 0;
193 | margin-top: 20px;
194 | margin-right: 20px;
195 | }
196 |
197 | .hamburger-icon {
198 | display: flex;
199 | flex-direction: column;
200 | justify-content: space-between;
201 | height: 18px;
202 | cursor: pointer;
203 | }
204 |
205 | .hamburger-icon span {
206 | display: block;
207 | width: 24px;
208 | height: 2px;
209 | background-color: var(--text-color);
210 | }
211 |
212 | .dropdown-menu {
213 | display: none;
214 | position: absolute;
215 | top: 100%;
216 | right: 0;
217 | padding-left: 5px;
218 | padding-right: 5px;
219 | user-select: none;
220 | border-color: var(--input-background-color);
221 | background-color: var(--body-color);
222 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
223 | animation: dropdown-menu-expand 0.3s ease;
224 | cursor: pointer;
225 | }
226 |
227 | @keyframes dropdown-menu-expand {
228 | from {
229 | opacity: 0;
230 | transform: translateY(-10px);
231 | }
232 |
233 | to {
234 | opacity: 1;
235 | transform: translateY(0);
236 | }
237 | }
238 |
239 | .dropdown-menu a {
240 | display: block;
241 | padding: 8px 4px;
242 | color: var(--text-color);
243 | text-decoration: none;
244 | }
245 |
246 | .dropdown-menu a:hover {
247 | background-color: rgba(var(--text-color-rgb), 0.1);
248 | }
249 |
250 | .show {
251 | display: block;
252 | }
253 |
254 | #categoryDisplay {
255 | padding-top: 1rem;
256 | transform: translate3d(0, 0, 0);
257 | }
258 |
259 | #font-select {
260 | color: var(--text-color);
261 | border: none;
262 | background-color: transparent;
263 | outline: none;
264 | // transition: color 1s ease;
265 | }
266 |
267 | #font-select:focus {
268 | outline: none;
269 | }
270 |
271 | #font-select:hover {
272 | cursor: pointer;
273 | }
274 |
275 | #font-select option {
276 | appearance: none;
277 | box-shadow: none;
278 | border: none;
279 | outline: none;
280 | background-color: var(--body-color);
281 | color: var(--text-color);
282 | }
283 |
284 | option[value="Open Sans"] {
285 | font-family: "Open Sans";
286 | }
287 |
288 | option[value="Roboto"] {
289 | font-family: "Roboto";
290 | }
291 |
292 | option[value="Oswald"] {
293 | font-family: "Oswald";
294 | }
295 |
296 | option[value="Play"] {
297 | font-family: "Play";
298 | }
299 |
300 | option[value="Ubuntu"] {
301 | font-family: "Ubuntu";
302 | }
303 |
304 | option[value="Anton"] {
305 | font-family: "Anton";
306 | }
307 |
308 | option[value="Arimo"] {
309 | font-family: "Arimo";
310 | }
311 |
312 | option[value="Assistant"] {
313 | font-family: "Assistant";
314 | }
315 |
316 | option[value="Dancing Script"] {
317 | font-family: "Dancing Script";
318 | }
319 |
320 | option[value="EB Garamond"] {
321 | font-family: "EB Garamond";
322 | }
323 |
324 | option[value="Lato"] {
325 | font-family: "Lato";
326 | }
327 |
328 | option[value="Nunito"] {
329 | font-family: "Nunito";
330 | }
331 |
332 | option[value="Montserrat"] {
333 | font-family: "Montserrat";
334 | }
335 |
336 | option[value="Pacifico"] {
337 | font-family: "Pacifico";
338 | }
339 |
340 | option[value="Poppins"] {
341 | font-family: "Poppins";
342 | }
343 |
344 | .block {
345 | position: relative;
346 | background: var(--body-color);
347 | }
348 |
349 | .glow::before,
350 | .glow::after {
351 | content: "";
352 | position: absolute;
353 | top: -1.5px;
354 | left: -1.5px;
355 | background: linear-gradient(
356 | 45deg,
357 | #e6fb04,
358 | #ff6600,
359 | #00ff66,
360 | #00ffff,
361 | #ff00ff,
362 | #6e0dd0,
363 | #ff3300,
364 | #099fff
365 | );
366 | background-size: 400%;
367 | width: calc(100% + 3px);
368 | height: calc(100% + 3px);
369 | z-index: -1;
370 | animation: glowing 20s linear infinite;
371 | }
372 |
373 | @keyframes glowing {
374 | 0% {
375 | background-position: 0 0;
376 | }
377 |
378 | 50% {
379 | background-position: 500% 0;
380 | }
381 |
382 | 100% {
383 | background-position: 0 0;
384 | }
385 | }
386 |
387 | .glow::after {
388 | filter: blur(20px);
389 | }
390 |
391 | #smoothCursor {
392 | cursor: pointer;
393 | user-select: none;
394 | transition: background-color 1s ease;
395 | // border: 1px solid var(--text-color);
396 | }
397 |
398 | .cursor {
399 | display: block;
400 | position: absolute;
401 | top: 0px;
402 | left: 0px;
403 | height: 1.5em;
404 | width: 2px;
405 | background-color: #4caf50;
406 | transition: top 0.25s ease-out;
407 | pointer-events: none;
408 | // animation: cursor-blink 1s infinite;
409 | }
410 |
411 | @keyframes cursor-blink {
412 | 0% {
413 | opacity: 1;
414 | }
415 |
416 | 50% {
417 | opacity: 0;
418 | }
419 |
420 | 100% {
421 | opacity: 1;
422 | }
423 | }
424 |
425 | .cursor.active {
426 | animation: cursor-active 1s forwards;
427 | }
428 |
429 | @keyframes cursor-active {
430 | 0% {
431 | opacity: 1;
432 | }
433 |
434 | 100% {
435 | opacity: 1;
436 | }
437 | }
438 |
439 | #highlighted-words {
440 | cursor: pointer;
441 | user-select: none;
442 | }
443 |
444 | .active-word {
445 | opacity: 1;
446 | }
447 |
448 | .subactive-word {
449 | opacity: 0.5;
450 | }
451 |
452 | .inactive-word {
453 | opacity: 0.3;
454 | }
455 |
456 | #timerDisplay {
457 | margin-bottom: 20px;
458 | }
459 |
460 | .shrink-animation {
461 | animation: shrinkAndGrow 0.5s;
462 | }
463 |
464 | @keyframes shrinkAndGrow {
465 | 0% {
466 | transform: scale(1);
467 | }
468 |
469 | 50% {
470 | transform: scale(0.9);
471 | }
472 |
473 | 100% {
474 | transform: scale(1);
475 | }
476 | }
477 |
478 | .ripple {
479 | position: absolute;
480 | border-radius: 50%;
481 | background-color: rgba(15, 95, 240, 0.7);
482 | transform: scale(0);
483 | animation: ripple-animation 0.6s linear;
484 | pointer-events: none;
485 | }
486 |
487 | @keyframes ripple-animation {
488 | to {
489 | transform: scale(4);
490 | opacity: 0;
491 | }
492 | }
493 |
494 | #resultImg {
495 | position: absolute;
496 | height: 50%;
497 | width: 30%;
498 | left: 70%;
499 | user-select: none;
500 | transform: translate(-100%, 0%);
501 | animation: slide-in linear 1s forwards,
502 | slide-in-alt ease-in 0.75s 0.25s forwards;
503 | transition: top 1s ease;
504 | }
505 |
506 | @keyframes slide-in {
507 | from {
508 | top: -200px;
509 | }
510 |
511 | to {
512 | top: 50%;
513 | }
514 | }
515 |
516 | @keyframes slide-in-alt {
517 | to {
518 | transform: translate(0%, -50%);
519 | }
520 | }
521 |
522 | .radio-container {
523 | display: flex;
524 | flex-direction: row;
525 | justify-content: center;
526 | align-items: center;
527 | }
528 |
529 | .radio-container label {
530 | display: flex;
531 | align-items: center;
532 | margin-right: 10px;
533 | cursor: pointer;
534 | }
535 |
536 | .radio-container input[type="radio"] {
537 | margin-right: 10px;
538 | appearance: none;
539 | -webkit-appearance: none;
540 | -moz-appearance: none;
541 | width: 20px;
542 | height: 20px;
543 | border-radius: 50%;
544 | border: 2px solid #ccc;
545 | outline: none;
546 | transition: border-color 0.2s ease-in-out;
547 | }
548 |
549 | .radio-container input[type="radio"]::before {
550 | content: "";
551 | display: block;
552 | width: 12px;
553 | height: 12px;
554 | margin-top: calc(50% - 6px);
555 | margin-left: calc(50% - 6px);
556 | border-radius: 50%;
557 | transition: background-color 0.2s ease-in-out;
558 | }
559 |
560 | .radio-container input[type="radio"]:checked::before {
561 | background-color: var(--text-color);
562 | }
563 |
564 | .radio-container input[type="radio"]:hover:not(:checked)::before {
565 | background-color: rgba(192, 192, 192, 0.5);
566 | }
567 |
568 | .stats {
569 | display: flex;
570 | justify-content: space-between;
571 | margin-bottom: 20px;
572 | align-items: center;
573 | // transition: color 1s ease;
574 | }
575 |
576 | .stat {
577 | flex: 1;
578 | text-align: center;
579 | }
580 |
581 | .underline-text {
582 | text-decoration-color: rgba(255, 2, 2, 0.5);
583 | text-decoration-thickness: 2px;
584 | text-underline-offset: 2px;
585 | text-decoration-skip-ink: none;
586 | text-decoration-style: dotted;
587 | text-decoration-line: underline;
588 | }
589 |
590 | .underline-text::before {
591 | content: "";
592 | position: absolute;
593 | width: 0;
594 | height: 2px;
595 | background-color: red;
596 | bottom: 0;
597 | left: 0;
598 | transition: width 0.5s ease;
599 | }
600 |
601 | .underline-animation::before {
602 | width: 100%;
603 | transition: width 0.5s ease;
604 | }
605 |
606 | .flash-out-green {
607 | animation: flash-green 1s ease-out;
608 | }
609 |
610 | .flash-out-red {
611 | animation: flash-red 1s ease-out;
612 | }
613 |
614 | @keyframes flash-red {
615 | 0% {
616 | color: red;
617 | }
618 |
619 | 100% {
620 | color: var(--text-color);
621 | }
622 | }
623 |
624 | @keyframes flash-green {
625 | 0% {
626 | color: rgb(0, 255, 0);
627 | }
628 |
629 | 100% {
630 | color: var(--text-color);
631 | }
632 | }
633 |
634 | .highlight {
635 | transform: translate3d(0, 0, 0);
636 | font-weight: bold;
637 | animation: font-size-highlight 1.5s ease 1s forwards;
638 | }
639 |
640 | .highlight-category {
641 | transform: translate3d(0, 0, 0);
642 | animation: font-size-category 1.5s ease 1s forwards;
643 | }
644 |
645 | .dropdown-avatar {
646 | width: 30px;
647 | height: 30px;
648 | margin-right: 10px;
649 | overflow: hidden;
650 | flex-shrink: 0;
651 | border-radius: 50%;
652 | }
653 |
654 | .profile-avatar {
655 | width: 200px;
656 | height: 200px;
657 | margin-right: 10px;
658 | border-radius: 50%;
659 | overflow: hidden;
660 | flex-shrink: 0;
661 | border-radius: 50%;
662 | cursor: pointer;
663 | }
664 |
665 | .profile-avatar:hover {
666 | cursor: pointer;
667 | }
668 |
669 | #capslockWarning {
670 | color: red;
671 | font-weight: bold;
672 | margin-bottom: 10px;
673 | }
674 |
675 | form {
676 | display: flex;
677 | flex-direction: column;
678 | align-items: center;
679 | margin-top: 20px;
680 | }
681 |
682 | form input {
683 | width: 100%;
684 | height: 40px;
685 | margin-bottom: 20px;
686 | padding: 5px;
687 | box-sizing: border-box;
688 | background-color: var(--input-background-color);
689 | border: 1px solid var(--text-color);
690 | color: var(--text-color);
691 | transition: background-color 1s ease;
692 | max-width: 80%;
693 | }
694 |
695 | form > * {
696 | margin-bottom: 10px;
697 | }
698 |
699 | $box-shadow-color: rgba(var(--text-color), 0.15);
700 | $box-shadow-offset: 10px;
701 |
702 | .button {
703 | position: relative;
704 | display: inline-block;
705 | overflow: hidden;
706 | padding: 10px min(2vw, 20px);
707 | border-radius: 0%;
708 | // box-shadow: 10px 10px rgba(0, 0, 0, .15);
709 | box-shadow: 10px 10px rgba(var(--text-color-rgb), 0.25);
710 |
711 | // transition: border-radius 0.4s ease, box-shadow 0.4s ease, background-color 1s ease;
712 | transition: all 0.4s ease;
713 | background-color: var(--input-background-color);
714 | // background-color: #a2d2ff;
715 | color: var(--text-color);
716 | // color: black;
717 | border: 1px solid var(--text-color);
718 | cursor: pointer;
719 | }
720 |
721 | .button.danger {
722 | margin-top: 15%;
723 | background-color: #ff7f7f;
724 | border: 1px solid #ff7f7f;
725 | }
726 |
727 | .button:hover {
728 | // border-radius: 0% 0% 50% 50% / 0% 0% 10% 10%;
729 | box-shadow: 5px 5px rgba(var(--text-color-rgb), 0.15);
730 | }
731 |
732 | .button:focus {
733 | outline: none;
734 | }
735 |
736 | .button:not(:last-child) {
737 | margin-right: min(1vw, 10px);
738 | }
739 |
740 | .button:active {
741 | color: rgba(15, 95, 240, 1);
742 | outline: none;
743 | }
744 |
745 | #flying-focus {
746 | position: absolute;
747 | margin: 0;
748 | background: transparent;
749 | transition-property: left, top, width, height;
750 | transition-timing-function: cubic-bezier(0, 1, 0, 1);
751 | visibility: hidden;
752 | pointer-events: none;
753 | }
754 |
755 | #flying-focus.flying-focus_visible {
756 | visibility: visible;
757 | z-index: 1;
758 | }
759 |
760 | .flying-focus_target {
761 | outline: none !important;
762 | }
763 |
764 | @media screen and (-webkit-min-device-pixel-ratio: 0) {
765 | #flying-focus {
766 | box-shadow: 0 0 30px rgba(15, 95, 240, 1);
767 |
768 | outline: 1px auto;
769 | // outline: 1px auto var(--input-background-color);
770 | }
771 | }
772 |
773 | .modal-content {
774 | background-color: rgb(255, 255, 255, 0.8);
775 | position: relative;
776 | top: 50%;
777 | left: 50%;
778 | transform: translate(-50%, -50%);
779 | padding: 20px;
780 | border: 1px solid #888;
781 | width: 50%;
782 | max-height: 100vh;
783 | }
784 |
785 | .modal {
786 | display: block;
787 | justify-content: center;
788 | align-items: center;
789 | z-index: 1;
790 | left: 0;
791 | width: 100%;
792 | height: 100vh;
793 | position: absolute;
794 | top: 0%;
795 | background-color: rgba(0, 0, 0, 0.4);
796 | color: black;
797 | }
798 |
799 | .modal input {
800 | width: 100%;
801 | height: 40px;
802 | font-size: 16px;
803 | margin-bottom: 20px;
804 | padding: 5px;
805 | border: #000 1px solid;
806 | box-sizing: border-box;
807 | }
808 |
809 | .modal .button {
810 | display: block;
811 | margin-top: 10px;
812 | margin-right: 0px;
813 | width: 100%;
814 | }
815 |
816 | @media only screen and (max-width: 768px) {
817 | /* For mobile devices with a maximum width of 768px */
818 | #resultImg {
819 | position: relative;
820 | top: -100vh;
821 | left: 70%;
822 | width: 70%;
823 | transform: translate(-100%, 0%);
824 | animation: slide-in ease 1s forwards;
825 | }
826 |
827 | .stat,
828 | .stats {
829 | font-size: 12px;
830 | }
831 | .container {
832 | width: 100%;
833 | overflow-x: hidden; /* Prevent horizontal scrolling */
834 | }
835 | @keyframes font-size-highlight {
836 | to {
837 | font-size: 24px;
838 | }
839 | }
840 |
841 | @keyframes font-size-category {
842 | to {
843 | font-size: 16px;
844 | }
845 | }
846 |
847 | .modal-content {
848 | width: 90%;
849 | top: 20%;
850 | }
851 |
852 | .radio-container {
853 | flex-wrap: wrap;
854 | // display: flex;
855 | // flex-wrap: wrap;
856 | // justify-content: space-between;
857 | }
858 |
859 | .radio-container label {
860 | align-items: center;
861 | font-size: 12px;
862 | margin-right: 10px;
863 | cursor: pointer;
864 | }
865 |
866 | @keyframes slide-in {
867 | from {
868 | top: -100vh;
869 | }
870 |
871 | to {
872 | transform: translate(-100%, 0%);
873 | top: 1vh;
874 | }
875 | }
876 | }
877 |
--------------------------------------------------------------------------------
/webpack.config.cjs:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | mode: 'production', // or 'production'
6 | entry: './src/scripts/app.cjs',
7 | output: {
8 | filename: 'bundle.js',
9 | path: path.resolve(__dirname, 'src/dist'),
10 | publicPath: 'src/dist/',
11 | },
12 | devServer: {
13 | contentBase: path.join(__dirname, 'src/'), // Your public directory
14 | port: 3000,
15 | hot: true, // Enable HMR
16 | },
17 | };
--------------------------------------------------------------------------------