├── src ├── react-app-env.d.ts ├── assets │ ├── Kanit.ttf │ ├── user.png │ ├── gh-logo.png │ ├── codeforces_logo.png │ ├── index.ts │ └── loop.svg ├── models │ ├── LogicalOperator.ts │ ├── ProblemStatistics.ts │ └── Problem.ts ├── components │ ├── common │ │ └── Row.tsx │ ├── problems-section │ │ ├── EmptySection.tsx │ │ ├── ProblemsSection.tsx │ │ └── ProblemCard.tsx │ ├── header │ │ └── Header.tsx │ ├── snackbar │ │ ├── CancelButton.tsx │ │ └── Snackbar.tsx │ ├── options-button │ │ └── index.tsx │ ├── footer │ │ └── Footer.tsx │ ├── clear-button │ │ └── ClearButton.tsx │ ├── topics │ │ ├── Tag.tsx │ │ └── Topics.tsx │ ├── randomize-button │ │ ├── LoadingIndicator.tsx │ │ └── RandomizeButton.tsx │ ├── options │ │ └── Options.tsx │ ├── slider │ │ └── Slider.tsx │ └── home │ │ └── Home.tsx ├── setupTests.ts ├── App.test.tsx ├── App.tsx ├── index.css ├── index.tsx ├── services │ ├── data.ts │ ├── storage.ts │ └── problems.ts └── serviceWorker.ts ├── public ├── robots.txt ├── CRicon.png ├── manifest.json └── index.html ├── screenshots ├── SS1.PNG ├── SS2.png ├── SS3.png └── SS4.png ├── .gitignore ├── tsconfig.json ├── README.md └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/CRicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/public/CRicon.png -------------------------------------------------------------------------------- /screenshots/SS1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/screenshots/SS1.PNG -------------------------------------------------------------------------------- /screenshots/SS2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/screenshots/SS2.png -------------------------------------------------------------------------------- /screenshots/SS3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/screenshots/SS3.png -------------------------------------------------------------------------------- /screenshots/SS4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/screenshots/SS4.png -------------------------------------------------------------------------------- /src/assets/Kanit.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/src/assets/Kanit.ttf -------------------------------------------------------------------------------- /src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/src/assets/user.png -------------------------------------------------------------------------------- /src/models/LogicalOperator.ts: -------------------------------------------------------------------------------- 1 | type LogicalOperator = "AND" | "OR"; 2 | 3 | export default LogicalOperator; 4 | -------------------------------------------------------------------------------- /src/assets/gh-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/src/assets/gh-logo.png -------------------------------------------------------------------------------- /src/assets/codeforces_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarimElghamry/Codeforces-Randomizer/HEAD/src/assets/codeforces_logo.png -------------------------------------------------------------------------------- /src/models/ProblemStatistics.ts: -------------------------------------------------------------------------------- 1 | export interface ProblemStatistics { 2 | contestId: number; 3 | index: string; 4 | solvedCount: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/Problem.ts: -------------------------------------------------------------------------------- 1 | export interface Problem { 2 | contestId: number; 3 | index: string; 4 | name: string; 5 | type: string; 6 | rating?: number; 7 | tags: Array; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/common/Row.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Row = styled.div` 4 | display: flex; 5 | width: 100%; 6 | flex-direction: row; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | export default Row; 12 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 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/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/assets/index.ts: -------------------------------------------------------------------------------- 1 | import codeforcesLogo from './codeforces_logo.png'; 2 | import userIcon from './user.png'; 3 | import githubLogo from './gh-logo.png'; 4 | import {ReactComponent as loopIcon} from './loop.svg'; 5 | 6 | export const images = { 7 | codeforcesLogo: codeforcesLogo, 8 | loopIcon: loopIcon, 9 | userIcon: userIcon, 10 | githubLogo: githubLogo, 11 | }; 12 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import Home from './components/home/Home'; 3 | import {getPromblemsListFromStorage} from './services/storage'; 4 | 5 | const App: React.FC<{}> = (): ReactElement => { 6 | const problemsList = getPromblemsListFromStorage(); 7 | return ; 8 | }; 9 | 10 | export default App; 11 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Kanit; 3 | src: url('../src/assets/Kanit.ttf'); 4 | } 5 | 6 | html, 7 | body { 8 | height: 100%; 9 | margin: 0px; 10 | padding: 0px; 11 | background-color: white; 12 | font-family: Kanit; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | body { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | -------------------------------------------------------------------------------- /.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 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/problems-section/EmptySection.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const StyledEmptySecion = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | width: 100%; 10 | height: 100%; 11 | `; 12 | 13 | const EmptySection: React.FC<{}> = (): ReactElement => { 14 | return ( 15 | 16 |
Choose topics and press Randomize
17 |
to start adding problems
18 |
19 | ); 20 | }; 21 | 22 | export default EmptySection; 23 | -------------------------------------------------------------------------------- /src/components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled from 'styled-components'; 3 | import * as assets from '../../assets'; 4 | 5 | const CenteredDiv = styled.div` 6 | text-align: center; 7 | font-size: 24px; 8 | font-weight: bold; 9 | `; 10 | 11 | const Header: React.FC = (): ReactElement => { 12 | return ( 13 |
14 | 20 | Randomizer 21 |
22 | ); 23 | }; 24 | 25 | export default Header; 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Codeforces Randomizer 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/snackbar/CancelButton.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface Props { 5 | onClick: Function; 6 | } 7 | 8 | const StyleCancelButton = styled.div` 9 | font-family: 'Helvetica', 'Arial', sans-serif; 10 | font-weight: bold; 11 | background-color: white; 12 | color: red; 13 | margin-left: 5px; 14 | width: 25px; 15 | height: 25px; 16 | border-radius: 12.5px; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | cursor: pointer; 21 | 22 | &:hover { 23 | opacity: 0.8; 24 | } 25 | `; 26 | 27 | const CancelButton: React.FC = (props: Props): ReactElement => { 28 | return ( 29 | props.onClick()}>X 30 | ); 31 | }; 32 | 33 | export default CancelButton; 34 | -------------------------------------------------------------------------------- /src/components/options-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import LogicalOperator from "../../models/LogicalOperator"; 4 | 5 | const Button = styled.div<{ isSelected: boolean }>` 6 | width: 50px; 7 | text-align: center; 8 | cursor: pointer; 9 | transition: 0.3s; 10 | color: white; 11 | background-color: ${(props) => (props.isSelected ? "#02b947" : "grey")}; 12 | `; 13 | 14 | type OptionsButtonProps = { 15 | opertator: LogicalOperator; 16 | isSelected: boolean; 17 | onClick: (operator: LogicalOperator) => void; 18 | }; 19 | 20 | const OptionsButton = ({ 21 | opertator, 22 | isSelected, 23 | onClick, 24 | }: OptionsButtonProps) => { 25 | return ( 26 | 29 | ); 30 | }; 31 | 32 | export default OptionsButton; 33 | -------------------------------------------------------------------------------- /src/services/data.ts: -------------------------------------------------------------------------------- 1 | export const getTags: () => Array = (): Array => { 2 | const tags: Array = [ 3 | "2-sat", 4 | "binary search", 5 | "bitmasks", 6 | "brute force", 7 | "combinatorics", 8 | "constructive algorithms", 9 | "data structures", 10 | "dfs and similar", 11 | "divide and conquer", 12 | "dp", 13 | "dsu", 14 | "expression parsing", 15 | "fft", 16 | "flow", 17 | "games", 18 | "geometry", 19 | "graph matchings", 20 | "graphs", 21 | "greedy", 22 | "hashing", 23 | "implementation", 24 | "interactive", 25 | "math", 26 | "matrices", 27 | "number theory", 28 | "probabilities", 29 | "schedules", 30 | "shortest paths", 31 | "sortings", 32 | "strings", 33 | "ternary search", 34 | "trees", 35 | "two pointers", 36 | ]; 37 | 38 | return tags; 39 | }; 40 | 41 | export const minRating = 800; 42 | export const maxRating = 3500; 43 | -------------------------------------------------------------------------------- /src/services/storage.ts: -------------------------------------------------------------------------------- 1 | import {Problem} from '../models/Problem'; 2 | import {ProblemStatistics} from '../models/ProblemStatistics'; 3 | 4 | export function getPromblemsListFromStorage(): Array<{ 5 | problem: Problem; 6 | problemStatistics: ProblemStatistics; 7 | }> { 8 | const result: string | null = localStorage.getItem('problemsList'); 9 | if (!result) return []; 10 | 11 | const problemsList: Array<{ 12 | problem: Problem; 13 | problemStatistics: ProblemStatistics; 14 | }> = JSON.parse(result).problemsList; 15 | return problemsList; 16 | } 17 | 18 | export function setProblemsListToStorage( 19 | list: Array<{problem: Problem; problemStatistics: ProblemStatistics}> 20 | ) { 21 | const storageObject = { 22 | problemsList: list, 23 | }; 24 | 25 | localStorage.setItem('problemsList', JSON.stringify(storageObject)); 26 | } 27 | 28 | export function clearProblemsList(): void { 29 | localStorage.setItem('problemsList', JSON.stringify({problemsList: []})); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled from 'styled-components'; 3 | import {images} from '../../assets'; 4 | import Row from '../common/Row'; 5 | 6 | const StyledFooter = styled.div` 7 | width: 100%; 8 | background-color: #24292e; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | color: white; 13 | user-select: none; 14 | `; 15 | 16 | const GithubLogo = styled.img` 17 | width: 25px; 18 | margin: 10px; 19 | `; 20 | 21 | const githubProfileUrl: string = 'https://github.com/KarimElghamry'; 22 | 23 | const Footer: React.FC = (): ReactElement => { 24 | return ( 25 | 26 | window.open(githubProfileUrl, '_blank')} 29 | > 30 | 31 |
KarimElghamry
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Footer; 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codeforces Randomizer 2 | 3 | Built using React, Styled components and Typescript. 4 | 5 | deployed at: https://karimelghamry.github.io/Codeforces-Randomizer/ 6 | 7 | ### Show some :heart: and :star: the repo to support the project 8 | 9 | ## How it works 10 | 11 | - Select topics from the provided list (max 4). 12 | 13 | - Adjust the range of ratings to your likings. 14 | 15 | - Press `Randomize` to retrieve a random problem from Codeforces based on your inputs. 16 | 17 |

18 |

    

19 | 20 | ## Features 21 | 22 | [✓] Ratings adjustment 23 | 24 | [✓] All current Codeforces topics included 25 | 26 | [✓] Automatically saves history local storage 27 | 28 | [✓] Clear history on demand 29 | 30 | ## Contribution 31 | 32 | Feel free to to suggest further features/improvements by opening an issue or by submitting a pull request. 33 | -------------------------------------------------------------------------------- /src/components/clear-button/ClearButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import styled from "styled-components"; 3 | import Row from "../common/Row"; 4 | 5 | interface Props { 6 | onClick: Function; 7 | disabled: boolean; 8 | } 9 | 10 | const StyledButton = styled.div` 11 | width: 120px; 12 | height: 35px; 13 | margin: 15px; 14 | display: flex; 15 | visibility: ${(props) => (props.disabled ? "hidden" : "none")}; 16 | justify-content: space-evenly; 17 | align-items: center; 18 | background-color: red; 19 | border: red solid 2px; 20 | color: white; 21 | font-weight: 600; 22 | border-radius: 6px; 23 | user-select: none; 24 | cursor: pointer; 25 | transition-duration: 0.3s; 26 | transition-property: background-color, color; 27 | 28 | &:hover { 29 | background-color: white; 30 | color: red; 31 | } 32 | `; 33 | 34 | const ClearButton: React.FC = (props: Props): ReactElement => { 35 | return ( 36 | 37 | props.onClick()}> 38 | Clear 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default ClearButton; 45 | -------------------------------------------------------------------------------- /src/components/topics/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import styled from "styled-components"; 3 | 4 | interface Props { 5 | selected: boolean; 6 | content: string; 7 | onClick: (selected: boolean, topic: string) => void; 8 | } 9 | 10 | interface StyledProps { 11 | selected: boolean; 12 | } 13 | 14 | const StyledTag = styled.div` 15 | background-color: ${(props) => (props.selected ? "#33AC71" : "#00bcd4")}; 16 | border-radius: 2px; 17 | color: white; 18 | font-weight: 700; 19 | min-width: 70px; 20 | min-height: 30px; 21 | margin: 5px; 22 | user-select: none; 23 | cursor: pointer; 24 | display: inline-flex; 25 | justify-content: center; 26 | align-items: center; 27 | transition-duration: 0.3s; 28 | `; 29 | 30 | const Tag: React.FC = (props: Props): ReactElement => { 31 | const selected: boolean = props.selected; 32 | const content: string = props.content; 33 | 34 | return ( 35 | props.onClick(selected, content)} 38 | > 39 |
{props.content}
40 |
41 | ); 42 | }; 43 | 44 | export default Tag; 45 | -------------------------------------------------------------------------------- /src/assets/loop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/randomize-button/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled, {keyframes} from 'styled-components'; 3 | 4 | interface CircleProps { 5 | color: string; 6 | } 7 | 8 | const BounceAnim = keyframes` 9 | from{ 10 | bottom: 5px; 11 | } 12 | to{ 13 | bottom: 15px; 14 | } 15 | `; 16 | 17 | const Circle = styled.div` 18 | position: absolute; 19 | display: inline-block; 20 | height: 10px; 21 | width: 10px; 22 | border-radius: 10px; 23 | margin: 2px; 24 | bottom: 5px; 25 | background-color: ${(props) => props.color}; 26 | animation: ${BounceAnim} 0.5s cubic-bezier(0.3, 0, 0, 1) infinite; 27 | animation-direction: alternate; 28 | 29 | &:nth-child(1) { 30 | left: 30%; 31 | animation-delay: 0s; 32 | } 33 | &:nth-child(2) { 34 | left: 45%; 35 | animation-delay: 0.2s; 36 | } 37 | &:nth-child(3) { 38 | left: 60%; 39 | animation-delay: 0.4s; 40 | } 41 | `; 42 | 43 | const StyledLoadingIndicator = styled.div` 44 | position: relative; 45 | height: 100%; 46 | width: 100%; 47 | `; 48 | 49 | const LoadingIndicator: React.FC = (): ReactElement => { 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default LoadingIndicator; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeforces-randomizer", 3 | "version": "0.1.0", 4 | "homepage": "https://KarimElghamry.github.io/Codeforces-Randomizer", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "@types/axios": "^0.14.0", 11 | "@types/jest": "^24.9.1", 12 | "@types/node": "^12.12.47", 13 | "@types/react": "^16.9.36", 14 | "@types/react-dom": "^16.9.8", 15 | "@types/styled-components": "^5.1.0", 16 | "axios": "^0.19.2", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-range": "^1.6.7", 20 | "react-scripts": "3.4.1", 21 | "styled-components": "^5.1.1", 22 | "typescript": "^3.7.5" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts --openssl-legacy-provider start", 26 | "build": "react-scripts --openssl-legacy-provider build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject", 29 | "predeploy": "npm run build", 30 | "deploy": "gh-pages -d build" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/react-rangeslider": "^2.2.2", 49 | "gh-pages": "^3.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/topics/Topics.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import styled from "styled-components"; 3 | import Tag from "./Tag"; 4 | import { getTags } from "../../services/data"; 5 | import Row from "../common/Row"; 6 | 7 | interface Props { 8 | selectedTopics: Array; 9 | setSelectedTopics: Function; 10 | } 11 | 12 | const Container = styled.div` 13 | max-width: 650px; 14 | min-width: 100px; 15 | border: lightgray solid 1px; 16 | border-radius: 10px; 17 | margin: 10px; 18 | margin-top: 5px; 19 | padding: 10px; 20 | display: block; 21 | `; 22 | 23 | const tags: Array = getTags(); 24 | 25 | const Topics: React.FC = ({ 26 | selectedTopics, 27 | setSelectedTopics, 28 | }: Props): ReactElement => { 29 | const handleTopicAddition = (selected: boolean, topic: string) => { 30 | let newSelectedTopics: Array; 31 | if (selected) { 32 | newSelectedTopics = selectedTopics.filter((val: string) => val !== topic); 33 | } else { 34 | newSelectedTopics = selectedTopics.concat(topic); 35 | } 36 | 37 | setSelectedTopics(newSelectedTopics); 38 | }; 39 | 40 | return ( 41 | 42 | 43 | {tags.map((val: string) => { 44 | const selected: boolean = selectedTopics.includes(val); 45 | return ( 46 | 52 | ); 53 | })} 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Topics; 60 | -------------------------------------------------------------------------------- /src/components/randomize-button/RandomizeButton.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled from 'styled-components'; 3 | import LoadingIndicator from './LoadingIndicator'; 4 | import {images} from '../../assets'; 5 | 6 | interface Props { 7 | isLoading: boolean; 8 | onClick: Function; 9 | } 10 | 11 | const StyledButton = styled.div` 12 | padding: 2px; 13 | width: 120px; 14 | height: 35px; 15 | display: flex; 16 | justify-content: space-evenly; 17 | align-items: center; 18 | background-color: ${(props) => (props.isLoading ? 'white' : '#f7b708')}; 19 | color: white; 20 | font-weight: 600; 21 | border-radius: 6px; 22 | user-select: none; 23 | cursor: pointer; 24 | transition-duration: 0.3s; 25 | 26 | &:hover { 27 | background-color: ${(props) => (props.isLoading ? 'white' : '#33ac71')}; 28 | } 29 | `; 30 | 31 | const LoopIcon = images.loopIcon; 32 | const StyledLoopIcon = styled(LoopIcon)` 33 | height: 25px; 34 | width: 25px; 35 | fill: white; 36 | `; 37 | 38 | const RandomizeButton: React.FC = (props: Props): ReactElement => { 39 | return ( 40 | { 43 | if (props.isLoading) return; 44 | props.onClick(); 45 | }} 46 | > 47 | {props.isLoading ? ( 48 | 49 | ) : ( 50 | 51 | 52 |
Randomize
53 |
54 | )} 55 |
56 | ); 57 | }; 58 | 59 | RandomizeButton.defaultProps = { 60 | isLoading: false, 61 | }; 62 | 63 | export default RandomizeButton; 64 | -------------------------------------------------------------------------------- /src/components/options/Options.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState } from "react"; 2 | import Slider from "../slider/Slider"; 3 | import { minRating, maxRating } from "../../services/data"; 4 | import RandomizeButton from "../randomize-button/RandomizeButton"; 5 | import styled from "styled-components"; 6 | import LogicalOperator from "../../models/LogicalOperator"; 7 | import OptionsButton from "../options-button"; 8 | import Row from "../common/Row"; 9 | 10 | interface Props { 11 | onRandomize: Function; 12 | onOperatorSelect: (operator: LogicalOperator) => void; 13 | operator: LogicalOperator; 14 | } 15 | 16 | const Container = styled.div` 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | flex-direction: column; 21 | `; 22 | 23 | const operators: LogicalOperator[] = ["AND", "OR"]; 24 | 25 | const Options: React.FC = (props: Props): ReactElement => { 26 | const [rating, setRating] = useState<{ min: number; max: number }>({ 27 | min: minRating, 28 | max: maxRating, 29 | }); 30 | const [isLoading, setIsLoading] = useState(false); 31 | 32 | const randomizeProblem = async () => { 33 | setIsLoading(true); 34 | await props.onRandomize(rating); 35 | setIsLoading(false); 36 | }; 37 | return ( 38 | 39 | 40 | {operators.map((value) => ( 41 | 46 | ))} 47 | 48 | 53 | 57 | 58 | ); 59 | }; 60 | 61 | export default Options; 62 | -------------------------------------------------------------------------------- /src/components/problems-section/ProblemsSection.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement, useEffect} from 'react'; 2 | import ProblemCard from './ProblemCard'; 3 | import {ProblemStatistics} from '../../models/ProblemStatistics'; 4 | import {Problem} from '../../models/Problem'; 5 | import styled from 'styled-components'; 6 | import EmptySection from './EmptySection'; 7 | import Row from '../common/Row'; 8 | 9 | interface Props { 10 | problemsList: Array<{problem: Problem; problemStatistics: ProblemStatistics}>; 11 | } 12 | 13 | const StyleProblemsSection = styled.div` 14 | margin-top: 20px; 15 | height: 300px; 16 | width: 500px; 17 | overflow-y: scroll; 18 | scrollbar-color: lightgray white; 19 | scrollbar-width: thin; 20 | 21 | &::-webkit-scrollbar { 22 | width: 8px; 23 | } 24 | 25 | &::-webkit-scrollbar-track { 26 | background-color: white; 27 | } 28 | 29 | &::-webkit-scrollbar-thumb { 30 | background-color: lightgray; 31 | border-radius: 5px; 32 | } 33 | 34 | @media screen and (max-width: 600px) { 35 | width: 400px; 36 | } 37 | `; 38 | 39 | const ProblemsSection: React.FC = (props: Props): ReactElement => { 40 | const problemsList = props.problemsList; 41 | let wrapperRef: HTMLDivElement | null = null; 42 | 43 | useEffect(() => { 44 | if (!wrapperRef) return; 45 | 46 | wrapperRef.scrollTo(0, 0); 47 | }, [problemsList, wrapperRef]); 48 | 49 | return ( 50 | 51 | (wrapperRef = ref)}> 52 | {problemsList.length === 0 ? ( 53 | 54 | ) : ( 55 | problemsList 56 | .map((val, index) => { 57 | return ( 58 | 63 | ); 64 | }) 65 | .reverse() 66 | )} 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default ProblemsSection; 73 | -------------------------------------------------------------------------------- /src/components/snackbar/Snackbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import CancelButton from "./CancelButton"; 4 | 5 | interface SnackbarProps { 6 | visible: boolean; 7 | type?: string; 8 | content: string; 9 | onCancel: Function; 10 | timeout: number; 11 | } 12 | 13 | const StyledSnackbar = styled.div` 14 | background-color: ${(props) => (props.type === "error" ? "red" : "green")}; 15 | color: white; 16 | opacity: ${(props) => (props.visible ? "0.9" : "0")}; 17 | visibility: ${(props) => (props.visible ? "none" : "hidden")}; 18 | position: fixed; 19 | bottom: ${(props) => (props.visible ? "2.5vh" : "0")}; 20 | left: 0; 21 | right: 0; 22 | margin: auto; 23 | padding-left: 15px; 24 | padding-right: 15px; 25 | width: fit-content; 26 | height: 55px; 27 | border-radius: 30px; 28 | font-weight: 600; 29 | font-size: 14px; 30 | display: flex; 31 | justify-content: space-evenly; 32 | align-items: center; 33 | user-select: none; 34 | transition-duration: 0.5s; 35 | `; 36 | 37 | const Snackbar: React.FC = ( 38 | props: SnackbarProps 39 | ): ReactElement => { 40 | const onCancel: Function = props.onCancel; 41 | const visible: boolean = props.visible; 42 | const timeout: number = props.timeout; 43 | 44 | useEffect(() => { 45 | if (visible) { 46 | const makeInvisible = setTimeout(() => onCancel(), timeout); 47 | return () => clearTimeout(makeInvisible); 48 | } 49 | }, [visible, onCancel, timeout]); 50 | 51 | return ( 52 | 53 |
63 | {props.content} 64 |
65 | 66 |
67 | ); 68 | }; 69 | 70 | Snackbar.defaultProps = { 71 | type: "error", 72 | }; 73 | 74 | export default Snackbar; 75 | -------------------------------------------------------------------------------- /src/components/problems-section/ProblemCard.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import styled, {keyframes} from 'styled-components'; 3 | import {Problem} from '../../models/Problem'; 4 | import {ProblemStatistics} from '../../models/ProblemStatistics'; 5 | import {images} from '../../assets'; 6 | 7 | interface CardProps { 8 | problem: Problem; 9 | problemStatistics: ProblemStatistics; 10 | } 11 | 12 | interface CellProps { 13 | flex: number; 14 | } 15 | 16 | const EnterAnim = keyframes` 17 | from{ 18 | margin-left: -50%; 19 | } 20 | to{ 21 | margin-left: 20px: 22 | } 23 | `; 24 | 25 | const StyledProblemCard = styled.div` 26 | width: 90%; 27 | min-height: 80px; 28 | background-color: #f8f8f8; 29 | margin: 20px; 30 | display: flex; 31 | justify-content: space-evenly; 32 | align-items: center; 33 | border-radius: 10px; 34 | cursor: pointer; 35 | user-select: none; 36 | transition-duration: 0.3s; 37 | animation: ${EnterAnim} 0.5s cubic-bezier(0.2, 0, 0, 1.2); 38 | 39 | &:hover { 40 | background-color: lightgray; 41 | } 42 | `; 43 | 44 | const Cell = styled.div` 45 | flex: ${(props) => props.flex}; 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | `; 50 | 51 | const ProblemCard: React.FC = (props: CardProps): ReactElement => { 52 | const problem: Problem = props.problem; 53 | const problemStats: ProblemStatistics = props.problemStatistics; 54 | const baseUrl: string = 'https://codeforces.com/problemset/problem'; 55 | 56 | const handleUrlRedirect = () => { 57 | const redirectUrl: string = `${baseUrl}/${problem.contestId}/${problem.index}`; 58 | window.open(redirectUrl, '_blank'); 59 | }; 60 | 61 | return ( 62 | { 64 | handleUrlRedirect(); 65 | }} 66 | > 67 | {`${problemStats.contestId}${problemStats.index}`} 68 | {`${problem.name}`} 69 | {`${ 70 | problem.rating === undefined ? 0 : problem.rating 71 | }`} 72 | 73 | 74 | {`x${problemStats.solvedCount}`} 75 | 76 | 77 | ); 78 | }; 79 | 80 | export default ProblemCard; 81 | -------------------------------------------------------------------------------- /src/components/slider/Slider.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactElement} from 'react'; 2 | import {Range, getTrackBackground} from 'react-range'; 3 | import styled from 'styled-components'; 4 | import {minRating, maxRating} from '../../services/data'; 5 | 6 | interface TrackProps { 7 | values: Array; 8 | min: number; 9 | max: number; 10 | } 11 | 12 | interface SliderProps { 13 | minRating: number; 14 | maxRating: number; 15 | onChange: Function; 16 | } 17 | 18 | const Container = styled.div` 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | flex-direction: column; 23 | width: 250px; 24 | margin: 20px; 25 | `; 26 | 27 | const Track = styled.div` 28 | background: ${(props) => 29 | getTrackBackground({ 30 | values: props.values, 31 | colors: ['#ccc', '#198FCE', '#ccc'], 32 | min: props.min, 33 | max: props.max, 34 | })}; 35 | width: 100%; 36 | height: 2px; 37 | margin-bottom: 10px; 38 | `; 39 | 40 | const Thumb = styled.div<{index: number}>` 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | background-color: ${(props) => (props.index === 0 ? '#F8CC52' : '#BA1D25')}; 45 | height: 16px; 46 | width: 16px; 47 | border-radius: 16px; 48 | `; 49 | 50 | const Indicator = styled.div<{isDragged: boolean}>` 51 | height: 8px; 52 | width: 8px; 53 | border-radius: 6px; 54 | background-color: ${(props) => (props.isDragged ? '#198FCE' : 'white')}; 55 | `; 56 | 57 | const Slider: React.FC = (props: SliderProps): ReactElement => { 58 | const values = [props.minRating, props.maxRating]; 59 | return ( 60 | 61 | props.onChange({min: values[0], max: values[1]})} 67 | renderTrack={({props, children}) => ( 68 | 69 | {children} 70 | 71 | )} 72 | renderThumb={({props, isDragged, index}) => { 73 | return ( 74 | 75 | 76 | 77 | ); 78 | }} 79 | /> 80 |
{`Rating: ${values[0]} - ${values[1]}`}
81 |
82 | ); 83 | }; 84 | 85 | export default Slider; 86 | -------------------------------------------------------------------------------- /src/services/problems.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { Problem } from "../models/Problem"; 3 | import { ProblemStatistics } from "../models/ProblemStatistics"; 4 | import { getTags } from "./data"; 5 | import LogicalOperator from "../models/LogicalOperator"; 6 | 7 | const baseUrl: string = "https://codeforces.com/api/problemset.problems"; 8 | 9 | function getRandomInt(max: number): number { 10 | return Math.floor(Math.random() * Math.floor(max)); 11 | } 12 | 13 | // codeforces API only allows for AND operations 14 | // OR operations are implemented manually 15 | async function getProblems( 16 | topics: string[], 17 | operator: LogicalOperator, 18 | ): Promise<[Problem[], ProblemStatistics[]]> { 19 | let problems: Problem[] = []; 20 | let problemsStatistics: ProblemStatistics[] = []; 21 | 22 | if (operator === "AND") { 23 | const tags: string = topics.reduce( 24 | (prev: string, current: string, _: number) => { 25 | return prev + ";" + current; 26 | }, 27 | ); 28 | 29 | const response = await axios.get(baseUrl, { 30 | params: { 31 | tags: tags, 32 | }, 33 | }); 34 | 35 | if (response.data.status !== "OK") throw new Error("Invalid combination"); 36 | 37 | problems = response.data.result.problems as Array; 38 | problemsStatistics = response.data.result 39 | .problemStatistics as Array; 40 | } else if (operator === "OR") { 41 | for (const topic of topics) { 42 | const response = await axios.get(baseUrl, { 43 | params: { 44 | tags: topic, 45 | }, 46 | }); 47 | 48 | if (response.data.status !== "OK") throw new Error("Invalid combination"); 49 | 50 | problems = problems.concat(response.data.result.problems as Problem); 51 | problemsStatistics = problemsStatistics.concat( 52 | response.data.result.problemStatistics as ProblemStatistics, 53 | ); 54 | } 55 | } 56 | 57 | return [problems, problemsStatistics]; 58 | } 59 | 60 | export async function getRandomProblem( 61 | topics: Array, 62 | ratings: { min: number; max: number }, 63 | operator: LogicalOperator, 64 | ): Promise<{ problem: Problem; problemStatistics: ProblemStatistics }> { 65 | if (topics.length === 0) 66 | topics = topics.concat(getTags()[getRandomInt(getTags().length)]); 67 | 68 | const [problems, problemsStatistics] = await getProblems(topics, operator); 69 | 70 | let filteredProblems: Array = []; 71 | problems.forEach((val: Problem, index: number) => { 72 | if (!val.rating) val.rating = ratings.min; 73 | 74 | if (val.rating >= ratings.min && val.rating <= ratings.max) 75 | filteredProblems = filteredProblems.concat(index); 76 | }); 77 | 78 | if (filteredProblems.length === 0) 79 | throw new Error( 80 | `No problems found for the entered combination. Try another combination.`, 81 | ); 82 | 83 | const probIndex: number = 84 | filteredProblems[getRandomInt(filteredProblems.length)]; 85 | return { 86 | problem: problems[probIndex], 87 | problemStatistics: problemsStatistics[probIndex], 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState } from "react"; 2 | import { Problem } from "../../models/Problem"; 3 | import { ProblemStatistics } from "../../models/ProblemStatistics"; 4 | import Header from "../header/Header"; 5 | import Topics from "../topics/Topics"; 6 | import Snackbar from "../snackbar/Snackbar"; 7 | import ProblemsSection from "../problems-section/ProblemsSection"; 8 | import { getRandomProblem } from "../../services/problems"; 9 | import { 10 | setProblemsListToStorage, 11 | clearProblemsList, 12 | } from "../../services/storage"; 13 | import ClearButton from "../clear-button/ClearButton"; 14 | import Options from "../options/Options"; 15 | import styled from "styled-components"; 16 | import Footer from "../footer/Footer"; 17 | import LogicalOperator from "../../models/LogicalOperator"; 18 | 19 | interface Props { 20 | initialProblemsList: Array<{ 21 | problem: Problem; 22 | problemStatistics: ProblemStatistics; 23 | }>; 24 | } 25 | 26 | const Container = styled.div` 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | flex-direction: column; 31 | min-height: 100vh; 32 | width: 100%; 33 | `; 34 | 35 | const Home: React.FC = (props: Props): ReactElement => { 36 | const [errContent, setErrContent] = useState(""); 37 | const [visible, setVisible] = useState(false); 38 | const [selectedTopics, setSelectedTopics] = useState>([]); 39 | const [operator, setOperator] = useState("AND"); 40 | const [problemsList, setProblemsList] = useState< 41 | Array<{ problem: Problem; problemStatistics: ProblemStatistics }> 42 | >(props.initialProblemsList); 43 | 44 | const triggerError: (content: string) => void = (content: string) => { 45 | setErrContent(content); 46 | setVisible(true); 47 | }; 48 | 49 | const randomizeProblem: (ratings: { 50 | min: number; 51 | max: number; 52 | }) => void = async (ratings: { min: number; max: number }): Promise => { 53 | try { 54 | const newProblem = await getRandomProblem( 55 | selectedTopics, 56 | ratings, 57 | operator, 58 | ); 59 | const newProblemsList = problemsList.concat(newProblem); 60 | setProblemsListToStorage(newProblemsList); 61 | setProblemsList(newProblemsList); 62 | } catch (e) { 63 | triggerError(e.message); 64 | } 65 | }; 66 | 67 | const clearProblemsHistory = (): void => { 68 | clearProblemsList(); 69 | setProblemsList([]); 70 | }; 71 | 72 | return ( 73 | 74 |
75 | 76 | 80 | setOperator(value)} 83 | onRandomize={randomizeProblem} 84 | > 85 | 86 | 87 | 91 | 92 |
93 | 94 | setVisible(false)} 100 | > 101 |
102 | ); 103 | }; 104 | 105 | export default Home; 106 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | --------------------------------------------------------------------------------