├── .babelrc
├── .gitignore
├── .prettierrc
├── cover.png
├── next-env.d.ts
├── package-lock.json
├── package.json
├── pages
├── [quiz]
│ └── index.tsx
├── _app.tsx
├── _document.tsx
├── index.tsx
└── login
│ ├── [quiz]
│ └── index.tsx
│ └── index.tsx
├── public
├── home-image.jpeg
├── loading-transparent.gif
├── loading.gif
├── logo-alura.svg
└── quiz-time.jpeg
├── src
├── components
│ ├── Button
│ │ └── index.tsx
│ ├── CustomHead
│ │ └── index.tsx
│ ├── GitHubCorner
│ │ └── index.tsx
│ ├── Input
│ │ └── index.tsx
│ ├── QuestionWidget
│ │ └── index.tsx
│ ├── QuizWidget
│ │ └── index.tsx
│ └── TextAreaInput
│ │ └── index.tsx
├── helpers
│ └── calculate-result.ts
├── services
│ ├── api.ts
│ ├── default-theme
│ │ └── index.ts
│ └── screen-states
│ │ └── index.ts
└── styles
│ ├── InputStyle
│ └── index.ts
│ ├── WidgetStyle
│ └── index.tsx
│ └── theme.ts
├── styled.d.ts
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [["styled-components", { "ssr": true }]]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 4,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fischerafael/alura-quiz-frontend/32b568669aef2dd752be5d2c8c404ea3df9beba0/cover.png
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alura-quiz-frontend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "axios": "^0.21.1",
16 | "next": "^10.0.5",
17 | "react": "^17.0.1",
18 | "react-dom": "^17.0.1",
19 | "styled-components": "^5.2.1"
20 | },
21 | "devDependencies": {
22 | "@types/axios": "^0.14.0",
23 | "@types/node": "^14.14.22",
24 | "@types/react": "^17.0.0",
25 | "@types/styled-components": "^5.1.7",
26 | "babel-plugin-styled-components": "^1.12.0",
27 | "typescript": "^4.1.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pages/[quiz]/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import Head from 'next/head'
3 | import { PageContainer } from '..'
4 | import styled from 'styled-components'
5 | import screenStates from '../../src/services/screen-states'
6 | import QuestinWidget from '../../src/components/QuestionWidget'
7 | import { GetServerSideProps } from 'next'
8 | import { useRouter } from 'next/router'
9 | import api from '../../src/services/api'
10 | import {
11 | Widget,
12 | WidgetContent,
13 | WidgetHeader
14 | } from '../../src/styles/WidgetStyle'
15 | import { MainButtonStyle } from '../../src/components/Button'
16 | import calculateResult from '../../src/helpers/calculate-result'
17 | import CustomHead from '../../src/components/CustomHead'
18 |
19 | const Quiz = ({ data, questions }) => {
20 | const [initialTime] = useState(Date.now())
21 | const [finalTime, setFinalTime] = useState(Date.now())
22 | const [answersArray, setAnswersArray] = useState([])
23 |
24 | const rightAnswers = answersArray.reduce((total, currentValue) => {
25 | const isRight = currentValue === 1
26 | if (isRight) {
27 | return total + 1
28 | }
29 | return total
30 | }, 0)
31 |
32 | const [screenState, setScreenState] = useState(screenStates.QUIZ)
33 |
34 | useEffect(() => {
35 | if (questions.length === 0) {
36 | setScreenState(screenStates.EMPTY)
37 | setTimeout(() => {
38 | router.push('/')
39 | }, 2000)
40 | }
41 | }, [])
42 |
43 | const router = useRouter()
44 | const { playername } = router.query
45 |
46 | const totalQuestions = questions.length
47 | const [currentQuestion, setCurrentQuestion] = useState(0)
48 | const question = questions[currentQuestion]
49 |
50 | function handleSubmitQuiz() {
51 | const nextQuestion = currentQuestion + 1
52 |
53 | if (nextQuestion === totalQuestions) {
54 | setScreenState(screenStates.RESULT)
55 | setFinalTime(Date.now() - initialTime)
56 | return
57 | }
58 |
59 | setScreenState(screenStates.LOADING)
60 | fakeLoading(1000)
61 | setCurrentQuestion(nextQuestion)
62 | }
63 |
64 | function fakeLoading(time: number) {
65 | setTimeout(function () {
66 | setScreenState(screenStates.QUIZ)
67 | }, time)
68 | }
69 |
70 | async function handleReplay(e) {
71 | e.preventDefault()
72 | await postScoreToQuiz()
73 | router.push('/')
74 | }
75 |
76 | async function postScoreToQuiz() {
77 | try {
78 | const response = await api.post(`/quiz/${data.login}/addplayer`, {
79 | name: playername,
80 | score: calculateResult(rightAnswers, finalTime, totalQuestions),
81 | time: finalTime
82 | })
83 | console.log(response)
84 | } catch (err) {
85 | console.log(err)
86 | }
87 | }
88 |
89 | if (screenState === screenStates.EMPTY)
90 | return (
91 | <>
92 |
93 |
94 |
95 | Ainda não existem perguntas cadastradas neste quiz!
96 | Tente jogar outros!
97 |
98 |
99 | >
100 | )
101 |
102 | if (screenState === screenStates.LOADING)
103 | return (
104 | <>
105 |
106 |
107 |
108 | Prepare-se para a próxima pergunta!
109 |
110 | >
111 | )
112 |
113 | if (screenState === screenStates.QUIZ)
114 | return (
115 |
116 |
117 |
118 |
119 |
124 | {question && (
125 |
132 | )}
133 |
134 |
135 | )
136 |
137 | if (screenState === screenStates.RESULT)
138 | return (
139 | <>
140 |
141 |
142 |
143 |
148 |
149 |
150 | Resultado
151 |
152 |
153 | {`Ei ${playername}! Em ${(
156 | finalTime / 1000
157 | ).toFixed(
158 | 2
159 | )} segundos você acertou ${rightAnswers} de ${totalQuestions} perguntas.`}
160 |
161 | {`Você fez ${calculateResult(
164 | rightAnswers,
165 | finalTime,
166 | totalQuestions
167 | )} pontos.`}
168 |
169 |
170 | Jogar outros quizes
171 |
172 |
173 |
174 |
175 | >
176 | )
177 | }
178 |
179 | export default Quiz
180 |
181 | export const getServerSideProps: GetServerSideProps = async (context) => {
182 | const login = context.params.quiz as string
183 |
184 | const data = await getQuiz(login)
185 | const questions = await getQuestions(login)
186 |
187 | async function getQuiz(quiz: string) {
188 | try {
189 | const response = await api.get(`/quiz/${quiz}`)
190 | const { data } = response
191 | return data
192 | } catch (err) {
193 | return err
194 | }
195 | }
196 |
197 | async function getQuestions(quiz: string) {
198 | try {
199 | const response = await api.get(`/quiz/${quiz}/questions`)
200 | const { data } = response
201 | return data
202 | } catch (err) {
203 | return err
204 | }
205 | }
206 |
207 | if (!data.login) {
208 | return {
209 | redirect: {
210 | destination: '/',
211 | permanent: false
212 | }
213 | }
214 | }
215 |
216 | return {
217 | props: {
218 | data,
219 | questions
220 | }
221 | }
222 | }
223 |
224 | export const QuizContainer = styled.div`
225 | width: 100%;
226 | max-width: 350px;
227 | padding-top: 15px;
228 | margin: auto 10%;
229 | @media screen and (max-width: 500px) {
230 | margin: auto;
231 | padding: 15px;
232 | }
233 |
234 | .quiz-logo {
235 | width: 50px;
236 | }
237 | `
238 |
239 | export const PageContainerLoading = styled(PageContainer)`
240 | display: flex;
241 | align-items: center;
242 | justify-content: center;
243 |
244 | img {
245 | width: 50px;
246 | }
247 |
248 | h2 {
249 | width: 300px;
250 | text-justify: center;
251 | text-align: center;
252 | }
253 | `
254 |
255 | export const ResultContainer = styled.div`
256 | width: 90%;
257 | max-width: 350px;
258 | padding-top: 45px;
259 | margin: auto 10%;
260 | display: flex;
261 | flex-direction: column;
262 | align-items: center;
263 | justify-content: center;
264 |
265 | @media screen and (max-width: 500px) {
266 | margin: auto;
267 | padding: 15px;
268 | }
269 | `
270 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle, ThemeProvider } from 'styled-components'
2 | import { defaultTheme } from '../src/styles/theme'
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | * {
6 | box-sizing: border-box;
7 | }
8 | body {
9 | margin: 0;
10 | padding: 0;
11 | box-sizing: border-box;
12 |
13 | font-family: 'Lato', sans-serif;
14 |
15 | color: ${({ theme }) => theme.colors.contrastText};
16 | }
17 | html, body {
18 | min-height: 100vh;
19 | }
20 | `
21 |
22 | export default function App({ Component, pageProps }) {
23 | return (
24 | <>
25 |
26 |
27 |
28 |
29 | >
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from 'next/document'
2 | import { ServerStyleSheet } from 'styled-components'
3 |
4 | export default class MyDocument extends Document {
5 | static async getInitialProps(ctx) {
6 | const sheet = new ServerStyleSheet()
7 | const originalRenderPage = ctx.renderPage
8 |
9 | try {
10 | ctx.renderPage = () =>
11 | originalRenderPage({
12 | enhanceApp: (App) => (props) =>
13 | sheet.collectStyles()
14 | })
15 |
16 | const initialProps = await Document.getInitialProps(ctx)
17 | return {
18 | ...initialProps,
19 | styles: (
20 | <>
21 | {initialProps.styles}
22 | {sheet.getStyleElement()}
23 | >
24 | )
25 | }
26 | } finally {
27 | sheet.seal()
28 | }
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { GetStaticProps } from 'next'
3 |
4 | import Button, { SecondaryButtonStyle } from '../src/components/Button'
5 | import GitHubCorner from '../src/components/GitHubCorner'
6 | import Input from '../src/components/Input'
7 | import QuizWidget from '../src/components/QuizWidget'
8 | import api from '../src/services/api'
9 | import { useEffect, useState } from 'react'
10 | import { useRouter } from 'next/router'
11 | import CustomHead from '../src/components/CustomHead'
12 |
13 | const AluraQuiz = ({ data }) => {
14 | const router = useRouter()
15 |
16 | const [initialData, setInitalData] = useState(data)
17 |
18 | const [search, setSearch] = useState('')
19 | const [filteredData, setFilteredData] = useState(initialData || [])
20 |
21 | useEffect(() => {
22 | setFilteredData(
23 | initialData.filter(
24 | (item) =>
25 | item.title.toLowerCase().includes(search) ||
26 | item.title.includes(search)
27 | )
28 | )
29 | }, [search])
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |

38 |
39 |
40 |
41 |

42 |
43 | Responda e crie quizes sobre os mais variados assuntos
44 |
45 |
46 | O AluraQuiz permite que você crie quizes personalizados
47 | e teste o conhecimento de seus amigos
48 |
49 |
50 |
56 | {
58 | router.push('/login')
59 | }}
60 | >
61 | CRIAR NOVO QUIZ
62 |
63 |
64 |
65 |
66 |
67 | {filteredData.lenght !== 0 &&
68 | filteredData.map((item) => (
69 |
77 | ))}
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default AluraQuiz
85 |
86 | export const getStaticProps: GetStaticProps = async (context) => {
87 | const data = await getQuizesService()
88 |
89 | async function getQuizesService() {
90 | try {
91 | const response = await api.get('/quiz')
92 | const { data } = response
93 | return data
94 | } catch (err) {
95 | return 'error'
96 | }
97 | }
98 |
99 | return {
100 | props: {
101 | data: data
102 | },
103 | revalidate: 30
104 | }
105 | }
106 |
107 | export const MainSectionContainer = styled.main`
108 | z-index: 10;
109 |
110 | margin-top: -50px;
111 |
112 | max-width: 1000px;
113 | width: 90%;
114 |
115 | min-height: 20vh;
116 |
117 | display: grid;
118 | grid-template-columns: 1fr 1fr 1fr;
119 | grid-gap: 15px;
120 |
121 | @media (max-width: 820px) {
122 | grid-template-columns: 1fr 1fr;
123 | }
124 |
125 | @media (max-width: 600px) {
126 | grid-template-columns: 1fr;
127 | }
128 |
129 | @media (max-width: 480px) {
130 | margin-top: 50px;
131 | }
132 | `
133 |
134 | export const PageContainer = styled.div`
135 | display: flex;
136 | flex-direction: column;
137 |
138 | align-items: center;
139 |
140 | min-height: 100vh;
141 |
142 | background: ${(props) => props.theme.colors.mainBg};
143 |
144 | padding-bottom: 50px;
145 | `
146 | export const HeroSectionContainer = styled.div`
147 | max-width: 1000px;
148 | width: 90%;
149 |
150 | min-height: 80vh;
151 |
152 | display: grid;
153 | grid-template-columns: 1fr 1fr;
154 |
155 | .hero-img {
156 | position: relative;
157 |
158 | .linear-gradient-a {
159 | position: absolute;
160 |
161 | background: linear-gradient(
162 | 90deg,
163 | ${(props) => props.theme.colors.mainBg} 0%,
164 | rgba(0, 0, 0, 0) 100%
165 | );
166 |
167 | height: 100%;
168 | width: 50%;
169 | }
170 |
171 | img {
172 | height: 100%;
173 | width: 100%;
174 | object-fit: cover;
175 | }
176 |
177 | .linear-gradient-b {
178 | position: absolute;
179 |
180 | right: 0;
181 | top: 0;
182 |
183 | background: linear-gradient(
184 | -90deg,
185 | ${(props) => props.theme.colors.mainBg} 0%,
186 | rgba(0, 0, 0, 0) 100%
187 | );
188 |
189 | height: 100%;
190 | width: 100%;
191 | }
192 | }
193 |
194 | .actions {
195 | display: flex;
196 | flex-direction: column;
197 |
198 | align-items: center;
199 | justify-content: center;
200 |
201 | padding-left: 45px;
202 | padding-bottom: 45px;
203 |
204 | img {
205 | padding-bottom: 45px;
206 | }
207 |
208 | h1 {
209 | padding: 0;
210 | font-size: 36px;
211 |
212 | font-weight: 900;
213 | line-height: 1.2;
214 | }
215 |
216 | p {
217 | font-size: 14px;
218 | padding-bottom: 24px;
219 | }
220 |
221 | .cta-buttons {
222 | width: 100%;
223 |
224 | display: grid;
225 | grid-template-columns: 1fr 1fr;
226 | grid-gap: 15px;
227 |
228 | @media (max-width: 480px) {
229 | grid-template-columns: 1fr;
230 | }
231 | }
232 | }
233 |
234 | @media (max-width: 820px) {
235 | grid-template-columns: 1fr;
236 |
237 | .hero-img {
238 | display: none;
239 | }
240 |
241 | .actions {
242 | padding: 0;
243 | }
244 | }
245 | `
246 |
--------------------------------------------------------------------------------
/pages/login/[quiz]/index.tsx:
--------------------------------------------------------------------------------
1 | import { GetServerSideProps } from 'next'
2 |
3 | import React, { useEffect, useState } from 'react'
4 | import styled from 'styled-components'
5 |
6 | import { PageContainer } from '../..'
7 |
8 | import api from '../../../src/services/api'
9 |
10 | import Link from 'next/link'
11 | import { MainButtonStyle } from '../../../src/components/Button'
12 | import {
13 | Widget,
14 | WidgetContent,
15 | WidgetHeader
16 | } from '../../../src/styles/WidgetStyle'
17 | import Input from '../../../src/components/Input'
18 | import CustomHead from '../../../src/components/CustomHead'
19 |
20 | const Questions = ({ data, questions }) => {
21 | const [existingQuestins, setExistingQuestions] = useState(questions)
22 |
23 | const [screenState, setScreenState] = useState('main')
24 |
25 | useEffect(() => {
26 | async function getQuestions(login) {
27 | try {
28 | const response = await api.get(`/quiz/${login.login}/questions`)
29 | const { data } = response
30 | setExistingQuestions(data)
31 | } catch (err) {
32 | return err
33 | }
34 | }
35 | getQuestions(data)
36 | }, [screenState])
37 |
38 | const [questionTitle, setQuestionTitle] = useState('')
39 | const [questionImage, setQuestionImage] = useState('')
40 | const [questionDescription, setQuestionDescription] = useState('')
41 | const [questionAlternative, setQuestionAlternative] = useState('')
42 |
43 | const [questionAlternatives, setQuestionAlternatives] = useState([])
44 | const [correctAnswer, setCorrectAnswer] = useState(undefined)
45 |
46 | function handleAddAlternative(e: any) {
47 | e.preventDefault()
48 |
49 | setQuestionAlternatives([...questionAlternatives, questionAlternative])
50 | setQuestionAlternative('')
51 | }
52 |
53 | async function handleSubmitQuestion(e) {
54 | e.preventDefault()
55 | setScreenState('loading')
56 | try {
57 | const response = await api.post(
58 | `/quiz/questions`,
59 | {
60 | title: questionTitle,
61 | image: questionImage,
62 | description: questionDescription,
63 | answer: correctAnswer,
64 | alternatives: questionAlternatives
65 | },
66 | {
67 | headers: {
68 | quiz: data._id
69 | }
70 | }
71 | )
72 |
73 | setScreenState('main')
74 | setQuestionTitle('')
75 | setQuestionImage('')
76 | setQuestionDescription('')
77 | setQuestionAlternative('')
78 | setQuestionAlternatives([])
79 | setCorrectAnswer(undefined)
80 | } catch (err) {
81 | console.log(err)
82 | setScreenState('new-question')
83 | }
84 | }
85 |
86 | if (screenState === 'loading')
87 | return (
88 | <>
89 |
90 | Carregando...
91 | >
92 | )
93 |
94 | if (screenState === 'new-question')
95 | return (
96 |
97 |
98 |
99 |
100 |
105 |
106 |
107 |
108 | Preencha os campos a seguir
109 |
110 | {questionImage !== '' && (
111 |
112 | )}
113 |
114 |
121 |
128 |
135 |
136 | {questionAlternatives.length < 4 ? (
137 |
144 | ) : null}
145 | {questionAlternatives.length < 4 &&
146 | questionAlternative !== '' ? (
147 |
148 | Adicionar
149 |
150 | ) : null}
151 |
152 |
153 | {questionAlternatives.length === 4 ? (
154 |
155 | Quantidade máxima de alternativas alcançada
156 |
157 | ) : (
158 |
159 | Alternativas (selecione a correta)
160 |
161 | )}
162 |
163 | {questionAlternatives &&
164 | questionAlternatives.map((alternative, index) =>
165 | correctAnswer === index ? (
166 | {
169 | e.preventDefault()
170 | setCorrectAnswer(index)
171 | }}
172 | style={{
173 | background: 'green'
174 | }}
175 | >
176 | {index + 1} - {alternative}
177 |
178 | ) : (
179 | {
182 | e.preventDefault()
183 | setCorrectAnswer(index)
184 | }}
185 | >
186 | {index + 1} - {alternative}
187 |
188 | )
189 | )}
190 |
191 | {correctAnswer !== undefined &&
192 | questionTitle &&
193 | questionDescription &&
194 | questionImage && (
195 |
196 | Cadastrar Pergunta
197 |
198 | )}
199 |
200 |
201 |
202 | )
203 |
204 | if (screenState === 'main')
205 | return (
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | setScreenState('new-question')}
216 | >
217 | Adicionar Questão
218 |
219 |
220 |
221 |
222 | {existingQuestins.length > 0 &&
223 | existingQuestins.map((question) => (
224 |
225 |
226 | {question.title}
227 |
228 |
229 |
230 |
231 | {question.alternatives.length > 0 &&
232 | question.alternatives.map(
233 | (alternative, index) =>
234 | index === +question.answer ? (
235 |
240 | {alternative}
241 |
242 | ) : (
243 | {alternative}
244 | )
245 | )}
246 |
247 |
248 | ))}
249 |
250 |
251 | )
252 | }
253 |
254 | export const getServerSideProps: GetServerSideProps = async (context) => {
255 | const login = context.params.quiz as string
256 |
257 | const data = await getQuiz(login)
258 | const questions = await getQuestions(login)
259 |
260 | async function getQuiz(quiz: string) {
261 | try {
262 | const response = await api.get(`/quiz/${quiz}`)
263 | const { data } = response
264 | return data
265 | } catch (err) {
266 | return err
267 | }
268 | }
269 |
270 | if (!data.login) {
271 | return {
272 | redirect: {
273 | destination: '/',
274 | permanent: false
275 | }
276 | }
277 | }
278 |
279 | return {
280 | props: {
281 | data,
282 | questions
283 | }
284 | }
285 | }
286 |
287 | async function getQuestions(quiz: string) {
288 | try {
289 | const response = await api.get(`/quiz/${quiz}/questions`)
290 | const { data } = response
291 | console.log(data)
292 | return data
293 | } catch (err) {
294 | return err
295 | }
296 | }
297 |
298 | export default Questions
299 |
300 | export const AlternativesFormStyle = styled.div`
301 | min-width: 300px;
302 | max-width: 900px;
303 | width: 100%;
304 |
305 | display: flex;
306 | flex-direction: column;
307 |
308 | margin-bottom: 15px;
309 |
310 | p {
311 | margin: 6px 0;
312 | padding: 0;
313 | }
314 |
315 | span {
316 | background: ${({ theme }) => theme.colors.secondary};
317 | min-height: 40px;
318 |
319 | display: flex;
320 | padding: 0 18px;
321 | align-items: center;
322 | justify-content: flex-start;
323 |
324 | margin: 3px 0;
325 |
326 | cursor: pointer;
327 | transition: 0.5s;
328 |
329 | &:hover {
330 | background: ${({ theme }) => theme.colors.primary};
331 | }
332 | }
333 | `
334 |
335 | export const DefaultContainerStyle = styled.div`
336 | border: 1px solid ${({ theme }) => theme.colors.primary};
337 | background-color: transparent;
338 | border-radius: ${({ theme }) => {
339 | return theme.borderRadius
340 | }};
341 | padding: 18px 32px;
342 | max-width: 900px;
343 | width: 90%;
344 | `
345 |
346 | export const NavBarStyle = styled(DefaultContainerStyle)`
347 | border: none;
348 |
349 | display: flex;
350 | align-items: center;
351 | justify-content: space-between;
352 |
353 | height: 15vh;
354 |
355 | img {
356 | width: 50px;
357 |
358 | @media (max-width: 480px) {
359 | width: 25px;
360 | }
361 | }
362 |
363 | button {
364 | max-width: 150px;
365 |
366 | @media (max-width: 480px) {
367 | width: 100px;
368 | font-size: 10px;
369 | }
370 | }
371 | `
372 |
373 | export const ThreeColumnsBodyStyle = styled(DefaultContainerStyle)`
374 | border: none;
375 |
376 | display: grid;
377 | grid-template-columns: 1fr 1fr 1fr;
378 | grid-gap: 15px;
379 |
380 | @media (max-width: 620px) {
381 | grid-template-columns: 1fr 1fr;
382 | }
383 |
384 | @media (max-width: 480px) {
385 | grid-template-columns: 1fr;
386 | }
387 | `
388 |
389 | export const QuestionCardStyle = styled.div`
390 | display: flex;
391 | flex-direction: column;
392 | justify-content: flex-start;
393 | align-items: flex-start;
394 | width: 100%;
395 | min-height: 35vh;
396 |
397 | background-color: transparent;
398 | `
399 | export const CardHeaderStyle = styled.div`
400 | display: flex;
401 | justify-content: flex-start;
402 | align-items: center;
403 |
404 | padding: 0 32px;
405 | margin: 0;
406 | background-color: ${({ theme }) => theme.colors.primary};
407 | width: 100%;
408 |
409 | border: 1px solid ${({ theme }) => theme.colors.primary};
410 | border-radius: ${({ theme }) => {
411 | return theme.borderRadius
412 | }}
413 | ${({ theme }) => {
414 | return theme.borderRadius
415 | }}
416 | 0 0;
417 |
418 | h2 {
419 | font-size: 14px;
420 | }
421 | `
422 |
423 | export const CardBodyStyle = styled.div`
424 | display: flex;
425 | flex-direction: column;
426 |
427 | align-items: center;
428 | justify-content: center;
429 |
430 | padding: 6px;
431 |
432 | width: 100%;
433 |
434 | border-radius: 0 0;
435 |
436 | border: 1px solid ${({ theme }) => theme.colors.primary};
437 |
438 | img {
439 | width: 100%;
440 | object-fit: cover;
441 | height: 100px;
442 | margin-bottom: 6px;
443 | }
444 |
445 | div {
446 | display: flex;
447 | align-items: center;
448 | justify-content: center;
449 |
450 | height: 40px;
451 | background: blue;
452 | width: 100%;
453 |
454 | margin: 3px;
455 | padding: 0 6px;
456 |
457 | font-size: 10px;
458 | }
459 | `
460 |
461 | const LoadingContainerStyle = styled(PageContainer)`
462 | display: flex;
463 | align-items: center;
464 | justify-content: center;
465 | `
466 |
467 | const FormImageStyle = styled.img`
468 | width: 300px;
469 | height: 100px;
470 | object-fit: cover;
471 | `
472 |
--------------------------------------------------------------------------------
/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import { useRouter } from 'next/router'
3 | import React, { useEffect, useState } from 'react'
4 | import styled from 'styled-components'
5 |
6 | import { PageContainer } from '..'
7 | import { MainButtonStyle } from '../../src/components/Button'
8 | import CustomHead from '../../src/components/CustomHead'
9 |
10 | import Input from '../../src/components/Input'
11 | import api from '../../src/services/api'
12 | import { defaultTheme } from '../../src/services/default-theme'
13 |
14 | import { DefaultInputStyle } from '../../src/styles/InputStyle'
15 | import {
16 | Widget,
17 | WidgetContent,
18 | WidgetHeader,
19 | WidgetImage
20 | } from '../../src/styles/WidgetStyle'
21 |
22 | const Login = () => {
23 | const router = useRouter()
24 |
25 | const [screenState, setScreenState] = useState('login')
26 |
27 | function handleLoginPage() {
28 | setScreenState('login')
29 | }
30 |
31 | function handleRegisterPage() {
32 | setScreenState('register')
33 | }
34 |
35 | const [titleRegister, setTitleRegister] = useState('')
36 | const [backgroundRegister, setBackgroundRegister] = useState('')
37 | const [descriptionRegister, setDescriptionRegister] = useState('')
38 |
39 | useEffect(() => {
40 | const lowerCase = titleRegister.toLowerCase()
41 | const splitTitle = lowerCase.split(' ')
42 | const sanitezed = splitTitle.map((word) => word.trim())
43 | const login = sanitezed.join('-')
44 |
45 | setLoginRegister(login)
46 | }, [titleRegister])
47 |
48 | const [loginRegister, setLoginRegister] = useState('')
49 |
50 | const [loginSession, setLoginSession] = useState('')
51 | const [errorLogin, setErrorLogin] = useState(false)
52 |
53 | async function handleCreateQuiz() {
54 | setScreenState('loading')
55 | try {
56 | const response = await api.post('/quiz', {
57 | login: loginRegister,
58 | title: titleRegister,
59 | background: backgroundRegister,
60 | description: descriptionRegister,
61 | theme: defaultTheme
62 | })
63 |
64 | const { data } = response
65 |
66 | const { login } = data
67 |
68 | router.push(`/login/${login}`)
69 | } catch (err) {
70 | console.log(err)
71 | setScreenState('register')
72 | }
73 | }
74 |
75 | async function handleCreateSession() {
76 | setScreenState('loading')
77 | try {
78 | const response = await api.post('/session', {
79 | login: loginSession
80 | })
81 |
82 | const { data } = response
83 |
84 | const { login } = data
85 |
86 | router.push(`/login/${login}`)
87 | } catch (err) {
88 | setScreenState('login')
89 | setLoginSession('')
90 | setErrorLogin(true)
91 | setTimeout(() => setErrorLogin(false), 2000)
92 | console.log(err)
93 | }
94 | }
95 |
96 | if (screenState === 'loading')
97 | return (
98 | <>
99 | {' '}
100 |
101 | Carregando...
102 | >
103 | )
104 |
105 | if (screenState === 'register')
106 | return (
107 |
108 |
109 |
110 |
115 |
116 |
117 | Criar novo quiz
118 |
119 |
120 |
127 | {titleRegister && (
128 |
129 | URL: {loginRegister}
130 |
131 | )}
132 |
133 |
140 |
147 |
154 |
155 | {titleRegister &&
156 | descriptionRegister &&
157 | backgroundRegister && (
158 |
159 | Criar
160 |
161 | )}
162 |
163 |
167 | Acessar quiz existente
168 |
169 |
170 |
171 | )
172 |
173 | return (
174 |
175 |
176 |
177 |
182 |
183 |
184 | Acessar Quiz
185 |
186 |
187 | {errorLogin && (
188 | Esse quiz não existe
189 | )}
190 |
197 |
198 | {loginSession && (
199 |
200 | Acessar
201 |
202 | )}
203 |
204 |
205 | Cadastrar novo quiz
206 |
207 |
208 |
209 | )
210 | }
211 |
212 | export default Login
213 |
214 | export const LoginInput = styled(DefaultInputStyle)`
215 | margin: 7.5px 0;
216 | `
217 | export const LoginWidgetImage = styled(WidgetImage)`
218 | width: 100%;
219 | max-width: 350px;
220 | `
221 | export const FormLoginContainer = styled.div`
222 | width: 100%;
223 | max-width: 350px;
224 | padding-top: 15px;
225 | margin: auto 10%;
226 | @media screen and (max-width: 500px) {
227 | margin: auto;
228 | padding: 15px;
229 | }
230 |
231 | .quiz-logo {
232 | width: 50px;
233 | }
234 | `
235 |
236 | export const LoginPageContainer = styled(PageContainer)`
237 | display: flex;
238 | align-items: center;
239 | justify-content: center;
240 |
241 | .change-screen-state {
242 | margin: 0;
243 |
244 | padding: 0;
245 |
246 | font-size: 14px;
247 |
248 | cursor: pointer;
249 |
250 | transition: 0.5s;
251 |
252 | &:hover {
253 | color: ${({ theme }) => {
254 | return theme.colors.secondary
255 | }};
256 | }
257 | }
258 |
259 | .quiz-url {
260 | margin-bottom: 15px;
261 |
262 | span {
263 | font-size: 10px;
264 | }
265 | }
266 | `
267 |
268 | export const RegisterPageContainer = styled(PageContainer)`
269 | display: flex;
270 | align-items: center;
271 | justify-content: center;
272 |
273 | .change-screen-state {
274 | margin: 0;
275 | margin-top: -10px;
276 | padding: 0;
277 |
278 | font-size: 14px;
279 |
280 | cursor: pointer;
281 |
282 | transition: 0.5s;
283 |
284 | &:hover {
285 | color: ${({ theme }) => {
286 | return theme.colors.secondary
287 | }};
288 | }
289 | }
290 |
291 | .quiz-url {
292 | margin-bottom: 15px;
293 | }
294 | `
295 |
--------------------------------------------------------------------------------
/public/home-image.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fischerafael/alura-quiz-frontend/32b568669aef2dd752be5d2c8c404ea3df9beba0/public/home-image.jpeg
--------------------------------------------------------------------------------
/public/loading-transparent.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fischerafael/alura-quiz-frontend/32b568669aef2dd752be5d2c8c404ea3df9beba0/public/loading-transparent.gif
--------------------------------------------------------------------------------
/public/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fischerafael/alura-quiz-frontend/32b568669aef2dd752be5d2c8c404ea3df9beba0/public/loading.gif
--------------------------------------------------------------------------------
/public/logo-alura.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/public/quiz-time.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fischerafael/alura-quiz-frontend/32b568669aef2dd752be5d2c8c404ea3df9beba0/public/quiz-time.jpeg
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | interface IButton {
4 | type: 'main' | 'secondary' | 'widget'
5 | onClick?: (e: any) => void
6 | }
7 |
8 | const Button: React.FC = ({ type, children, onClick }) => {
9 | if (type === 'main')
10 | return {children}
11 | if (type === 'widget')
12 | return (
13 | {children}
14 | )
15 | return (
16 |
17 | {children}
18 |
19 | )
20 | }
21 |
22 | export default Button
23 |
24 | export const ButtonStyle = styled.button`
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 |
29 | width: 100%;
30 |
31 | height: 50px;
32 | border-radius: ${({ theme }) => {
33 | return theme.borderRadius
34 | }};
35 | border: none;
36 |
37 | font-size: 12px;
38 | font-weight: bold;
39 |
40 | cursor: pointer;
41 | transition: 0.5s;
42 |
43 | &:hover {
44 | background: ${({ theme }) => {
45 | return theme.colors.secondary
46 | }};
47 | }
48 | `
49 | export const MainButtonStyle = styled(ButtonStyle)`
50 | background: ${({ theme }) => {
51 | return theme.colors.primary
52 | }};
53 | color: ${({ theme }) => {
54 | return theme.colors.contrastText
55 | }};
56 | &:hover {
57 | background: ${({ theme }) => {
58 | return theme.colors.secondary
59 | }};
60 | }
61 | `
62 | export const SecondaryButtonStyle = styled(ButtonStyle)`
63 | background: ${({ theme }) => {
64 | return theme.colors.secondary
65 | }};
66 | color: ${({ theme }) => {
67 | return theme.colors.contrastText
68 | }};
69 | &:hover {
70 | background: ${({ theme }) => {
71 | return theme.colors.primary
72 | }};
73 | }
74 | `
75 | export const WidgetButtonStyle = styled(MainButtonStyle)`
76 | margin-top: 15px;
77 | `
78 |
--------------------------------------------------------------------------------
/src/components/CustomHead/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import React from 'react'
3 |
4 | const CustomHead: React.FC<{ pageTitle: string }> = ({ pageTitle }) => {
5 | return (
6 |
7 | {pageTitle}
8 |
9 | )
10 | }
11 |
12 | export default CustomHead
13 |
--------------------------------------------------------------------------------
/src/components/GitHubCorner/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const GitHubCorner: React.FC<{ projectUrl: string }> = ({ projectUrl }) => {
4 | return (
5 |
6 |
7 |
14 |
15 |
21 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export default GitHubCorner
33 |
34 | export const GitHubWrapper = styled.div`
35 | position: absolute;
36 | top: 0;
37 | border: 0;
38 | right: 0;
39 | z-index: 100;
40 | `
41 |
42 | const SVGWrapper = styled.svg`
43 | fill: ${({ theme }) => theme.colors.secondary};
44 | color: ${({ theme }) => theme.colors.contrastText};
45 | cursor: pointer;
46 | &:hover .octo-arm {
47 | animation: octocat-wave 560ms ease-in-out;
48 | }
49 | @keyframes octocat-wave {
50 | 0%,
51 | 100% {
52 | transform: rotate(0);
53 | }
54 | 20%,
55 | 60% {
56 | transform: rotate(-25deg);
57 | }
58 | 40%,
59 | 80% {
60 | transform: rotate(10deg);
61 | }
62 | }
63 | @media (max-width: 500px) {
64 | &:hover .octo-arm {
65 | animation: none;
66 | }
67 | & .octo-arm {
68 | animation: octocat-wave 560ms ease-in-out;
69 | }
70 | }
71 | `
72 |
--------------------------------------------------------------------------------
/src/components/Input/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { DefaultInputStyle } from '../../styles/InputStyle'
3 |
4 | interface IInput {
5 | label: string
6 | placeholder?: string
7 | value?: any
8 | login?: boolean
9 | setValue?: (e: any) => void
10 | }
11 |
12 | const Input: React.FC = ({
13 | label,
14 | setValue,
15 | value,
16 | placeholder,
17 | login
18 | }) => {
19 | if (login)
20 | return (
21 |
22 | {label}
23 | setValue(e.target.value)}
27 | value={value}
28 | />
29 |
30 | )
31 |
32 | return (
33 |
34 | {label}
35 | setValue(e.target.value)}
39 | value={value}
40 | />
41 |
42 | )
43 | }
44 |
45 | export default Input
46 |
47 | export const HomeInputStyle = styled(DefaultInputStyle)``
48 | export const LoginInputStyle = styled(DefaultInputStyle)`
49 | margin: 7.5px 0;
50 | `
51 |
--------------------------------------------------------------------------------
/src/components/QuestionWidget/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import {
3 | Widget,
4 | WidgetForm,
5 | WidgetHeader,
6 | WidgetImage,
7 | WidgetQuestionContent,
8 | WidgetTopic
9 | } from '../../styles/WidgetStyle'
10 | import { MainButtonStyle } from '../Button'
11 |
12 | const QuestinWidget = ({
13 | totalQuestions,
14 | questionIndex,
15 | question,
16 | onSubmit,
17 | setAnswersArray
18 | }) => {
19 | const questionId = `question__${questionIndex}`
20 |
21 | const [selectedAlternative, setSelectedAlternative] = useState(undefined)
22 | const correctAlternative = +question.answer
23 |
24 | function isCorrect() {
25 | if (correctAlternative !== selectedAlternative) return 0
26 | return 1
27 | }
28 |
29 | function handleNextQuestion(e: any) {
30 | e.preventDefault()
31 | setAnswersArray((prevState: number[]) => [...prevState, isCorrect()])
32 | onSubmit()
33 | }
34 |
35 | return (
36 |
37 |
38 | {`Pergunta ${questionIndex + 1} de ${totalQuestions}`}
39 |
40 |
41 |
42 | {question.title}
43 | {question.description}
44 |
45 |
46 | {question.alternatives.map((alternative, alternativeIndex) => {
47 | const alternativeId = `alternative__${alternativeIndex}`
48 | return (
49 |
54 | {
59 | setSelectedAlternative(alternativeIndex)
60 | }}
61 | />
62 | {alternative}
63 |
64 | )
65 | })}
66 |
67 |
68 | Confirmar
69 |
70 |
71 | )
72 | }
73 |
74 | export default QuestinWidget
75 |
--------------------------------------------------------------------------------
/src/components/QuizWidget/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | QuizWidgetContainer,
3 | QuizWidgetContentContainer,
4 | Widget,
5 | WidgetContent,
6 | WidgetFooter,
7 | WidgetHeader
8 | } from '../../styles/WidgetStyle'
9 |
10 | import Button from '../Button'
11 | import Input from '../Input'
12 |
13 | import { useRouter } from 'next/router'
14 | import { useEffect, useState } from 'react'
15 | import styled from 'styled-components'
16 |
17 | interface IQuizWidget {
18 | title: string
19 | description: string
20 | login: string
21 | background: string
22 | players: {
23 | time: number
24 | score: number
25 | name: string
26 | }[]
27 | }
28 |
29 | const QuizWidget: React.FC = ({
30 | title,
31 | description,
32 | login,
33 | background,
34 | players
35 | }) => {
36 | const router = useRouter()
37 | const [name, setName] = useState('')
38 | const [rankedPlayers, setRankedPlayers] = useState(players)
39 | const [showRanking, setShowRanking] = useState(false)
40 |
41 | useEffect(() => {
42 | const rankedScores = players.sort((a, b) => b.score - a.score)
43 | const topFiveScores = rankedScores.slice(0, 10)
44 | setRankedPlayers(topFiveScores)
45 | }, [])
46 |
47 | function quizNavigateHandler(e: any) {
48 | e.preventDefault()
49 | router.push(`/${login}?playername=${name}`)
50 | }
51 |
52 | function handleShowRanking(e: any) {
53 | e.preventDefault()
54 | setShowRanking(!showRanking)
55 | }
56 |
57 | return (
58 |
59 |
60 |
63 | {title}
64 |
65 |
66 | {description}
67 |
68 |
74 | {name.length < 3 ? null : (
75 |
78 | )}
79 |
80 |
81 | {players.length > 0 && (
82 | <>
83 |
84 | Ver Top 10
85 |
86 | {showRanking ? (
87 |
88 |
89 | {rankedPlayers.map((player, index) => (
90 |
91 | {index === 0 ? (
92 |
95 | 1
96 |
97 | ) : index === 1 ? (
98 |
101 | 2
102 |
103 | ) : index === 2 ? (
104 |
109 | 3
110 |
111 | ) : (
112 |
113 | )}
114 |
115 |
116 | {player.name}
117 |
118 |
119 |
120 | {player.score}
121 |
122 |
123 | pontos
124 |
125 |
126 |
127 | ))}
128 |
129 |
130 | ) : null}
131 | >
132 | )}
133 |
134 | )
135 | }
136 |
137 | export default QuizWidget
138 |
139 | export const ClickableWidgetHeader = styled(WidgetHeader)`
140 | cursor: pointer;
141 |
142 | h2 {
143 | font-size: 18px;
144 | }
145 |
146 | &:hover {
147 | background: ${({ theme }) => theme.colors.secondary};
148 | }
149 | `
150 |
151 | export const QuizWidgetRankingStyle = styled.div`
152 | background: ${({ theme }) => theme.colors.mainBg};
153 | `
154 | export const QuizWidgetRankingContainer = styled.div`
155 | padding: 32px;
156 | border: 1px solid ${({ theme }) => theme.colors.primary};
157 |
158 | .ranking-container {
159 | border: 1px solid ${({ theme }) => theme.colors.primary};
160 | border-radius: ${({ theme }) => theme.borderRadius};
161 | padding: 0 6px;
162 |
163 | .ranking-card {
164 | background: ${({ theme }) => theme.colors.primary};
165 |
166 | display: grid;
167 | grid-template-columns: 1fr 3fr 1fr;
168 | grid-gap: 6px;
169 |
170 | margin: 6px 0;
171 |
172 | transition: 0.5s;
173 |
174 | &:hover {
175 | background: ${({ theme }) => theme.colors.secondary};
176 | }
177 |
178 | span {
179 | width: 25px;
180 | height: 25px;
181 | border-radius: 12.5px;
182 | align-self: center;
183 | justify-self: center;
184 | background: ${({ theme }) => theme.colors.contrastText};
185 |
186 | display: flex;
187 | align-items: center;
188 | justify-content: center;
189 |
190 | font-weight: bold;
191 | font-size: 10px;
192 | }
193 |
194 | .ranking-username {
195 | font-weight: bold;
196 | font-size: 12px;
197 | align-self: center;
198 | justify-self: flex-start;
199 | }
200 |
201 | .ranking-score {
202 | display: flex;
203 | flex-direction: column;
204 | align-items: center;
205 | justify-content: center;
206 |
207 | .ranking-score-point {
208 | font-size: 12px;
209 | font-weight: bold;
210 | padding: 0;
211 | margin: 0;
212 | }
213 |
214 | .ranking-score-label {
215 | font-size: 10px;
216 | padding: 0;
217 | margin: 0;
218 | }
219 | }
220 | }
221 | }
222 | `
223 | export const QuizWidgetRanking = styled.div`
224 | padding: 0 32px;
225 | padding-bottom: 18px;
226 |
227 | display: flex;
228 | flex-direction: column;
229 |
230 | align-items: center;
231 | justify-content: center;
232 |
233 | .ranking-title {
234 | align-self: flex-start;
235 | margin: 0;
236 | padding: 0;
237 | font-size: 14px;
238 | margin-bottom: 12px;
239 | }
240 |
241 | .ranking {
242 | display: flex;
243 | align-items: center;
244 | justify-content: space-between;
245 |
246 | padding: 0 32px;
247 |
248 | width: 100%;
249 | border: 1px solid ${({ theme }) => theme.colors.primary};
250 | border-radius: ${({ theme }) => theme.borderRadius};
251 |
252 | h2 {
253 | font-size: 14px;
254 | line-height: 1;
255 | }
256 |
257 | div {
258 | display: flex;
259 | flex-direction: column;
260 | margin: 0;
261 | padding: 0;
262 |
263 | width: 100%;
264 |
265 | align-items: flex-end;
266 |
267 | h3 {
268 | margin-top: 15px;
269 | margin-bottom: 0;
270 |
271 | padding: 0;
272 | line-height: 1;
273 | }
274 |
275 | p {
276 | font-size: 10px;
277 | padding: 0;
278 | line-height: 1;
279 | }
280 | }
281 | }
282 | `
283 |
284 | export const QuizWidgetHeader = styled.header`
285 | position: relative;
286 |
287 | display: flex;
288 | justify-content: flex-start;
289 | align-items: flex-end;
290 |
291 | height: 200px;
292 |
293 | background-image: url('home-image.jpeg');
294 | background-repeat: no-repeat;
295 | background-size: cover;
296 | background-position: center;
297 |
298 | border-radius: ${({ theme }) => theme.borderRadius};
299 |
300 | margin: 0;
301 | z-index: 99;
302 |
303 | .linear {
304 | position: absolute;
305 |
306 | z-index: 100;
307 |
308 | height: 100px;
309 | width: 100%;
310 | background: linear-gradient(0deg, black, rgba(0, 0, 0, 0));
311 | }
312 |
313 | h2 {
314 | z-index: 101;
315 | padding: 18px 32px;
316 | }
317 | `
318 |
--------------------------------------------------------------------------------
/src/components/TextAreaInput/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { DefaultInputStyle } from '../../styles/InputStyle'
3 |
4 | interface ITextInput {
5 | label: string
6 | placeholder?: string
7 | value?: any
8 | login?: boolean
9 | setValue?: (e: any) => void
10 | }
11 |
12 | const TextAreaInput: React.FC = ({
13 | label,
14 | setValue,
15 | value,
16 | placeholder,
17 | login
18 | }) => {
19 | if (login)
20 | return (
21 |
22 | {label}
23 |
29 | )
30 |
31 | return (
32 |
33 | {label}
34 |
40 | )
41 | }
42 |
43 | export default TextAreaInput
44 |
45 | export const DefaultTextAreaStyle = styled.label`
46 | background-color: ${({ theme }) => {
47 | return theme.colors.mainBg
48 | }};
49 | border-radius: ${({ theme }) => {
50 | return theme.borderRadius
51 | }};
52 |
53 | border: 1px solid ${({ theme }) => theme.colors.primary};
54 |
55 | height: 50px;
56 |
57 | padding: 0 15px;
58 |
59 | width: 100%;
60 |
61 | display: flex;
62 | flex-direction: column;
63 | justify-content: center;
64 |
65 | span {
66 | font-size: 10px;
67 | color: ${({ theme }) => theme.colors.secondary};
68 | margin-bottom: 3px;
69 | }
70 |
71 | textarea {
72 | background: transparent;
73 | border: none;
74 | outline: none;
75 | color: ${({ theme }) => theme.colors.contrastText};
76 | resize: none;
77 | font: inherit;
78 | font-size: 12px;
79 |
80 | display: flex;
81 | flex-direction: column;
82 | align-items: center;
83 | justify-content: center;
84 | }
85 | `
86 | export const HomeTextAreaInputStyle = styled(DefaultTextAreaStyle)`
87 | background: transparent;
88 | `
89 | export const LoginTextAreaInputStyle = styled(DefaultTextAreaStyle)`
90 | margin: 7.5px 0;
91 | `
92 |
--------------------------------------------------------------------------------
/src/helpers/calculate-result.ts:
--------------------------------------------------------------------------------
1 | function calculateResult(
2 | correctAnswer: number,
3 | time: number,
4 | totalQuestions: number
5 | ) {
6 | const timeInSecondes = (time / 1000) * 0.25
7 | const score = (100 * correctAnswer) / totalQuestions
8 | const finalScore = score - timeInSecondes
9 |
10 | if (+finalScore <= 0) return '0'
11 |
12 | const formattedScore = finalScore.toFixed(2)
13 | return formattedScore
14 | }
15 |
16 | export default calculateResult
17 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | const api = axios.create({
4 | baseURL: 'https://alura-quiz-backend.herokuapp.com'
5 | })
6 |
7 | export default api
8 |
--------------------------------------------------------------------------------
/src/services/default-theme/index.ts:
--------------------------------------------------------------------------------
1 | export const defaultTheme = {
2 | colors: {
3 | primary: '#0d47a1',
4 | secondary: '#29b6f6',
5 | mainBG: '#181818',
6 | contrastText: '#FFFFFF',
7 | wrong: '#FF5722',
8 | success: '#4CAF50'
9 | },
10 | borderRadius: '4px'
11 | }
12 |
--------------------------------------------------------------------------------
/src/services/screen-states/index.ts:
--------------------------------------------------------------------------------
1 | interface IScreenStates {
2 | QUIZ: string
3 | LOADING: string
4 | RESULT: string
5 | EMPTY: string
6 | }
7 |
8 | const screenStates: IScreenStates = {
9 | QUIZ: 'QUIZ',
10 | LOADING: 'LOADING',
11 | RESULT: 'RESULT',
12 | EMPTY: 'EMPTY'
13 | }
14 |
15 | export default screenStates
16 |
--------------------------------------------------------------------------------
/src/styles/InputStyle/index.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const DefaultInputStyle = styled.label`
4 | background-color: ${({ theme }) => {
5 | return theme.colors.mainBg
6 | }};
7 | border-radius: ${({ theme }) => {
8 | return theme.borderRadius
9 | }};
10 |
11 | border: 1px solid ${({ theme }) => theme.colors.primary};
12 |
13 | height: 50px;
14 |
15 | padding: 0 15px;
16 |
17 | width: 100%;
18 |
19 | display: flex;
20 | flex-direction: column;
21 | justify-content: center;
22 |
23 | span {
24 | font-size: 10px;
25 | color: ${({ theme }) => theme.colors.secondary};
26 | margin-bottom: 3px;
27 | }
28 |
29 | input {
30 | background: transparent;
31 | border: none;
32 | outline: none;
33 | color: ${({ theme }) => theme.colors.contrastText};
34 | }
35 | `
36 |
--------------------------------------------------------------------------------
/src/styles/WidgetStyle/index.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const QuizWidgetContainer = styled.div`
4 | margin-top: 24px;
5 | margin-bottom: 24px;
6 |
7 | background-color: ${({ theme }) => {
8 | return theme.colors.mainBg
9 | }};
10 | border-radius: ${({ theme }) => {
11 | return theme.borderRadius
12 | }};
13 |
14 | overflow: hidden;
15 | z-index: 10;
16 | `
17 | export const QuizWidgetContentContainer = styled.div`
18 | border: 1px solid ${({ theme }) => theme.colors.primary};
19 | border-radius: ${({ theme }) => {
20 | return theme.borderRadius
21 | }}
22 | ${({ theme }) => {
23 | return theme.borderRadius
24 | }}
25 | 0 0;
26 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
27 | `
28 | export const Widget = styled.div`
29 | margin-top: 24px;
30 | margin-bottom: 24px;
31 | border: 1px solid ${({ theme }) => theme.colors.primary};
32 | background-color: ${({ theme }) => {
33 | return theme.colors.mainBg
34 | }};
35 | border-radius: ${({ theme }) => {
36 | return theme.borderRadius
37 | }};
38 | overflow: hidden;
39 |
40 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
41 |
42 | h1,
43 | h2,
44 | h3 {
45 | font-size: 16px;
46 | font-weight: 700;
47 | line-height: 1;
48 | margin-bottom: 0;
49 | }
50 |
51 | p {
52 | font-size: 14px;
53 | font-weight: 400;
54 | line-height: 1.3;
55 | padding: 5px;
56 | }
57 | `
58 | export const WidgetHeader = styled.header`
59 | display: flex;
60 | justify-content: flex-start;
61 | align-items: center;
62 | padding: 18px 32px;
63 | background-color: ${({ theme }) => theme.colors.primary};
64 |
65 | * {
66 | margin: 0;
67 | }
68 | `
69 | export const WidgetContent = styled.div`
70 | padding: 24px 32px 32px 32px;
71 | & > *:first-child {
72 | margin-top: 0;
73 | }
74 | & > *:last-child {
75 | margin-bottom: 0;
76 | }
77 | ul {
78 | list-style: none;
79 | padding: 0;
80 | }
81 | `
82 | export const WidgetQuestionContent = styled.div`
83 | padding: 24px 32px 32px 32px;
84 | h2 {
85 | margin: 0;
86 | font-size: 18px;
87 | }
88 | p {
89 | margin-top: 15px;
90 | margin-bottom: 0;
91 | padding: 0;
92 | line-height: 1;
93 | font-size: 12px;
94 | }
95 | `
96 | export const WidgetFooter = styled.header`
97 | display: flex;
98 | flex-direction: column;
99 | justify-content: flex-start;
100 | align-items: center;
101 | padding: 24px 32px 32px 32px;
102 | `
103 | export const WidgetImage = styled.img`
104 | width: 100%;
105 | height: 150px;
106 | object-fit: cover;
107 | `
108 | export const WidgetForm = styled.form``
109 | export const WidgetTopic = styled.a`
110 | outline: 0;
111 | text-decoration: none;
112 | color: ${({ theme }) => theme.colors.contrastText};
113 | background-color: ${({ theme }) => `${theme.colors.primary}40`};
114 | padding: 10px 15px;
115 | margin-bottom: 8px;
116 | cursor: pointer;
117 | border-radius: ${({ theme }) => theme.borderRadius};
118 | transition: 0.3s;
119 | display: block;
120 |
121 | position: relative;
122 |
123 | &:hover,
124 | &:focus {
125 | opacity: 0.5;
126 | }
127 |
128 | input {
129 | }
130 | `
131 |
--------------------------------------------------------------------------------
/src/styles/theme.ts:
--------------------------------------------------------------------------------
1 | import { DefaultTheme } from 'styled-components'
2 |
3 | export const defaultTheme: DefaultTheme = {
4 | colors: {
5 | primary: '#0d47a1',
6 | secondary: '#29b6f6',
7 | mainBg: '#171B35',
8 | contrastText: '#FFFFFF',
9 | wrong: '#FF5722',
10 | success: '#4CAF50'
11 | },
12 | borderRadius: '4px'
13 | }
14 |
--------------------------------------------------------------------------------
/styled.d.ts:
--------------------------------------------------------------------------------
1 | import 'styled-components'
2 |
3 | declare module 'styled-components' {
4 | export interface DefaultTheme {
5 | colors: {
6 | primary: string
7 | secondary: string
8 | mainBg: string
9 | contrastText: string
10 | wrong: string
11 | success: string
12 | }
13 | borderRadius: string
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------