├── .env.example
├── .gitignore
├── README.md
├── components
├── Auth
│ └── index.js
├── Date
│ └── index.js
├── Input
│ └── index.js
├── Logo
│ ├── index.js
│ └── logo.svg
├── TimeBlock
│ └── index.js
└── index.js
├── config
└── firebase
│ ├── client.js
│ └── server.js
├── docs
└── semana-logo.png
├── next.config.js
├── package.json
├── pages
├── [username].js
├── _app.js
├── agenda.js
├── api
│ ├── agenda.js
│ ├── profile.js
│ └── schedule.js
├── index.js
├── login.js
└── signup.js
├── public
├── favicon.ico
└── vercel.svg
├── styles
├── Home.module.css
└── globals.css
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | ## Client Firebase Config
2 | NEXT_PUBLIC_API_KEY=
3 | NEXT_PUBLIC_AUTH_DOMAIN=
4 | NEXT_PUBLIC_PROJECT_ID=
5 | NEXT_PUBLIC_STORAGE_BUCKET=
6 | NEXT_PUBLIC_MESSAGING_SENDER_ID=
7 | NEXT_PUBLIC_APP_ID=
8 | NEXT_PUBLIC_MEASUREMENT_ID=
9 |
10 | ## Server Firebase Config
11 | PROJECT_ID=
12 | PRIVATE_KEY_ID=
13 | PRIVATE_KEY=
14 | CLIENT_EMAIL=
15 | CLIENT_ID=
16 | CLIENT_CERT=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bem vindo à Semana Full Stack Sem Custo, da Codar.me!
2 |
3 | 
4 |
5 | Entre os dias 5 e 9 de abril, vamos desenvolver uma aplicação completa: front, back e banco de dados, totalmente escalável, sem se preocupar com servidor, nem gastar 1 centavo com infra!
6 |
7 | [Cadastre-se](http://codar.me/webinario) para não perder nenhuma aula!
8 |
9 | Ficou com dúvidas ou teve algum problema? Manda lá no [canal da semana no discord](https://discord.gg/Pdr3kVHF) pra galera te ajudar!
10 | ## O Projeto
11 |
12 | Durante estas aulas, desenvolveremos uma aplicação de agendamento de horários chamada Clocker, utilizando React com NextJS e Firebase para autenticação e banco de dados.
13 |
14 | **Para rodar o projeto:**
15 |
16 | ```bash
17 | npm run dev
18 | # or
19 | yarn dev
20 | ```
21 |
22 | Abra [http://localhost:3000](http://localhost:3000) no seu navegador e veja o resultado!
23 |
24 | ## Material de apoio
25 |
26 | - [Figma (layout do projeto)](https://www.figma.com/file/OlPhiP13rGlapw5OOuQDOV/Clocker?node-id=0%3A1)
27 |
28 | ## E as redes sociais, já tá seguindo?
29 |
30 | 📸 [Instagram](http://bit.ly/2Xr94S2)
31 | 👨🏻💻 [Facebook](http://bit.ly/2KqIrar)
32 |
33 |
34 | 📬 No [canal do Telegram](https://bit.ly/2AcRz0e), temos conteúdos conteúdos exclusivos!
--------------------------------------------------------------------------------
/components/Auth/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { useState, useEffect, useContext } from 'react'
3 | import axios from 'axios'
4 |
5 | import { firebaseClient, persistenceMode } from './../../config/firebase/client'
6 |
7 | const AuthContext = React.createContext([{}, () => { }])
8 |
9 | export const logout = () => firebaseClient.auth().signOut()
10 |
11 | export const login = async ({ email, password }) => {
12 | firebaseClient.auth().setPersistence(persistenceMode)
13 |
14 | try {
15 | await firebaseClient.auth().signInWithEmailAndPassword(email, password)
16 | return firebaseClient.auth().currentUser
17 | } catch (error) {
18 | console.log('LOGIN ERROR:', error)
19 | }
20 | }
21 |
22 | export const signup = async ({ email, password, username }) => {
23 | try {
24 | await firebaseClient.auth().createUserWithEmailAndPassword(email, password)
25 | const user = await login({ email, password })
26 | const token = await user.getIdToken()
27 |
28 | const { data } = await axios({
29 | method: 'post',
30 | url: '/api/profile',
31 | data: { username },
32 | headers: {
33 | 'Authorization': `Bearer ${token}`
34 | },
35 | })
36 |
37 | console.log(data)
38 | } catch (error) {
39 | console.log('SIGNUP ERROR:', error)
40 | }
41 | }
42 |
43 | export const useAuth = () => {
44 | const [auth] = useContext(AuthContext)
45 | return [auth, { login, logout, signup }]
46 | }
47 |
48 | export const AuthProvider = ({ children }) => {
49 | const [auth, setAuth] = useState({
50 | loading: true,
51 | user: false
52 | })
53 |
54 | useEffect(() => {
55 | const unsubscribe = firebaseClient.auth().onAuthStateChanged(user => {
56 | setAuth({
57 | loading: false,
58 | user
59 | })
60 | })
61 |
62 | return () => unsubscribe()
63 | }, [])
64 |
65 |
66 | return (
67 |
68 | {children}
69 |
70 | )
71 | }
--------------------------------------------------------------------------------
/components/Date/index.js:
--------------------------------------------------------------------------------
1 | import { ptBR } from 'date-fns/locale'
2 | import { format } from 'date-fns'
3 |
4 | export const formatDate = (date, pattern) => format(date, pattern, { locale: ptBR })
--------------------------------------------------------------------------------
/components/Input/index.js:
--------------------------------------------------------------------------------
1 | import { mask, unMask } from 'remask'
2 |
3 | import {
4 | Input as InputBase,
5 | FormControl,
6 | FormLabel,
7 | FormHelperText
8 | } from '@chakra-ui/react'
9 |
10 | export const Input = ({ error, touched, label, onChange, mask: pattern, ...props }) => {
11 | const handleChange = event => {
12 | const unmaskedValue = unMask(event.target.value)
13 | const maskedValue = mask(unmaskedValue, pattern)
14 |
15 | onChange && onChange(event.target.name)(maskedValue)
16 | }
17 |
18 | return (
19 |
20 | {label}
21 |
22 | {touched && {error}}
23 |
24 | )
25 | }
--------------------------------------------------------------------------------
/components/Logo/index.js:
--------------------------------------------------------------------------------
1 | import logo from './logo.svg'
2 |
3 | export const Logo = ({ size }) => (
4 |
11 |
12 | )
--------------------------------------------------------------------------------
/components/Logo/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/components/TimeBlock/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useFormik } from 'formik'
3 | import * as yup from 'yup'
4 | import axios from 'axios'
5 | import { format } from 'date-fns'
6 |
7 | import {
8 | Button,
9 | Modal,
10 | ModalOverlay,
11 | ModalContent,
12 | ModalHeader,
13 | ModalCloseButton,
14 | ModalBody,
15 | ModalFooter
16 | } from '@chakra-ui/react'
17 |
18 | import { Input } from '../Input'
19 |
20 | const setSchedule = async ({ date, ...data }) => axios({
21 | method: 'post',
22 | url: '/api/schedule',
23 | data: {
24 | ...data,
25 | date: format(date, 'yyyy-MM-dd'),
26 | username: window.location.pathname.replace('/', ''),
27 | },
28 | })
29 |
30 | const ModalTimeBlock = ({ isOpen, onClose, onComplete, isSubmitting, children }) => (
31 |
32 |
33 |
34 | Faça sua reserva
35 |
36 |
37 | {children}
38 |
39 |
40 |
41 | {!isSubmitting && }
42 |
45 |
46 |
47 |
48 | )
49 |
50 |
51 | export const TimeBlock = ({ time, date, disabled, onSuccess }) => {
52 | const [isOpen, setIsOpen] = useState(false)
53 | const toggle = () => setIsOpen(prevState => !prevState)
54 |
55 | const { values, handleSubmit, handleChange, handleBlur, errors, touched, isSubmitting } = useFormik({
56 | onSubmit: async (values) => {
57 | try {
58 | await setSchedule({ ...values, time, date })
59 | toggle()
60 | onSuccess()
61 | } catch (error) {
62 | console.log(error)
63 | }
64 | },
65 | initialValues: {
66 | name: '',
67 | email: ''
68 | },
69 | validationSchema: yup.object().shape({
70 | name: yup.string().required('Preenchimento obrigatório'),
71 | phone: yup.string().required('Preenchimento obrigatório')
72 | })
73 | })
74 |
75 | return (
76 |
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/components/index.js:
--------------------------------------------------------------------------------
1 | export * from './Logo'
2 | export * from './Auth'
3 | export * from './Date'
4 | export * from './TimeBlock'
5 | export * from './Input'
--------------------------------------------------------------------------------
/config/firebase/client.js:
--------------------------------------------------------------------------------
1 | import firebaseClient from 'firebase/app'
2 | import 'firebase/auth'
3 |
4 | const firebaseConfig = {
5 | apiKey: process.env.NEXT_PUBLIC_API_KEY,
6 | authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
7 | projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
8 | storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
9 | messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID,
10 | appId: process.env.NEXT_PUBLIC_APP_ID,
11 | measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID,
12 | }
13 |
14 | const app = firebaseClient.apps.length
15 | ? firebaseClient.app()
16 | : firebaseClient.initializeApp(firebaseConfig)
17 |
18 | export const persistenceMode = firebaseClient.auth.Auth.Persistence.LOCAL
19 |
20 | export const getToken = () => firebaseClient.auth().currentUser?.getIdToken()
21 |
22 | export { firebaseClient }
--------------------------------------------------------------------------------
/config/firebase/server.js:
--------------------------------------------------------------------------------
1 | import firebaseServer from 'firebase-admin'
2 |
3 | const app = firebaseServer.apps.length
4 | ? firebaseServer.app()
5 | : firebaseServer.initializeApp({
6 | credential: firebaseServer.credential.cert({
7 | type: "service_account",
8 | auth_uri: "https://accounts.google.com/o/oauth2/auth",
9 | token_uri: "https://oauth2.googleapis.com/token",
10 | auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
11 | project_id: process.env.PROJECT_ID,
12 | private_key_id: process.env.PRIVATE_KEY_ID,
13 | private_key: process.env.PRIVATE_KEY.replace(/\\n/g, '\n'),
14 | client_email: process.env.CLIENT_EMAIL,
15 | client_id: process.env.CLIENT_ID,
16 | client_cert: process.env.CLIENT_CERT
17 | })
18 | })
19 |
20 | export { firebaseServer }
--------------------------------------------------------------------------------
/docs/semana-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codarme/clocker/1b5e9b0fb3bf19ab09e2c7cc22d9627600147b48/docs/semana-logo.png
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack(config) {
3 | config.module.rules.push({
4 | test: /\.svg$/,
5 | issuer: {
6 | test: /\.(js|ts)x?$/,
7 | },
8 | use: ['@svgr/webpack'],
9 | });
10 |
11 | return config;
12 | },
13 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "clocker",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@chakra-ui/icons": "^1.0.5",
12 | "@chakra-ui/react": "^1.1.6",
13 | "@emotion/react": "^11.1.4",
14 | "@emotion/styled": "^11.0.0",
15 | "@refetty/react": "1.0.0-rc.11",
16 | "@svgr/webpack": "^5.5.0",
17 | "axios": "^0.21.1",
18 | "date-fns": "^2.17.0",
19 | "firebase": "^8.2.5",
20 | "firebase-admin": "^9.4.2",
21 | "formik": "^2.2.6",
22 | "framer-motion": "^3.2.2-rc.1",
23 | "next": "10.0.6",
24 | "react": "17.0.1",
25 | "react-dom": "17.0.1",
26 | "remask": "^0.1.0-alpha.5",
27 | "yup": "^0.32.8"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pages/[username].js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useRouter } from 'next/router'
3 | import { useFetch } from '@refetty/react'
4 | import { addDays, subDays, format } from 'date-fns'
5 | import axios from 'axios'
6 |
7 | import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
8 | import { Container, Box, IconButton, SimpleGrid, Spinner } from '@chakra-ui/react'
9 |
10 | import { formatDate, Logo, TimeBlock } from '../components'
11 |
12 | const getSchedule = async ({ when, username }) => axios({
13 | method: 'get',
14 | url: '/api/schedule',
15 | params: {
16 | username,
17 | date: format(when, 'yyyy-MM-dd'),
18 | },
19 | })
20 |
21 | const Header = ({ children }) => (
22 |
23 | {children}
24 |
25 | )
26 |
27 | export default function Schedule() {
28 | const router = useRouter()
29 | const [when, setWhen] = useState(() => new Date())
30 | const [data, { loading }, fetch] = useFetch(getSchedule, { lazy: true })
31 |
32 | const addDay = () => setWhen(prevState => addDays(prevState, 1))
33 | const removeDay = () => setWhen(prevState => subDays(prevState, 1))
34 |
35 | const refresh = () =>
36 | fetch({ when, username: router.query.username })
37 |
38 | useEffect(() => {
39 | refresh()
40 | }, [when, router.query.username])
41 |
42 | return (
43 |
44 |
47 |
48 |
49 | } bg="transparent" onClick={removeDay} />
50 | {formatDate(when, 'PPPP')}
51 | } bg="transparent" onClick={addDay} />
52 |
53 |
54 |
55 | {loading && }
56 | {data?.map(({ time, isBlocked }) => )}
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { ChakraProvider } from "@chakra-ui/react"
2 | import { AuthProvider } from "../components/Auth"
3 |
4 | function MyApp({ Component, pageProps }) {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default MyApp
15 |
--------------------------------------------------------------------------------
/pages/agenda.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useRouter } from 'next/router'
3 | import { useFetch } from '@refetty/react'
4 | import { addDays, subDays, format } from 'date-fns'
5 | import axios from 'axios'
6 |
7 | import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
8 | import { Button, Container, Box, IconButton, Spinner, Text } from '@chakra-ui/react'
9 |
10 | import { getToken } from './../config/firebase/client'
11 | import { useAuth, Logo, formatDate } from './../components'
12 |
13 | const getAgenda = async (when) => {
14 | const token = await getToken()
15 |
16 | return axios({
17 | method: 'get',
18 | url: '/api/agenda',
19 | params: { date: format(when, 'yyyy-MM-dd'), },
20 | headers: {
21 | Authorization: `Bearer ${token}`
22 | }
23 | })
24 | }
25 |
26 | const Header = ({ children }) => (
27 |
28 | {children}
29 |
30 | )
31 |
32 | const AgendaBlock = ({ time, name, phone, ...props }) => (
33 |
34 | {time}
35 |
36 | {name}
37 | {phone}
38 |
39 |
40 | )
41 |
42 | export default function Agenda() {
43 | const router = useRouter()
44 | const [auth, { logout }] = useAuth()
45 | const [when, setWhen] = useState(() => new Date())
46 | const [data, { loading }, fetch] = useFetch(getAgenda, { lazy: true })
47 |
48 | const addDay = () => setWhen(prevState => addDays(prevState, 1))
49 | const removeDay = () => setWhen(prevState => subDays(prevState, 1))
50 |
51 | useEffect(() => {
52 | !auth.user && router.push('/')
53 | }, [auth.user])
54 |
55 | useEffect(() => {
56 | fetch(when)
57 | }, [when])
58 |
59 | return (
60 |
61 |
65 |
66 |
67 | } bg="transparent" onClick={removeDay} />
68 | {formatDate(when, 'PPPP')}
69 | } bg="transparent" onClick={addDay} />
70 |
71 |
72 | {loading && }
73 |
74 | {data?.map(doc => (
75 |
76 | ))}
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/pages/api/agenda.js:
--------------------------------------------------------------------------------
1 | import { firebaseServer } from './../../config/firebase/server'
2 |
3 | const db = firebaseServer.firestore()
4 | const agenda = db.collection('agenda')
5 |
6 | export default async (req, res) => {
7 | const [, token] = req.headers.authorization.split(' ')
8 |
9 | if (!token) {
10 | return res.status(401)
11 | }
12 |
13 | try {
14 | const { user_id } = await firebaseServer.auth().verifyIdToken(token)
15 |
16 | const snapshot = await agenda
17 | .where('userId', '==', user_id)
18 | .where('date', '==', req.query.date)
19 | .get()
20 |
21 | const docs = snapshot.docs.map(doc => doc.data())
22 |
23 | return res.status(200).json(docs)
24 | } catch (error) {
25 | console.log('FB ERROR:', error)
26 | return res.status(401)
27 | }
28 | }
--------------------------------------------------------------------------------
/pages/api/profile.js:
--------------------------------------------------------------------------------
1 | import { firebaseServer } from './../../config/firebase/server'
2 |
3 | const db = firebaseServer.firestore()
4 | const profile = db.collection('profiles')
5 |
6 | export default async (req, res) => {
7 | const [, token] = req.headers.authorization.split(' ')
8 | const { user_id } = await firebaseServer.auth().verifyIdToken(token)
9 |
10 | profile.doc(req.body.username).set({
11 | userId: user_id,
12 | username: req.body.username
13 | })
14 |
15 | res.status(200).json({ name: 'John Doe' })
16 | }
17 |
--------------------------------------------------------------------------------
/pages/api/schedule.js:
--------------------------------------------------------------------------------
1 | import { firebaseServer } from '../../config/firebase/server'
2 | import { differenceInHours, format, addHours } from 'date-fns'
3 |
4 | const db = firebaseServer.firestore()
5 | const profile = db.collection('profiles')
6 | const agenda = db.collection('agenda')
7 |
8 | const startAt = new Date(2021, 1, 1, 8, 0)
9 | const endAt = new Date(2021, 1, 1, 17, 0)
10 | const totalHours = differenceInHours(endAt, startAt)
11 |
12 | const timeBlocksList = []
13 |
14 | for (let blockIndex = 0; blockIndex <= totalHours; blockIndex++) {
15 | const time = format(addHours(startAt, blockIndex), 'HH:mm')
16 | timeBlocksList.push(time)
17 | }
18 |
19 | const getUserId = async (username) => {
20 | const profileDoc = await profile
21 | .where('username', '==', username)
22 | .get()
23 |
24 | if (!profileDoc.docs.length) {
25 | return false
26 | }
27 |
28 | const { userId } = profileDoc.docs[0].data()
29 |
30 | return userId
31 | }
32 |
33 |
34 | const setSchedule = async (req, res) => {
35 | const userId = await getUserId(req.body.username)
36 | const docId = `${userId}#${req.body.date}#${req.body.time}`
37 |
38 | const doc = await agenda.doc(docId).get()
39 |
40 | if (doc.exists) {
41 | console.log('doc')
42 | res.status(400).json({ message: 'Time blocked!' })
43 | return
44 | }
45 |
46 | const block = await agenda.doc(docId).set({
47 | userId,
48 | date: req.body.date,
49 | time: req.body.time,
50 | name: req.body.name,
51 | phone: req.body.phone,
52 | })
53 |
54 | return res.status(200).json(block)
55 | }
56 |
57 | const getSchedule = async (req, res) => {
58 | try {
59 | const userId = await getUserId(req.query.username)
60 |
61 | if (!userId) {
62 | return res.status(404).json({ message: 'Invalid username' })
63 | }
64 |
65 | const snapshot = await agenda
66 | .where('userId', '==', userId)
67 | .where('date', '==', req.query.date)
68 | .get()
69 |
70 | const docs = snapshot.docs.map(doc => doc.data())
71 |
72 | const result = timeBlocksList.map(time => ({
73 | time,
74 | isBlocked: !!docs.find(doc => doc.time === time)
75 | }))
76 |
77 | return res.status(200).json(result)
78 | } catch (error) {
79 | console.log('FB ERROR:', error)
80 | return res.status(401)
81 | }
82 | }
83 |
84 | const methods = {
85 | POST: setSchedule,
86 | GET: getSchedule
87 | }
88 |
89 | export default async (req, res) => methods[req.method]
90 | ? methods[req.method](req, res)
91 | : res.status(405)
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useRouter } from 'next/router'
3 | import { Container, Spinner } from '@chakra-ui/react'
4 |
5 | import { useAuth } from './../components'
6 |
7 | export default function Home() {
8 | const [auth] = useAuth()
9 | const router = useRouter()
10 |
11 | useEffect(() => {
12 | if (!auth.loading) {
13 | auth.user
14 | ? router.push('/agenda')
15 | : router.push('/login')
16 | }
17 | }, [auth.user])
18 |
19 | return (
20 |
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/pages/login.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useRouter } from 'next/router'
3 | import Link from 'next/link'
4 | import { useFormik } from 'formik'
5 | import * as yup from 'yup'
6 |
7 | import {
8 | Container,
9 | Box,
10 | Input,
11 | Button,
12 | Text,
13 | FormControl,
14 | FormLabel,
15 | FormHelperText,
16 | } from '@chakra-ui/react'
17 |
18 | import { Logo, useAuth } from './../components'
19 |
20 | const validationSchema = yup.object().shape({
21 | email: yup.string().email('E-mail inválido').required('Preenchimento obrigatório'),
22 | password: yup.string().required('Preenchimento obrigatório'),
23 | })
24 |
25 | export default function Login() {
26 | const [auth, { login }] = useAuth()
27 | const router = useRouter()
28 |
29 | const {
30 | values,
31 | errors,
32 | touched,
33 | handleChange,
34 | handleBlur,
35 | handleSubmit,
36 | isSubmitting
37 | } = useFormik({
38 | onSubmit: login,
39 | validationSchema,
40 | initialValues: {
41 | email: '',
42 | username: '',
43 | password: ''
44 | }
45 | })
46 |
47 | useEffect(() => {
48 | auth.user && router.push('/agenda')
49 | }, [auth.user])
50 |
51 | return (
52 |
53 |
54 |
55 | Crie sua agenda compartilhada
56 |
57 |
58 |
59 |
60 | Email
61 |
62 | {touched.email && {errors.email}}
63 |
64 |
65 |
66 | Senha
67 |
68 | {touched.password && {errors.password}}
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Ainda não tem uma conta? Cadastre-se
77 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/pages/signup.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import Link from 'next/link'
3 | import { useRouter } from 'next/router'
4 | import { useFormik } from 'formik'
5 | import * as yup from 'yup'
6 |
7 | import {
8 | Container,
9 | Box,
10 | Input,
11 | Button,
12 | Text,
13 | FormControl,
14 | FormLabel,
15 | FormHelperText,
16 | InputLeftAddon,
17 | InputGroup
18 | } from '@chakra-ui/react'
19 |
20 | import { Logo, useAuth } from '../components'
21 |
22 | const validationSchema = yup.object().shape({
23 | email: yup.string().email('E-mail inválido').required('Preenchimento obrigatório'),
24 | password: yup.string().required('Preenchimento obrigatório'),
25 | username: yup.string().required('Preenchimento obrigatório'),
26 | })
27 |
28 | export default function Home() {
29 | const [auth, { signup }] = useAuth()
30 | const router = useRouter()
31 |
32 | const {
33 | values,
34 | errors,
35 | touched,
36 | handleChange,
37 | handleBlur,
38 | handleSubmit,
39 | isSubmitting
40 | } = useFormik({
41 | onSubmit: signup,
42 | validationSchema,
43 | initialValues: {
44 | email: '',
45 | username: '',
46 | password: ''
47 | }
48 | })
49 |
50 | useEffect(() => {
51 | auth.user && router.push('/agenda')
52 | }, [auth.user])
53 |
54 | return (
55 |
56 |
57 |
58 | Crie sua agenda compartilhada
59 |
60 |
61 |
62 |
63 | Email
64 |
65 | {touched.email && {errors.email}}
66 |
67 |
68 |
69 | Senha
70 |
71 | {touched.password && {errors.password}}
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {touched.username && {errors.username}}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | Já tem uma conta? Acesse!
88 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codarme/clocker/1b5e9b0fb3bf19ab09e2c7cc22d9627600147b48/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .title a {
39 | color: #0070f3;
40 | text-decoration: none;
41 | }
42 |
43 | .title a:hover,
44 | .title a:focus,
45 | .title a:active {
46 | text-decoration: underline;
47 | }
48 |
49 | .title {
50 | margin: 0;
51 | line-height: 1.15;
52 | font-size: 4rem;
53 | }
54 |
55 | .title,
56 | .description {
57 | text-align: center;
58 | }
59 |
60 | .description {
61 | line-height: 1.5;
62 | font-size: 1.5rem;
63 | }
64 |
65 | .code {
66 | background: #fafafa;
67 | border-radius: 5px;
68 | padding: 0.75rem;
69 | font-size: 1.1rem;
70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
71 | Bitstream Vera Sans Mono, Courier New, monospace;
72 | }
73 |
74 | .grid {
75 | display: flex;
76 | align-items: center;
77 | justify-content: center;
78 | flex-wrap: wrap;
79 | max-width: 800px;
80 | margin-top: 3rem;
81 | }
82 |
83 | .card {
84 | margin: 1rem;
85 | flex-basis: 45%;
86 | padding: 1.5rem;
87 | text-align: left;
88 | color: inherit;
89 | text-decoration: none;
90 | border: 1px solid #eaeaea;
91 | border-radius: 10px;
92 | transition: color 0.15s ease, border-color 0.15s ease;
93 | }
94 |
95 | .card:hover,
96 | .card:focus,
97 | .card:active {
98 | color: #0070f3;
99 | border-color: #0070f3;
100 | }
101 |
102 | .card h3 {
103 | margin: 0 0 1rem 0;
104 | font-size: 1.5rem;
105 | }
106 |
107 | .card p {
108 | margin: 0;
109 | font-size: 1.25rem;
110 | line-height: 1.5;
111 | }
112 |
113 | .logo {
114 | height: 1em;
115 | }
116 |
117 | @media (max-width: 600px) {
118 | .grid {
119 | width: 100%;
120 | flex-direction: column;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------