├── backend ├── .gitignore ├── src │ ├── database │ │ ├── connection.ts │ │ └── migrations │ │ │ ├── 1602977561226-create_users.ts │ │ │ ├── 1602642226180-create_images.ts │ │ │ └── 1602610238153-create_orphanages.ts │ ├── views │ │ ├── images_view.ts │ │ ├── users_view.ts │ │ └── orphanages_view.ts │ ├── models │ │ ├── User.ts │ │ ├── Image.ts │ │ └── Orphanage.ts │ ├── config │ │ └── upload.ts │ ├── errors │ │ └── handler.ts │ ├── index.ts │ ├── routes.ts │ └── controllers │ │ ├── UsersController.ts │ │ └── OrphanagesController.ts ├── uploads │ ├── 1603577047776-image1.jpg │ ├── 1603577047821-image2.jpg │ ├── 1603577155505-image2.jpg │ ├── 1603577205146-image1.jpg │ └── 1603583625766-image2.jpg ├── .env.example ├── ormconfig.js ├── package.json └── tsconfig.json ├── web ├── public │ ├── _redirects │ ├── index.html │ └── imges │ │ └── header.svg ├── src │ ├── react-app-env.d.ts │ ├── images │ │ ├── landingPage.PNG │ │ └── icons │ │ │ ├── not-found.svg │ │ │ ├── map-marker.svg │ │ │ ├── logo.svg │ │ │ └── deleted.svg │ ├── components │ │ ├── index.ts │ │ ├── MiniMap │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Sidebar │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── Form │ │ │ ├── styles.ts │ │ │ └── index.tsx │ ├── pages │ │ ├── Private │ │ │ ├── ApproveOrphanage │ │ │ │ ├── styles.ts │ │ │ │ └── index.tsx │ │ │ ├── EditOrphanage │ │ │ │ ├── styles.ts │ │ │ │ └── index.tsx │ │ │ └── Dashboard │ │ │ │ ├── styles.ts │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── Home │ │ │ ├── index.tsx │ │ │ └── styles.tsx │ │ ├── CreateOrphanage │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── OrphanagesMap │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Signin │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ └── Orphanage │ │ │ ├── styles.ts │ │ │ └── index.tsx │ ├── services │ │ ├── api.ts │ │ └── user.ts │ ├── App.tsx │ ├── index.tsx │ ├── contexts │ │ └── userContext.ts │ ├── utils │ │ └── MapIcon.ts │ ├── GlobalStyles.ts │ ├── helpers │ │ └── protectedRoutes.tsx │ ├── hooks │ │ └── useAuth.ts │ └── routes.tsx ├── .gitignore ├── tsconfig.json └── package.json ├── mobile ├── src │ ├── @types │ │ └── index.d.ts │ ├── images │ │ ├── map-marker.png │ │ ├── map-marker@2x.png │ │ └── map-marker@3x.png │ ├── services │ │ └── api.ts │ ├── components │ │ └── Header.tsx │ ├── routes.tsx │ └── pages │ │ ├── CreateOrphanage │ │ ├── SelectMapPosition.tsx │ │ └── OrphanageData.tsx │ │ ├── OrphanagesMap.tsx │ │ └── OrphanageDetails.tsx ├── assets │ ├── icon.png │ ├── splash.png │ └── favicon.png ├── babel.config.js ├── .expo-shared │ └── assets.json ├── .gitignore ├── tsconfig.json ├── App.tsx ├── app.json └── package.json └── README.md /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /web/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /mobile/src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /mobile/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/mobile/assets/icon.png -------------------------------------------------------------------------------- /mobile/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/mobile/assets/splash.png -------------------------------------------------------------------------------- /backend/src/database/connection.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from 'typeorm'; 2 | 3 | createConnection(); -------------------------------------------------------------------------------- /mobile/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/mobile/assets/favicon.png -------------------------------------------------------------------------------- /mobile/src/images/map-marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/mobile/src/images/map-marker.png -------------------------------------------------------------------------------- /web/src/images/landingPage.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/web/src/images/landingPage.PNG -------------------------------------------------------------------------------- /mobile/src/images/map-marker@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/mobile/src/images/map-marker@2x.png -------------------------------------------------------------------------------- /mobile/src/images/map-marker@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/mobile/src/images/map-marker@3x.png -------------------------------------------------------------------------------- /backend/uploads/1603577047776-image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/backend/uploads/1603577047776-image1.jpg -------------------------------------------------------------------------------- /backend/uploads/1603577047821-image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/backend/uploads/1603577047821-image2.jpg -------------------------------------------------------------------------------- /backend/uploads/1603577155505-image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/backend/uploads/1603577155505-image2.jpg -------------------------------------------------------------------------------- /backend/uploads/1603577205146-image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/backend/uploads/1603577205146-image1.jpg -------------------------------------------------------------------------------- /backend/uploads/1603583625766-image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasdevzero/Happy/HEAD/backend/uploads/1603583625766-image2.jpg -------------------------------------------------------------------------------- /mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /mobile/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'http://10.0.0.147:3001', 5 | }); 6 | 7 | export default api; -------------------------------------------------------------------------------- /web/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Sidebar } from './Sidebar'; 2 | export { default as MiniMap } from './MiniMap'; 3 | export { default as Form } from './Form'; -------------------------------------------------------------------------------- /web/src/pages/Private/ApproveOrphanage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div``; 4 | 5 | export const Content = styled.div``; -------------------------------------------------------------------------------- /web/src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const api = axios.create({ 4 | baseURL: 'https://happy-backend-devzero.herokuapp.com', 5 | }); 6 | 7 | export default api; 8 | -------------------------------------------------------------------------------- /mobile/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Routes from './routes'; 3 | 4 | function App() { 5 | return ( 6 | 7 | ); 8 | }; 9 | 10 | export default App; 11 | -------------------------------------------------------------------------------- /web/src/pages/Private/EditOrphanage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div` 4 | 5 | `; 6 | 7 | export const Content = styled.div` 8 | flex: 1; 9 | `; -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | API_URL = http://localhost:3001 2 | TYPEORM_HOST = 3 | TYPEORM_PORT = 4 | TYPEORM_USERNAME = 5 | TYPEORM_PASSWORD = 6 | TYPEORM_DATABASE = 7 | TYPEORM_ENTITIES = ./src/models/*.ts 8 | TYPEORM_MIGRATIONS = ./src/database/migrations/*.ts 9 | TYPEORM_MIGRATIONS_DIR = ./src/database/migrations -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | import { GlobalStyles } from './GlobalStyles'; 6 | 7 | ReactDOM.render( 8 | <> 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/views/images_view.ts: -------------------------------------------------------------------------------- 1 | import Image from "../models/Image"; 2 | 3 | export default { 4 | render(image: Image) { 5 | return { 6 | id: image.id, 7 | url: `${process.env.API_URL}/uploads/${image.path}` 8 | }; 9 | }, 10 | 11 | renderMany(images: Image[]) { 12 | return images.map(image => this.render(image)); 13 | } 14 | }; -------------------------------------------------------------------------------- /backend/src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity('users') 4 | export default class User { 5 | @PrimaryGeneratedColumn('increment') 6 | id: number; 7 | 8 | @Column() 9 | name: string; 10 | 11 | @Column() 12 | email: string; 13 | 14 | @Column() 15 | password: string; 16 | }; -------------------------------------------------------------------------------- /backend/src/views/users_view.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/User'; 2 | 3 | export default { 4 | render(users: User) { 5 | return { 6 | id: users.id, 7 | name: users.name, 8 | email: users.email, 9 | }; 10 | }, 11 | 12 | renderMany(users: User[]) { 13 | return users.map(user => this.render(user)) 14 | } 15 | }; -------------------------------------------------------------------------------- /backend/src/config/upload.ts: -------------------------------------------------------------------------------- 1 | import multer from 'multer'; 2 | import path from 'path'; 3 | 4 | export default { 5 | storage: multer.diskStorage({ 6 | destination: path.join(__dirname, '..', '..', 'uploads'), 7 | filename: (req, file, callback) => { 8 | const fileName = `${Date.now()}-${file.originalname}`; 9 | 10 | callback(null, fileName) 11 | } 12 | }) 13 | }; -------------------------------------------------------------------------------- /web/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /web/src/contexts/userContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | interface User { 4 | name: string, 5 | id: number; 6 | email: string; 7 | token: string; 8 | }; 9 | 10 | interface UserProvider { 11 | user: User | undefined; 12 | setUser: React.Dispatch> | undefined; 13 | }; 14 | 15 | export const UserContext = createContext({ user: undefined, setUser: undefined }); -------------------------------------------------------------------------------- /backend/ormconfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'postgres', 3 | host: process.env.TYPEORM_HOST, 4 | port: process.env.TYPEORM_PORT, 5 | username: process.env.TYPEORM_USERNAME, 6 | password: process.env.TYPEORM_PASSWORD, 7 | database: process.env.TYPEORM_DATABASE, 8 | entities: [process.env.TYPEORM_ENTITIES], 9 | migrations: [process.env.TYPEORM_MIGRATIONS], 10 | cli: { 11 | migrationsDir: process.env.TYPEORM_MIGRATIONS_DIR, 12 | }, 13 | } -------------------------------------------------------------------------------- /backend/src/models/Image.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm'; 2 | import Orphanage from './Orphanage'; 3 | 4 | @Entity('images') 5 | export default class Image { 6 | @PrimaryGeneratedColumn('increment') 7 | id: number; 8 | 9 | @Column() 10 | path: string; 11 | 12 | @ManyToOne(_ => Orphanage, orphanage => orphanage.images) 13 | @JoinColumn({ name: 'orphanage_id' }) 14 | orphanage: Orphanage; 15 | } -------------------------------------------------------------------------------- /web/src/utils/MapIcon.ts: -------------------------------------------------------------------------------- 1 | import Leaflet from 'leaflet'; 2 | 3 | import mapMarker from '../images/icons/map-marker.svg'; 4 | 5 | export const MapIcon = Leaflet.icon({ 6 | iconUrl: mapMarker, 7 | 8 | iconSize: [58, 68], 9 | iconAnchor: [29, 68], 10 | popupAnchor: [0, -60] 11 | }); 12 | 13 | export const MapIconMarker = Leaflet.icon({ 14 | iconUrl: mapMarker, 15 | iconSize: [58, 68], 16 | iconAnchor: [29, 68], 17 | popupAnchor: [170, 2] 18 | }); 19 | -------------------------------------------------------------------------------- /web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Home } from './Home'; 2 | export { default as OrphanagesMap } from './OrphanagesMap'; 3 | export { default as CreateOrphanages} from './CreateOrphanage'; 4 | export { default as Orphanage } from './Orphanage'; 5 | export { default as Signin } from './Signin'; 6 | export { default as Dashboard } from './Private/Dashboard'; 7 | export { default as EditOrphanage } from './Private/EditOrphanage'; 8 | export { default as ApproveOrphanage } from './Private/ApproveOrphanage'; -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Happy 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /mobile/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useFonts } from 'expo-font'; 4 | import { Nunito_600SemiBold, Nunito_700Bold, Nunito_800ExtraBold } from '@expo-google-fonts/nunito'; 5 | 6 | import Routes from './src/routes'; 7 | 8 | export default function App() { 9 | const [fontsLoaded] = useFonts({ 10 | Nunito_600SemiBold, 11 | Nunito_700Bold, 12 | Nunito_800ExtraBold 13 | }); 14 | 15 | if (!fontsLoaded) { 16 | return null; 17 | }; 18 | 19 | return ( 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /web/src/GlobalStyles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import 'leaflet/dist/leaflet.css'; 3 | 4 | export const GlobalStyles = createGlobalStyle` 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | }; 10 | 11 | html { 12 | font-size: 62.5%; 13 | }; 14 | 15 | body { 16 | color: #fff; 17 | background-color: #ebf2f5; 18 | }; 19 | 20 | body, input, button, textarea { 21 | font: 600 1.8rem Nunito, sans-serif; 22 | }; 23 | `; -------------------------------------------------------------------------------- /web/src/services/user.ts: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | 3 | interface LoginParams { 4 | email: string, 5 | password: string, 6 | }; 7 | 8 | export async function login(params: LoginParams, remember: boolean) { 9 | const { email, password } = params; 10 | 11 | const response = await api.post('/users/login', { email, password }); 12 | const { token, user, error } = response.data; 13 | 14 | if (error) 15 | return { error }; 16 | if (remember) { 17 | localStorage.setItem('token', JSON.stringify(token)); 18 | }; 19 | 20 | return { user, token }; 21 | }; -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /mobile/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "mobile", 4 | "slug": "mobile", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "web": { 23 | "favicon": "./assets/favicon.png" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/errors/handler.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequestHandler } from 'express'; 2 | import { ValidationError } from 'yup'; 3 | 4 | interface ValidateErrors { 5 | [key: string]: string[]; 6 | }; 7 | 8 | const errorHandler: ErrorRequestHandler = (error, req, res, next) => { 9 | if (error instanceof ValidationError) { 10 | let errors: ValidateErrors = {} 11 | 12 | error.inner.forEach(err => { 13 | errors[err.path] = err.errors; 14 | }); 15 | 16 | return res.status(400).json({ message: 'Validate fails', errors }); 17 | }; 18 | 19 | console.log(error); 20 | 21 | return res.status(500).json({ message: 'Internal server error' }); 22 | }; 23 | 24 | export default errorHandler; -------------------------------------------------------------------------------- /web/src/helpers/protectedRoutes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Redirect, Route } from "react-router-dom"; 3 | import { UserContext } from "../contexts/UserContext"; 4 | 5 | export const PrivateRoute: React.FC<{ component: React.FC; path: string; exact: boolean; }> = ({ component, path, exact }) => { 6 | const { user } = useContext(UserContext); 7 | 8 | return user ? () : (); 9 | }; 10 | 11 | export const IsUserRedirect: React.FC<{ component: React.FC; redirectTo: string; path: string }> = ({ component, redirectTo, path }) => { 12 | const { user } = useContext(UserContext); 13 | 14 | return user ? () : () 15 | } -------------------------------------------------------------------------------- /web/src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import api from '../services/api'; 3 | 4 | interface User { 5 | name: string, 6 | id: number; 7 | email: string; 8 | token: string; 9 | }; 10 | 11 | export function useAuth() { 12 | const [user, setUser] = useState() 13 | 14 | useEffect(() => { 15 | const token = JSON.parse(localStorage.getItem('token') as string); 16 | if (token) { 17 | api.post('/users/auth', { token }) 18 | .then(response => { 19 | const { user: { id, name, email } } = response.data; 20 | 21 | setUser({ id, name, email, token }); 22 | }); 23 | }; 24 | }, []); 25 | 26 | return { user, setUser }; 27 | }; -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import path from 'path'; 4 | import cors from 'cors'; 5 | 6 | import 'express-async-errors'; 7 | import './database/connection'; 8 | 9 | import routes from './routes'; 10 | import errorHandler from './errors/handler'; 11 | 12 | const app = express(); 13 | 14 | const PORT = process.env.PORT || 3001; 15 | 16 | app.use((req, res, next) => { 17 | res.header("Access-Control-Allow-Origin", "https://happy-devzero.netlify.app"); 18 | res.header("Access-Control-Allow-Methods", 'GET,PUT,POST,DELETE'); 19 | res.header("Access-Control-Allow-Headers", "*") 20 | app.use(cors()); 21 | next(); 22 | }); 23 | app.use(express.json()); 24 | app.use(routes); 25 | app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads'))) 26 | app.use(errorHandler); 27 | 28 | app.listen(PORT); -------------------------------------------------------------------------------- /backend/src/views/orphanages_view.ts: -------------------------------------------------------------------------------- 1 | import Orphanage from '../models/Orphanage'; 2 | import imagesView from './images_view'; 3 | 4 | export default { 5 | render(orphanage: Orphanage) { 6 | return { 7 | id: orphanage.id, 8 | name: orphanage.name, 9 | latitude: Number(orphanage.latitude), 10 | longitude: Number(orphanage.longitude), 11 | about: orphanage.about, 12 | instructions: orphanage.instructions, 13 | openning_hours: orphanage.openning_hours, 14 | open_on_weekends: orphanage.open_on_weekends, 15 | contact: orphanage.contact, 16 | images: imagesView.renderMany(orphanage.images) 17 | }; 18 | }, 19 | 20 | renderMany(orphanages: Orphanage[]) { 21 | return orphanages.map(orphanage => this.render(orphanage)) 22 | } 23 | }; -------------------------------------------------------------------------------- /web/src/images/icons/not-found.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/src/models/Orphanage.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, OneToMany, JoinColumn } from 'typeorm'; 2 | import Image from './Image'; 3 | 4 | @Entity('orphanages') 5 | export default class Orphanage { 6 | @PrimaryGeneratedColumn('increment') 7 | id: number; 8 | 9 | @Column() 10 | name: string; 11 | 12 | @Column() 13 | latitude: number; 14 | 15 | @Column() 16 | longitude: number; 17 | 18 | @Column() 19 | about: string; 20 | 21 | @Column() 22 | instructions: string; 23 | 24 | @Column() 25 | openning_hours: string; 26 | 27 | @Column() 28 | open_on_weekends: boolean; 29 | 30 | @Column() 31 | contact: string; 32 | 33 | @Column() 34 | approved: boolean; 35 | 36 | @OneToMany(_ => Image, image => image.orphanage, { 37 | cascade: ['insert', 'update', 'remove'] 38 | }) 39 | @JoinColumn({ name: 'orphanage_id' }) 40 | images: Image[]; 41 | } -------------------------------------------------------------------------------- /web/public/imges/header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /web/src/images/icons/map-marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node dist/index.js", 8 | "dev": "tsnd --transpile-only --ignore-watch node_modules src/server.ts", 9 | "build": "tsc", 10 | "typeorm": "tsnd ./node_modules/typeorm/cli.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bcryptjs": "^2.4.3", 17 | "cors": "^2.8.5", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "express-async-errors": "^3.1.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "multer": "^1.4.2", 23 | "pg": "^8.4.1", 24 | "typeorm": "^0.2.28", 25 | "yup": "^0.29.3" 26 | }, 27 | "devDependencies": { 28 | "@types/bcryptjs": "^2.4.2", 29 | "@types/cors": "^2.8.8", 30 | "@types/express": "^4.17.8", 31 | "@types/jsonwebtoken": "^8.5.0", 32 | "@types/multer": "^1.4.4", 33 | "@types/yup": "^0.29.8", 34 | "ts-node-dev": "^1.0.0-pre.63", 35 | "typescript": "^4.0.3" 36 | } 37 | } -------------------------------------------------------------------------------- /backend/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import multer from 'multer'; 3 | 4 | import OrphanagesController from './controllers/OrphanagesController'; 5 | import UsersController from './controllers/UsersController'; 6 | import uploadConfig from './config/upload'; 7 | 8 | const routes = Router() 9 | const upload = multer(uploadConfig) 10 | 11 | // MVC - Model Views Controllers 12 | 13 | routes.get('/orphanages', OrphanagesController.index); 14 | routes.get('/orphanages/:id', OrphanagesController.show); 15 | routes.post('/orphanages', upload.array('images'), OrphanagesController.create); 16 | routes.put('/orphanages/:id', upload.array('images'), OrphanagesController.update); 17 | routes.delete('/orphanages/:id', OrphanagesController.delete); 18 | 19 | routes.get('/users', UsersController.index); 20 | routes.post('/users/create', UsersController.create); 21 | routes.post('/users/login', UsersController.login); 22 | routes.post('/users/auth', UsersController.auth, UsersController.show); 23 | routes.delete('/users/:id', UsersController.delete); 24 | 25 | export default routes; -------------------------------------------------------------------------------- /backend/src/database/migrations/1602977561226-create_users.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from "typeorm"; 2 | 3 | export class createUsers1602977561226 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.createTable(new Table({ 7 | name: 'users', 8 | columns: [ 9 | { 10 | name: 'id', 11 | type: 'integer', 12 | unsigned: true, 13 | isPrimary: true, 14 | isGenerated: true, 15 | generationStrategy: 'increment', 16 | }, 17 | { 18 | name: 'name', 19 | type: 'varchar', 20 | }, 21 | { 22 | name: 'email', 23 | type: 'varchar', 24 | isUnique: true, 25 | }, 26 | { 27 | name: 'password', 28 | type: 'varchar', 29 | } 30 | ], 31 | })) 32 | } 33 | 34 | public async down(queryRunner: QueryRunner): Promise { 35 | await queryRunner.dropTable('users'); 36 | }; 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "@expo-google-fonts/nunito": "^0.1.0", 12 | "@react-native-community/masked-view": "0.1.10", 13 | "@react-navigation/native": "^5.7.6", 14 | "@react-navigation/stack": "^5.9.3", 15 | "axios": "^0.20.0", 16 | "expo": "~39.0.2", 17 | "expo-font": "~8.3.0", 18 | "expo-status-bar": "~1.0.2", 19 | "react": "16.13.1", 20 | "react-dom": "16.13.1", 21 | "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz", 22 | "react-native-gesture-handler": "~1.7.0", 23 | "react-native-maps": "0.27.1", 24 | "react-native-reanimated": "~1.13.0", 25 | "react-native-safe-area-context": "3.1.4", 26 | "react-native-screens": "~2.10.1", 27 | "react-native-web": "~0.13.12", 28 | "expo-image-picker": "~9.1.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "~7.9.0", 32 | "@types/react": "~16.9.35", 33 | "@types/react-dom": "~16.9.8", 34 | "@types/react-native": "~0.63.2", 35 | "typescript": "~3.9.5" 36 | }, 37 | "private": true 38 | } 39 | -------------------------------------------------------------------------------- /web/src/components/MiniMap/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | max-width: 54.4rem; 7 | flex: 1; 8 | background-color: #fff; 9 | border: 1px solid #DDE3F0; 10 | border-radius: 2rem; 11 | margin: 1rem; 12 | `; 13 | 14 | export const MapContainer = styled.div` 15 | width: 100%; 16 | height: 22.7rem; 17 | border-radius: 2rem; 18 | `; 19 | 20 | export const Info = styled.div` 21 | display: flex; 22 | justify-content: space-between; 23 | align-items: center; 24 | flex: 1; 25 | padding: 1rem; 26 | `; 27 | 28 | export const OrphanageName = styled.h2` 29 | color: #4D6F80; 30 | margin-left: 2rem; 31 | font-weight: 700; 32 | line-height: 3.4rem; 33 | font-size: 2.4rem; 34 | `; 35 | 36 | export const ButtonWrapper = styled.div` 37 | display: flex; 38 | align-items: center; 39 | justify-content: center; 40 | margin-right: 2rem; 41 | `; 42 | 43 | export const Button = styled.button` 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | width: 4.8rem; 48 | height: 4.8rem; 49 | border: none; 50 | margin: 0 .5rem; 51 | border-radius: 1.6rem; 52 | cursor: pointer; 53 | `; 54 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "@types/jest": "^24.9.1", 10 | "@types/node": "^12.12.67", 11 | "@types/react": "^16.9.52", 12 | "@types/react-dom": "^16.9.8", 13 | "axios": "^0.20.0", 14 | "leaflet": "^1.7.1", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "react-icons": "^3.11.0", 18 | "react-leaflet": "^2.7.0", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "3.4.3", 21 | "styled-components": "^5.2.0", 22 | "typescript": "^3.7.5" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@types/react-leaflet": "^2.5.2", 47 | "@types/react-router-dom": "^5.1.6", 48 | "@types/styled-components": "^5.1.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web/src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Container, 5 | Inner, 6 | Header, 7 | HeaderContainer, 8 | Logo, 9 | Content, 10 | Title, 11 | SubTitle, 12 | Location, 13 | City, 14 | State, 15 | Link, 16 | AccessRestrict 17 | } from './styles'; 18 | import { FiArrowRight } from 'react-icons/fi'; 19 | import logo from '../../images/icons/logo.svg'; 20 | 21 | function Home() { 22 | return ( 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | Nova Cruz 31 | Rio Grande do Norte 32 | 33 | 34 | 35 | Acesso restrito 36 |
37 | 38 | 39 | 40 | Leve Felicidade para o mundo 41 | Visite orfanatos e mude o dia de muitas crianças. 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Home; 53 | -------------------------------------------------------------------------------- /web/src/pages/CreateOrphanage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | `; 6 | 7 | export const Inner = styled.main` 8 | flex: 1; 9 | `; 10 | 11 | export const ConfirmContainer = styled.div` 12 | display: flex; 13 | background-color: #37C77F; 14 | flex: 1; 15 | height: 100vh; 16 | align-items: center; 17 | justify-content: center; 18 | `; 19 | 20 | export const ConfirmInfo = styled.div` 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | margin-right: 10rem; 25 | max-width: 40rem; 26 | `; 27 | 28 | export const ConfirmTitle = styled.h1` 29 | font-weight: 800; 30 | font-size: 8rem; 31 | line-height: 8rem; 32 | text-align: center; 33 | margin-bottom: 4rem; 34 | `; 35 | 36 | export const ConfirmSubtitle = styled.h2` 37 | font-size: 2.4rem; 38 | line-height: 3.4rem; 39 | text-align: center; 40 | margin-bottom: 5rem; 41 | `; 42 | 43 | export const ConfirmImage = styled.img` 44 | object-fit: contain; 45 | height: auto; 46 | `; 47 | 48 | export const ConfirmButton = styled.button` 49 | width: 24.3rem; 50 | height: 6.4rem; 51 | border-radius: 2rem; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | border: none; 56 | cursor: pointer; 57 | background-color: #31B272; 58 | color: #fff; 59 | 60 | &:hover { 61 | background-color: #3BD689; 62 | }; 63 | `; -------------------------------------------------------------------------------- /backend/src/database/migrations/1602642226180-create_images.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, Table} from "typeorm"; 2 | 3 | export class createImages1602642226180 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.createTable(new Table({ 7 | name: 'images', 8 | columns: [ 9 | { 10 | name: 'id', 11 | type: 'integer', 12 | unsigned: true, 13 | isPrimary: true, 14 | isGenerated: true, 15 | generationStrategy: 'increment', 16 | }, 17 | { 18 | name: 'path', 19 | type: 'varchar', 20 | }, 21 | { 22 | name: 'orphanage_id', 23 | type: 'integer' 24 | } 25 | ], 26 | foreignKeys: [ 27 | { 28 | name: 'ImageOrphanages', 29 | columnNames: ['orphanage_id'], 30 | referencedTableName: 'orphanages', 31 | referencedColumnNames: ['id'], 32 | onUpdate: 'CASCADE', 33 | onDelete: 'CASCADE' 34 | }, 35 | ], 36 | })) 37 | } 38 | 39 | public async down(queryRunner: QueryRunner): Promise { 40 | await queryRunner.dropTable('images'); 41 | }; 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /web/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import { PrivateRoute, IsUserRedirect } from './helpers/protectedRoutes'; 4 | 5 | import { UserContext } from './contexts/UserContext'; 6 | import { useAuth } from './hooks/useAuth'; 7 | 8 | import { 9 | Home, 10 | OrphanagesMap, 11 | CreateOrphanages, 12 | Orphanage, 13 | Signin, 14 | Dashboard, 15 | EditOrphanage, 16 | ApproveOrphanage 17 | } from './pages'; 18 | 19 | function Routes() { 20 | const { user, setUser } = useAuth(); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default Routes; 43 | -------------------------------------------------------------------------------- /mobile/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, StyleSheet, Text } from 'react-native'; 3 | import { BorderlessButton } from 'react-native-gesture-handler'; 4 | import { Feather } from '@expo/vector-icons'; 5 | import { useNavigation } from '@react-navigation/native'; 6 | 7 | interface HeaderProps { 8 | title: String; 9 | showCancel?: Boolean; 10 | } 11 | 12 | function Header({ showCancel = true, title }: HeaderProps) { 13 | const navigation = useNavigation(); 14 | 15 | function backToHome() { 16 | navigation.navigate('OrphanagesMap'); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | {title} 26 | 27 | {showCancel ? 28 | 29 | 30 | 31 | : 32 | 33 | } 34 | 35 | 36 | ); 37 | }; 38 | 39 | const styles = StyleSheet.create({ 40 | container: { 41 | padding: 24, 42 | backgroundColor: '#f9fafc', 43 | borderBottomWidth: 1, 44 | borderColor: '#dde3f0', 45 | paddingTop: 44, 46 | 47 | flexDirection: 'row', 48 | justifyContent: 'space-between', 49 | alignItems: 'center', 50 | }, 51 | 52 | title: { 53 | fontFamily: 'Nunito_600SemiBold', 54 | color: '#8fa7b3', 55 | fontSize: 16, 56 | }, 57 | }) 58 | 59 | export default Header; 60 | -------------------------------------------------------------------------------- /web/src/pages/OrphanagesMap/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { createGlobalStyle } from 'styled-components/macro'; 2 | import { Link as ReactRouterLink } from 'react-router-dom'; 3 | 4 | export const Container = styled.div` 5 | width: 100vw; 6 | height: 100vh; 7 | display: flex; 8 | position: relative; 9 | 10 | .leaflet-container { 11 | z-index: 1; 12 | }; 13 | `; 14 | 15 | export const Link = styled(ReactRouterLink)` 16 | position: absolute; 17 | right: 4rem; 18 | bottom: 4rem; 19 | 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | width: 6.4rem; 24 | height: 6.4rem; 25 | border-radius: 2rem; 26 | background-color: #15c3d6; 27 | transition: background-color .2s; 28 | z-index: 2; 29 | 30 | &:hover { 31 | background-color: #17d6eb; 32 | } 33 | `; 34 | 35 | export const MapStyle = createGlobalStyle` 36 | #map-container .map-popup .leaflet-popup-content-wrapper { 37 | background: rgba(255, 255, 255, .8); 38 | border-radius: 2rem; 39 | box-shadow: none; 40 | padding: .5rem; 41 | }; 42 | #map-container .map-popup .leaflet-popup-content { 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | 47 | margin: .8rem 1.2rem; 48 | font-size: 2rem; 49 | font-weight: bold; 50 | color: #0089a5; 51 | 52 | position: relative; 53 | }; 54 | #map-container .map-popup .leaflet-popup-content a { 55 | display: flex; 56 | justify-content: center; 57 | align-items: center; 58 | 59 | width: 3.5rem; 60 | height: 3.5rem; 61 | background: #15c3d6; 62 | box-shadow: 17.2868px 27.6589px 41.4884px rgba(23, 142, 166, .16); 63 | border-radius: 1.2rem; 64 | 65 | position: absolute; 66 | right: 0; 67 | top: -15%; 68 | }; 69 | #map-container .map-popup .leaflet-popup-tip-container { 70 | display: none; 71 | }; 72 | `; -------------------------------------------------------------------------------- /mobile/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { NavigationContainer } from '@react-navigation/native'; 4 | import { createStackNavigator } from '@react-navigation/stack'; 5 | 6 | const { Navigator, Screen } = createStackNavigator(); 7 | 8 | import OrphanagesMap from './pages/OrphanagesMap'; 9 | import OrphanageDetails from './pages/OrphanageDetails'; 10 | import SelectMapPosition from './pages/CreateOrphanage/SelectMapPosition'; 11 | import OrphanageData from './pages/CreateOrphanage/OrphanageData'; 12 | 13 | import Header from './components/Header'; 14 | 15 | function Routes() { 16 | return ( 17 | 18 | 19 | 23 |
29 | }} 30 | /> 31 |
37 | }} 38 | /> 39 |
45 | }} 46 | /> 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default Routes; -------------------------------------------------------------------------------- /backend/src/database/migrations/1602610238153-create_orphanages.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner, Table } from "typeorm"; 2 | 3 | export class createOrphanages1602610238153 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.createTable(new Table({ 7 | name: 'orphanages', 8 | columns: [ 9 | { 10 | name: 'id', 11 | type: 'integer', 12 | unsigned: true, 13 | isPrimary: true, 14 | isGenerated: true, 15 | generationStrategy: 'increment', 16 | }, 17 | { 18 | name: 'name', 19 | type: 'varchar', 20 | }, 21 | { 22 | name: 'latitude', 23 | type: 'varchar', 24 | }, 25 | { 26 | name: 'longitude', 27 | type: 'varchar', 28 | }, 29 | { 30 | name: 'about', 31 | type: 'text', 32 | }, 33 | { 34 | name: 'instructions', 35 | type: 'text', 36 | }, 37 | { 38 | name: 'openning_hours', 39 | type: 'varchar', 40 | }, 41 | { 42 | name: 'open_on_weekends', 43 | type: 'boolean', 44 | default: false, 45 | }, 46 | { 47 | name: 'contact', 48 | type: 'varchar', 49 | }, 50 | { 51 | name: 'approved', 52 | type: 'boolean', 53 | default: false, 54 | }, 55 | ], 56 | })); 57 | }; 58 | 59 | public async down(queryRunner: QueryRunner): Promise { 60 | await queryRunner.dropTable('orphanages') 61 | }; 62 | 63 | }; 64 | -------------------------------------------------------------------------------- /mobile/src/pages/CreateOrphanage/SelectMapPosition.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, StyleSheet, Dimensions, Text } from 'react-native'; 3 | 4 | import { useNavigation } from '@react-navigation/native'; 5 | import { RectButton } from 'react-native-gesture-handler'; 6 | import MapView, { Marker, MapEvent } from 'react-native-maps'; 7 | 8 | import mapMarkerImg from '../../images/map-marker.png'; 9 | 10 | export default function SelectMapPosition() { 11 | const [position, setPosition] = useState({ latitude: 0, longitude: 0 }); 12 | const navigation = useNavigation(); 13 | 14 | function handleNextStep() { 15 | navigation.navigate('OrphanageData', { position }); 16 | }; 17 | 18 | function selectMapPosition(e: MapEvent) { 19 | setPosition(e.nativeEvent.coordinate); 20 | }; 21 | 22 | return ( 23 | 24 | 34 | {position.latitude !== 0 && 35 | 39 | } 40 | 41 | 42 | 43 | Próximo 44 | 45 | 46 | ) 47 | } 48 | 49 | const styles = StyleSheet.create({ 50 | container: { 51 | flex: 1, 52 | position: 'relative' 53 | }, 54 | 55 | mapStyle: { 56 | width: Dimensions.get('window').width, 57 | height: Dimensions.get('window').height, 58 | }, 59 | 60 | nextButton: { 61 | backgroundColor: '#15c3d6', 62 | borderRadius: 20, 63 | justifyContent: 'center', 64 | alignItems: 'center', 65 | height: 56, 66 | 67 | position: 'absolute', 68 | left: 24, 69 | right: 24, 70 | bottom: 40, 71 | }, 72 | 73 | nextButtonText: { 74 | fontFamily: 'Nunito_800ExtraBold', 75 | fontSize: 16, 76 | color: '#FFF', 77 | } 78 | }) -------------------------------------------------------------------------------- /web/src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Container, 5 | FixedContainer, 6 | Header, 7 | Icon, 8 | GroupIcon, 9 | Logo, 10 | Title, 11 | SubTitle, 12 | Footer, 13 | State, 14 | City, 15 | Button 16 | } from './styles'; 17 | import logo from '../../images/icons/map-marker.svg'; 18 | 19 | interface SidebarChildren { 20 | children: React.ReactNode; 21 | }; 22 | 23 | interface ButtonProps { 24 | children: React.ReactNode; 25 | type?: 'button' | 'submit'; 26 | onClick(e: React.MouseEvent): void; 27 | isActive?: true | false; 28 | } 29 | 30 | interface IconProps { 31 | children: React.ReactNode; 32 | type?: 'button' | 'submit'; 33 | onClick(e: React.MouseEvent): void; 34 | isActive: true | false; 35 | } 36 | 37 | function Sidebar({ children }: SidebarChildren) { 38 | return {children} 39 | }; 40 | 41 | Sidebar.FixedContainer = function SidebarFixedContainer({ children }: SidebarChildren) { 42 | return {children} 43 | } 44 | 45 | Sidebar.Header = function SidebarHeader({ children }: SidebarChildren) { 46 | return
{children}
47 | }; 48 | 49 | Sidebar.Logo = function SidebarLogo() { 50 | return // Static 51 | }; 52 | 53 | Sidebar.GroupIcon = function SidebarGroupIcon({ children }: SidebarChildren) { 54 | return {children} 55 | } 56 | 57 | Sidebar.Icon = function SidebarIcon({ children, onClick, type, isActive }: IconProps) { 58 | return {children} 59 | }; 60 | 61 | Sidebar.Title = function SidebarTitle({ children }: SidebarChildren) { 62 | return {children} 63 | }; 64 | 65 | Sidebar.SubTitle = function SidebarSubTitle({ children }: SidebarChildren) { 66 | return {children} 67 | }; 68 | 69 | Sidebar.City = function SidebarCity({ children }: SidebarChildren) { 70 | return {children} 71 | }; 72 | 73 | Sidebar.State = function SidebarState({ children }: SidebarChildren) { 74 | return {children} 75 | }; 76 | 77 | Sidebar.Button = function SideBarButton({ children, type, onClick }: ButtonProps) { 78 | return 79 | }; 80 | 81 | Sidebar.Footer = function SidebarFooter({ children }: SidebarChildren) { 82 | return
{children}
83 | }; 84 | 85 | export default Sidebar; 86 | -------------------------------------------------------------------------------- /web/src/pages/Home/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | import { Link as ReactRouterLink } from 'react-router-dom'; 3 | 4 | import landing from '../../images/icons/landing.svg'; 5 | 6 | export const Container = styled.div` 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | width: 100vw; 11 | height: 100vh; 12 | background: linear-gradient(329.54deg, #29B6D1 0%, #00C7C7 100%); 13 | `; 14 | 15 | export const Inner = styled.div` 16 | display: flex; 17 | align-items: flex-start; 18 | justify-content: space-between; 19 | flex-direction: column; 20 | width: 100%; 21 | max-width: 110rem; 22 | height: 100%; 23 | max-height: 68rem; 24 | position: relative; 25 | background: url(${landing}) no-repeat 80% center; 26 | `; 27 | 28 | export const Header = styled.header` 29 | display: flex; 30 | width: 100%; 31 | align-items: center; 32 | justify-content: space-between; 33 | `; 34 | 35 | export const HeaderContainer = styled.div` 36 | display: flex; 37 | `; 38 | 39 | export const Logo = styled.img``; 40 | 41 | export const Content = styled.main` 42 | max-width: 35rem; 43 | `; 44 | 45 | export const Title = styled.h1` 46 | font-size: 7.6rem; 47 | font-weight: 900; 48 | line-height: 7rem; 49 | `; 50 | 51 | export const SubTitle = styled.p` 52 | margin-top: 4.8rem; 53 | font-size: 2.4rem; 54 | line-height: 3.4rem; 55 | `; 56 | 57 | export const Location = styled.div` 58 | display: flex; 59 | flex-direction: column; 60 | text-align: left; 61 | margin-left: 7rem; 62 | `; 63 | 64 | export const City = styled.strong` 65 | font-weight: 800; 66 | `; 67 | 68 | export const State = styled.span``; 69 | 70 | export const Link = styled(ReactRouterLink)` 71 | position: absolute; 72 | bottom: 0; 73 | right: 0; 74 | 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | 79 | width: 8rem; 80 | height: 8rem; 81 | background-color: #ffd666; 82 | border-radius: 3rem; 83 | transition: background-color .2s; 84 | cursor: pointer; 85 | 86 | &:hover { 87 | background-color: #96feff; 88 | } 89 | `; 90 | 91 | export const AccessRestrict = styled(ReactRouterLink)` 92 | display: flex; 93 | align-items: center; 94 | justify-content: center; 95 | width: 22rem; 96 | height: 6rem; 97 | border-radius: 2rem; 98 | background-color: #12D4E0; 99 | text-decoration: none; 100 | color: #FFF; 101 | transition: background-color ease .3s; 102 | 103 | &:hover { 104 | background: #96FEFF; 105 | color: #15C3D6; 106 | }; 107 | `; -------------------------------------------------------------------------------- /web/src/components/Sidebar/styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { StyledComponent } from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-between; 7 | width: 44rem; 8 | padding: 8rem; 9 | background: linear-gradient(329.54deg, #29B6D1 0%, #00C7C7 100%); 10 | `; 11 | 12 | export const FixedContainer = styled.div` 13 | position: fixed; 14 | height: 100%; 15 | padding: 3.2rem 2.4rem; 16 | background: linear-gradient(329.54deg, #15B6D6 0%, #15D6D6 100%); 17 | 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-between; 21 | align-items: center; 22 | top: 0; 23 | `; 24 | 25 | export const Header = styled.header``; 26 | 27 | interface IconProps { 28 | isActive: boolean; 29 | } 30 | 31 | export const Icon: StyledComponent<"button", any, { isActive: boolean }, never> = styled.button` 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | width: 4.8rem; 36 | height: 4.8rem; 37 | border-radius: 1.6rem; 38 | background-color: ${({ isActive }: IconProps) => isActive ? '#FFD666' : '#12AFCB'}; 39 | border: none; 40 | outline: none; 41 | cursor: pointer; 42 | 43 | svg { 44 | color: ${({ isActive }: IconProps) => isActive ? '#12AFCB' : '#fff'}; 45 | }; 46 | 47 | &:hover { 48 | background-color: #FFD666; 49 | 50 | svg { 51 | color: #12AFCB; 52 | }; 53 | }; 54 | 55 | & + & { 56 | margin-top: 1.5rem; 57 | }; 58 | `; 59 | 60 | export const GroupIcon = styled.div` 61 | display: flex; 62 | flex-direction: column; 63 | `; 64 | 65 | export const Logo = styled.img``; 66 | 67 | export const Title = styled.h2` 68 | font-size: 4.8rem; 69 | font-weight: 800; 70 | line-height: 4.2rem; 71 | margin-top: 6.4rem; 72 | `; 73 | 74 | export const SubTitle = styled.p` 75 | line-height: 2.8rem; 76 | margin-top: 2.4rem; 77 | `; 78 | 79 | export const Footer = styled.footer` 80 | display: flex; 81 | flex-direction: column; 82 | line-height: 2.4rem; 83 | `; 84 | 85 | export const City = styled.strong` 86 | font-weight: 800; 87 | `; 88 | 89 | export const State = styled.span``; 90 | 91 | export const Button = styled.button` 92 | width: 4.8rem; 93 | height: 4.8rem; 94 | 95 | border: 0; 96 | 97 | background: #12AFCB; 98 | border-radius: 16px; 99 | 100 | cursor: pointer; 101 | 102 | transition: background-color 0.2s; 103 | 104 | display: flex; 105 | justify-content: center; 106 | align-items: center; 107 | 108 | &:hover { 109 | background: #17D6EB; 110 | }; 111 | `; -------------------------------------------------------------------------------- /web/src/components/MiniMap/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Map, Marker, TileLayer } from 'react-leaflet'; 3 | 4 | import { 5 | Container, 6 | MapContainer, 7 | Info, 8 | OrphanageName, 9 | ButtonWrapper, 10 | Button, 11 | } from './styles'; 12 | import { FiEdit3, FiTrash, FiArrowRight } from 'react-icons/fi'; 13 | import { MapIcon } from '../../utils/MapIcon'; 14 | 15 | 16 | interface MiniMapProps { 17 | name: string; 18 | latitude: number; 19 | longitude: number; 20 | Edit?(e: React.MouseEvent): void; 21 | Delete?(e: React.MouseEvent): void; 22 | RegistrationApproval?(e: React.MouseEvent): void; 23 | }; 24 | 25 | function MiniMap({ name, latitude, longitude, Edit, Delete, RegistrationApproval }: MiniMapProps) { 26 | return ( 27 | 28 | 29 | 39 | 42 | 47 | 48 | 49 | 50 | {name} 51 | 52 | 53 | {RegistrationApproval ? 54 | ( 55 | 58 | ) 59 | : 60 | ( 61 | <> 62 | 65 | 68 | 69 | ) 70 | } 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | 78 | export default MiniMap; 79 | -------------------------------------------------------------------------------- /web/src/pages/Private/Dashboard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex: 1; 6 | 7 | `; 8 | 9 | export const Content = styled.div` 10 | flex: 1; 11 | margin-top: 7rem; 12 | margin-left: 11.2rem; 13 | `; 14 | 15 | export const OrphanagesContainer = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | flex: 1; 19 | `; 20 | 21 | export const TitleContainer = styled.div` 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | margin: 0 9rem 1rem 9rem; 26 | padding-bottom: 2rem; 27 | border-bottom: solid .1rem #D3E2E5; 28 | `; 29 | 30 | export const Title = styled.h1` 31 | font-size: 3.2rem; 32 | font-weight: 700; 33 | line-height: 3.4rem; 34 | color: #4D6F80; 35 | `; 36 | 37 | export const OrphanagesTotal = styled.p` 38 | color: #8FA7B2; 39 | font-size: 1.6rem; 40 | line-height: 2.2rem; 41 | `; 42 | 43 | export const OrphanagesContent = styled.div` 44 | display: flex; 45 | flex: 1; 46 | flex-wrap: wrap; 47 | margin: 2rem 8rem; 48 | justify-content: flex-start; 49 | `; 50 | 51 | export const NotFoundContainer = styled.div` 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | height: 70vh; 56 | `; 57 | 58 | export const NotFoundContent = styled.div` 59 | display: flex; 60 | flex-direction: column; 61 | `; 62 | 63 | export const NotFoundMessage = styled.h2` 64 | font-size: 2.4rem; 65 | line-height: 3.4rem; 66 | color: #8FA7B2; 67 | text-align: center; 68 | margin-top: 1rem; 69 | `; 70 | 71 | export const DeletedContainer = styled.div` 72 | display: flex; 73 | background-color: #FF669D; 74 | flex: 1; 75 | height: 100vh; 76 | align-items: center; 77 | justify-content: center; 78 | `; 79 | 80 | export const DeletedInfo = styled.div` 81 | display: flex; 82 | flex-direction: column; 83 | align-items: center; 84 | margin-right: 10rem; 85 | `; 86 | 87 | export const DeletedTitle = styled.h1` 88 | font-weight: 800; 89 | font-size: 8rem; 90 | line-height: 8rem; 91 | text-align: center; 92 | margin-bottom: 3rem; 93 | `; 94 | 95 | export const DeletedSubtitle = styled.h2` 96 | font-size: 2.4rem; 97 | line-height: 3.4rem; 98 | text-align: center; 99 | margin-bottom: 4rem; 100 | `; 101 | 102 | export const DeletedImage = styled.img` 103 | object-fit: contain; 104 | height: auto; 105 | `; 106 | 107 | export const DeletedButton = styled.button` 108 | width: 24.3rem; 109 | height: 6.4rem; 110 | border-radius: 2rem; 111 | display: flex; 112 | align-items: center; 113 | justify-content: center; 114 | border: none; 115 | cursor: pointer; 116 | background-color: #D6487B; 117 | color: #fff; 118 | `; -------------------------------------------------------------------------------- /web/src/pages/Signin/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex: 1; 6 | height: 100vh; 7 | `; 8 | 9 | export const Banner = styled.div` 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | flex: .7; 15 | background: linear-gradient(329.54deg, #29B6D1 0%, #00C7C7 100%); 16 | `; 17 | 18 | export const Logo = styled.img` 19 | width: 11rem; 20 | object-fit: cover; 21 | `; 22 | 23 | export const Title = styled.h1` 24 | font-size: 10rem; 25 | font-weight: 800; 26 | `; 27 | 28 | export const State = styled.strong` 29 | font-weight: 800; 30 | margin-top: 8rem; 31 | `; 32 | 33 | export const City = styled.span``; 34 | 35 | export const Content = styled.div` 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | flex: .3; 40 | background-color: #fff; 41 | 42 | position: relative; 43 | `; 44 | 45 | export const GoBack = styled.div` 46 | display: flex; 47 | align-items: center; 48 | justify-content: center; 49 | width: 5.8rem; 50 | height: 5.8rem; 51 | border-radius: 1.6rem; 52 | position: absolute; 53 | top: 5%; 54 | right: 10%; 55 | background-color: #F5F8FA; 56 | cursor: pointer; 57 | `; 58 | 59 | export const Form = styled.form` 60 | display: flex; 61 | flex-direction: column; 62 | `; 63 | 64 | export const FormTitle = styled.h1` 65 | color: #5C8599; 66 | font-size: 3.2rem; 67 | line-height: 2.4rem; 68 | font-weight: bold; 69 | margin-bottom: 5rem; 70 | `; 71 | 72 | export const InputWrapper = styled.div` 73 | display: flex; 74 | flex-direction: column; 75 | flex: 1; 76 | margin-bottom: .5rem; 77 | `; 78 | 79 | export const Label = styled.label` 80 | color: #8FA7B2; 81 | margin: 1rem 0; 82 | `; 83 | 84 | export const Input = styled.input` 85 | width: 36rem; 86 | height: 6.4rem; 87 | border-radius: 2rem; 88 | border: 1px solid #D3E2E5; 89 | background-color: #F5F8FA; 90 | outline: none; 91 | padding: 0 2.5rem; 92 | `; 93 | 94 | export const CheckWrapper = styled(InputWrapper)` 95 | flex-direction: row; 96 | align-items: center; 97 | margin-top: 1.5rem; 98 | `; 99 | 100 | export const Check = styled.button` 101 | width: 2.4rem; 102 | height: 2.4rem; 103 | border: 1px solid #D3E2E5; 104 | border-radius: .8rem; 105 | background-color: #F5F8FA; 106 | box-sizing: border-box; 107 | margin-right: 1.7rem; 108 | outline: none; 109 | cursor: pointer; 110 | 111 | .check { 112 | background-color: #37C77F; 113 | }; 114 | `; 115 | 116 | 117 | export const Help = styled.button` 118 | color: #8FA7B2; 119 | margin-left: auto; 120 | cursor: pointer; 121 | background: none; 122 | border: none; 123 | outline: none; 124 | `; 125 | 126 | export const Button = styled.button` 127 | width: 36rem; 128 | height: 6.4rem; 129 | background-color: #37C77F; 130 | color: #fff; 131 | border: none; 132 | border-radius: 2rem; 133 | margin-top: 3rem; 134 | cursor: pointer; 135 | `; 136 | 137 | export const Error = styled.strong` 138 | display: flex; 139 | align-items: center; 140 | justify-content: center; 141 | color: #FF669D; 142 | background-color: #FDF0F5; 143 | height: 6.4rem; 144 | border-radius: 2rem; 145 | `; -------------------------------------------------------------------------------- /web/src/pages/OrphanagesMap/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Map, TileLayer, Marker, Popup } from 'react-leaflet'; 3 | 4 | import { Sidebar } from '../../components'; 5 | 6 | import { 7 | MapStyle, 8 | Container, 9 | Link 10 | } from './styles'; 11 | import { FiPlus, FiArrowRight } from 'react-icons/fi'; 12 | import { MapIconMarker } from '../../utils/MapIcon'; 13 | 14 | import api from '../../services/api'; 15 | 16 | interface Orphanage { 17 | id: number, 18 | name: string, 19 | latitude: number, 20 | longitude: number 21 | }; 22 | 23 | function OrphanagesMap() { 24 | const [orphanages, setOrphanages] = useState([]); 25 | const [position, setPosition] = useState({ longitude: 0, latitude: 0, }) 26 | 27 | useEffect(() => { 28 | api.get('/orphanages?filter=approved').then(resp => { 29 | setOrphanages(resp.data.orphanages); 30 | }); 31 | 32 | navigator.geolocation.getCurrentPosition(position => { 33 | setPosition({ latitude: position.coords.latitude, longitude: position.coords.longitude }) 34 | }); 35 | }, []); 36 | 37 | return ( 38 | <> 39 | 40 | 41 | 42 | 43 | 44 | 45 | Escolha um orfanato no mapa 46 | Muitas crianças estão esperando a sua visita :) 47 | 48 | 49 | 50 | Nova Cruz 51 | Rio Grande do Norte 52 | 53 | 54 | 55 | 60 | {/* */} 61 | 64 | 65 | {orphanages.map(orphanage => ( 66 | 71 | 72 | {orphanage.name} 73 | 74 | 75 | 76 | 77 | 78 | ))} 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default OrphanagesMap; 91 | -------------------------------------------------------------------------------- /web/src/pages/Signin/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, FormEvent, useContext } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import { 5 | Container, 6 | Banner, 7 | GoBack, 8 | Logo, 9 | Title, 10 | State, 11 | City, 12 | Content, 13 | Form, 14 | FormTitle, 15 | InputWrapper, 16 | Input, 17 | CheckWrapper, 18 | Check, 19 | Label, 20 | Help, 21 | Button, 22 | Error 23 | } from './styles'; 24 | import { FiArrowLeft, FiCheck } from 'react-icons/fi'; 25 | import logo from '../../images/icons/map-marker.svg'; 26 | 27 | import { login } from '../../services/user'; 28 | import { UserContext } from '../../contexts/UserContext'; 29 | 30 | function Signin() { 31 | const [email, setEmail] = useState(''); 32 | const [password, setPassword] = useState(''); 33 | const [remember, setRemember] = useState(false); 34 | const [errorMessage, setErrorMessage] = useState(''); 35 | 36 | const history = useHistory(); 37 | const { goBack } = useHistory(); 38 | 39 | const { setUser } = useContext(UserContext); 40 | 41 | async function handleAuthUser(e: FormEvent) { 42 | e.preventDefault(); 43 | 44 | login({ email, password }, remember).then(response => { 45 | const { user, token, error } = response; 46 | 47 | if (error) { 48 | setEmail(''); 49 | setPassword(''); 50 | setErrorMessage(error); 51 | return; 52 | }; 53 | 54 | const { id, name, email } = user 55 | 56 | setUser!({ id, name, email, token }); 57 | history.push('/dashboard'); 58 | }); 59 | }; 60 | 61 | return ( 62 | 63 | 64 | 65 | happy 66 | 67 | Rio Grande do Norte 68 | Nova Cruz 69 | 70 | 71 | 72 | 73 | 74 |
handleAuthUser(e)}> 75 | Fazer login 76 | 77 | {errorMessage && {errorMessage}} 78 | 79 | 80 | 81 | setEmail(e.target.value)} 86 | /> 87 | 88 | 89 | 90 | 91 | setPassword(e.target.value)} 96 | /> 97 | 98 | 99 | 100 | setRemember(!remember)} style={{ backgroundColor: remember ? '#37C77F' : '#F5F8FA' }}> 101 | 102 | 103 | 104 | Esqueci minha senha 105 | 106 | 107 | 108 |
109 |
110 |
111 | ); 112 | }; 113 | 114 | export default Signin; 115 | -------------------------------------------------------------------------------- /web/src/pages/Orphanage/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | min-height: 100vh; 6 | `; 7 | 8 | export const Content = styled.main` 9 | flex: 1; 10 | `; 11 | 12 | export const Details = styled.div` 13 | width: 70rem; 14 | margin: 6.4rem auto; 15 | 16 | border: 1px solid #D3E2E5; 17 | border-radius: 2rem; 18 | overflow: hidden; 19 | background: #FFFFFF; 20 | `; 21 | 22 | export const Image = styled.img` 23 | width: 100%; 24 | height: 30rem; 25 | object-fit: cover; 26 | `; 27 | 28 | export const Button = styled.button` 29 | height: 8.8rem; 30 | background: none; 31 | border: 0; 32 | border-radius: 2rem; 33 | outline: none; 34 | cursor: pointer; 35 | overflow: hidden; 36 | 37 | opacity: 0.6; 38 | 39 | &.active { 40 | opacity: 1; 41 | }; 42 | `; 43 | 44 | export const ImageButton = styled.img` 45 | width: 100%; 46 | height: 8.8rem; 47 | object-fit: cover; 48 | `; 49 | 50 | export const Images = styled.div` 51 | display: grid; 52 | grid-template-columns: repeat(6 ,1fr); 53 | column-gap: 1.6rem; 54 | margin: 1.6rem 4rem 0; 55 | `; 56 | 57 | export const DetailsContent = styled.div` 58 | padding: 8rem; 59 | `; 60 | 61 | export const Name = styled.h1` 62 | color: #4D6F80; 63 | font-size: 5.4rem; 64 | line-height: 5.4rem; 65 | margin-bottom: .8rem; 66 | `; 67 | 68 | export const Description = styled.p` 69 | color: #5C8599; 70 | line-height: 2.8rem; 71 | margin-top: 2.4rem; 72 | `; 73 | 74 | export const MapContainer = styled.div` 75 | background: #E6F7FB; 76 | border: .1rem solid #B3DAE2; 77 | border-radius: 2rem; 78 | margin-top: 6.4rem; 79 | 80 | .leaflet-container { 81 | border-bottom: .1rem solid #DDE3F0; 82 | border-radius: 2rem; 83 | }; 84 | `; 85 | 86 | export const DetailsFooter = styled.footer` 87 | padding: 2rem 0; 88 | text-align: center; 89 | 90 | a { 91 | color: #0089A5; 92 | text-decoration: none; 93 | line-height: 2.4rem; 94 | }; 95 | `; 96 | 97 | export const Hr = styled.hr` 98 | width: 100%; 99 | height: .1rem; 100 | border: 0; 101 | margin: 6.4rem 0; 102 | background: #D3E2E6; 103 | `; 104 | 105 | export const InstructionTitle = styled.h2` 106 | font-size: 3.6rem; 107 | line-height: 4.6rem; 108 | color: #4D6F80; 109 | `; 110 | 111 | export const OpenDetails = styled.div` 112 | display: grid; 113 | grid-template-columns: 1fr 1fr; 114 | column-gap: 2rem; 115 | margin-top: 24px; 116 | 117 | div { 118 | padding: 3.2rem 2.4rem; 119 | border-radius: 2rem; 120 | line-height: 2.8rem; 121 | 122 | svg { 123 | display: block; 124 | margin-bottom: 2rem; 125 | }; 126 | }; 127 | `; 128 | 129 | export const DetailHour = styled.div` 130 | background: linear-gradient(149.97deg, #E6F7FB 8.13%, #FFFFFF 92.67%); 131 | border: .1rem solid #B3DAE2; 132 | color: #5C8599; 133 | `; 134 | 135 | export const DetailOpenOnWeekends = styled.div` 136 | background: linear-gradient(154.16deg, #EDFFF6 7.85%, #FFFFFF 91.03%); 137 | border: .1rem solid #A1E9C5; 138 | color: #37C77F; 139 | 140 | &.dont-open { 141 | background: linear-gradient(154.16deg, #FDF0F5 7.85%, #FFFFFF 91.03%); 142 | border: .1rem solid #FFBCD4; 143 | color: #FF669D; 144 | }; 145 | `; 146 | 147 | export const Contact = styled.a` 148 | display: flex; 149 | justify-content: center; 150 | align-items: center; 151 | width: 100%; 152 | height: 6.4rem; 153 | border: 0; 154 | border-radius: 2rem; 155 | margin-top: 6.4rem; 156 | color: #FFFFFF; 157 | font-weight: 800; 158 | background: #3CDC8C; 159 | cursor: pointer; 160 | text-decoration: none; 161 | 162 | transition: background-color 0.2s; 163 | 164 | svg { 165 | margin-right: 1rem; 166 | }; 167 | 168 | &:hover { 169 | margin-right: 1.6rem; 170 | }; 171 | 172 | svg { 173 | background: #36CF82; 174 | }; 175 | `; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Happy

2 | 3 | 4 | ## Index 5 | 6 | * [About the Project](#about-the-project) 7 | * [Built with](#built-with) 8 | * [Preview](#preview) 9 | * [Deploy](#deploy) 10 | * [Getting started](#getting-started) 11 | * [Prerequisites](#prerequisites) 12 | * [Instalation](#instalation) 13 | * [Contributing](#contributing) 14 | * [What Contribute](#what-contribute) 15 | * [Contact](#contact) 16 | * [Knowledge Acquired](#knowledge-acquired) 17 | 18 | ## About the Project 19 | 20 | Created during nlw3 .This project is an orphanage search site, aimed at visiting and making children happier. Contain 4 pages: Home, orphanagesMap, orphanages and createOrphanage. 21 | The data is stored in a sqlite database, with upload image api and orphanages api. 22 | 23 | ### Built with 24 | 25 | | Frontend | Backend | Mobile | 26 | |----------------------------------------------------|-------------------------------------------------------------|-----------------------------| 27 | | [React](https://reactjs.org/) | [express](https://expressjs.com/pt-br/) | [expo](https://expo.io/) | 28 | |[styled-components](https://styled-components.com/) |[express-async-errors](https://www.npmjs.com/package/express-async-errors)|[react native](https://reactnative.dev/) | 29 | |[react-icons](https://react-icons.github.io/react-icons/)| [multer](https://www.npmjs.com/package/multer) | | 30 | | [leaflet](https://leafletjs.com/) | [sqlite3](https://www.sqlite.org/index.html) | | 31 | | [react-leaflet](https://react-leaflet.js.org/)| [bcryptjs](https://www.npmjs.com/package/bcryptjs) | | 32 | | [axios](https://github.com/axios/axios) | [cors](https://www.npmjs.com/package/co) | | 33 | | [Mapbox](https://mapbox.com) | [nodemon](https://nodemon.io/) | 34 | | | [typeorm](https://typeorm.io/#/) | 35 | | | [yup](https://github.com/jquense/yup) | 36 | 37 | 38 |

Preview

39 |

40 | 41 |

42 | 43 | ## Deploy 44 | https://happy-devzero.netlify.app/ - Use 80% zoom 45 | 46 | ## Getting started 47 | To get a local copy up and running follow these simple example steps. 48 | 49 | ### Prerequisites 50 | 51 | * npm 52 | * node 53 | 54 | ### Instalation 55 | 56 | 1. Clone this repository 57 | ```sh 58 | $ git clone https://github.com/jonasdevzero/Happy.git 59 | ``` 60 | 2. Get a key in Mapbox and use on line 55 in 61 | ```sh 62 | /web/src/pages/OrphanagesMap 63 | ``` 64 | 3. Run the projects in the folders 65 | 66 | 3.1 web - 67 | ```sh 68 | $ npm start 69 | or 70 | $ yarn start 71 | ``` 72 | 3.2 backend - 73 | ```sh 74 | $ npm run dev 75 | or 76 | $ yarn dev 77 | ``` 78 | 79 | ## Contributing 80 | Contributions are very welcome :smiley: 81 | 82 | 1. Fork the Project 83 | 2. Create your Feature Branch (git checkout -b feature/AmazingFeature) 84 | 3. Commit your Changes (git commit -m 'Add some AmazingFeature') 85 | 4. Push to the Branch (git push origin feature/AmazingFeature) 86 | 5. Open a Pull Request 87 | 88 | ### What contribute 89 | - Responsivity 90 | - Optimization 91 | - Security 92 | - Improve code 93 | 94 | ## Contact 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | ## Knowledge Acquired 103 | 104 | - Map api 105 | - A little of typescript 106 | - typeorm 107 | - MVC design pattern 108 | -------------------------------------------------------------------------------- /web/src/images/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mobile/src/pages/OrphanagesMap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { StyleSheet, Text, View, Dimensions } from 'react-native'; 3 | import MapView, { Marker, Callout, PROVIDER_GOOGLE } from 'react-native-maps'; 4 | import { useNavigation, useFocusEffect } from '@react-navigation/native'; 5 | 6 | import { Feather } from '@expo/vector-icons'; 7 | import mapMarker from '../images/map-marker.png'; 8 | import { RectButton } from 'react-native-gesture-handler'; 9 | 10 | import api from '../services/api'; 11 | 12 | interface Orphanage { 13 | id: number; 14 | name: string; 15 | latitude: number; 16 | longitude: number; 17 | } 18 | 19 | function OrphanagesMap() { 20 | const [orphanages, setOrphanages] = useState([]); 21 | const navigation = useNavigation() 22 | 23 | function NavigateToOrphanageDetails(id: number) { 24 | navigation.navigate('OrphanageDetails', { id }) 25 | }; 26 | 27 | function NavigateToCreateOrphanage() { 28 | navigation.navigate('SelectMapPosition'); 29 | }; 30 | 31 | useFocusEffect(() => { 32 | api.get('/orphanages') 33 | .then(resp => { 34 | setOrphanages(resp.data); 35 | }); 36 | }); 37 | 38 | return ( 39 | 40 | 50 | {orphanages.map(orphanage => ( 51 | 63 | NavigateToOrphanageDetails(orphanage.id)}> 64 | 65 | {orphanage.name} 66 | 67 | 68 | 69 | ))} 70 | 71 | 72 | 73 | {orphanages.length} orfanatos encontrados 74 | 75 | 76 | 77 | 78 | 79 | ) 80 | }; 81 | 82 | const styles = StyleSheet.create({ 83 | container: { 84 | flex: 1, 85 | }, 86 | 87 | map: { 88 | width: Dimensions.get('window').width, 89 | // height: Dimensions.get('window').height, 90 | height: '100%', 91 | }, 92 | 93 | calloutContainer: { 94 | width: 160, 95 | height: 46, 96 | paddingHorizontal: 16, 97 | backgroundColor: 'rgba(255, 255, 255, .8)', 98 | borderRadius: 16, 99 | justifyContent: 'center', 100 | }, 101 | 102 | calloutText: { 103 | color: '#0089a5', 104 | fontSize: 14, 105 | fontFamily: 'Nunito_700Bold', 106 | }, 107 | 108 | footer: { 109 | position: 'absolute', 110 | left: 24, 111 | right: 24, 112 | bottom: 32, 113 | 114 | backgroundColor: '#fff', 115 | borderRadius: 20, 116 | height: 56, 117 | paddingLeft: 24, 118 | flexDirection: 'row', 119 | justifyContent: 'space-between', 120 | alignItems: 'center', 121 | elevation: 3, 122 | }, 123 | 124 | footerText: { 125 | color: '#8fa7d3', 126 | fontFamily: 'Nunito_700Bold', 127 | }, 128 | 129 | createOrphanageButton: { 130 | width: 56, 131 | height: 56, 132 | backgroundColor: '#15c3d6', 133 | borderRadius: 20, 134 | justifyContent: 'center', 135 | alignItems: 'center', 136 | }, 137 | }); 138 | 139 | export default OrphanagesMap; 140 | -------------------------------------------------------------------------------- /backend/src/controllers/UsersController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { getRepository } from 'typeorm'; 3 | 4 | import User from '../models/User'; 5 | import UserView from '../views/users_view'; 6 | 7 | import bcrypt from 'bcryptjs'; 8 | import jwt from 'jsonwebtoken'; 9 | import * as Yup from 'yup'; 10 | 11 | const tokenSecret = process.env.TOKEN_SECRET || 'undefined'; 12 | 13 | function encryptPassword(password: string) { 14 | const salt = bcrypt.genSaltSync(10); 15 | return bcrypt.hashSync(password, salt); 16 | }; 17 | 18 | function generateToken(params: object) { 19 | return jwt.sign(params, tokenSecret, { 20 | expiresIn: 86400 21 | }); 22 | }; 23 | 24 | export default { 25 | async index(req: Request, res: Response) { 26 | try { 27 | const usersRepository = getRepository(User); 28 | 29 | const users = await usersRepository.find(); 30 | 31 | return res.status(200).json({ user: UserView.renderMany(users) }); 32 | } catch (err) { 33 | console.log('[Error on index users] -> ', err); 34 | return res.status(500).json({ error: 'Internal Server Error' }); 35 | }; 36 | }, 37 | 38 | async show(req: Request, res: Response) { 39 | const { user: { id } } = req.body; 40 | 41 | try { 42 | const usersRepository = getRepository(User); 43 | 44 | const user = await usersRepository.findOne({ id }); 45 | 46 | return res.status(200).json({ user: UserView.render(user as User) }); 47 | } catch (err) { 48 | console.log('[Error on show user] -> ', err); 49 | return res.status(500).json({ error: 'Internal Server Error' }); 50 | }; 51 | }, 52 | 53 | async delete(req: Request, res: Response) { 54 | const { id } = req.body; 55 | 56 | try { 57 | const usersRepository = getRepository(User); 58 | 59 | const userDeleted = await usersRepository.delete(id); 60 | 61 | return res.status(200).json({ user: userDeleted }); 62 | } catch (err) { 63 | console.log('[Error on delete user] -> ', err); 64 | return res.status(500).json({ error: 'Internal Server Error' }) 65 | }; 66 | }, 67 | 68 | async create(req: Request, res: Response) { 69 | const { 70 | name, 71 | email, 72 | password 73 | } = req.body; 74 | 75 | try { 76 | const usersRepository = getRepository(User); 77 | 78 | const existsUser = await usersRepository.findOne({ email }); 79 | if (existsUser) return res.status(400).json({ error: 'User already exists' }); 80 | 81 | const data = { 82 | name, 83 | email, 84 | password: encryptPassword(password), 85 | }; 86 | 87 | const schema = Yup.object().shape({ 88 | name: Yup.string().required(), 89 | email: Yup.string().required(), 90 | password: Yup.string().required() 91 | }); 92 | await schema.validate(data, { 93 | abortEarly: false, 94 | }); 95 | 96 | const user = usersRepository.create(data); 97 | await usersRepository.save(user); 98 | 99 | return res.status(200).json({ token: generateToken({ id: user.id }) }); 100 | } catch (err) { 101 | console.log('[Error on create user] -> ', err); 102 | return res.status(500).json({ error: 'Internal Server Error' }); 103 | }; 104 | }, 105 | 106 | async login(req: Request, res: Response) { 107 | const { 108 | email, 109 | password 110 | } = req.body; 111 | 112 | try { 113 | const usersRepository = getRepository(User); 114 | 115 | const user = await usersRepository.findOne({ email }); 116 | 117 | if (!user) 118 | return res.json({ error: 'User not found' }); 119 | 120 | if (!bcrypt.compareSync(password, user.password)) 121 | return res.json({ error: 'Invalid password' }); 122 | 123 | return res.status(200).json({ 124 | user: UserView.render(user), 125 | token: generateToken({ id: user.id }), 126 | }); 127 | } catch (err) { 128 | return res.status(500).json({ error: 'Internal Server Error' }); 129 | }; 130 | }, 131 | 132 | async auth(req: Request, res: Response, next: NextFunction) { 133 | const { token } = req.body; 134 | 135 | if (!token) return res.status(401).json({ error: 'Access Denied' }); 136 | 137 | try { 138 | const verified = jwt.verify(token, tokenSecret) 139 | 140 | req.body.user = verified; 141 | 142 | next(); 143 | } catch (err) { 144 | return res.status(400).json({ error: 'Invalid Token' }); 145 | }; 146 | }, 147 | }; -------------------------------------------------------------------------------- /web/src/components/Form/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/macro'; 2 | 3 | export const FormWrapper = styled.div` 4 | width: 70rem; 5 | margin: 64px auto; 6 | 7 | background: #FFFFFF; 8 | border: .1rem solid #D3E2E5; 9 | border-radius: 20px; 10 | `; 11 | 12 | export const Container = styled.form` 13 | padding: 6.4rem 8rem; 14 | 15 | overflow: hidden; 16 | 17 | .leaflet-container { 18 | margin-bottom: 4rem; 19 | border: .1rem solid #D3E2E5; 20 | border-radius: 2rem; 21 | }; 22 | `; 23 | 24 | export const Fieldset = styled.fieldset` 25 | border: 0;; 26 | 27 | & + & { 28 | margin-top: 8rem; 29 | }; 30 | `; 31 | 32 | export const Legend = styled.legend` 33 | width: 100%; 34 | 35 | font-size: 3.2rem; 36 | line-height: 3.4rem; 37 | color: #5C8599; 38 | font-weight: 70rem; 39 | 40 | border-bottom: .1rem solid #D3E2E5; 41 | margin-bottom: 4rem; 42 | padding-bottom: 2.4rem; 43 | `; 44 | 45 | export const TextArea = styled.textarea` 46 | width: 100%; 47 | background: #F5F8FA; 48 | border: .1rem solid #D3E2E5; 49 | border-radius: 2rem; 50 | outline: none; 51 | color: #5C8599; 52 | 53 | min-height: 12rem; 54 | max-height: 24rem; 55 | resize: vertical; 56 | padding: 1.6rem; 57 | line-height: 2.8rem; 58 | `; 59 | 60 | export const InputWrapper = styled.div` 61 | & + & { 62 | margin-top: 2.4rem; 63 | } 64 | `; 65 | 66 | export const Label = styled.label` 67 | display: flex; 68 | color: #8FA7B3; 69 | margin-bottom: .8rem; 70 | line-height: 2.4rem; 71 | `; 72 | 73 | export const Input = styled.input` 74 | width: 100%; 75 | background: #F5F8FA; 76 | border: .1rem solid #D3E2E5; 77 | border-radius: 2rem; 78 | outline: none; 79 | color: #5C8599; 80 | 81 | height: 6.4rem; 82 | padding: 0 1.6rem; 83 | `; 84 | 85 | export const Span = styled.span` 86 | font-size: 1.4rem; 87 | color: #8FA7B3; 88 | margin-left: 2.4rem; 89 | line-height: 2.4rem; 90 | `; 91 | 92 | export const ImagesWrapper = styled.div` 93 | display: grid; 94 | grid-template-columns: repeat(5, 1fr); 95 | grid-gap: 1.6rem; 96 | `; 97 | 98 | export const ImageWrapper = styled.div` 99 | position: relative; 100 | 101 | img { 102 | width: 100%; 103 | height: 9.6rem; 104 | object-fit: cover; 105 | border-radius: 2rem; 106 | border: solid .1rem #EBF2F5; 107 | }; 108 | `; 109 | 110 | export const ImageLabel = styled.label` 111 | display: flex; 112 | justify-content: center; 113 | align-items: center; 114 | height: 9.6rem; 115 | background: #F5F8FA; 116 | border: .1rem dashed #96D2F0; 117 | border-radius: 2rem; 118 | cursor: pointer; 119 | `; 120 | 121 | export const ImageInput = styled.input` 122 | display: none; 123 | `; 124 | export const ImageButton = styled.button` 125 | position: absolute; 126 | top: 0; 127 | right: 0; 128 | width: 4rem; 129 | height: 4rem; 130 | display: flex; 131 | justify-content: center; 132 | align-items: center; 133 | background-color: #fff; 134 | border: none; 135 | border-radius: 0 2rem; 136 | cursor: pointer; 137 | `; 138 | 139 | export const SelectWrapper = styled.div` 140 | display: grid; 141 | grid-template-columns: 1fr 1fr; 142 | `; 143 | 144 | export const Select = styled.button` 145 | height: 6.4rem; 146 | background: #F5F8FA; 147 | border: .1rem solid #D3E2E5; 148 | color: #5C8599; 149 | cursor: pointer; 150 | border-radius: 2rem 0px 0px 2rem; 151 | 152 | & + & { 153 | border-radius: 0 2rem 2rem 0; 154 | border-left: 0; 155 | }; 156 | 157 | &.active { 158 | background: #EDFFF6; 159 | border: .1rem solid #A1E9C5; 160 | color: #37C77F; 161 | }; 162 | 163 | & + &.active { 164 | background-color: #FDF0F5; 165 | border: .1rem solid #FFBCD4; 166 | color: #FF669D; 167 | }; 168 | `; 169 | 170 | export const Submit = styled.button` 171 | margin-top: 6.4rem; 172 | 173 | width: 100%; 174 | height: 6.4rem; 175 | border: 0; 176 | cursor: pointer; 177 | background: #3CDC8C; 178 | border-radius: 2rem; 179 | color: #FFFFFF; 180 | font-weight: 800; 181 | 182 | display: flex; 183 | justify-content: center; 184 | align-items: center; 185 | 186 | transition: background-color 0.2s; 187 | 188 | &:hover { 189 | background: #36CF82; 190 | }; 191 | 192 | svg { 193 | margin-right: 1.6rem; 194 | }; 195 | `; 196 | 197 | export const ButtonWrapper = styled.div` 198 | background-color: #F5F8FA; 199 | border-radius: 0px 0px 20px 20px; 200 | width: 100%; 201 | display: flex; 202 | align-items: center; 203 | justify-content: space-between; 204 | padding: 3rem 7.5rem; 205 | `; 206 | 207 | export const Button = styled.button` 208 | width: 26.4rem; 209 | height: 6.4rem; 210 | border-radius: 2rem; 211 | border: none; 212 | line-height: 2.8rem; 213 | font-weight: 800; 214 | font-size: 1.8rem; 215 | color: #fff; 216 | cursor: pointer; 217 | 218 | display: flex; 219 | align-items: center; 220 | justify-content: center; 221 | 222 | svg { 223 | margin-right: 1rem; 224 | }; 225 | 226 | background-color: #FF669D; 227 | & + & { 228 | background-color: #3CDC8C; 229 | }; 230 | `; 231 | -------------------------------------------------------------------------------- /web/src/components/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Container, 5 | FormWrapper, 6 | Fieldset, 7 | Legend, 8 | TextArea, 9 | InputWrapper, 10 | Label, 11 | Input, 12 | Span, 13 | ImagesWrapper, 14 | ImageWrapper, 15 | ImageLabel, 16 | ImageInput, 17 | ImageButton, 18 | SelectWrapper, 19 | Select, 20 | Submit, 21 | ButtonWrapper, 22 | Button, 23 | } from './styles'; 24 | 25 | interface FormProps { 26 | children: React.ReactNode; 27 | onSubmit?: ((event: React.FormEvent) => void) | undefined; 28 | }; 29 | 30 | interface TextAreaProps { 31 | id?: string | undefined; 32 | maxLength?: number | undefined; 33 | value?: string | number | readonly string[] | undefined; 34 | onChange?: ((event: React.ChangeEvent) => void) | undefined; 35 | 36 | }; 37 | 38 | interface LabelProps { 39 | children: React.ReactNode; 40 | htmlFor?: string | undefined; 41 | }; 42 | 43 | interface InputProps { 44 | id?: string | undefined; 45 | value?: string | number | readonly string[] | undefined; 46 | onChange?: ((event: React.ChangeEvent) => void) | undefined; 47 | type?: 'file'; 48 | multiple?: boolean | undefined; 49 | }; 50 | 51 | interface ButtonProps { 52 | children: React.ReactNode; 53 | onClick?: ((event: React.MouseEvent) => void) | undefined; 54 | type?: 'button' | undefined; 55 | className?: string | undefined; 56 | }; 57 | 58 | function Form({ children, onSubmit }: FormProps) { 59 | return {children}; 60 | }; 61 | 62 | Form.FormWrapper = function FormFormWrapper({ children }: { children: React.ReactNode }) { 63 | return {children} 64 | }; 65 | 66 | Form.Fieldset = function FormFieldset({ children, ...props }: { children: React.ReactNode }) { 67 | return
{children}
; 68 | }; 69 | 70 | Form.Legend = function FormLegend({ children, ...props }: { children: React.ReactNode }) { 71 | return {children}; 72 | }; 73 | 74 | Form.TextArea = function FormTextArea({ id, maxLength, value, onChange }: TextAreaProps) { 75 | return ( 76 |