Abdul Basit
1 year ago
commit
4b43ca39e1
85 changed files with 19561 additions and 0 deletions
-
23.gitignore
-
15.prettierignore
-
6.prettierrc
-
206README.md
-
17006package-lock.json
-
50package.json
-
BINpublic/favicon.ico
-
41public/index.html
-
BINpublic/logo192.png
-
BINpublic/logo512.png
-
25public/manifest.json
-
3public/robots.txt
-
BINpublic/xeven-quiz.png
-
9src/App.test.tsx
-
17src/App.tsx
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-100.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-100.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-200.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-200.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-300.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-300.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-400.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-400.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-500.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-500.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-600.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-600.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-700.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-700.woff2
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-800.woff
-
BINsrc/assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-800.woff2
-
12src/assets/icons/angular.svg
-
21src/assets/icons/app-logo.svg
-
11src/assets/icons/check.svg
-
69src/assets/icons/colored-logo.svg
-
12src/assets/icons/css-3.svg
-
4src/assets/icons/dj.svg
-
10src/assets/icons/gatsby.svg
-
12src/assets/icons/javascript.svg
-
15src/assets/icons/kotlin.svg
-
10src/assets/icons/laravel.svg
-
64src/assets/icons/logo.svg
-
3src/assets/icons/next.svg
-
19src/assets/icons/python.svg
-
4src/assets/icons/react.svg
-
5src/assets/icons/refresh.svg
-
3src/assets/icons/start.svg
-
5src/assets/icons/timer.svg
-
BINsrc/assets/images/code-snippet-format.png
-
BINsrc/assets/images/quiz-app-folder-structure.png
-
32src/components/Main/index.tsx
-
71src/components/QuestionScreen/Answer/index.tsx
-
66src/components/QuestionScreen/Question/index.tsx
-
33src/components/QuestionScreen/QuizHeader/Counter/index.tsx
-
41src/components/QuestionScreen/QuizHeader/index.tsx
-
174src/components/QuestionScreen/index.tsx
-
78src/components/QuizDetailsScreen/index.tsx
-
61src/components/ResultScreen/ResultOverview/index.tsx
-
37src/components/ResultScreen/RightAnswer/index.tsx
-
193src/components/ResultScreen/index.tsx
-
51src/components/SplashScreen/index.tsx
-
30src/components/ui/Button/index.tsx
-
52src/components/ui/Button/styled.tsx
-
74src/components/ui/ModalWrapper/index.tsx
-
10src/config/icons.ts
-
90src/context/QuizContext.tsx
-
30src/data/QuizQuestions/index.ts
-
76src/data/QuizQuestions/react.ts
-
61src/data/quizTopics.tsx
-
4src/hooks/index.ts
-
17src/hooks/useShuffleQuestions.ts
-
34src/hooks/useTimer.ts
-
16src/index.tsx
-
1src/logo.svg
-
1src/react-app-env.d.ts
-
15src/reportWebVitals.ts
-
5src/setupTests.ts
-
26src/styles/BreakPoints.ts
-
208src/styles/Global.ts
-
42src/styles/Theme.ts
-
90src/styles/fonts.module.css
-
46src/styles/styled.d.ts
-
36src/types/index.ts
-
54src/utils/helpers.ts
-
26tsconfig.json
@ -0,0 +1,23 @@ |
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
|||
|
|||
# dependencies |
|||
/node_modules |
|||
/.pnp |
|||
.pnp.js |
|||
|
|||
# testing |
|||
/coverage |
|||
|
|||
# production |
|||
/build |
|||
|
|||
# misc |
|||
.DS_Store |
|||
.env.local |
|||
.env.development.local |
|||
.env.test.local |
|||
.env.production.local |
|||
|
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
@ -0,0 +1,15 @@ |
|||
# directories |
|||
.yarn/ |
|||
**/build |
|||
**/dist |
|||
**/node_modules |
|||
|
|||
# files |
|||
*.env |
|||
*.log |
|||
*.tsbuildinfo |
|||
.pnp.* |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
README.md |
@ -0,0 +1,6 @@ |
|||
{ |
|||
"semi": false, |
|||
"singleQuote": true, |
|||
"tabWidth": 2, |
|||
"printWidth": 90 |
|||
} |
@ -0,0 +1,206 @@ |
|||
# Xeven Quiz - ReactJS Quiz App Template Code Documentations |
|||
|
|||
Welcome to the Code Quiz App documentation! This guide will walk you through the steps to start using and customizing the app according to your needs. The Code Quiz App is designed to help you create interactive quizzes with various question types, including Multiple Choice Questions (MCQs), Multiple Answer Questions (MAQs), and True/False questions. |
|||
|
|||
## Demo App |
|||
|
|||
To experience the Demo App, visit the link: https://xeven-quiz.vercel.app/ |
|||
|
|||
## **How to Start a Project** |
|||
|
|||
To start the project, follow these steps: |
|||
|
|||
1. Open the terminal and navigate to the project directory. |
|||
2. Run the command **`npm install`** to download and install all the project dependencies. |
|||
3. Once the dependencies are installed, run the command **`npm start`** to start the development server. |
|||
|
|||
### Folder Structure Explanation |
|||
|
|||
Understanding the folder structure is essential for working with the app. Here's an overview of the main folders: |
|||
|
|||
![quiz-app-folder-structure.png](./src/assets/images/quiz-app-folder-structure.png) |
|||
|
|||
- **assets**: Contains all the app's assets, such as fonts, icons, and images |
|||
- **components**: Contains all the components of app |
|||
- **components/UI**: Contains reusable UI components of app |
|||
- **context**: Includes a context for sharing logic across the app |
|||
- **styles**: Contains styles and their configurations using Styled Components |
|||
- **hooks**: Includes reusable hooks used in the app |
|||
- **utils**: Contains Javascript helper functions |
|||
- **data**: Contains quiz questions and quiz topic screens data |
|||
- **types**: Contains TypeScript types used throughout the app |
|||
- **config**: Imports all the icons, providing centralized access |
|||
|
|||
### Components Architecture |
|||
|
|||
The **Xeven Quiz App** consists of 5 main screens/components that are displayed conditionally: |
|||
|
|||
1. Splash Screen |
|||
2. Quiz Topics Screen |
|||
3. Quiz Details Screen |
|||
4. Questions Screen |
|||
5. Result Screen |
|||
|
|||
The screens are organized in the **`components`** folder since the app does not utilize routing. If a component is reusable and can be used in multiple places within the app (e.g., Button, ModalWrapper, and CodeSnippet), it is placed in the **`components/UI`** folder. On the other hand, if a component is screen-specific and separated just to make other components smaller and more manageable, it is placed in the relevant components folder. For example, the components `**QuizHeader**`, `**Question**`, and `**Answer**` are inside the **`QuestionScreen`** folder. |
|||
|
|||
## How to customize the quiz layout and styling |
|||
|
|||
### **Changing the App Theme** |
|||
|
|||
To change the theme of the app, follow these steps: |
|||
|
|||
1. Open the **`styles/Themes`** file. |
|||
2. Modify the colors in the themes to customize the app's appearance. |
|||
|
|||
### Changing the App Font |
|||
|
|||
To change the font of the app, follow these steps: |
|||
|
|||
1. Go to **`assets`** ⇒ **`fonts`**. |
|||
2. Replace the current fonts (e.g., "anek-malayalam") with the fonts you want to use. |
|||
3. Go to **`fonts.module.css`** file ****and replace the font name and path with new font you added. |
|||
4. Go to the **`theme`** file and change the font name. |
|||
5. Go to the global styles and update the font in the **`body`** section. |
|||
|
|||
### **Modifying the Quiz Topic Screen or Adding New Categories** |
|||
|
|||
To modify the Quiz Topics Screen or add new categories of topics/icons, follow these steps: |
|||
|
|||
1. Open the **`data/quizTopics`** file. |
|||
2. Make changes to the titles, icons or add new topics (by adding new object in `quizTopics`) as needed. |
|||
3. Ensure that the title in the **`QuizTopic`** data match the topic of **`data/QuizQuestions`** folder. |
|||
|
|||
For example |
|||
|
|||
```jsx |
|||
export const quizTopics: QuizTopic[] = [ |
|||
{ |
|||
title: 'React', // match topic value with this line |
|||
icon: <ReactIcon />, |
|||
}, |
|||
{ |
|||
title: 'JavaScript', // match topic value with this line |
|||
icon: <JavaScript />, |
|||
}, |
|||
.... |
|||
] |
|||
|
|||
export const javascript: Topic = { |
|||
topic: 'Javascript', // match value with topic key |
|||
level: 'Beginner', |
|||
totalQuestions: 14, |
|||
..... |
|||
} |
|||
``` |
|||
|
|||
### Adding a New Screen |
|||
|
|||
This app is designed with scalability in mind, allowing you to easily add new screens. Here's how you can add a new screen, such as a "Quiz Types" screen (where you can select quiz type for example individual question timer, with or without timer): |
|||
|
|||
**Step 1: Create a component** |
|||
|
|||
Create a new component called **`QuizType`** in the **`components`** folder. |
|||
|
|||
**Step 2: Update the Main component** |
|||
|
|||
Go to the main components file (**`Main/index.ts`**) and render the **`QuizType`** screen in the **`screenComponents`** section/object. Don't forget to add the screen name in the typescript **`screenTypes`** as well. |
|||
|
|||
```jsx |
|||
const screenComponents = { |
|||
[ScreenTypes.SplashScreen]: <SplashScreen />, |
|||
[ScreenTypes.QuizTopicsScreen]: <QuizTopicsScreen />, |
|||
[ScreenTypes.QuizTypesScreen]: <QuizTypesScreen />, // new screen |
|||
[ScreenTypes.QuizDetailsScreen]: <QuizDetailsScreen />, |
|||
[ScreenTypes.QuestionScreen]: <QuestionScreen />, |
|||
[ScreenTypes.ResultScreen]: <ResultScreen />, |
|||
} |
|||
``` |
|||
|
|||
If you have multiple conditions to show the screen, you can change the object to a switch or if-else statement. Here's an example using a switch statement: |
|||
|
|||
```jsx |
|||
import { useEffect } from 'react' |
|||
|
|||
import { useQuiz } from '../../context/QuizContext' |
|||
import { ScreenTypes } from '../../types' |
|||
|
|||
import QuestionScreen from '../QuestionScreen' |
|||
import QuizDetailsScreen from '../QuizDetailsScreen' |
|||
import QuizTopicsScreen from '../QuizTopicsScreen' |
|||
import ResultScreen from '../ResultScreen' |
|||
import SplashScreen from '../SplashScreen' |
|||
|
|||
function Main() { |
|||
const { currentScreen, setCurrentScreen } = useQuiz() |
|||
|
|||
useEffect(() => { |
|||
setTimeout(() => { |
|||
setCurrentScreen(ScreenTypes.QuizTopicsScreen) |
|||
}, 1000) |
|||
}, []) |
|||
|
|||
switch (currentScreen) { |
|||
case ScreenTypes.SplashScreen: |
|||
return <SplashScreen /> |
|||
case ScreenTypes.QuizTopicsScreen: |
|||
return <QuizTopicsScreen /> |
|||
case ScreenTypes.QuizDetailsScreen: |
|||
return <QuizDetailsScreen /> |
|||
case ScreenTypes.QuestionScreen: |
|||
return <QuestionScreen /> |
|||
case ScreenTypes.ResultScreen: |
|||
return <ResultScreen /> |
|||
default: |
|||
return <SplashScreen /> |
|||
} |
|||
} |
|||
|
|||
export default Main |
|||
``` |
|||
|
|||
### **How to Add Code Snippets in Questions** |
|||
|
|||
Each question supports a **`code`** key, which is conditionally shown only if the question contains a code snippet. |
|||
|
|||
### How to format code snippet |
|||
|
|||
In the Xeven Quiz App, code snippets are pieces of code represented as text. To make them look nice and readable, we use an npm package called **`prismjs`**. This tool highlights the code with different colors so that it stands out and is easy to understand. |
|||
|
|||
To display code correctly, we need to pay attention to the spaces and how the code is structured, just like we do with the existing questions. This way, the code will appear neatly formatted and will be easier for users to read and comprehend. |
|||
|
|||
Here's an example image to illustrate the correct format for displaying code snippets: |
|||
|
|||
![code-snippet-format.png](./src/assets/images/code-snippet-format.png) |
|||
|
|||
### **Implementing Different Types of Quiz Questions** |
|||
|
|||
The Code Quiz App supports various types of quiz questions, including Multiple Choice Questions (MCQs), Multiple Answer Questions (MAQs), and True/False questions. To add different question types, you can modify the question components and their associated data structures. You can refer to the existing question formats in the **`data/QuizQuestions`** folder as examples when creating new questions. |
|||
|
|||
For example, if you want to create a Multiple Choice Question (MCQ), you need to set its **`type`** to **`MCQs`** in the question data. Similarly, for a Multiple Answer Question (MAQ), set the **`type`** to **`MAQs`**, and for a True/False question, set it to the appropriate type as well. |
|||
|
|||
**Remember:** For MAQs, users can select multiple answer options, while for MCQs and True/False questions, users can select only one option. Make sure to set the correct **`type`** to match the question's behavior accordingly. |
|||
|
|||
### **Important Note** |
|||
|
|||
Before making the Code Quiz App your own, remember to customize the meta and title tags in the **`index.html`** file, as well as the logo, preview image, and favicon image in the **`public`** folder. This ensures that the app reflects your branding and identity. |
|||
|
|||
## **Deploying the Quiz App to a Production Environment** |
|||
|
|||
To share your quiz app with the world, you need to deploy it on a server that supports single-page applications or the React ecosystem. Here are some popular options for deploying your app: |
|||
|
|||
1. Digital Ocean |
|||
2. Vercel |
|||
3. Netlify |
|||
4. AWS Amplify |
|||
|
|||
Choose the one that best suits your needs and follow their deployment instructions to make your app accessible to users. |
|||
|
|||
### What to expect in next update? |
|||
|
|||
The next app update, scheduled for September 2023, will bring exciting new features and improvements: |
|||
|
|||
1. Dark mode support. |
|||
2. Image support in Quiz Questions. |
|||
3. Faster Typography with Typography Styled components |
|||
|
|||
I hope this documentation helps you get started with the Xeven Quiz App. If you have any questions or feedback, please feel free to reach out to me at **[abdul_basit313@outlook.com](mailto:abdul_basit313@outlook.com)**. Happy quizzing! |
17006
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,50 @@ |
|||
{ |
|||
"homepage": ".", |
|||
"name": "quiz-app", |
|||
"version": "0.1.0", |
|||
"private": true, |
|||
"dependencies": { |
|||
"@testing-library/jest-dom": "^5.16.5", |
|||
"@testing-library/react": "^13.4.0", |
|||
"@testing-library/user-event": "^13.5.0", |
|||
"@types/jest": "^27.5.2", |
|||
"@types/node": "^16.18.12", |
|||
"@types/react": "^18.0.28", |
|||
"@types/react-dom": "^18.0.11", |
|||
"prismjs": "^1.29.0", |
|||
"react": "^18.2.0", |
|||
"react-dom": "^18.2.0", |
|||
"react-scripts": "5.0.1", |
|||
"styled-components": "^5.3.6", |
|||
"typescript": "^4.9.5", |
|||
"web-vitals": "^2.1.4" |
|||
}, |
|||
"scripts": { |
|||
"start": "react-scripts start", |
|||
"build": "react-scripts build", |
|||
"test": "react-scripts test", |
|||
"eject": "react-scripts eject" |
|||
}, |
|||
"eslintConfig": { |
|||
"extends": [ |
|||
"react-app", |
|||
"react-app/jest" |
|||
] |
|||
}, |
|||
"browserslist": { |
|||
"production": [ |
|||
">0.2%", |
|||
"not dead", |
|||
"not op_mini all" |
|||
], |
|||
"development": [ |
|||
"last 1 chrome version", |
|||
"last 1 firefox version", |
|||
"last 1 safari version" |
|||
] |
|||
}, |
|||
"devDependencies": { |
|||
"@types/prismjs": "^1.26.0", |
|||
"@types/styled-components": "^5.1.26" |
|||
} |
|||
} |
@ -0,0 +1,41 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|||
<meta name="theme-color" content="#800080" /> |
|||
<meta name="description" content="ReactJS Quiz App Template" /> |
|||
<meta property="og:image" content="%PUBLIC_URL%/xeven-quiz.png" /> |
|||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> |
|||
<!-- |
|||
manifest.json provides metadata used when your web app is installed on a |
|||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
|||
--> |
|||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
|||
<!-- |
|||
Notice the use of %PUBLIC_URL% in the tags above. |
|||
It will be replaced with the URL of the `public` folder during the build. |
|||
Only files inside the `public` folder can be referenced from the HTML. |
|||
|
|||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
|||
work correctly both with client-side routing and a non-root public URL. |
|||
Learn how to configure a non-root public URL by running `npm run build`. |
|||
--> |
|||
<title>Xeven Quiz - ReactJS Quiz App Template</title> |
|||
</head> |
|||
<body> |
|||
<noscript>You need to enable JavaScript to run this app.</noscript> |
|||
<div id="root"></div> |
|||
<!-- |
|||
This HTML file is a template. |
|||
If you open it directly in the browser, you will see an empty page. |
|||
|
|||
You can add webfonts, meta tags, or analytics to this file. |
|||
The build step will place the bundled scripts into the <body> tag. |
|||
|
|||
To begin the development, run `npm start` or `yarn start`. |
|||
To create a production bundle, use `npm run build` or `yarn build`. |
|||
--> |
|||
</body> |
|||
</html> |
After Width: 192 | Height: 192 | Size: 23 KiB |
After Width: 512 | Height: 512 | Size: 98 KiB |
@ -0,0 +1,25 @@ |
|||
{ |
|||
"short_name": "Xeven Quiz", |
|||
"name": "Xeven Quiz - ReactJS Quiz App Template", |
|||
"icons": [ |
|||
{ |
|||
"src": "favicon.ico", |
|||
"sizes": "64x64 32x32 24x24 16x16", |
|||
"type": "image/x-icon" |
|||
}, |
|||
{ |
|||
"src": "logo192.png", |
|||
"type": "image/png", |
|||
"sizes": "192x192" |
|||
}, |
|||
{ |
|||
"src": "logo512.png", |
|||
"type": "image/png", |
|||
"sizes": "512x512" |
|||
} |
|||
], |
|||
"start_url": ".", |
|||
"display": "standalone", |
|||
"theme_color": "#000000", |
|||
"background_color": "#ffffff" |
|||
} |
@ -0,0 +1,3 @@ |
|||
# https://www.robotstxt.org/robotstxt.html |
|||
User-agent: * |
|||
Disallow: |
After Width: 1920 | Height: 932 | Size: 27 KiB |
@ -0,0 +1,9 @@ |
|||
import React from 'react'; |
|||
import { render, screen } from '@testing-library/react'; |
|||
import App from './App'; |
|||
|
|||
test('renders learn react link', () => { |
|||
render(<App />); |
|||
const linkElement = screen.getByText(/learn react/i); |
|||
expect(linkElement).toBeInTheDocument(); |
|||
}); |
@ -0,0 +1,17 @@ |
|||
import { ThemeProvider } from 'styled-components' |
|||
|
|||
import Main from './components/Main' |
|||
import QuizProvider from './context/QuizContext' |
|||
import { GlobalStyles } from './styles/Global' |
|||
import { theme as AppTheme } from './styles/Theme' |
|||
|
|||
const App = () => ( |
|||
<ThemeProvider theme={AppTheme}> |
|||
<GlobalStyles /> |
|||
<QuizProvider> |
|||
<Main /> |
|||
</QuizProvider> |
|||
</ThemeProvider> |
|||
) |
|||
|
|||
export default App |
@ -0,0 +1,12 @@ |
|||
<svg width="38" height="40" viewBox="0 0 38 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_603)"> |
|||
<path d="M0.0151367 6.69445L18.5163 0.102539L37.5157 6.57724L34.4394 31.0552L18.5163 39.8737L2.84234 31.1724L0.0151367 6.69445Z" fill="#E23237"/> |
|||
<path d="M37.5155 6.57724L18.5161 0.102539V39.8737L34.4392 31.0699L37.5155 6.57724Z" fill="#B52E31"/> |
|||
<path d="M18.5455 4.74609L7.01709 30.3959L11.3237 30.3227L13.6383 24.5364H23.9802L26.5144 30.3959L30.6306 30.4692L18.5455 4.74609ZM18.5749 12.964L22.4714 21.1086H15.1469L18.5749 12.964Z" fill="white"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_1063_603"> |
|||
<rect width="37.648" height="40" fill="white"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
21
src/assets/icons/app-logo.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,11 @@ |
|||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_140_557)"> |
|||
<path d="M1.5 28C1.5 13.3886 13.3886 1.5 28 1.5C42.6114 1.5 54.5 13.3886 54.5 28C54.5 42.6114 42.6114 54.5 28 54.5C13.3886 54.5 1.5 42.6114 1.5 28Z" stroke="#800080" stroke-width="3"/> |
|||
<path d="M42.1916 22.0662L27.0248 37.2325C26.5698 37.6876 25.9725 37.9166 25.3752 37.9166C24.778 37.9166 24.1807 37.6876 23.7256 37.2325L16.1425 29.6494C15.2299 28.7372 15.2299 27.2623 16.1425 26.3502C17.0546 25.4376 18.5291 25.4376 19.4416 26.3502L25.3752 32.2838L38.8925 18.767C39.8046 17.8544 41.279 17.8544 42.1916 18.767C43.1038 19.6791 43.1038 21.1536 42.1916 22.0662Z" fill="#800080"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_140_557"> |
|||
<rect width="56" height="56" fill="white"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
69
src/assets/icons/colored-logo.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,12 @@ |
|||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_645)"> |
|||
<path d="M3.14145 35.8095L0.216797 0.0668945L35.1448 0.138722L32.0523 35.8095L17.8126 39.9327L3.14145 35.8095Z" fill="#1B73BA"/> |
|||
<path d="M17.8125 36.481V3.47119L32.1961 3.51908L29.6311 33.053L17.8125 36.481Z" fill="#1C88C7"/> |
|||
<path d="M28.6242 7.69043H6.49756L7.09697 12.0052H17.5968L7.26485 16.4402L7.86398 20.6113H23.0867L22.5351 26.4363L17.381 27.4433L12.7066 26.2448L12.3469 22.9365H8.0558L8.63098 29.8168L17.8365 32.3817L26.6586 29.577L27.7853 16.1284H18.4596L28.6239 11.9094L28.6242 7.69043Z" fill="white"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_1063_645"> |
|||
<rect width="34.928" height="40" fill="white" transform="translate(0.216797)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
@ -0,0 +1,4 @@ |
|||
<svg width="33" height="40" viewBox="0 0 33 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M14.8912 0H21.4277V29.9592C18.0796 30.5925 15.6148 30.8417 12.9479 30.8417C4.96439 30.8333 0.808594 27.2667 0.808594 20.4167C0.808594 13.8167 5.21676 9.53331 12.0478 9.53331C13.1078 9.53331 13.9153 9.61674 14.8912 9.86665V0ZM15.1202 15.2656C14.3546 15.0156 13.7238 14.9323 12.9161 14.9323C9.61007 14.9323 7.70044 16.9489 7.70044 20.483C7.70044 23.9239 9.52603 25.8239 12.8742 25.8239C13.5975 25.8239 14.1864 25.7831 15.1202 25.6581V15.2656Z" fill="#2BA977"/> |
|||
<path d="M32.117 10.3376V25.3378C32.117 30.5035 31.7301 32.9877 30.5944 35.1295C29.5344 37.1885 28.1379 38.4869 25.2524 39.921L19.187 37.0628C22.0725 35.7218 23.4689 34.5369 24.3607 32.7286C25.2944 30.8795 25.5888 28.7377 25.5888 23.1044V10.3378L32.117 10.3376ZM24.9301 0H31.4667V6.64169H24.9301V0Z" fill="#2BA977"/> |
|||
</svg> |
@ -0,0 +1,10 @@ |
|||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_586)"> |
|||
<path d="M20.5884 0C9.54198 0 0.588379 8.9536 0.588379 20C0.588379 31.0464 9.54198 40 20.5884 40C31.6348 40 40.5884 31.0464 40.5884 20C40.5884 8.9536 31.6348 0 20.5884 0ZM4.88598 20.208L20.3804 35.7024C11.8716 35.592 4.99638 28.7168 4.88598 20.208ZM24.1036 35.3104L5.27798 16.4848C6.87478 9.504 13.1228 4.2944 20.5884 4.2944C25.806 4.2944 30.43 6.84 33.286 10.7568L31.1116 12.6752C28.7948 9.3536 24.9452 7.1792 20.5884 7.1792C15.0412 7.1792 10.3164 10.7024 8.53078 15.6336L24.9548 32.056C28.9436 30.6112 32.0108 27.2448 33.038 23.0752H26.2316V20H36.2956C36.294 27.4656 31.0844 33.7136 24.1036 35.3104Z" fill="#744C9E"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_1063_586"> |
|||
<rect width="40" height="40" fill="white" transform="translate(0.588379)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
@ -0,0 +1,12 @@ |
|||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_564)"> |
|||
<path d="M3.48081 35.8101L0.556152 0.0668945L35.4842 0.138722L32.3916 35.8101L18.152 39.9333L3.48081 35.8101Z" fill="#E9CA32"/> |
|||
<path d="M18.1519 36.481V3.4707L32.5354 3.51859L29.9705 33.0527L18.1519 36.481Z" fill="#FFDE25"/> |
|||
<path d="M16.6118 7.39062H12.8091V29.3615L9.85153 28.7981V25.8405L6.33057 25.2771V31.8966L16.6118 34.5725V7.39062ZM19.4643 7.39062H29.9915L29.2873 11.6017H23.483V18.5121H29.2873L28.5831 32.4599L19.4643 34.5725V30.2065L25.6255 28.0939L25.9812 22.3195L19.4643 23.2631V7.39062Z" fill="white"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_1063_564"> |
|||
<rect width="34.928" height="40" fill="white" transform="translate(0.556152)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
@ -0,0 +1,15 @@ |
|||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_654)"> |
|||
<path d="M40.392 39.9975H0.375977V0.00292969H40.392L19.9712 19.7033L40.392 39.9975Z" fill="url(#paint0_radial_1063_654)"/> |
|||
</g> |
|||
<defs> |
|||
<radialGradient id="paint0_radial_1063_654" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.2204 1.47819) scale(46.0048)"> |
|||
<stop stop-color="#E44857"/> |
|||
<stop offset="0.47" stop-color="#C711E1"/> |
|||
<stop offset="1" stop-color="#7F52FF"/> |
|||
</radialGradient> |
|||
<clipPath id="clip0_1063_654"> |
|||
<rect width="40.016" height="40" fill="white" transform="translate(0.375977)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
@ -0,0 +1,10 @@ |
|||
<svg width="59" height="40" viewBox="0 0 59 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_664)"> |
|||
<path d="M58.561 18.6811C58.1445 18.2646 52.7996 11.5315 51.8972 10.4208C50.9254 9.31021 50.509 9.51846 49.8842 9.58787C49.2595 9.65728 42.5264 10.8373 41.7628 10.9067C40.9992 11.0456 40.5133 11.3232 40.9992 12.0174C41.4157 12.6421 45.8582 18.8893 46.83 20.347L29.1295 24.5813L15.1078 1.04996C14.5525 0.21699 14.4137 -0.0606661 13.1643 0.00874785C11.9148 0.0781618 2.12744 0.911129 1.4333 0.911129C0.739165 0.980543 -0.0243881 1.2582 0.669751 2.92413C1.36389 4.59007 12.4701 28.4685 12.7478 29.1626C13.0254 29.8567 13.8584 30.9674 15.7326 30.5509C17.6762 30.065 24.3399 28.3296 28.0188 27.3578C29.9624 30.8285 33.8496 37.9088 34.6132 38.95C35.585 40.3382 36.2791 40.0606 37.7368 39.6441C38.9168 39.297 55.9232 33.1886 56.6868 32.8415C57.4503 32.4945 57.9362 32.2862 57.3809 31.5227C56.9644 30.9674 52.522 24.9283 50.1619 21.8047C51.7584 21.3882 57.5198 19.8611 58.1445 19.6529C58.8386 19.4446 58.9775 19.0976 58.561 18.6811ZM26.4223 25.2754C26.2141 25.3448 16.2879 27.7049 15.802 27.8437C15.2467 27.9826 15.2467 27.9131 15.2467 27.7049C15.1078 27.4967 3.44631 3.34062 3.23807 3.06296C3.09924 2.78531 3.09924 2.50765 3.23807 2.50765C3.37689 2.50765 12.6089 1.67468 12.8866 1.67468C13.2337 1.67468 13.1643 1.7441 13.3031 1.95234C13.3031 1.95234 26.2835 24.373 26.4917 24.7201C26.7694 25.0672 26.6306 25.206 26.4223 25.2754ZM54.3267 30.4815C54.4655 30.7591 54.6738 30.8979 54.1185 31.0368C53.6326 31.245 37.3897 36.7287 37.0427 36.8675C36.6956 37.0064 36.4873 37.0758 36.0709 36.4511C35.6544 35.8263 30.3789 26.7331 30.3789 26.7331L47.663 22.2212C48.0795 22.0824 48.2183 22.013 48.4959 22.4294C48.7736 22.9153 54.1879 30.2732 54.3267 30.4815ZM55.4373 18.2646C55.0209 18.334 48.7042 19.9305 48.7042 19.9305L43.4981 12.8503C43.3593 12.6421 43.2205 12.4338 43.5676 12.3644C43.9146 12.295 49.8148 11.2538 50.0925 11.1844C50.3701 11.115 50.5784 11.0456 50.9254 11.5315C51.2725 11.9479 55.715 17.6399 55.9232 17.8481C56.1315 18.0564 55.8538 18.1952 55.4373 18.2646Z" fill="#FB503B"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_1063_664"> |
|||
<rect width="58.3771" height="40" fill="white" transform="translate(0.39209)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
64
src/assets/icons/logo.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,3 @@ |
|||
<svg width="10" height="18" viewBox="0 0 10 18" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M6.87178 9.51297C6.8998 9.29313 6.8998 9.07068 6.87178 8.85084C6.82715 8.50072 6.65947 8.10978 6.17068 7.50115C5.6675 6.87459 4.92386 6.13514 3.83978 5.06068L1.1686 2.41324C0.72285 1.97144 0.71964 1.25195 1.16143 0.806192C1.60323 0.360437 2.32273 0.357229 2.76848 0.799023L5.48779 3.49417C6.51199 4.50923 7.34907 5.33885 7.94271 6.07805C8.56003 6.84672 9.0069 7.62694 9.12627 8.56348C9.17861 8.97412 9.17861 9.38969 9.12627 9.80033C9.0069 10.7369 8.56003 11.5171 7.94271 12.2858C7.34906 13.025 6.51198 13.8546 5.48777 14.8697L2.76848 17.5648C2.32273 18.0066 1.60323 18.0034 1.16143 17.5576C0.71964 17.1119 0.72285 16.3924 1.1686 15.9506L3.83977 13.3031C4.92386 12.2287 5.6675 11.4892 6.17068 10.8627C6.65947 10.254 6.82715 9.86309 6.87178 9.51297Z" fill="#9fa3a9"/> |
|||
</svg> |
@ -0,0 +1,19 @@ |
|||
<svg width="41" height="40" viewBox="0 0 41 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_1063_607)"> |
|||
<path d="M20.5565 0.0112305C10.3868 0.0112305 11.0218 4.42143 11.0218 4.42143L11.0331 8.99053H20.7379V10.3623H7.17851C7.17851 10.3623 0.670898 9.62425 0.670898 19.8854C0.670898 30.1469 6.3509 29.783 6.3509 29.783H9.7407V25.0213C9.7407 25.0213 9.55796 19.3413 15.33 19.3413H24.9554C24.9554 19.3413 30.3633 19.4286 30.3633 14.1148V5.32856C30.3633 5.32856 31.1846 0.0112305 20.5565 0.0112305ZM15.2053 3.08386C15.4347 3.08365 15.6618 3.12867 15.8737 3.21634C16.0856 3.304 16.2782 3.43259 16.4403 3.59475C16.6025 3.75692 16.7311 3.94946 16.8187 4.16138C16.9064 4.37329 16.9514 4.60041 16.9512 4.82974C16.9514 5.05907 16.9064 5.28619 16.8187 5.49811C16.7311 5.71002 16.6025 5.90257 16.4403 6.06473C16.2782 6.22689 16.0856 6.35548 15.8737 6.44315C15.6618 6.53081 15.4347 6.57583 15.2053 6.57562C14.976 6.57583 14.7489 6.53081 14.537 6.44315C14.3251 6.35548 14.1325 6.22689 13.9703 6.06473C13.8082 5.90257 13.6796 5.71002 13.5919 5.49811C13.5043 5.28619 13.4592 5.05907 13.4594 4.82974C13.4592 4.60041 13.5043 4.37329 13.5919 4.16138C13.6796 3.94946 13.8082 3.75692 13.9703 3.59475C14.1325 3.43259 14.3251 3.304 14.537 3.21634C14.7489 3.12867 14.976 3.08365 15.2053 3.08386Z" fill="url(#paint0_linear_1063_607)"/> |
|||
<path d="M20.8451 39.8627C31.0148 39.8627 30.3798 35.4525 30.3798 35.4525L30.3685 30.8836H20.6636V29.5118H34.2229C34.2229 29.5118 40.7305 30.2499 40.7305 19.9884C40.7305 9.72703 35.0505 10.091 35.0505 10.091H31.6607V14.8525C31.6607 14.8525 31.8435 20.5325 26.0714 20.5325H16.446C16.446 20.5325 11.0381 20.4451 11.0381 25.7592V34.5455C11.0381 34.5455 10.217 39.8627 20.8449 39.8627H20.8451ZM26.1963 36.7904C25.9669 36.7906 25.7398 36.7456 25.5279 36.6579C25.316 36.5703 25.1234 36.4417 24.9613 36.2795C24.7991 36.1173 24.6705 35.9248 24.5829 35.7129C24.4952 35.501 24.4502 35.2739 24.4504 35.0445C24.4502 34.8152 24.4952 34.588 24.5828 34.3761C24.6705 34.1642 24.7991 33.9716 24.9612 33.8094C25.1234 33.6473 25.3159 33.5187 25.5279 33.431C25.7398 33.3433 25.9669 33.2983 26.1963 33.2985C26.4256 33.2983 26.6527 33.3433 26.8646 33.431C27.0765 33.5186 27.2691 33.6472 27.4313 33.8094C27.5934 33.9715 27.722 34.1641 27.8097 34.376C27.8973 34.5879 27.9424 34.815 27.9422 35.0444C27.9424 35.2737 27.8973 35.5008 27.8097 35.7127C27.722 35.9246 27.5934 36.1172 27.4313 36.2793C27.2691 36.4415 27.0765 36.5701 26.8646 36.6578C26.6527 36.7454 26.4256 36.7906 26.1963 36.7904Z" fill="url(#paint1_linear_1063_607)"/> |
|||
</g> |
|||
<defs> |
|||
<linearGradient id="paint0_linear_1063_607" x1="385.645" y1="358.453" x2="2370.89" y2="2323.9" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#387EB8"/> |
|||
<stop offset="1" stop-color="#366994"/> |
|||
</linearGradient> |
|||
<linearGradient id="paint1_linear_1063_607" x1="579.261" y1="622.795" x2="2711.19" y2="2638.17" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FFE052"/> |
|||
<stop offset="1" stop-color="#FFC331"/> |
|||
</linearGradient> |
|||
<clipPath id="clip0_1063_607"> |
|||
<rect width="40.1606" height="40" fill="white" transform="translate(0.647949)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
@ -0,0 +1,4 @@ |
|||
<svg width="36" height="32" viewBox="0 0 36 32" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M35.5884 16.0035C35.5884 13.678 32.6762 11.4742 28.2112 10.1075C29.2416 5.55675 28.7837 1.93616 26.7659 0.776994C26.3008 0.505091 25.757 0.376296 25.1631 0.376296V1.97193C25.4922 1.97193 25.757 2.03633 25.9788 2.15797C26.9519 2.71609 27.3741 4.84122 27.0449 7.57455C26.9662 8.24715 26.8374 8.95552 26.68 9.67821C25.2776 9.33476 23.7463 9.07001 22.1364 8.89828C21.1704 7.57455 20.1687 6.37245 19.1598 5.32062C21.4924 3.15256 23.6819 1.96478 25.1702 1.96478V0.369141C23.2025 0.369141 20.6266 1.77158 18.0221 4.20439C15.4175 1.78589 12.8416 0.397762 10.8739 0.397762V1.9934C12.3551 1.9934 14.5517 3.17403 16.8844 5.32778C15.8826 6.37961 14.8809 7.57455 13.9292 8.89828C12.3121 9.07001 10.7809 9.33476 9.37844 9.68537C9.21387 8.96983 9.09222 8.27577 9.00636 7.61032C8.67006 4.87699 9.08507 2.75186 10.051 2.18659C10.2657 2.0578 10.5448 2.00055 10.8739 2.00055V0.404917C10.2729 0.404917 9.72905 0.533713 9.2568 0.805615C7.24615 1.96478 6.79537 5.57821 7.83289 10.1147C3.38228 11.4885 0.484375 13.6852 0.484375 16.0035C0.484375 18.329 3.39659 20.5328 7.86151 21.8995C6.83114 26.4503 7.28908 30.0709 9.30688 31.23C9.77198 31.5019 10.3158 31.6307 10.9168 31.6307C12.8845 31.6307 15.4605 30.2283 18.065 27.7955C20.6695 30.214 23.2454 31.6021 25.2132 31.6021C25.8142 31.6021 26.358 31.4733 26.8303 31.2014C28.8409 30.0422 29.2917 26.4288 28.2542 21.8923C32.6905 20.5257 35.5884 18.3218 35.5884 16.0035ZM26.2721 11.2309C26.0074 12.154 25.6783 13.1056 25.3062 14.0573C25.0128 13.4848 24.7051 12.9124 24.3688 12.34C24.0397 11.7676 23.6891 11.2094 23.3385 10.6656C24.3545 10.8159 25.3348 11.0019 26.2721 11.2309ZM22.995 18.8513C22.4369 19.8173 21.8645 20.7332 21.2706 21.5847C20.2044 21.6777 19.124 21.7278 18.0364 21.7278C16.9559 21.7278 15.8755 21.6777 14.8165 21.5918C14.2226 20.7403 13.643 19.8316 13.0849 18.8728C12.5411 17.9354 12.0474 16.9838 11.5966 16.025C12.0402 15.0662 12.5411 14.1074 13.0777 13.17C13.6359 12.204 14.2083 11.2882 14.8022 10.4367C15.8683 10.3437 16.9488 10.2936 18.0364 10.2936C19.1168 10.2936 20.1973 10.3437 21.2563 10.4295C21.8502 11.281 22.4297 12.1897 22.9879 13.1485C23.5317 14.0859 24.0254 15.0375 24.4762 15.9964C24.0254 16.9552 23.5317 17.914 22.995 18.8513ZM25.3062 17.9211C25.6926 18.88 26.0217 19.8388 26.2936 20.769C25.3563 20.9979 24.3688 21.1911 23.3456 21.3414C23.6962 20.7904 24.0468 20.2252 24.376 19.6456C24.7051 19.0731 25.0128 18.4936 25.3062 17.9211ZM18.0507 25.5559C17.3852 24.869 16.7198 24.1033 16.0615 23.2662C16.7055 23.2948 17.3638 23.3162 18.0292 23.3162C18.7018 23.3162 19.3673 23.3019 20.0184 23.2662C19.3744 24.1033 18.709 24.869 18.0507 25.5559ZM12.7271 21.3414C11.7111 21.1911 10.7308 21.0051 9.79345 20.7761C10.0582 19.8531 10.3873 18.9014 10.7594 17.9498C11.0528 18.5222 11.3605 19.0946 11.6968 19.667C12.0331 20.2395 12.3765 20.7976 12.7271 21.3414ZM18.0149 6.45116C18.6804 7.13807 19.3458 7.90369 20.0041 8.74086C19.3601 8.71224 18.7018 8.69078 18.0364 8.69078C17.3638 8.69078 16.6983 8.70509 16.0472 8.74086C16.6912 7.90369 17.3566 7.13807 18.0149 6.45116ZM12.72 10.6656C12.3694 11.2166 12.0188 11.7819 11.6896 12.3615C11.3605 12.9339 11.0528 13.5063 10.7594 14.0787C10.373 13.1199 10.0439 12.1611 9.77198 11.2309C10.7093 11.0091 11.6968 10.8159 12.72 10.6656ZM6.24441 19.6241C3.71143 18.5437 2.07286 17.1269 2.07286 16.0035C2.07286 14.8801 3.71143 13.4562 6.24441 12.3829C6.85977 12.1182 7.53236 11.882 8.22643 11.6602C8.63428 13.0627 9.17093 14.5224 9.83638 16.0178C9.17809 17.5061 8.6486 18.9587 8.2479 20.3539C7.53952 20.1321 6.86692 19.8889 6.24441 19.6241ZM10.094 29.8491C9.12085 29.2909 8.69868 27.1658 9.02783 24.4325C9.10654 23.7599 9.23533 23.0515 9.39275 22.3288C10.7952 22.6723 12.3264 22.937 13.9364 23.1087C14.9023 24.4325 15.9041 25.6346 16.913 26.6864C14.5804 28.8545 12.3908 30.0423 10.9025 30.0423C10.5805 30.0351 10.3086 29.9707 10.094 29.8491ZM27.0664 24.3967C27.4027 27.13 26.9877 29.2552 26.0217 29.8204C25.8071 29.9492 25.528 30.0065 25.1988 30.0065C23.7177 30.0065 21.521 28.8258 19.1884 26.6721C20.1901 25.6203 21.1919 24.4253 22.1435 23.1016C23.7606 22.9299 25.2919 22.6651 26.6943 22.3145C26.8589 23.0372 26.9877 23.7313 27.0664 24.3967ZM29.8212 19.6241C29.2058 19.8889 28.5332 20.125 27.8392 20.3468C27.4313 18.9444 26.8947 17.4847 26.2292 15.9892C26.8875 14.5009 27.417 13.0484 27.8177 11.6531C28.5261 11.8749 29.1987 12.1182 29.8283 12.3829C32.3613 13.4634 33.9999 14.8801 33.9999 16.0035C33.9927 17.1269 32.3542 18.5508 29.8212 19.6241Z" fill="#61DAFB"/> |
|||
<path d="M18.0293 19.2734C19.8352 19.2734 21.2992 17.8093 21.2992 16.0034C21.2992 14.1974 19.8352 12.7334 18.0293 12.7334C16.2233 12.7334 14.7593 14.1974 14.7593 16.0034C14.7593 17.8093 16.2233 19.2734 18.0293 19.2734Z" fill="#61DAFB"/> |
|||
</svg> |
@ -0,0 +1,5 @@ |
|||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M12.9999 24.6459C7.36659 24.6459 2.79492 20.0634 2.79492 14.4409C2.79492 8.81843 7.36659 4.2251 12.9999 4.2251C14.1591 4.2251 15.2858 4.3876 16.3691 4.72343C16.8024 4.85343 17.0408 5.30843 16.9108 5.74176C16.7808 6.1751 16.3258 6.41343 15.8924 6.28343C14.9716 6.00176 13.9966 5.8501 12.9999 5.8501C8.26576 5.8501 4.41992 9.69593 4.41992 14.4301C4.41992 19.1643 8.26576 23.0101 12.9999 23.0101C17.7341 23.0101 21.5799 19.1643 21.5799 14.4301C21.5799 12.7184 21.0816 11.0718 20.1391 9.66343C19.8899 9.2951 19.9874 8.78593 20.3666 8.53676C20.7349 8.2876 21.2441 8.3851 21.4933 8.76426C22.6199 10.4434 23.2158 12.4043 23.2158 14.4409C23.2049 20.0634 18.6333 24.6459 12.9999 24.6459Z" fill="white"/> |
|||
<path d="M17.4742 6.57573C17.2467 6.57573 17.0192 6.47823 16.8567 6.29407L13.7258 2.6974C13.4333 2.36157 13.4658 1.84157 13.8017 1.54907C14.1375 1.25657 14.6575 1.28907 14.95 1.6249L18.0808 5.22157C18.3733 5.5574 18.3408 6.0774 18.005 6.3699C17.8642 6.51073 17.6692 6.57573 17.4742 6.57573Z" fill="white"/> |
|||
<path d="M13.8234 9.24089C13.5742 9.24089 13.3251 9.12172 13.1626 8.90505C12.9026 8.54755 12.9784 8.03839 13.3359 7.76755L16.9867 5.10255C17.3442 4.83172 17.8534 4.91839 18.1242 5.27589C18.3951 5.63339 18.3084 6.14255 17.9509 6.41339L14.3001 9.08922C14.1592 9.19755 13.9967 9.24089 13.8234 9.24089Z" fill="white"/> |
|||
</svg> |
@ -0,0 +1,3 @@ |
|||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M0.700012 4.02495V15.9875C0.700012 18.4375 3.36251 19.975 5.48751 18.75L10.675 15.7625L15.8625 12.7625C17.9875 11.5375 17.9875 8.47495 15.8625 7.24995L10.675 4.24995L5.48751 1.26245C3.36251 0.0374511 0.700012 1.56245 0.700012 4.02495Z" fill="white"/> |
|||
</svg> |
@ -0,0 +1,5 @@ |
|||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M21.9999 41.7083C12.3933 41.7083 4.58325 33.8983 4.58325 24.2917C4.58325 14.685 12.3933 6.875 21.9999 6.875C31.6066 6.875 39.4166 14.685 39.4166 24.2917C39.4166 33.8983 31.6066 41.7083 21.9999 41.7083ZM21.9999 9.625C13.9149 9.625 7.33325 16.2067 7.33325 24.2917C7.33325 32.3767 13.9149 38.9583 21.9999 38.9583C30.0849 38.9583 36.6666 32.3767 36.6666 24.2917C36.6666 16.2067 30.0849 9.625 21.9999 9.625Z" fill="#800080"/> |
|||
<path d="M22 25.2084C21.2483 25.2084 20.625 24.5851 20.625 23.8334V14.6667C20.625 13.9151 21.2483 13.2917 22 13.2917C22.7517 13.2917 23.375 13.9151 23.375 14.6667V23.8334C23.375 24.5851 22.7517 25.2084 22 25.2084Z" fill="#800080"/> |
|||
<path d="M27.5 5.04175H16.5C15.7483 5.04175 15.125 4.41841 15.125 3.66675C15.125 2.91508 15.7483 2.29175 16.5 2.29175H27.5C28.2517 2.29175 28.875 2.91508 28.875 3.66675C28.875 4.41841 28.2517 5.04175 27.5 5.04175Z" fill="#800080"/> |
|||
</svg> |
After Width: 1020 | Height: 801 | Size: 79 KiB |
After Width: 303 | Height: 852 | Size: 27 KiB |
@ -0,0 +1,32 @@ |
|||
import { useEffect } from 'react' |
|||
|
|||
import { useQuiz } from '../../context/QuizContext' |
|||
import { ScreenTypes } from '../../types' |
|||
|
|||
import QuestionScreen from '../QuestionScreen' |
|||
import QuizDetailsScreen from '../QuizDetailsScreen' |
|||
import ResultScreen from '../ResultScreen' |
|||
import SplashScreen from '../SplashScreen' |
|||
|
|||
function Main() { |
|||
const { currentScreen, setCurrentScreen } = useQuiz() |
|||
|
|||
useEffect(() => { |
|||
setTimeout(() => { |
|||
setCurrentScreen(ScreenTypes.QuizDetailsScreen) |
|||
}, 1000) |
|||
}, [setCurrentScreen]) |
|||
|
|||
const screenComponents = { |
|||
[ScreenTypes.SplashScreen]: <SplashScreen />, |
|||
[ScreenTypes.QuizDetailsScreen]: <QuizDetailsScreen />, |
|||
[ScreenTypes.QuestionScreen]: <QuestionScreen />, |
|||
[ScreenTypes.ResultScreen]: <ResultScreen />, |
|||
} |
|||
|
|||
const ComponentToRender = screenComponents[currentScreen] || <SplashScreen /> |
|||
|
|||
return <>{ComponentToRender}</> |
|||
} |
|||
|
|||
export default Main |
@ -0,0 +1,71 @@ |
|||
import { FC } from 'react' |
|||
import styled, { css } from 'styled-components' |
|||
import { device } from '../../../styles/BreakPoints' |
|||
|
|||
const AnswerStyle = styled.div<{ highlightAnswer: boolean }>`
|
|||
font-size: clamp(18px, 4vw, 16px); |
|||
color: ${({ theme }) => theme.colors.secondaryText}; |
|||
font-weight: 400; |
|||
border: 1px solid |
|||
${({ highlightAnswer, theme }) => |
|||
highlightAnswer ? `${theme.colors.themeColor}` : `${theme.colors.lightGray}`}; |
|||
background-color: ${({ highlightAnswer, theme }) => |
|||
highlightAnswer ? `${theme.colors.lightPink}` : `${theme.colors.white}`}; |
|||
border-radius: 16px; |
|||
margin-top: clamp(13px, calc(10px + 6 * ((100vw - 600px) / 1320)), 16px); |
|||
cursor: pointer; |
|||
${({ highlightAnswer }) => |
|||
highlightAnswer && |
|||
css`
|
|||
transition: border 0.2s ease-in; |
|||
`}
|
|||
@media ${device.md} { |
|||
font-weight: 500; |
|||
} |
|||
input { |
|||
visibility: hidden; |
|||
margin: 0; |
|||
} |
|||
`
|
|||
|
|||
const AnswerLabel = styled.label`
|
|||
padding: 16px; |
|||
display: flex; |
|||
cursor: pointer; |
|||
@media ${device.md} { |
|||
padding: 14px; |
|||
} |
|||
`
|
|||
|
|||
const ChoiceLabel = styled.span`` |
|||
|
|||
interface AnswerProps { |
|||
index: number |
|||
choice: string |
|||
type: string |
|||
selectedAnswer: string[] |
|||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void |
|||
} |
|||
|
|||
const Answer: FC<AnswerProps> = ({ onChange, index, choice, type, selectedAnswer }) => { |
|||
// Convert index to alphabet character to show ABCD before question
|
|||
const label = String.fromCharCode(65 + index) |
|||
|
|||
return ( |
|||
<AnswerStyle key={index} highlightAnswer={selectedAnswer.includes(choice)}> |
|||
<AnswerLabel> |
|||
<ChoiceLabel>{label}.</ChoiceLabel> |
|||
<input |
|||
name={choice} |
|||
// radio is for checked one option and checkbox is for checked multiple options
|
|||
type={type === 'MAQs' ? 'checkbox' : 'radio'} |
|||
checked={selectedAnswer.includes(choice)} |
|||
onChange={onChange} |
|||
/> |
|||
{choice} |
|||
</AnswerLabel> |
|||
</AnswerStyle> |
|||
) |
|||
} |
|||
|
|||
export default Answer |
@ -0,0 +1,66 @@ |
|||
import { FC } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { device } from '../../../styles/BreakPoints' |
|||
|
|||
import Answer from '../Answer' |
|||
|
|||
const QuestionContainer = styled.div`
|
|||
margin-top: 30px; |
|||
margin-bottom: 40px; |
|||
max-width: 88%; |
|||
@media ${device.sm} { |
|||
max-width: 100%; |
|||
} |
|||
`
|
|||
|
|||
const AnswersContainer = styled.div`
|
|||
max-width: 78%; |
|||
@media ${device.sm} { |
|||
max-width: 100%; |
|||
} |
|||
`
|
|||
|
|||
const QuestionStyle = styled.h2`
|
|||
font-size: clamp(18px, 4vw, 28px); |
|||
font-weight: 500; |
|||
margin-bottom: 25px; |
|||
color: ${({ theme }) => theme.colors.primaryText}; |
|||
line-height: 1.3; |
|||
`
|
|||
|
|||
interface QuestionTypes { |
|||
question: string |
|||
type: string |
|||
choices: string[] |
|||
selectedAnswer: string[] |
|||
handleAnswerSelection: (e: React.ChangeEvent<HTMLInputElement>, index: number) => void |
|||
} |
|||
|
|||
const Question: FC<QuestionTypes> = ({ |
|||
question, |
|||
type, |
|||
choices, |
|||
selectedAnswer, |
|||
handleAnswerSelection, |
|||
}) => { |
|||
return ( |
|||
<QuestionContainer> |
|||
<QuestionStyle>{question}</QuestionStyle> |
|||
<AnswersContainer> |
|||
{choices.map((choice, index) => ( |
|||
<Answer |
|||
choice={choice} |
|||
index={index} |
|||
key={index} |
|||
onChange={(e) => handleAnswerSelection(e, index)} |
|||
type={type} |
|||
selectedAnswer={selectedAnswer} |
|||
/> |
|||
))} |
|||
</AnswersContainer> |
|||
</QuestionContainer> |
|||
) |
|||
} |
|||
|
|||
export default Question |
@ -0,0 +1,33 @@ |
|||
import { FC } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { TimerIcon } from '../../../../config/icons' |
|||
import { Flex } from '../../../../styles/Global' |
|||
import { device } from '../../../../styles/BreakPoints' |
|||
|
|||
const TimerStyle = styled.span`
|
|||
min-width: 60px; |
|||
font-size: clamp(16px, 5vw, 24px); |
|||
font-weight: 500; |
|||
margin-left: 8px; |
|||
color: ${({ theme }) => theme.colors.themeColor}; |
|||
@media ${device.md} { |
|||
margin-left: 5px; |
|||
min-width: 55px; |
|||
} |
|||
`
|
|||
|
|||
interface CounterProps { |
|||
time: string |
|||
} |
|||
|
|||
const Counter: FC<CounterProps> = ({ time }) => { |
|||
return ( |
|||
<Flex center> |
|||
<TimerIcon /> |
|||
<TimerStyle>{time}</TimerStyle> |
|||
</Flex> |
|||
) |
|||
} |
|||
|
|||
export default Counter |
@ -0,0 +1,41 @@ |
|||
import { FC } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { Flex } from '../../../styles/Global' |
|||
import { addLeadingZero, formatTime } from '../../../utils/helpers' |
|||
|
|||
import Counter from './Counter' |
|||
|
|||
const ActiveQuestionNo = styled.span`
|
|||
font-size: clamp(40px, 5vw, 60px); |
|||
font-weight: 500; |
|||
color: ${({ theme }) => theme.colors.themeColor}; |
|||
`
|
|||
|
|||
const TotalQuestionNo = styled.span`
|
|||
font-size: clamp(20px, 5vw, 30px); |
|||
font-weight: 500; |
|||
color: ${({ theme }) => theme.colors.darkerGray}; |
|||
`
|
|||
|
|||
interface QuizHeaderProps { |
|||
activeQuestion: number |
|||
totalQuestions: number |
|||
timer: number |
|||
} |
|||
|
|||
const QuizHeader: FC<QuizHeaderProps> = ({ activeQuestion, totalQuestions, timer }) => { |
|||
return ( |
|||
<Flex spaceBetween gap="6px"> |
|||
<div> |
|||
<ActiveQuestionNo>{addLeadingZero(activeQuestion + 1)}</ActiveQuestionNo> |
|||
<TotalQuestionNo>/{addLeadingZero(totalQuestions)}</TotalQuestionNo> |
|||
</div> |
|||
<Flex> |
|||
<Counter time={`${formatTime(timer)}`} /> |
|||
</Flex> |
|||
</Flex> |
|||
) |
|||
} |
|||
|
|||
export default QuizHeader |
@ -0,0 +1,174 @@ |
|||
import { FC, useEffect, useState } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { AppLogo, CheckIcon, Next, TimerIcon } from '../../config/icons' |
|||
import { useQuiz } from '../../context/QuizContext' |
|||
import { useTimer } from '../../hooks' |
|||
import { device } from '../../styles/BreakPoints' |
|||
import { PageCenter } from '../../styles/Global' |
|||
import { ScreenTypes } from '../../types' |
|||
|
|||
import Button from '../ui/Button' |
|||
import ModalWrapper from '../ui/ModalWrapper' |
|||
import Question from './Question' |
|||
import QuizHeader from './QuizHeader' |
|||
|
|||
const QuizContainer = styled.div<{ selectedAnswer: boolean }>`
|
|||
width: 900px; |
|||
min-height: 500px; |
|||
background: ${({ theme }) => theme.colors.white}; |
|||
border-radius: 4px; |
|||
padding: 30px 60px 80px 60px; |
|||
margin-bottom: 70px; |
|||
position: relative; |
|||
@media ${device.md} { |
|||
width: 100%; |
|||
padding: 15px 15px 80px 15px; |
|||
} |
|||
button { |
|||
span { |
|||
svg { |
|||
path { |
|||
fill: ${({ selectedAnswer, theme }) => |
|||
selectedAnswer ? `${theme.colors.white}` : `${theme.colors.darkGrayText}`}; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
`
|
|||
|
|||
const LogoContainer = styled.div`
|
|||
margin-top: 50px; |
|||
margin-bottom: 50px; |
|||
@media ${device.md} { |
|||
margin-top: 10px; |
|||
margin-bottom: 20px; |
|||
svg { |
|||
width: 185px; |
|||
height: 80px; |
|||
} |
|||
} |
|||
`
|
|||
|
|||
const ButtonWrapper = styled.div`
|
|||
position: absolute; |
|||
right: 60px; |
|||
bottom: 30px; |
|||
display: flex; |
|||
gap: 20px; |
|||
@media ${device.sm} { |
|||
justify-content: flex-end; |
|||
width: 90%; |
|||
right: 15px; |
|||
} |
|||
`
|
|||
|
|||
const QuestionScreen: FC = () => { |
|||
const [activeQuestion, setActiveQuestion] = useState<number>(0) |
|||
const [selectedAnswer, setSelectedAnswer] = useState<string[]>([]) |
|||
const [showTimerModal, setShowTimerModal] = useState<boolean>(false) |
|||
const [showResultModal, setShowResultModal] = useState<boolean>(false) |
|||
|
|||
const { |
|||
questions, |
|||
setQuestions, |
|||
quizDetails, |
|||
result, |
|||
setResult, |
|||
setCurrentScreen, |
|||
timer, |
|||
setTimer, |
|||
setEndTime, |
|||
} = useQuiz() |
|||
|
|||
const currentQuestion = questions[activeQuestion] |
|||
|
|||
const { question, type, choices, correctAnswers } = currentQuestion |
|||
|
|||
const onClickNext = () => { |
|||
const isMatch: boolean = correctAnswers.every( |
|||
(answer: string, index: number) => answer === selectedAnswer[index] |
|||
) |
|||
|
|||
// adding selected answer, and if answer matches key to result array with current question
|
|||
setResult([...result, { ...currentQuestion, selectedAnswer, isMatch }]) |
|||
|
|||
if (activeQuestion !== questions.length - 1) { |
|||
setActiveQuestion((prev) => prev + 1) |
|||
} else { |
|||
// how long does it take to finish the quiz
|
|||
const timeTaken = quizDetails.totalTime - timer |
|||
setEndTime(timeTaken) |
|||
setShowResultModal(true) |
|||
} |
|||
setSelectedAnswer([]) |
|||
} |
|||
|
|||
const handleAnswerSelection = (e: React.ChangeEvent<HTMLInputElement>) => { |
|||
const { name, checked } = e.target |
|||
|
|||
if (type === 'MCQs' || type === 'boolean') { |
|||
if (checked) { |
|||
setSelectedAnswer([name]) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const handleModal = () => { |
|||
setCurrentScreen(ScreenTypes.ResultScreen) |
|||
document.body.style.overflow = 'auto' |
|||
} |
|||
|
|||
// to prevent scrolling when modal is opened
|
|||
useEffect(() => { |
|||
if (showTimerModal || showResultModal) { |
|||
document.body.style.overflow = 'hidden' |
|||
} |
|||
}, [showTimerModal, showResultModal]) |
|||
|
|||
// timer hooks, handle conditions related to time
|
|||
useTimer(timer, quizDetails, setEndTime, setTimer, setShowTimerModal, showResultModal) |
|||
|
|||
return ( |
|||
<PageCenter> |
|||
<LogoContainer> |
|||
<AppLogo /> |
|||
</LogoContainer> |
|||
<QuizContainer selectedAnswer={selectedAnswer.length > 0}> |
|||
<QuizHeader |
|||
activeQuestion={activeQuestion} |
|||
totalQuestions={quizDetails.totalQuestions} |
|||
timer={timer} |
|||
/> |
|||
<Question |
|||
question={question} |
|||
choices={choices} |
|||
type={type} |
|||
handleAnswerSelection={handleAnswerSelection} |
|||
selectedAnswer={selectedAnswer} |
|||
/> |
|||
<ButtonWrapper> |
|||
<Button |
|||
text={activeQuestion === questions.length - 1 ? 'Finish' : 'Next'} |
|||
onClick={onClickNext} |
|||
icon={<Next />} |
|||
iconPosition="right" |
|||
disabled={selectedAnswer.length === 0} |
|||
/> |
|||
</ButtonWrapper> |
|||
</QuizContainer> |
|||
{/* timer or finish quiz modal*/} |
|||
{(showTimerModal || showResultModal) && ( |
|||
<ModalWrapper |
|||
title={showResultModal ? 'Done!' : 'Your time is up!'} |
|||
subtitle={`You have attempted ${result.length} questions in total.`} |
|||
onClick={handleModal} |
|||
icon={showResultModal ? <CheckIcon /> : <TimerIcon />} |
|||
buttonTitle="SHOW RESULT" |
|||
/> |
|||
)} |
|||
</PageCenter> |
|||
) |
|||
} |
|||
|
|||
export default QuestionScreen |
@ -0,0 +1,78 @@ |
|||
import styled from 'styled-components' |
|||
|
|||
import { Logo, StartIcon } from '../../config/icons' |
|||
import { useQuiz } from '../../context/QuizContext' |
|||
import { CenterCardContainer, HighlightedText, PageCenter } from '../../styles/Global' |
|||
import { ScreenTypes } from '../../types' |
|||
import { convertSeconds } from '../../utils/helpers' |
|||
import { useShuffleQuestions } from '../../hooks' |
|||
|
|||
import Button from '../ui/Button' |
|||
|
|||
const AppTitle = styled.h2`
|
|||
font-weight: 700; |
|||
font-size: 32px; |
|||
color: ${({ theme }) => theme.colors.themeColor}; |
|||
margin-top: 34px; |
|||
`
|
|||
|
|||
const DetailTextContainer = styled.div`
|
|||
font-size: 20px; |
|||
font-weight: 500; |
|||
margin-top: 15px; |
|||
margin-bottom: 40px; |
|||
text-align: center; |
|||
max-width: 500px; |
|||
`
|
|||
|
|||
const DetailText = styled.p`
|
|||
font-size: 20px; |
|||
font-weight: 500; |
|||
margin-top: 15px; |
|||
line-height: 1.3; |
|||
`
|
|||
|
|||
const QuizDetailsScreen = () => { |
|||
const { setCurrentScreen, quizDetails } = useQuiz() |
|||
|
|||
const { selectedQuizTopic, totalQuestions, totalScore, totalTime } = quizDetails |
|||
|
|||
const goToQuestionScreen = () => { |
|||
setCurrentScreen(ScreenTypes.QuestionScreen) |
|||
} |
|||
|
|||
// to shuffle or randomize quiz questions
|
|||
useShuffleQuestions() |
|||
|
|||
return ( |
|||
<PageCenter light justifyCenter> |
|||
<CenterCardContainer> |
|||
<Logo /> |
|||
<AppTitle>XEVEN QUIZ</AppTitle> |
|||
<DetailTextContainer> |
|||
<DetailText> |
|||
Selected Quiz Topic: <HighlightedText>{selectedQuizTopic}</HighlightedText> |
|||
</DetailText> |
|||
<DetailText> |
|||
Total questions to attempt:{' '} |
|||
<HighlightedText>{totalQuestions}</HighlightedText> |
|||
</DetailText> |
|||
<DetailText> |
|||
Score in total: <HighlightedText>{totalScore}</HighlightedText> |
|||
</DetailText> |
|||
<DetailText> |
|||
Total time: <HighlightedText>{convertSeconds(totalTime)}</HighlightedText> |
|||
</DetailText> |
|||
</DetailTextContainer> |
|||
<Button |
|||
text="Start" |
|||
icon={<StartIcon />} |
|||
iconPosition="left" |
|||
onClick={goToQuestionScreen} |
|||
/> |
|||
</CenterCardContainer> |
|||
</PageCenter> |
|||
) |
|||
} |
|||
|
|||
export default QuizDetailsScreen |
@ -0,0 +1,61 @@ |
|||
import { FC } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { useQuiz } from '../../../context/QuizContext' |
|||
import { device } from '../../../styles/BreakPoints' |
|||
import { HighlightedText } from '../../../styles/Global' |
|||
import { convertSeconds } from '../../../utils/helpers' |
|||
import { Result } from '../../../types' |
|||
|
|||
const ResultOverviewStyle = styled.div`
|
|||
text-align: center; |
|||
margin-bottom: 70px; |
|||
@media ${device.md} { |
|||
margin-bottom: 30px; |
|||
} |
|||
p { |
|||
margin-top: 15px; |
|||
font-weight: 500; |
|||
font-size: 18px; |
|||
} |
|||
`
|
|||
|
|||
interface ResultOverviewProps { |
|||
result: Result[] |
|||
} |
|||
|
|||
const ResultOverview: FC<ResultOverviewProps> = ({ result }) => { |
|||
const { quizDetails, endTime } = useQuiz() |
|||
|
|||
const totalQuestionAttempted = result.length |
|||
|
|||
const obtainedScore = result |
|||
.filter((item) => item.isMatch && typeof item.score === 'number') |
|||
.reduce((accumulator, currentValue) => accumulator + (currentValue.score || 0), 0) |
|||
|
|||
// Passed if 60 or more than 60% marks
|
|||
const calculateStatus = |
|||
(obtainedScore / quizDetails.totalScore) * 100 >= 60 ? 'Passed' : 'Failed' |
|||
|
|||
return ( |
|||
<ResultOverviewStyle> |
|||
<p> |
|||
You attempted questions:{' '} |
|||
<HighlightedText> {totalQuestionAttempted} </HighlightedText>/{' '} |
|||
{quizDetails.totalQuestions} |
|||
</p> |
|||
<p> |
|||
Score secured:<HighlightedText> {obtainedScore} </HighlightedText>/{' '} |
|||
{quizDetails.totalScore} |
|||
</p> |
|||
<p> |
|||
Time Spent:<HighlightedText> {convertSeconds(endTime)} </HighlightedText> |
|||
</p> |
|||
<p> |
|||
Status:<HighlightedText> {calculateStatus}</HighlightedText> |
|||
</p> |
|||
</ResultOverviewStyle> |
|||
) |
|||
} |
|||
|
|||
export default ResultOverview |
@ -0,0 +1,37 @@ |
|||
import { FC } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { HighlightedText } from '../../../styles/Global' |
|||
import { theme } from '../../../styles/Theme' |
|||
|
|||
interface RightAnswerProps { |
|||
correctAnswers: string[] |
|||
choices: string[] |
|||
} |
|||
|
|||
const RightAnswerContainer = styled.p`
|
|||
font-size: 18px; |
|||
font-weight: 400; |
|||
color: ${({ theme }) => theme.colors.darkerGray}; |
|||
margin-top: 15px; |
|||
line-height: 1.2; |
|||
`
|
|||
|
|||
const RightAnswer: FC<RightAnswerProps> = ({ correctAnswers, choices }) => { |
|||
return ( |
|||
<RightAnswerContainer> |
|||
{`Right ${correctAnswers.length < 2 ? 'Answer' : 'Answers'}: `} |
|||
{correctAnswers.map((item: string, index: number) => { |
|||
const label = String.fromCharCode(65 + choices.indexOf(item)) |
|||
|
|||
return ( |
|||
<HighlightedText key={index} color={theme.colors.primaryText}> |
|||
{`${label} (${item})${index !== correctAnswers.length - 1 ? ', ' : ''}`} |
|||
</HighlightedText> |
|||
) |
|||
})} |
|||
</RightAnswerContainer> |
|||
) |
|||
} |
|||
|
|||
export default RightAnswer |
@ -0,0 +1,193 @@ |
|||
import { FC } from 'react' |
|||
import styled, { css } from 'styled-components' |
|||
|
|||
import { AppColoredLogo, Refresh } from '../../config/icons' |
|||
import { useQuiz } from '../../context/QuizContext' |
|||
import { device } from '../../styles/BreakPoints' |
|||
import { Flex, ResizableBox } from '../../styles/Global' |
|||
import { refreshPage } from '../../utils/helpers' |
|||
|
|||
import Button from '../ui/Button' |
|||
import ResultOverview from './ResultOverview' |
|||
import RightAnswer from './RightAnswer' |
|||
|
|||
const ResultScreenContainer = styled.div`
|
|||
max-width: 900px; |
|||
margin: 80px auto; |
|||
@media ${device.md} { |
|||
width: 90%; |
|||
margin: 30px auto; |
|||
} |
|||
`
|
|||
|
|||
const LogoContainer = styled.div`
|
|||
text-align: center; |
|||
margin-bottom: 50px; |
|||
@media ${device.md} { |
|||
margin-bottom: 30px; |
|||
} |
|||
`
|
|||
|
|||
const InnerContainer = styled.div`
|
|||
background: ${({ theme }) => theme.colors.white}; |
|||
border-radius: 4px; |
|||
margin: 0 auto; |
|||
margin-bottom: 40px; |
|||
padding: 40px 90px 90px 90px; |
|||
@media ${device.md} { |
|||
padding: 15px; |
|||
} |
|||
`
|
|||
|
|||
const QuestionContainer = styled.div`
|
|||
display: flex; |
|||
justify-content: space-between; |
|||
margin-top: 40px; |
|||
@media ${device.md} { |
|||
flex-direction: column; |
|||
} |
|||
`
|
|||
|
|||
const QuestionNumber = styled.h6`
|
|||
font-size: clamp(16px, 5vw, 24px); |
|||
font-weight: 500; |
|||
line-height: 1.3; |
|||
color: ${({ theme }) => theme.colors.primaryText}; |
|||
`
|
|||
|
|||
const QuestionStyle = styled.span`
|
|||
font-size: clamp(16px, 5vw, 24px); |
|||
font-weight: 500; |
|||
line-height: 1.3; |
|||
color: ${({ theme }) => theme.colors.primaryText}; |
|||
margin-bottom: 20px; |
|||
@media ${device.md} { |
|||
margin-bottom: 10px; |
|||
} |
|||
`
|
|||
|
|||
interface AnswerProps { |
|||
correct?: boolean |
|||
wrong?: boolean |
|||
} |
|||
|
|||
const Answer = styled.li<AnswerProps>`
|
|||
border: 1px solid ${({ theme }) => theme.colors.lightGray}; |
|||
width: 90%; |
|||
@media ${device.md} { |
|||
width: 100%; |
|||
} |
|||
border-radius: 16px; |
|||
font-size: clamp(16px, 5vw, 18px); |
|||
font-weight: 600; |
|||
padding: 15px; |
|||
color: ${({ theme }) => theme.colors.secondaryText}; |
|||
margin-top: clamp(13px, calc(10px + 6 * ((100vw - 600px) / 1320)), 16px); |
|||
|
|||
// if user answer matches to correct answer make answer background success color otherwise danger color
|
|||
${({ correct }) => |
|||
correct && |
|||
css`
|
|||
border: 1px solid ${({ theme }) => theme.colors.success}; |
|||
background-color: ${({ theme }) => theme.colors.successLight}; |
|||
`}
|
|||
|
|||
${({ wrong }) => |
|||
wrong && |
|||
css`
|
|||
border: 1px solid ${({ theme }) => theme.colors.danger}; |
|||
background-color: ${({ theme }) => theme.colors.dangerLight}; |
|||
`}
|
|||
|
|||
span { |
|||
margin-right: 14px; |
|||
} |
|||
|
|||
@media ${device.md} { |
|||
font-weight: 400; |
|||
} |
|||
`
|
|||
|
|||
const Score = styled.span<{ right: boolean }>`
|
|||
font-weight: 500; |
|||
font-size: 16px; |
|||
color: ${({ right, theme }) => |
|||
right ? `${theme.colors.success}` : `${theme.colors.danger}`}; |
|||
margin-top: 4px; |
|||
@media ${device.md} { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
margin-top: 10px; |
|||
margin-right: 10px; |
|||
} |
|||
`
|
|||
|
|||
const ResultScreen: FC = () => { |
|||
const { result } = useQuiz() |
|||
|
|||
const onClickRetry = () => { |
|||
refreshPage() |
|||
} |
|||
|
|||
return ( |
|||
<ResultScreenContainer> |
|||
<LogoContainer> |
|||
<AppColoredLogo /> |
|||
</LogoContainer> |
|||
<InnerContainer> |
|||
<ResultOverview result={result} /> |
|||
{result.map( |
|||
( |
|||
{ question, choices, correctAnswers, selectedAnswer, score, isMatch }, |
|||
index: number |
|||
) => { |
|||
return ( |
|||
<QuestionContainer key={question}> |
|||
<ResizableBox width="90%"> |
|||
<Flex gap="4px"> |
|||
<QuestionNumber>{`${index + 1}. `}</QuestionNumber> |
|||
<QuestionStyle>{question}</QuestionStyle> |
|||
</Flex> |
|||
<div> |
|||
<ul> |
|||
{choices.map((ans: string, index: number) => { |
|||
// Convert index to alphabet character
|
|||
const label = String.fromCharCode(65 + index) |
|||
const correct = |
|||
selectedAnswer.includes(ans) && correctAnswers.includes(ans) |
|||
const wrong = |
|||
selectedAnswer.includes(ans) && !correctAnswers.includes(ans) |
|||
|
|||
return ( |
|||
<Answer key={ans} correct={correct} wrong={wrong}> |
|||
<span>{label}.</span> |
|||
{ans} |
|||
</Answer> |
|||
) |
|||
})} |
|||
</ul> |
|||
{/* only show if the answer is wrong */} |
|||
{!isMatch && ( |
|||
<RightAnswer correctAnswers={correctAnswers} choices={choices} /> |
|||
)} |
|||
</div> |
|||
</ResizableBox> |
|||
<Score right={isMatch}>{`Score ${isMatch ? score : 0}`}</Score> |
|||
</QuestionContainer> |
|||
) |
|||
} |
|||
)} |
|||
</InnerContainer> |
|||
<Flex flxEnd> |
|||
<Button |
|||
text="RETRY" |
|||
onClick={onClickRetry} |
|||
icon={<Refresh />} |
|||
iconPosition="left" |
|||
/> |
|||
</Flex> |
|||
</ResultScreenContainer> |
|||
) |
|||
} |
|||
|
|||
export default ResultScreen |
@ -0,0 +1,51 @@ |
|||
import { useEffect, useState } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import { AppLogo } from '../../config/icons' |
|||
import { PageCenter } from '../../styles/Global' |
|||
|
|||
interface LogoAnimationProps { |
|||
logoSize: string |
|||
} |
|||
|
|||
const LogoAnimation = styled.div<LogoAnimationProps>`
|
|||
svg { |
|||
width: ${({ logoSize }) => logoSize}; |
|||
transition: width 1s; |
|||
} |
|||
`
|
|||
|
|||
const SplashScreen = () => { |
|||
const [logoSize, setLogoSize] = useState('80px') |
|||
|
|||
useEffect(() => { |
|||
const handleResize = () => { |
|||
if (window.innerWidth < 900) { |
|||
setLogoSize('240px') |
|||
} else { |
|||
setLogoSize('350px') |
|||
} |
|||
} |
|||
|
|||
// Set initial logo size
|
|||
handleResize() |
|||
|
|||
// Update logo size on window resize
|
|||
window.addEventListener('resize', handleResize) |
|||
|
|||
// Clean up event listener on component unmount
|
|||
return () => { |
|||
window.removeEventListener('resize', handleResize) |
|||
} |
|||
}, []) |
|||
|
|||
return ( |
|||
<PageCenter justifyCenter> |
|||
<LogoAnimation logoSize={logoSize}> |
|||
<AppLogo /> |
|||
</LogoAnimation> |
|||
</PageCenter> |
|||
) |
|||
} |
|||
|
|||
export default SplashScreen |
@ -0,0 +1,30 @@ |
|||
import { FC, ReactNode } from 'react' |
|||
import { ButtonStyle, IconLeft, IconRight } from './styled' |
|||
|
|||
interface ButtonTypes { |
|||
text: string |
|||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void |
|||
icon?: ReactNode |
|||
iconPosition?: 'left' | 'right' |
|||
outline?: boolean |
|||
disabled?: boolean |
|||
} |
|||
|
|||
const Button: FC<ButtonTypes> = ({ |
|||
text, |
|||
onClick, |
|||
icon, |
|||
iconPosition, |
|||
outline, |
|||
disabled, |
|||
}) => { |
|||
return ( |
|||
<ButtonStyle onClick={onClick} outline={outline} disabled={disabled}> |
|||
{icon && iconPosition === 'left' && <IconLeft>{icon}</IconLeft>} |
|||
{text} |
|||
{icon && iconPosition === 'right' && <IconRight>{icon}</IconRight>} |
|||
</ButtonStyle> |
|||
) |
|||
} |
|||
|
|||
export default Button |
@ -0,0 +1,52 @@ |
|||
import styled from 'styled-components' |
|||
import { device } from '../../../styles/BreakPoints' |
|||
interface ButtonType { |
|||
outline?: boolean |
|||
} |
|||
|
|||
export const ButtonStyle = styled.button.attrs(({ outline }: ButtonType) => ({ |
|||
outline, |
|||
}))`
|
|||
width: 195px; |
|||
min-height: 50px; |
|||
color: ${({ theme, outline }) => |
|||
outline ? theme.colors.themeColor : theme.colors.white}; |
|||
background: ${({ theme, outline }) => |
|||
outline ? theme.colors.white : theme.colors.themeGradient}; |
|||
font-size: clamp(16px, 5vw, 24px); |
|||
border: 1px solid |
|||
${({ theme, outline }) => (!outline ? 'none' : theme.colors.themeColor)}; |
|||
font-weight: 400; |
|||
border-radius: 9px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
@media ${device.md} { |
|||
width: 150px; |
|||
min-height: 40px; |
|||
tap-highlight-color: transparent; |
|||
-webkit-tap-highlight-color: transparent; |
|||
} |
|||
&:active { |
|||
transform: scale(0.98); |
|||
box-shadow: ${({ theme }) => theme.shadows.activeButton}; |
|||
transition: 0.2s all; |
|||
} |
|||
&:disabled { |
|||
background: ${({ theme }) => theme.colors.disabledButton}; |
|||
color: ${({ theme }) => theme.colors.darkGrayText}; |
|||
cursor: not-allowed; |
|||
transform: unset; |
|||
box-shadow: unset; |
|||
} |
|||
`
|
|||
|
|||
export const IconLeft = styled.span`
|
|||
margin-right: 10px; |
|||
display: flex; |
|||
`
|
|||
|
|||
export const IconRight = styled.span`
|
|||
margin-left: 20px; |
|||
display: flex; |
|||
`
|
@ -0,0 +1,74 @@ |
|||
import { FC } from 'react' |
|||
import styled from 'styled-components' |
|||
|
|||
import Button from '../Button' |
|||
|
|||
const ModalContainer = styled.div`
|
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 15px; |
|||
`
|
|||
|
|||
const ModalContent = styled.div`
|
|||
width: 600px; |
|||
padding: 50px 25px; |
|||
background-color: ${({ theme }) => theme.colors.white}; |
|||
border-radius: 10px; |
|||
display: flex; |
|||
align-items: center; |
|||
flex-direction: column; |
|||
`
|
|||
|
|||
const ModalTitle = styled.h6`
|
|||
font-size: clamp(24px, 4vw, 32px); |
|||
font-weight: 700; |
|||
color: ${({ theme }) => theme.colors.themeColor}; |
|||
margin-top: 26px; |
|||
margin-bottom: 20px; |
|||
`
|
|||
|
|||
const ModalSubtitle = styled.p`
|
|||
font-size: 24px; |
|||
font-size: clamp(18px, 4vw, 24px); |
|||
font-weight: 500; |
|||
text-align: center; |
|||
line-height: 1.3; |
|||
color: ${({ theme }) => theme.colors.primaryText}; |
|||
margin-bottom: clamp(18px, calc(18px + 28 * ((100vw - 600px) / 1320)), 48px); |
|||
`
|
|||
|
|||
interface ModalWrapperProps { |
|||
title: string |
|||
subtitle: string |
|||
icon: JSX.Element |
|||
buttonTitle: string |
|||
onClick: () => void |
|||
} |
|||
|
|||
const ModalWrapper: FC<ModalWrapperProps> = ({ |
|||
title, |
|||
subtitle, |
|||
icon, |
|||
buttonTitle, |
|||
onClick, |
|||
}) => { |
|||
return ( |
|||
<ModalContainer> |
|||
<ModalContent> |
|||
{icon} |
|||
<ModalTitle>{title}</ModalTitle> |
|||
<ModalSubtitle>{subtitle}</ModalSubtitle> |
|||
<Button text={buttonTitle} onClick={onClick} /> |
|||
</ModalContent> |
|||
</ModalContainer> |
|||
) |
|||
} |
|||
|
|||
export default ModalWrapper |
@ -0,0 +1,10 @@ |
|||
import { ReactComponent as AppLogo } from '../assets/icons/app-logo.svg' |
|||
import { ReactComponent as CheckIcon } from '../assets/icons/check.svg' |
|||
import { ReactComponent as AppColoredLogo } from '../assets/icons/colored-logo.svg' |
|||
import { ReactComponent as Next } from '../assets/icons/next.svg' |
|||
import { ReactComponent as Refresh } from '../assets/icons/refresh.svg' |
|||
import { ReactComponent as TimerIcon } from '../assets/icons/timer.svg' |
|||
import { ReactComponent as Logo } from '../assets/icons/logo.svg' |
|||
import { ReactComponent as StartIcon } from '../assets/icons/start.svg' |
|||
|
|||
export { AppColoredLogo, AppLogo, CheckIcon, Next, Refresh, TimerIcon, Logo, StartIcon } |
@ -0,0 +1,90 @@ |
|||
import { ReactNode, createContext, useContext, useEffect, useState } from 'react' |
|||
|
|||
import { quiz } from '../data/QuizQuestions' |
|||
import { QuizContextTypes, Result, ScreenTypes } from '../types' |
|||
|
|||
const initialState: QuizContextTypes = { |
|||
currentScreen: ScreenTypes.SplashScreen, |
|||
setCurrentScreen: () => {}, |
|||
quizTopic: 'React', |
|||
selectQuizTopic: () => {}, |
|||
questions: [], |
|||
setQuestions: () => {}, |
|||
result: [], |
|||
setResult: () => {}, |
|||
timer: 15, |
|||
setTimer: () => {}, |
|||
endTime: 0, |
|||
setEndTime: () => {}, |
|||
quizDetails: { |
|||
totalQuestions: 0, |
|||
totalScore: 0, |
|||
totalTime: 0, |
|||
selectedQuizTopic: 'React', |
|||
}, |
|||
} |
|||
|
|||
export const QuizContext = createContext<QuizContextTypes>(initialState) |
|||
|
|||
export function useQuiz() { |
|||
return useContext(QuizContext) |
|||
} |
|||
|
|||
type QuizProviderProps = { |
|||
children: ReactNode |
|||
} |
|||
|
|||
const QuizProvider = ({ children }: QuizProviderProps) => { |
|||
const [timer, setTimer] = useState<number>(initialState.timer) |
|||
const [endTime, setEndTime] = useState<number>(initialState.endTime) |
|||
const [quizTopic, setQuizTopic] = useState<string>(initialState.quizTopic) |
|||
const [result, setResult] = useState<Result[]>(initialState.result) |
|||
const [currentScreen, setCurrentScreen] = useState<ScreenTypes>( |
|||
initialState.currentScreen |
|||
) |
|||
|
|||
const [questions, setQuestions] = useState(quiz[initialState.quizTopic].questions) |
|||
|
|||
const { |
|||
questions: quizQuestions, |
|||
totalQuestions, |
|||
totalTime, |
|||
totalScore, |
|||
} = quiz[quizTopic] |
|||
|
|||
const selectQuizTopic = (topic: string) => { |
|||
setQuizTopic(topic) |
|||
} |
|||
|
|||
useEffect(() => { |
|||
setTimer(totalTime) |
|||
setQuestions(quizQuestions) |
|||
}, [quizTopic]) |
|||
|
|||
const quizDetails = { |
|||
totalQuestions, |
|||
totalScore, |
|||
totalTime, |
|||
selectedQuizTopic: quizTopic, |
|||
} |
|||
|
|||
const quizContextValue: QuizContextTypes = { |
|||
currentScreen, |
|||
setCurrentScreen, |
|||
quizTopic, |
|||
selectQuizTopic, |
|||
questions, |
|||
setQuestions, |
|||
result, |
|||
setResult, |
|||
quizDetails, |
|||
timer, |
|||
setTimer, |
|||
endTime, |
|||
setEndTime, |
|||
} |
|||
|
|||
return <QuizContext.Provider value={quizContextValue}>{children}</QuizContext.Provider> |
|||
} |
|||
|
|||
export default QuizProvider |
@ -0,0 +1,30 @@ |
|||
import { react } from './react' |
|||
|
|||
// Question Types
|
|||
// 1. MCQs | Multiple Choice | single
|
|||
// 2. boolean | true/false | single
|
|||
// 3. MAQs | Multiple Answers | multiple
|
|||
|
|||
type Choice = string |
|||
type CorrectAnswers = string[] |
|||
|
|||
export type Question = { |
|||
question: string |
|||
choices: Choice[] |
|||
type: 'MCQs' | 'MAQs' | 'boolean' |
|||
correctAnswers: CorrectAnswers |
|||
score: number |
|||
} |
|||
|
|||
export type Topic = { |
|||
topic: string |
|||
level: string |
|||
totalQuestions: number |
|||
totalScore: number |
|||
totalTime: number |
|||
questions: Question[] |
|||
} |
|||
|
|||
export const quiz: Record<string, Topic> = { |
|||
React: react, |
|||
} |
@ -0,0 +1,76 @@ |
|||
// Question Types
|
|||
// 1. MCQs | Multiple Choice | single
|
|||
// 2. boolean | true/false | single
|
|||
// 3. MAQs | Multiple Answers | multiple
|
|||
|
|||
import { Topic } from '.' |
|||
|
|||
export const react: Topic = { |
|||
topic: 'React', |
|||
level: 'Intermediate', |
|||
totalQuestions: 6, |
|||
totalScore: 45, |
|||
totalTime: 180, |
|||
questions: [ |
|||
{ |
|||
question: 'What is JSX in React?', |
|||
choices: [ |
|||
'A syntax extension for JavaScript that allows writing HTML-like code in JavaScript', |
|||
'A state management library for React applications', |
|||
'A build tool for bundling React applications', |
|||
'A testing framework for React components', |
|||
], |
|||
type: 'MCQs', |
|||
correctAnswers: [ |
|||
'A syntax extension for JavaScript that allows writing HTML-like code in JavaScript', |
|||
], |
|||
score: 10, |
|||
}, |
|||
{ |
|||
question: 'React components must always return a single JSX element.', |
|||
choices: ['True', 'False'], |
|||
type: 'boolean', |
|||
correctAnswers: ['True'], |
|||
score: 5, |
|||
}, |
|||
{ |
|||
question: 'What is the purpose of React components?', |
|||
choices: [ |
|||
'To handle HTTP requests and fetch data from APIs', |
|||
'To manage the state of a React application', |
|||
'To define the structure and appearance of the user interface', |
|||
'To handle user interactions and events', |
|||
], |
|||
type: 'MCQs', |
|||
correctAnswers: ['To define the structure and appearance of the user interface'], |
|||
score: 10, |
|||
}, |
|||
{ |
|||
question: |
|||
'In React, props are used to pass data from parent components to child components.', |
|||
choices: ['True', 'False'], |
|||
type: 'boolean', |
|||
correctAnswers: ['True'], |
|||
score: 5, |
|||
}, |
|||
{ |
|||
question: 'In React, what is the purpose of keys in lists?', |
|||
choices: [ |
|||
'To provide a unique identifier for each item in the list', |
|||
'To control the order of items in the list', |
|||
'To enable sorting and filtering of the list', |
|||
'To handle user interactions within the list', |
|||
], |
|||
type: 'MCQs', |
|||
correctAnswers: ['To provide a unique identifier for each item in the list'], |
|||
score: 10, |
|||
}, |
|||
{ |
|||
question: 'React uses a virtual DOM to optimize rendering performance.', |
|||
choices: ['True', 'False'], |
|||
type: 'boolean', |
|||
correctAnswers: ['True'], |
|||
score: 5, |
|||
}, |
|||
], |
|||
} |
@ -0,0 +1,61 @@ |
|||
import { ReactNode } from 'react' |
|||
import { ReactComponent as Angular } from '../assets/icons/angular.svg' |
|||
import { ReactComponent as CSS } from '../assets/icons/css-3.svg' |
|||
import { ReactComponent as Django } from '../assets/icons/dj.svg' |
|||
import { ReactComponent as Gatsby } from '../assets/icons/gatsby.svg' |
|||
import { ReactComponent as JavaScript } from '../assets/icons/javascript.svg' |
|||
import { ReactComponent as Kotlin } from '../assets/icons/kotlin.svg' |
|||
import { ReactComponent as Laravel } from '../assets/icons/laravel.svg' |
|||
import { ReactComponent as Python } from '../assets/icons/python.svg' |
|||
import { ReactComponent as ReactIcon } from '../assets/icons/react.svg' |
|||
|
|||
type QuizTopic = { |
|||
title: string |
|||
icon: ReactNode |
|||
disabled?: boolean |
|||
} |
|||
|
|||
export const quizTopics: QuizTopic[] = [ |
|||
{ |
|||
title: 'React', |
|||
icon: <ReactIcon />, |
|||
}, |
|||
{ |
|||
title: 'JavaScript', |
|||
icon: <JavaScript />, |
|||
}, |
|||
{ |
|||
title: 'Python', |
|||
icon: <Python />, |
|||
}, |
|||
{ |
|||
title: 'Gatsby', |
|||
icon: <Gatsby />, |
|||
disabled: true, |
|||
}, |
|||
{ |
|||
title: 'Angular', |
|||
icon: <Angular />, |
|||
disabled: true, |
|||
}, |
|||
{ |
|||
title: 'Django', |
|||
icon: <Django />, |
|||
disabled: true, |
|||
}, |
|||
{ |
|||
title: 'CSS', |
|||
icon: <CSS />, |
|||
disabled: true, |
|||
}, |
|||
{ |
|||
title: 'Kotlin', |
|||
icon: <Kotlin />, |
|||
disabled: true, |
|||
}, |
|||
{ |
|||
title: 'Laravel', |
|||
icon: <Laravel />, |
|||
disabled: true, |
|||
}, |
|||
] |
@ -0,0 +1,4 @@ |
|||
import useShuffleQuestions from './useShuffleQuestions' |
|||
import useTimer from './useTimer' |
|||
|
|||
export { useShuffleQuestions, useTimer } |
@ -0,0 +1,17 @@ |
|||
import { useEffect } from 'react' |
|||
|
|||
import { useQuiz } from '../context/QuizContext' |
|||
import { ScreenTypes } from '../types' |
|||
import { shuffleArray } from '../utils/helpers' |
|||
|
|||
export const useShuffleQuestions = () => { |
|||
const { setQuestions, currentScreen, questions } = useQuiz() |
|||
|
|||
useEffect(() => { |
|||
if (currentScreen === ScreenTypes.QuizDetailsScreen) { |
|||
setQuestions(shuffleArray(questions)) |
|||
} |
|||
}, [currentScreen]) |
|||
} |
|||
|
|||
export default useShuffleQuestions |
@ -0,0 +1,34 @@ |
|||
import { Dispatch, SetStateAction, useEffect } from 'react' |
|||
|
|||
interface QuizDetails { |
|||
totalTime: number |
|||
} |
|||
|
|||
const useTimer = ( |
|||
timer: number, |
|||
quizDetails: QuizDetails, |
|||
setEndTime: (time: number) => void, |
|||
setTimer: Dispatch<SetStateAction<number>>, |
|||
setShowTimerModal: (time: boolean) => void, |
|||
showResultModal: boolean |
|||
) => { |
|||
useEffect(() => { |
|||
if (timer <= 0) { |
|||
const timeTaken = quizDetails.totalTime |
|||
setEndTime(timeTaken) |
|||
setShowTimerModal(true) |
|||
setTimer(0) |
|||
} |
|||
}, [timer, quizDetails.totalTime, setEndTime, setShowTimerModal, setTimer]) |
|||
|
|||
useEffect(() => { |
|||
if (!showResultModal) { |
|||
const countTimer = setTimeout(() => { |
|||
setTimer((prevTimer) => prevTimer - 1) |
|||
}, 1000) |
|||
return () => clearTimeout(countTimer) |
|||
} |
|||
}, [timer, setTimer]) |
|||
} |
|||
|
|||
export default useTimer |
@ -0,0 +1,16 @@ |
|||
import React from 'react' |
|||
import ReactDOM from 'react-dom/client' |
|||
import App from './App' |
|||
import reportWebVitals from './reportWebVitals' |
|||
|
|||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) |
|||
root.render( |
|||
<React.StrictMode> |
|||
<App /> |
|||
</React.StrictMode> |
|||
) |
|||
|
|||
// If you want to start measuring performance in your app, pass a function
|
|||
// to log results (for example: reportWebVitals(console.log))
|
|||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|||
reportWebVitals() |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> |
@ -0,0 +1 @@ |
|||
/// <reference types="react-scripts" />
|
@ -0,0 +1,15 @@ |
|||
import { ReportHandler } from 'web-vitals'; |
|||
|
|||
const reportWebVitals = (onPerfEntry?: ReportHandler) => { |
|||
if (onPerfEntry && onPerfEntry instanceof Function) { |
|||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { |
|||
getCLS(onPerfEntry); |
|||
getFID(onPerfEntry); |
|||
getFCP(onPerfEntry); |
|||
getLCP(onPerfEntry); |
|||
getTTFB(onPerfEntry); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
export default reportWebVitals; |
@ -0,0 +1,5 @@ |
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|||
// allows you to do things like:
|
|||
// expect(element).toHaveTextContent(/react/i)
|
|||
// learn more: https://github.com/testing-library/jest-dom
|
|||
import '@testing-library/jest-dom'; |
@ -0,0 +1,26 @@ |
|||
interface Size { |
|||
xs: string |
|||
sm: string |
|||
md: string |
|||
lg: string |
|||
xl: string |
|||
xxl: string |
|||
} |
|||
|
|||
const size: Size = { |
|||
xs: '400px', // for small screen mobile
|
|||
sm: '600px', // for mobile screen
|
|||
md: '900px', // for tablets
|
|||
lg: '1280px', // for laptops
|
|||
xl: '1440px', // for desktop / monitors
|
|||
xxl: '1920px', // for big screens
|
|||
} |
|||
|
|||
export const device = { |
|||
xs: `(max-width: ${size.xs})`, |
|||
sm: `(max-width: ${size.sm})`, |
|||
md: `(max-width: ${size.md})`, |
|||
lg: `(max-width: ${size.lg})`, |
|||
xl: `(max-width: ${size.xl})`, |
|||
xxl: `(max-width: ${size.xxl})`, |
|||
} |
@ -0,0 +1,208 @@ |
|||
import styled, { createGlobalStyle, css } from 'styled-components' |
|||
import { device } from './BreakPoints' |
|||
import fontsCss from './fonts.module.css' |
|||
|
|||
export const GlobalStyles = createGlobalStyle`
|
|||
${fontsCss} // this works as a normal styled css
|
|||
|
|||
/* Box sizing rules */ |
|||
*, |
|||
*::before, |
|||
*::after { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
html { |
|||
font-size: 100%; |
|||
} |
|||
|
|||
body { |
|||
margin: 0; |
|||
padding: 0; |
|||
overflow-x: hidden; |
|||
min-height: 100vh; |
|||
text-rendering: optimizeSpeed; |
|||
font-family: ${({ theme }) => theme.fonts.anekMalayalam}, sans-serif; |
|||
font-size: 1rem; |
|||
color: ${({ theme }) => theme.colors.text}; |
|||
background-color: ${({ theme }) => theme.colors.background}; |
|||
line-height: 1; |
|||
overflow-x: hidden; |
|||
position: relative; |
|||
} |
|||
h1, |
|||
h2, |
|||
h3, |
|||
h4, |
|||
h5, |
|||
h6, |
|||
p, |
|||
ul, |
|||
figure, |
|||
blockquote, |
|||
dl, |
|||
caption, |
|||
dd { |
|||
padding: 0; |
|||
margin: 0; |
|||
} |
|||
button { |
|||
border: none; |
|||
background-color: transparent; |
|||
font-family: inherit; |
|||
padding: 0; |
|||
cursor: pointer; |
|||
} |
|||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ |
|||
ul[role="list"], |
|||
ol[role="list"] { |
|||
list-style: none; |
|||
} |
|||
li { |
|||
list-style-type: none; |
|||
} |
|||
/* Set core root defaults */ |
|||
html:focus-within { |
|||
scroll-behavior: smooth; |
|||
} |
|||
/* A elements that don't have a class get default styles */ |
|||
a:not([class]) { |
|||
text-decoration-skip-ink: auto; |
|||
} |
|||
|
|||
/* Make images easier to work with */ |
|||
img, |
|||
picture { |
|||
max-width: 100%; |
|||
display: block; |
|||
} |
|||
|
|||
/* Inherit fonts for inputs and buttons */ |
|||
input, |
|||
button, |
|||
textarea, |
|||
select { |
|||
font: inherit; |
|||
} |
|||
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ |
|||
@media (prefers-reduced-motion: reduce) { |
|||
html:focus-within { |
|||
scroll-behavior: auto; |
|||
} |
|||
|
|||
*, |
|||
*::before, |
|||
*::after { |
|||
animation-duration: 0.01ms !important; |
|||
animation-iteration-count: 1 !important; |
|||
transition-duration: 0.01ms !important; |
|||
scroll-behavior: auto !important; |
|||
} |
|||
} |
|||
`
|
|||
|
|||
export const Container = styled.div`
|
|||
width: 100%; |
|||
max-width: 1360px; |
|||
margin: 0 auto; |
|||
padding: 0 ${({ theme }) => theme.paddings.container}; |
|||
`
|
|||
|
|||
interface BoxStyleTypes { |
|||
mt?: number |
|||
flxRight?: boolean |
|||
} |
|||
|
|||
export const Box = styled.div<BoxStyleTypes>`
|
|||
margin-top: calc(${({ mt }) => mt} * 1px); |
|||
${({ flxRight }) => |
|||
flxRight && |
|||
css`
|
|||
display: flex; |
|||
justify-content: flex-end; |
|||
`}
|
|||
`
|
|||
|
|||
interface PageCenterTypes { |
|||
light?: boolean |
|||
justifyCenter?: boolean |
|||
} |
|||
|
|||
export const PageCenter = styled.div<PageCenterTypes>`
|
|||
background: ${({ light, theme }) => |
|||
light ? `${theme.colors.background}` : `${theme.colors.themeGradient}`}; |
|||
min-height: 100vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
padding: 20px; |
|||
// due to flex direction column
|
|||
${({ justifyCenter }) => |
|||
justifyCenter && |
|||
css`
|
|||
justify-content: center; |
|||
`}
|
|||
`
|
|||
|
|||
interface FlexProps { |
|||
center?: boolean |
|||
spaceBetween?: boolean |
|||
flxEnd?: boolean |
|||
gap?: string |
|||
// Add any other necessary props
|
|||
} |
|||
|
|||
export const Flex = styled.div<FlexProps>`
|
|||
display: flex; |
|||
${({ center }) => |
|||
center && |
|||
css`
|
|||
justify-content: center; |
|||
align-items: center; |
|||
`}
|
|||
${({ spaceBetween }) => |
|||
spaceBetween && |
|||
css`
|
|||
justify-content: space-between; |
|||
align-items: center; |
|||
`}
|
|||
${({ flxEnd }) => |
|||
flxEnd && |
|||
css`
|
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
`}
|
|||
${({ gap }) => |
|||
gap && |
|||
css`
|
|||
gap: ${gap}; |
|||
`}
|
|||
`
|
|||
|
|||
export const CenterCardContainer = styled.div`
|
|||
background: ${({ theme }) => theme.colors.white}; |
|||
border-radius: 4px; |
|||
min-width: 773px; |
|||
min-height: 620px; |
|||
padding: 50px 10px 60px 10px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
@media ${device.md} { |
|||
min-width: 100%; |
|||
} |
|||
`
|
|||
|
|||
export const HighlightedText = styled.span`
|
|||
color: ${({ color, theme }) => (color ? color : theme.colors.themeColor)}; |
|||
`
|
|||
|
|||
interface ResizableBoxProps { |
|||
width: string | number |
|||
} |
|||
|
|||
export const ResizableBox = styled.div<ResizableBoxProps>`
|
|||
width: ${(props) => |
|||
typeof props.width === 'string' ? props.width : `${props.width}px`}; |
|||
`
|
@ -0,0 +1,42 @@ |
|||
import { DefaultTheme } from 'styled-components' |
|||
|
|||
export const theme: DefaultTheme = { |
|||
colors: { |
|||
primaryText: '#11052C', // question text color
|
|||
secondaryText: '#2D264B', // answer text color
|
|||
themeColor: '#800080', |
|||
themeGradient: 'linear-gradient(to right,#800080, #FFC0CB)', |
|||
text: '#000000', |
|||
background: '#E5E5E5', |
|||
success: '#12B40E', |
|||
successLight: '#DDFFDC', |
|||
danger: '#FF143E', |
|||
dangerLight: '#FFD7DE', |
|||
lightPink: '#FFD6FF', |
|||
infoText: '#FF783F', // skip button text
|
|||
infoBackground: '#ffb23f26', // skip button background
|
|||
codeBackground: '#F9F9F9', |
|||
disabledCard: '#fbf4ecbc', |
|||
disabledButton: '#e7e8e9', |
|||
white: '#FFFFFF', |
|||
black: '#000000', |
|||
grayText: '#9993A3', |
|||
darkGrayText: '#9fa3a9', |
|||
darkerGray: '#817a8e', |
|||
lightGray: '#eaeaea', |
|||
darkText: '', |
|||
}, |
|||
fonts: { |
|||
anekMalayalam: 'Anek Malayalam', |
|||
}, |
|||
shadows: { |
|||
activeButton: '3px 2px 22px 1px rgba(0, 0, 0, 0.24)', |
|||
}, |
|||
paddings: { |
|||
container: '15px', |
|||
pageTop: '30px', |
|||
}, |
|||
margins: { |
|||
pageTop: '30px', |
|||
}, |
|||
} |
@ -0,0 +1,90 @@ |
|||
/* woff2 : Super Modern Browsers */ |
|||
/* woff2 :Modern Browsers */ |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 100; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-100.woff') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-100.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 200; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-200.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-200.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 300; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-300.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-300.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 400; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-400.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-400.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-500.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-500.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 600; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-600.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-600.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-700.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-700.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
|||
|
|||
@font-face { |
|||
font-family: 'Anek Malayalam'; |
|||
font-style: normal; |
|||
font-weight: 800; |
|||
src: url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-800.woff2') |
|||
format('woff2'), |
|||
url('../assets/fonts/anek-malayalam/anek-malayalam-v4-malayalam-800.woff') |
|||
format('woff'); |
|||
font-display: swap; |
|||
} |
@ -0,0 +1,46 @@ |
|||
// import original module declarations
|
|||
import 'styled-components' |
|||
|
|||
// and extend them!
|
|||
declare module 'styled-components' { |
|||
export interface DefaultTheme { |
|||
colors: { |
|||
primaryText: '#11052C' // question text color
|
|||
secondaryText: '#2D264B' // answer text color
|
|||
themeColor: '#800080' |
|||
themeGradient: 'linear-gradient(to right,#800080, #FFC0CB)' |
|||
text: '#000000' |
|||
background: '#E5E5E5' |
|||
success: '#12B40E' |
|||
successLight: '#DDFFDC' |
|||
danger: '#FF143E' |
|||
dangerLight: '#FFD7DE' |
|||
lightPink: '#FFD6FF' |
|||
infoText: '#FF783F' // skip button text
|
|||
infoBackground: '#ffb23f26' // skip button background
|
|||
codeBackground: '#F9F9F9' |
|||
disabledCard: '#fbf4ecbc' |
|||
disabledButton: '#e7e8e9' |
|||
white: '#FFFFFF' |
|||
black: '#000000' |
|||
grayText: '#9993A3' |
|||
darkGrayText: '#9fa3a9' |
|||
darkerGray: '#817a8e' |
|||
lightGray: '#eaeaea' |
|||
darkText: '' |
|||
} |
|||
fonts: { |
|||
anekMalayalam: 'Anek Malayalam' |
|||
} |
|||
shadows: { |
|||
activeButton: '3px 2px 22px 1px rgba(0, 0, 0, 0.24)' |
|||
} |
|||
paddings: { |
|||
container: '15px' |
|||
pageTop: '30px' |
|||
} |
|||
margins: { |
|||
pageTop: '30px' |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
import { Dispatch, SetStateAction } from 'react' |
|||
import { Question } from '../data/QuizQuestions' |
|||
|
|||
export enum ScreenTypes { |
|||
SplashScreen, |
|||
QuizDetailsScreen, |
|||
QuestionScreen, |
|||
ResultScreen, |
|||
} |
|||
|
|||
export interface Result extends Question { |
|||
selectedAnswer: string[] |
|||
isMatch: boolean |
|||
score: number |
|||
} |
|||
|
|||
export type QuizContextTypes = { |
|||
currentScreen: ScreenTypes |
|||
setCurrentScreen: Dispatch<SetStateAction<ScreenTypes>> |
|||
quizTopic: string |
|||
selectQuizTopic: (type: string) => void |
|||
questions: Question[] |
|||
setQuestions: Dispatch<SetStateAction<any[]>> |
|||
result: Result[] |
|||
setResult: Dispatch<SetStateAction<any[]>> |
|||
timer: number |
|||
setTimer: Dispatch<SetStateAction<number>> |
|||
endTime: number |
|||
setEndTime: (type: number) => void |
|||
quizDetails: { |
|||
totalQuestions: number |
|||
totalScore: number |
|||
totalTime: number |
|||
selectedQuizTopic: string |
|||
} |
|||
} |
@ -0,0 +1,54 @@ |
|||
export const addLeadingZero = (number: number) => { |
|||
if (number > 9) { |
|||
return number |
|||
} else { |
|||
return '0' + number |
|||
} |
|||
} |
|||
|
|||
// utility function to format the remaining time as minutes:seconds
|
|||
export function formatTime(time: number) { |
|||
const minutes = Math.floor(time / 60) |
|||
const seconds = time % 60 |
|||
return `${minutes}:${seconds.toString().padStart(2, '0')}` |
|||
} |
|||
|
|||
export const convertSeconds = (seconds: number): string => { |
|||
const hours = Math.floor(seconds / 3600) |
|||
const minutes = Math.floor((seconds % 3600) / 60) |
|||
const remainingSeconds = seconds % 60 |
|||
|
|||
const hourString = hours > 0 ? `${hours} hour${hours > 1 ? 's' : ''}` : '' |
|||
const minuteString = minutes > 0 ? `${minutes} minute${minutes > 1 ? 's' : ''}` : '' |
|||
const secondString = |
|||
remainingSeconds > 0 |
|||
? `${remainingSeconds} second${remainingSeconds > 1 ? 's' : ''}` |
|||
: '' |
|||
|
|||
if (hours > 0) { |
|||
return `${hourString} : ${minuteString || '0 minute'} ${ |
|||
secondString && `: ${secondString}` |
|||
}`
|
|||
} else if (!hours && minutes > 0) { |
|||
return `${minuteString} ${secondString && `: ${secondString}`}` |
|||
} |
|||
|
|||
return secondString |
|||
} |
|||
|
|||
export const refreshPage = (): void => { |
|||
window.location.reload() |
|||
} |
|||
|
|||
export const shuffleArray = <T>(array: T[]): T[] => { |
|||
const shuffledArray = [...array] |
|||
|
|||
for (let i = shuffledArray.length - 1; i > 0; i--) { |
|||
const j = Math.floor(Math.random() * (i + 1)) |
|||
|
|||
// Swap elements using array destructuring
|
|||
;[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]] |
|||
} |
|||
|
|||
return shuffledArray |
|||
} |
@ -0,0 +1,26 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es5", |
|||
"lib": [ |
|||
"dom", |
|||
"dom.iterable", |
|||
"esnext" |
|||
], |
|||
"allowJs": true, |
|||
"skipLibCheck": true, |
|||
"esModuleInterop": true, |
|||
"allowSyntheticDefaultImports": true, |
|||
"strict": true, |
|||
"forceConsistentCasingInFileNames": true, |
|||
"noFallthroughCasesInSwitch": true, |
|||
"module": "esnext", |
|||
"moduleResolution": "node", |
|||
"resolveJsonModule": true, |
|||
"isolatedModules": true, |
|||
"noEmit": true, |
|||
"jsx": "react-jsx" |
|||
}, |
|||
"include": [ |
|||
"src" |
|||
] |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue