├── .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 | ![Logo Semana Full Stack sem custo](./docs/semana-logo.png) 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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ) -------------------------------------------------------------------------------- /components/Logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 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 |
45 | 46 |
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 |
62 | 63 | 64 |
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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------