├── .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 | carregando 108 |

Prepare-se para a próxima pergunta!

109 |
110 | 111 | ) 112 | 113 | if (screenState === screenStates.QUIZ) 114 | return ( 115 | 116 | 117 | 118 | 119 | Logo Alura 124 | {question && ( 125 | 132 | )} 133 | 134 | 135 | ) 136 | 137 | if (screenState === screenStates.RESULT) 138 | return ( 139 | <> 140 | 141 | 142 | 143 | Logo Alura 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 | Logo Alura 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 | Logo Alura 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 | Logo Alura 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 | Logo Alura 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 | Logo Alura 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 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 |