├── 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 |
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 |
7 |
--------------------------------------------------------------------------------
/web/src/images/icons/map-marker.svg:
--------------------------------------------------------------------------------
1 |
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
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
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 |
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 |
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 |
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 |
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 ;
68 | };
69 |
70 | Form.Legend = function FormLegend({ children, ...props }: { children: React.ReactNode }) {
71 | return ;
72 | };
73 |
74 | Form.TextArea = function FormTextArea({ id, maxLength, value, onChange }: TextAreaProps) {
75 | return (
76 |
82 | );
83 | };
84 |
85 | Form.InputWrapper = function FormInputWrapper({ children, ...props }: { children: React.ReactNode }) {
86 | return {children};
87 | };
88 |
89 | Form.Label = function FormLabel({ children, ...props }: LabelProps) {
90 | return ;
91 | };
92 |
93 | Form.Input = function FormInput({ id, value, onChange }: InputProps) {
94 | return (
95 |
100 | )
101 | };
102 |
103 | Form.Span = function FormSpan({ children, ...props }: { children: React.ReactNode }) {
104 | return {children};
105 | };
106 |
107 | Form.ImagesWrapper = function FormImagesWrapper({ children, ...props }: { children: React.ReactNode }) {
108 | return {children};
109 | };
110 |
111 | Form.ImageWrapper = function FormImageWrapper({ children }: { children: React.ReactNode }) {
112 | return (
113 |
114 | {children}
115 |
116 | )
117 | }
118 |
119 | Form.ImageLabel = function FormImageLabel({ children, htmlFor }: LabelProps) {
120 | return (
121 |
124 | {children}
125 |
126 | )
127 | };
128 |
129 | Form.ImageInput = function FormImageInput({ type, id, onChange, multiple, value }: InputProps) {
130 | return ;
131 | };
132 |
133 | Form.ImageButton = function FormImageButton({ children, type, onClick }: ButtonProps) {
134 | return (
135 |
136 | {children}
137 |
138 | );
139 | };
140 |
141 | Form.SelectWrapper = function FormSelectWrapper({ children, ...props }: { children: React.ReactNode }) {
142 | return {children};
143 | };
144 |
145 | Form.Select = function FormSelect({ children, onClick, type, className }: ButtonProps) {
146 | return (
147 |
154 | );
155 | };
156 |
157 | Form.Submit = function FormSubmit({ children, type }: { children: React.ReactNode, type?: 'submit' | undefined }) {
158 | return (
159 |
162 | {children}
163 |
164 | )
165 | };
166 |
167 | Form.ButtonWrapper = function FormButtonWrapper({ children }: { children: React.ReactNode }) {
168 | return {children}
169 | };
170 |
171 | Form.Button = function FormButton({ children, onClick, type }: ButtonProps) {
172 | return (
173 |
176 | )
177 | }
178 |
179 | export default Form;
180 |
--------------------------------------------------------------------------------
/web/src/pages/Orphanage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useHistory, useParams } from "react-router-dom";
3 | import { Map, Marker, TileLayer } from "react-leaflet";
4 |
5 | import { Sidebar } from '../../components';
6 |
7 | import { FaWhatsapp } from "react-icons/fa";
8 | import { FiClock, FiInfo, FiArrowLeft } from "react-icons/fi";
9 | import { MapIcon } from '../../utils/MapIcon';
10 |
11 | import api from '../../services/api';
12 |
13 | import {
14 | Container,
15 | Content,
16 | Details,
17 | Image,
18 | Images,
19 | Button,
20 | ImageButton,
21 | DetailsContent,
22 | Name,
23 | Description,
24 | MapContainer,
25 | DetailsFooter,
26 | Hr,
27 | InstructionTitle,
28 | OpenDetails,
29 | DetailHour,
30 | DetailOpenOnWeekends,
31 | Contact,
32 | } from './styles';
33 |
34 | interface Orphanage {
35 | name: string;
36 | about: string;
37 | instructions: string;
38 | latitude: number;
39 | longitude: number;
40 | openning_hours: string;
41 | open_on_weekends: string;
42 | contact: number;
43 | images: Array<{
44 | id: number;
45 | url: string;
46 | }>;
47 | };
48 |
49 | interface OrphanageParams {
50 | id: string,
51 | };
52 |
53 | export default function Orphanage() {
54 | const [orphanage, setOrphanage] = useState();
55 | const [currentImage, setCurrentImage] = useState(0);
56 | const params = useParams();
57 | const { goBack } = useHistory();
58 |
59 | useEffect(() => {
60 | api.get(`/orphanages/${params.id}`)
61 | .then(resp => {
62 | setOrphanage(resp.data.orphanage);
63 | });
64 | }, [params.id]);
65 |
66 | return (
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | {orphanage?.images.map((image, i) => (
84 |
92 | ))}
93 |
94 |
95 |
96 | {orphanage?.name}
97 |
98 | {orphanage?.instructions}
99 |
100 |
101 |
102 |
122 |
123 |
124 |
125 | Ver rotas no Google Maps
126 |
127 |
128 |
129 |
130 |
131 |
132 | Instruções para visita
133 | {orphanage?.instructions}
134 |
135 |
136 |
137 |
138 | Segunda à Sexta
139 | {orphanage?.openning_hours}
140 |
141 | {orphanage?.open_on_weekends ?
142 |
143 |
144 | Atendemos
145 | fim de semana
146 |
147 | :
148 |
149 |
150 | Não atendemos
151 | fim de semana
152 |
153 | }
154 |
155 |
156 |
157 |
158 | Entrar em contato
159 |
160 |
161 |
162 |
163 |
164 | );
165 | }
--------------------------------------------------------------------------------
/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 |
5 | /* Basic Options */
6 | // "incremental": true, /* Enable incremental compilation */
7 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9 | // "lib": [], /* Specify library files to be included in the compilation. */
10 | "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "./dist", /* Redirect output structure to the directory. */
18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | // "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | "strict": true, /* Enable all strict type-checking options. */
29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | // "strictNullChecks": true, /* Enable strict null checks. */
31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | // "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true, /* Skip type checking of declaration files. */
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | },
69 | "include": ["./src"]
70 | }
71 |
--------------------------------------------------------------------------------
/mobile/src/pages/CreateOrphanage/OrphanageData.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ScrollView, View, StyleSheet, Switch, Text, TextInput, TouchableOpacity, Image } from 'react-native';
3 | import { Feather } from '@expo/vector-icons';
4 | import { RectButton } from 'react-native-gesture-handler';
5 | import { useNavigation, useRoute } from '@react-navigation/native';
6 | import * as ImagePicker from 'expo-image-picker';
7 | import api from '../../services/api';
8 |
9 | interface OrphanageDataRoutesParams {
10 | position: {
11 | latitude: number;
12 | longitude: number;
13 | };
14 | };
15 |
16 | export default function OrphanageData() {
17 | const [name, setName] = useState('');
18 | const [about, setAbout] = useState('');
19 | const [instructions, setInstructions] = useState('');
20 | const [openning_hours, setOpenningHours] = useState('');
21 | const [open_on_weekends, setOpenOnWeekends] = useState(true);
22 | const [images, setImages] = useState([]);
23 |
24 | const navigation = useNavigation()
25 | const route = useRoute();
26 | const { position: { latitude, longitude } } = route.params as OrphanageDataRoutesParams;
27 |
28 | async function selectImages() {
29 | const { status } = await ImagePicker.requestCameraRollPermissionsAsync();
30 |
31 | if (status !== 'granted') {
32 | alert('É necessario de permissão para adicionar imagens');
33 | return;
34 | };
35 |
36 | const result = await ImagePicker.launchImageLibraryAsync({
37 | allowsEditing: true,
38 | quality: 1,
39 | mediaTypes: ImagePicker.MediaTypeOptions.Images
40 | })
41 |
42 | if (result.cancelled)
43 | return;
44 |
45 | const { uri: image } = result;
46 |
47 | setImages([ ...images, image ])
48 | };
49 |
50 | async function createOrphanage() {
51 | const data = new FormData();
52 | data.append('name', name);
53 | data.append('about', about);
54 | data.append('instructions', instructions);
55 | data.append('latitude', String(latitude));
56 | data.append('longitude', String(longitude));
57 | data.append('openning_hours', openning_hours);
58 | data.append('open_on_weekends', String(open_on_weekends));
59 |
60 | images.forEach((image, i) => {
61 | data.append('images', {
62 | type: 'image/jpg',
63 | uri: image,
64 | name: `image_${i}.jpg`
65 |
66 | } as any);
67 | });
68 |
69 | await api.post('orphanages', data);
70 |
71 | navigation.navigate('OrphanagesMap');
72 | }
73 |
74 | return (
75 |
76 | Dados
77 |
78 | Nome
79 |
84 |
85 | Sobre
86 |
92 |
93 | {/* Whatsapp
94 | */}
97 |
98 | Fotos
99 |
100 |
101 | {images.map(image => (
102 |
103 | ))}
104 |
105 |
106 |
107 |
108 |
109 |
110 | Visitação
111 |
112 | Instruções
113 |
119 |
120 | Horario de visitas
121 |
126 |
127 |
128 | Atende final de semana?
129 |
135 |
136 |
137 |
138 | Cadastrar
139 |
140 |
141 | )
142 | }
143 |
144 | const styles = StyleSheet.create({
145 | container: {
146 | flex: 1,
147 | },
148 |
149 | title: {
150 | color: '#5c8599',
151 | fontSize: 24,
152 | fontFamily: 'Nunito_700Bold',
153 | marginBottom: 32,
154 | paddingBottom: 24,
155 | borderBottomWidth: 0.8,
156 | borderBottomColor: '#D3E2E6'
157 | },
158 |
159 | label: {
160 | color: '#8fa7b3',
161 | fontFamily: 'Nunito_600SemiBold',
162 | marginBottom: 8,
163 | },
164 |
165 | comment: {
166 | fontSize: 11,
167 | color: '#8fa7b3',
168 | },
169 |
170 | input: {
171 | backgroundColor: '#fff',
172 | borderWidth: 1.4,
173 | borderColor: '#d3e2e6',
174 | borderRadius: 20,
175 | height: 56,
176 | paddingVertical: 18,
177 | paddingHorizontal: 24,
178 | marginBottom: 16,
179 | textAlignVertical: 'top',
180 | },
181 |
182 | uploadedImagesContainer: {
183 | flexDirection: 'row',
184 | },
185 |
186 | uploadedImage: {
187 | width: 64,
188 | height: 64,
189 | borderRadius: 20,
190 | marginBottom: 32,
191 | marginRight: 8,
192 | },
193 |
194 | imagesInput: {
195 | backgroundColor: 'rgba(255, 255, 255, 0.5)',
196 | borderStyle: 'dashed',
197 | borderColor: '#96D2F0',
198 | borderWidth: 1.4,
199 | borderRadius: 20,
200 | height: 56,
201 | justifyContent: 'center',
202 | alignItems: 'center',
203 | marginBottom: 32,
204 | },
205 |
206 | switchContainer: {
207 | flexDirection: 'row',
208 | alignItems: 'center',
209 | justifyContent: 'space-between',
210 | marginTop: 16,
211 | },
212 |
213 | nextButton: {
214 | backgroundColor: '#15c3d6',
215 | borderRadius: 20,
216 | justifyContent: 'center',
217 | alignItems: 'center',
218 | height: 56,
219 | marginTop: 32,
220 | },
221 |
222 | nextButtonText: {
223 | fontFamily: 'Nunito_800ExtraBold',
224 | fontSize: 16,
225 | color: '#FFF',
226 | }
227 | })
--------------------------------------------------------------------------------
/backend/src/controllers/OrphanagesController.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 | import { getRepository } from 'typeorm';
3 | import Orphanage from '../models/Orphanage';
4 | import Image from '../models/Image';
5 | import OrphanageView from '../views/orphanages_view';
6 | import * as Yup from 'yup';
7 |
8 | export default {
9 | async index(req: Request, res: Response) {
10 | const { filter } = req.query;
11 |
12 | try {
13 | const orphanagesRepository = getRepository(Orphanage);
14 |
15 | if (filter) {
16 | let orphanages;
17 | switch (filter) {
18 | case 'not_approved':
19 | orphanages = await orphanagesRepository.find({ approved: false })
20 | return res.status(200).json({ orphanages });
21 | case 'approved':
22 | orphanages = await orphanagesRepository.find({ approved: true });
23 | return res.status(200).json({ orphanages });
24 | }
25 | }
26 |
27 | const orphanages = await orphanagesRepository.find({
28 | relations: ['images']
29 | });
30 |
31 | return res.json(OrphanageView.renderMany(orphanages));
32 | } catch (err) {
33 | console.log('[Error on index orphanages] -> ', err);
34 | return res.status(500).json({ error: 'Internal server error' });
35 | }
36 | },
37 |
38 | async show(req: Request, res: Response) {
39 | const { id } = req.params;
40 |
41 | try {
42 | const orphanagesRepository = getRepository(Orphanage);
43 |
44 | const orphanage = await orphanagesRepository.findOneOrFail(id, {
45 | relations: ['images']
46 | });
47 |
48 | return res.status(200).json({ orphanage: OrphanageView.render(orphanage) });
49 | } catch (err) {
50 | console.log('[Error on show orphanage] -> ', err);
51 | return res.status(500).json({ error: 'Internal Server Error' })
52 | };
53 | },
54 |
55 | async create(req: Request, res: Response) {
56 | const {
57 | name,
58 | latitude,
59 | longitude,
60 | about,
61 | instructions,
62 | openning_hours,
63 | open_on_weekends,
64 | contact,
65 | } = req.body;
66 |
67 | try {
68 | const orphanagesRepository = getRepository(Orphanage);
69 |
70 | // Getting images file
71 | const requestImages = req.files as Express.Multer.File[];
72 | const images = requestImages.map(image => {
73 | return { path: image.filename }
74 | });
75 |
76 | const data = {
77 | name,
78 | latitude,
79 | longitude,
80 | about,
81 | instructions,
82 | openning_hours,
83 | open_on_weekends: open_on_weekends === 'true',
84 | contact,
85 | approved: false, // When creating the value it will always be false.
86 | images,
87 | };
88 |
89 | // Validating the data
90 | const schema = Yup.object().shape({
91 | name: Yup.string().required(),
92 | latitude: Yup.number().required(),
93 | longitude: Yup.number().required(),
94 | about: Yup.string().required().max(300),
95 | instructions: Yup.string().required(),
96 | openning_hours: Yup.string().required(),
97 | open_on_weekends: Yup.boolean().required(),
98 | contact: Yup.string().required(),
99 | approved: Yup.boolean().required(),
100 | images: Yup.array(Yup.object().shape({
101 | path: Yup.string().required()
102 | })),
103 | });
104 | await schema.validate(data, {
105 | abortEarly: false,
106 | });
107 |
108 | // Creating, saving and sending the orphanage.
109 | const orphanage = orphanagesRepository.create(data);
110 | await orphanagesRepository.save(orphanage);
111 |
112 | return res.status(200).json({ orphanage: OrphanageView.render(orphanage) });
113 | } catch (err) {
114 | console.log('[Error on create orphanage] -> ', err);
115 | return res.status(500).json({ error: 'Internal Server Error' });
116 | }
117 | },
118 |
119 | async delete(req: Request, res: Response) {
120 | const { id } = req.params;
121 |
122 | try {
123 | const orphanagesRepository = getRepository(Orphanage);
124 | const imagesRepository = getRepository(Image);
125 |
126 | // Deleting images related to the orphanage and deleting the orphanage.
127 | await imagesRepository.delete({ orphanage: { id: Number(id) } });
128 | const orphanage = await orphanagesRepository.delete(id);
129 |
130 | return res.status(200).json({ orphanage });
131 | } catch (err) {
132 | console.log('[Error on delete orphanage] -> ', err);
133 | return res.status(500).json({ error: 'Internal Server Error' });
134 | };
135 | },
136 |
137 | async update(req: Request, res: Response) {
138 | const { id } = req.params;
139 | const {
140 | name,
141 | latitude,
142 | longitude,
143 | about,
144 | instructions,
145 | openning_hours,
146 | open_on_weekends,
147 | contact,
148 | images_id,
149 | approved,
150 | } = req.body;
151 |
152 | try {
153 | const orphanagesRepository = getRepository(Orphanage);
154 | const imagesRepository = getRepository(Image);
155 |
156 | const requestImages = req.files as Express.Multer.File[];
157 | const images = requestImages.map(image => {
158 | // defining the images with the relation of your orphanage -> orphanage_id. To save correctly.
159 | return {
160 | path: image.filename,
161 | orphanage: {
162 | id: Number(id),
163 | },
164 | };
165 | });
166 |
167 | // values of imagesIdArray is ["", ...id].
168 | const imagesIdArray = images_id.split(' ');
169 |
170 | // Excluding images required to delete.
171 | if (imagesIdArray.length > 1) {
172 | imagesIdArray.forEach(async (id: string, i: number) => {
173 | if (i !== 0) { // 0 === [""]
174 | await imagesRepository.delete(id);
175 | };
176 | });
177 | };
178 |
179 | // Adding the new images.
180 | if (images.length) {
181 | const newImages = await imagesRepository.create(images);
182 | await imagesRepository.save(newImages);
183 | };
184 |
185 | const orphanage = await orphanagesRepository.update(id, {
186 | name,
187 | latitude,
188 | longitude,
189 | about,
190 | instructions,
191 | openning_hours,
192 | open_on_weekends: open_on_weekends === 'true',
193 | contact,
194 | approved: approved === 'true',
195 | });
196 |
197 | return res.status(200).json({ orphanage });
198 | } catch (err) {
199 | console.log('[Error on update orphanage] -> ', err)
200 | return res.status(500).json({ error: 'Internal Server Error' });
201 | }
202 | },
203 | };
204 |
--------------------------------------------------------------------------------
/web/src/pages/Private/Dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useEffect } from 'react';
2 | import { useHistory } from 'react-router-dom';
3 |
4 | import { MiniMap, Sidebar } from '../../../components';
5 |
6 | import {
7 | Container,
8 | Content,
9 | OrphanagesContainer,
10 | OrphanagesContent,
11 | TitleContainer,
12 | Title,
13 | OrphanagesTotal,
14 | NotFoundContainer,
15 | NotFoundContent,
16 | NotFoundMessage,
17 | DeletedContainer,
18 | DeletedInfo,
19 | DeletedTitle,
20 | DeletedSubtitle,
21 | DeletedButton,
22 | DeletedImage,
23 | } from './styles';
24 | import { FiMapPin, FiPower, FiAlertCircle } from 'react-icons/fi';
25 | import notFound from '../../../images/icons/not-found.svg';
26 | import Deleted from '../../../images/icons/deleted.svg';
27 |
28 | import { UserContext } from '../../../contexts/UserContext';
29 |
30 | import api from '../../../services/api';
31 |
32 | interface Orphanage {
33 | id: number;
34 | name: string;
35 | latitude: number;
36 | longitude: number;
37 | };
38 |
39 | function Dashboard() {
40 | const [orphanagesContainer, setOrphanagesContainer] = useState(0); // 0 -> registered orphanages, 1 -> pending entries
41 | const [orphanages, setOrphanages] = useState([]);
42 | const [orphanagesNotApproved, setOrphanagesNotApproved] = useState([]);
43 |
44 | const { setUser } = useContext(UserContext);
45 | const history = useHistory()
46 |
47 | const [deletedContainer, setDeletedContainer] = useState(false);
48 |
49 | useEffect(() => {
50 | api.get('/orphanages?filter=approved')
51 | .then(response => { setOrphanages(response.data.orphanages) });
52 | }, []);
53 |
54 | useEffect(() => {
55 | api.get('/orphanages?filter=not_approved')
56 | .then(response => { setOrphanagesNotApproved(response.data.orphanages); })
57 | }, [])
58 |
59 | function EditOrphanage(id: number) {
60 | history.push(`/orphanages/edit/${id}`);
61 | };
62 |
63 | async function deleteOrphanage(id: number) {
64 | await api.delete(`/orphanages/${id}`);
65 | setDeletedContainer(true);
66 |
67 | const currentOrphanages = orphanages.filter(orphanage => orphanage.id !== id ? orphanage : null);
68 | setOrphanages(currentOrphanages);
69 | };
70 |
71 | function ApproveOrphanage(id: number) {
72 | history.push(`/orphanages/approve/${id}`)
73 | };
74 |
75 | function switchContainer() {
76 | if (orphanagesContainer === 0) {
77 | setOrphanagesContainer(1);
78 | return;
79 | };
80 |
81 | setOrphanagesContainer(0);
82 | };
83 |
84 | function logout() {
85 | setUser!(undefined);
86 | history.push('/');
87 | };
88 |
89 | return (
90 | deletedContainer ?
91 | (
92 |
93 |
94 | Excluído
95 | Orfanato deletado
96 | setDeletedContainer(false)}>Voltar
97 |
98 |
99 |
100 | )
101 | :
102 | (
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | {orphanagesContainer === 0 ?
121 | (
122 |
123 |
124 |
125 | Orfanatos cadastrados
126 |
127 | {orphanages.length} orfanatos
128 |
129 |
130 | {orphanages.map(orphanage => (
131 | EditOrphanage(orphanage.id)}
137 | Delete={() => deleteOrphanage(orphanage.id)}
138 | />
139 | ))}
140 |
141 |
142 | )
143 | :
144 | (
145 |
146 |
147 | Cadastros pendentes
148 |
149 | {orphanagesNotApproved.length ? orphanagesNotApproved.length + 'orfanatos' : ''}
150 |
151 |
152 | {orphanagesNotApproved.length ?
153 |
154 | {orphanagesNotApproved.map(orphanage => (
155 | ApproveOrphanage(orphanage.id)}
161 | />
162 | ))}
163 |
164 | :
165 |
166 |
167 |
168 | Nenhum no momento
169 |
170 |
171 | }
172 |
173 |
174 | )
175 | }
176 |
177 |
178 | )
179 | );
180 | };
181 |
182 | export default Dashboard;
183 |
--------------------------------------------------------------------------------
/mobile/src/pages/OrphanageDetails.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Image, View, ScrollView, Text, StyleSheet, Dimensions, Linking } from 'react-native';
3 | import MapView, { Marker } from 'react-native-maps';
4 | import { Feather, FontAwesome } from '@expo/vector-icons';
5 | import { useRoute } from '@react-navigation/native';
6 |
7 | import mapMarkerImg from '../images/map-marker.png';
8 | // import { RectButton } from 'react-native-gesture-handler';
9 |
10 | import api from '../services/api';
11 | import { TouchableOpacity } from 'react-native';
12 |
13 | interface OprhanageDetailsRouteParams {
14 | id: number;
15 | };
16 |
17 | interface Orphanage {
18 | id: number;
19 | name: string;
20 | latitude: number;
21 | longitude: number;
22 | about: string;
23 | instructions: string;
24 | openning_hours: string;
25 | open_on_weekends: boolean;
26 | images: Array<{
27 | id: number;
28 | url: string;
29 | }>;
30 | };
31 |
32 | export default function OrphanageDetails() {
33 | const [orphanage, setOrphanage] = useState();
34 | const route = useRoute();
35 |
36 | const params = route.params as OprhanageDetailsRouteParams;
37 |
38 | function goToGoogleMaps() {
39 | Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${orphanage?.latitude},${orphanage?.longitude}`)
40 | }
41 |
42 | useEffect(() => {
43 | api.get(`/orphanages/${params.id}`)
44 | .then(resp => {
45 | setOrphanage(resp.data);
46 | });
47 | }, [params.id]);
48 |
49 | if (!orphanage) {
50 | return (
51 |
52 | Carregando...
53 |
54 | )
55 | }
56 |
57 | return (
58 |
59 |
60 |
61 | {orphanage.images.map(image => (
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 | {orphanage.name}
70 |
71 |
72 | {orphanage.about}
73 |
74 |
75 |
76 |
89 |
96 |
97 |
98 |
99 | Ver rotas no Google Maps
100 |
101 |
102 |
103 |
104 |
105 | Instruções para visita
106 | {orphanage.instructions}
107 |
108 |
109 |
110 |
111 |
112 | {orphanage.openning_hours}
113 |
114 |
115 | {orphanage.open_on_weekends ?
116 |
117 |
118 |
119 | Atendemos fim de semana
120 |
121 |
122 | :
123 |
124 |
125 |
126 | Não atendemos fim de semana
127 |
128 |
129 | }
130 |
131 |
132 |
133 | {/* { }}>
134 |
135 | Entrar em contato
136 | */}
137 |
138 |
139 | )
140 | }
141 |
142 | const styles = StyleSheet.create({
143 | container: {
144 | flex: 1,
145 | },
146 |
147 | imagesContainer: {
148 | height: 240,
149 | },
150 |
151 | image: {
152 | width: Dimensions.get('window').width,
153 | height: 240,
154 | resizeMode: 'cover',
155 | },
156 |
157 | detailsContainer: {
158 | padding: 24,
159 | },
160 |
161 | title: {
162 | color: '#4D6F80',
163 | fontSize: 30,
164 | fontFamily: 'Nunito_700Bold',
165 | },
166 |
167 | description: {
168 | fontFamily: 'Nunito_600SemiBold',
169 | color: '#5c8599',
170 | lineHeight: 24,
171 | marginTop: 16,
172 | },
173 |
174 | mapContainer: {
175 | borderRadius: 20,
176 | overflow: 'hidden',
177 | borderWidth: 1.2,
178 | borderColor: '#B3DAE2',
179 | marginTop: 40,
180 | backgroundColor: '#E6F7FB',
181 | },
182 |
183 | mapStyle: {
184 | width: '100%',
185 | height: 150,
186 | },
187 |
188 | routesContainer: {
189 | padding: 16,
190 | alignItems: 'center',
191 | justifyContent: 'center',
192 | },
193 |
194 | routesText: {
195 | fontFamily: 'Nunito_700Bold',
196 | color: '#0089a5'
197 | },
198 |
199 | separator: {
200 | height: 0.8,
201 | width: '100%',
202 | backgroundColor: '#D3E2E6',
203 | marginVertical: 40,
204 | },
205 |
206 | scheduleContainer: {
207 | marginTop: 24,
208 | flexDirection: 'row',
209 | justifyContent: 'space-between'
210 | },
211 |
212 | scheduleItem: {
213 | width: '48%',
214 | padding: 20,
215 | },
216 |
217 | scheduleItemBlue: {
218 | backgroundColor: '#E6F7FB',
219 | borderWidth: 1,
220 | borderColor: '#B3DAE2',
221 | borderRadius: 20,
222 | },
223 |
224 | scheduleItemGreen: {
225 | backgroundColor: '#EDFFF6',
226 | borderWidth: 1,
227 | borderColor: '#A1E9C5',
228 | borderRadius: 20,
229 | },
230 |
231 | scheduleItemRed: {
232 | backgroundColor: '#FEF6F9',
233 | borderWidth: 1,
234 | borderColor: '#FFBCD4',
235 | borderRadius: 20,
236 | },
237 |
238 | scheduleText: {
239 | fontFamily: 'Nunito_600SemiBold',
240 | fontSize: 16,
241 | lineHeight: 24,
242 | marginTop: 20,
243 | },
244 |
245 | scheduleTextBlue: {
246 | color: '#5C8599'
247 | },
248 |
249 | scheduleTextGreen: {
250 | color: '#37C77F'
251 | },
252 |
253 | scheduleTextRed: {
254 | color: '#FF669D'
255 | },
256 |
257 | contactButton: {
258 | backgroundColor: '#3CDC8C',
259 | borderRadius: 20,
260 | flexDirection: 'row',
261 | justifyContent: 'center',
262 | alignItems: 'center',
263 | height: 56,
264 | marginTop: 40,
265 | },
266 |
267 | contactButtonText: {
268 | fontFamily: 'Nunito_800ExtraBold',
269 | color: '#FFF',
270 | fontSize: 16,
271 | marginLeft: 16,
272 | }
273 | })
--------------------------------------------------------------------------------
/web/src/pages/CreateOrphanage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, FormEvent, ChangeEvent, useEffect } from "react";
2 | import { useHistory } from "react-router-dom";
3 | import api from '../../services/api';
4 |
5 | import { Map, Marker, TileLayer } from 'react-leaflet';
6 |
7 | import { FiArrowLeft, FiPlus, FiX } from "react-icons/fi";
8 | import { MapIcon } from '../../utils/MapIcon';
9 | import created from '../../images/icons/created.svg';
10 |
11 | import {
12 | ConfirmButton,
13 | ConfirmContainer,
14 | ConfirmImage,
15 | ConfirmInfo,
16 | ConfirmSubtitle,
17 | ConfirmTitle,
18 | Container,
19 | Inner,
20 | } from './styles';
21 | import { Sidebar, Form } from '../../components';
22 |
23 | export default function CreateOrphanage() {
24 | const [position, setPosition] = useState({ latitude: 0, longitude: 0 });
25 | const history = useHistory();
26 | const { goBack } = useHistory();
27 |
28 | const [name, setName] = useState('');
29 | const [about, setAbout] = useState('');
30 | const [contact, setContact] = useState('');
31 | const [instructions, setInstructions] = useState('');
32 | const [openning_hours, setOpenningHours] = useState('');
33 | const [open_on_weekends, setOpenOnWeekends] = useState(true);
34 | const [images, setImages] = useState([]);
35 | const [imagesPreview, setImagesPreview] = useState([]);
36 |
37 | const [confirm, setConfirm] = useState(false);
38 |
39 | useEffect(() => {
40 | navigator.geolocation.getCurrentPosition(position => {
41 | setPosition({ latitude: position.coords.latitude, longitude: position.coords.longitude })
42 | });
43 | }, [])
44 |
45 | function handleMapClick(e: any) {
46 | const { lat, lng } = e.latlng;
47 | setPosition({ latitude: lat, longitude: lng });
48 | };
49 |
50 | function handleSelectImages(e: ChangeEvent) {
51 | if (!e.target.files) return;
52 |
53 | const selectedImages = Array.from(e.target.files);
54 | setImages(selectedImages);
55 |
56 | const selectImagesPreview = selectedImages.map(image => {
57 | return URL.createObjectURL(image)
58 | });
59 |
60 | setImagesPreview(selectImagesPreview);
61 | };
62 |
63 | function handleRemoveImage(i: number) {
64 | const currentFileImages = images.filter((image, index) => i === index ? null : image)
65 | const currentImages = imagesPreview.filter((image, index) => i === index ? null : image);
66 |
67 | setImages(currentFileImages)
68 | setImagesPreview(currentImages);
69 | };
70 |
71 | async function handleSubmit(e: FormEvent) {
72 | e.preventDefault();
73 |
74 | const { latitude, longitude } = position;
75 |
76 | const data = new FormData();
77 | data.append('name', name);
78 | data.append('about', about);
79 | data.append('contact', contact);
80 | data.append('instructions', instructions);
81 | data.append('latitude', String(latitude));
82 | data.append('longitude', String(longitude));
83 | data.append('openning_hours', openning_hours);
84 | data.append('open_on_weekends', String(open_on_weekends));
85 |
86 | images.forEach(image => {
87 | data.append('images', image);
88 | });
89 |
90 | await api.post('orphanages', data)
91 | .then(resp => {
92 | setConfirm(true)
93 | });
94 | };
95 |
96 | function goToMap() {
97 | history.push('/app')
98 | }
99 |
100 | return (
101 | confirm ?
102 | (
103 |
104 |
105 |
106 | Ebaaa!
107 |
108 | O cadastro deu certo e foi enviado ao administrador para ser aprovado. Agora é só esperar :)
109 |
110 | Voltar para o mapa
111 |
112 |
113 |
114 |
115 | )
116 | :
117 | (
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
133 | Dados
134 |
135 |
151 |
152 |
153 | Nome
154 | setName(e.target.value)} />
155 |
156 |
157 |
158 |
159 | Sobre
160 | Máximo de 300 caracteres
161 |
162 | setAbout(e.target.value)} />
163 |
164 |
165 |
166 | Número de whatsapp
167 | setContact(e.target.value)} />
168 |
169 |
170 |
171 | Fotos
172 |
173 |
174 | {imagesPreview.map((image, i) => {
175 | return (
176 |
177 |
178 | handleRemoveImage(i)}>
179 |
180 |
181 |
182 |
183 |
184 | )
185 | })}
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | Visitação
198 |
199 |
200 | Instruções
201 | setInstructions(e.target.value)} />
202 |
203 |
204 |
205 | Horário de funcionamento
206 | setOpenningHours(e.target.value)} />
207 |
208 |
209 |
210 | Atende fim de semana
211 |
212 |
213 | setOpenOnWeekends(true)}
217 | >
218 | Sim
219 |
220 | setOpenOnWeekends(false)}
224 | >
225 | Não
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | Confirmar
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | )
242 | );
243 | }
244 |
245 | // return `https://a.tile.openstreetmap.org/${z}/${x}/${y}.png`;
246 |
--------------------------------------------------------------------------------
/web/src/pages/Private/EditOrphanage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useParams, useHistory } from 'react-router-dom';
3 | import { Map, TileLayer, Marker } from 'react-leaflet';
4 | import api from '../../../services/api';
5 |
6 | import { Sidebar, Form } from '../../../components';
7 | import {
8 | Container,
9 | Content
10 | } from './styles';
11 | import { FiArrowLeft, FiPlus, FiX } from 'react-icons/fi';
12 | import { MapIcon } from '../../../utils/MapIcon';
13 |
14 | function EditOrphanage() {
15 | const params = useParams<{ id: string }>();
16 | const { goBack } = useHistory();
17 |
18 | const [name, setName] = useState('');
19 | const [about, setAbout] = useState('');
20 | const [contact, setContact] = useState('');
21 | const [instructions, setInstructions] = useState('');
22 | const [openning_hours, setOpenningHours] = useState('');
23 | const [open_on_weekends, setOpenOnWeekends] = useState(true);
24 |
25 | const [position, setPosition] = useState({ latitude: 0, longitude: 0 });
26 |
27 | const [images, setImages] = useState([]);
28 | const [imagesPreview, setImagesPreview] = useState<{ url: string; id?: number }[]>([]);
29 | const [images_id, setImagesId] = useState('');
30 |
31 | useEffect(() => {
32 | api.get(`/orphanages/${params.id}`)
33 | .then(response => {
34 | const {
35 | name,
36 | about,
37 | instructions,
38 | contact,
39 | openning_hours,
40 | open_on_weekends,
41 | images,
42 | latitude,
43 | longitude,
44 | } = response.data.orphanage;
45 |
46 | setName(name);
47 | setAbout(about);
48 | setInstructions(instructions);
49 | setContact(contact);
50 | setPosition({ latitude, longitude })
51 | setOpenningHours(openning_hours);
52 | setOpenOnWeekends(open_on_weekends);
53 |
54 | setImagesPreview(images);
55 | });
56 | }, [params.id]);
57 |
58 | async function handleSubmit(e: React.FormEvent) {
59 | e.preventDefault();
60 |
61 | const { latitude, longitude } = position;
62 |
63 | const data = new FormData();
64 | data.append('name', name);
65 | data.append('about', about);
66 | data.append('contact', contact);
67 | data.append('instructions', instructions);
68 | data.append('latitude', String(latitude));
69 | data.append('longitude', String(longitude));
70 | data.append('openning_hours', openning_hours);
71 | data.append('open_on_weekends', String(open_on_weekends));
72 | data.append('images_id', images_id);
73 | data.append('approved', String(true));
74 |
75 | images.forEach(image => {
76 | data.append('images', image);
77 | });
78 |
79 | await api.put(`/orphanages/${params.id}`, data)
80 | .then(response => {
81 | goBack();
82 | });
83 | };
84 |
85 | function handleMapClick(e: any) {
86 | const { lat, lng } = e.latlng;
87 | setPosition({ latitude: lat, longitude: lng });
88 | };
89 |
90 | function handleSelectImages(e: React.ChangeEvent) {
91 | if (!e.target.files) return;
92 |
93 | const selectedImages = Array.from(e.target.files);
94 | setImages([...images, ...selectedImages]);
95 |
96 | const selectImagesPreview = selectedImages.map(image => {
97 | return {
98 | url: URL.createObjectURL(image)
99 | }
100 | });
101 |
102 | setImagesPreview([...imagesPreview, ...selectImagesPreview]);
103 | };
104 |
105 | function handleRemoveImage(i: number, imageIndex?: number) {
106 | if (imageIndex) {
107 | const newString = images_id + ' ' + imageIndex
108 | setImagesId(newString);
109 | };
110 |
111 | const currentImages = imagesPreview.filter((image, index) => i === index ? null : image);
112 | setImagesPreview(currentImages);
113 | };
114 |
115 | return (
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
131 | Dados
132 |
133 |
149 |
150 |
151 | Nome
152 | setName(e.target.value)} />
153 |
154 |
155 |
156 |
157 | Sobre
158 | Máximo de 300 caracteres
159 |
160 | setAbout(e.target.value)} />
161 |
162 |
163 |
164 | Número de whatsapp
165 | setContact(e.target.value)} />
166 |
167 |
168 |
169 | Fotos
170 |
171 |
172 | {imagesPreview.map((image: { id?: number, url: string }, i) => {
173 | return (
174 |
175 |
176 | handleRemoveImage(i, image?.id)}>
177 |
178 |
179 |
180 |
181 |
182 | )
183 | })}
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | Visitação
198 |
199 |
200 | Instruções
201 | setInstructions(e.target.value)} />
202 |
203 |
204 |
205 | Horário de funcionamento
206 | setOpenningHours(e.target.value)} />
207 |
208 |
209 |
210 | Atende fim de semana
211 |
212 |
213 | setOpenOnWeekends(true)}
217 | >
218 | Sim
219 |
220 | setOpenOnWeekends(false)}
224 | >
225 | Não
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | Confirmar
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | )
242 | }
243 |
244 | export default EditOrphanage;
245 |
--------------------------------------------------------------------------------
/web/src/pages/Private/ApproveOrphanage/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useHistory, useParams } from 'react-router-dom';
3 | import { Map, TileLayer, Marker } from 'react-leaflet';
4 |
5 | import api from '../../../services/api';
6 |
7 | import { Sidebar, Form } from '../../../components';
8 | import {
9 | Container,
10 | Content
11 | } from './styles';
12 | import { FiArrowLeft, FiPlus, FiX, FiXCircle, FiCheck } from 'react-icons/fi';
13 | import { MapIcon } from '../../../utils/MapIcon';
14 |
15 | function ApproveOrphanage() {
16 | const params = useParams<{ id: string }>();
17 | const history = useHistory();
18 |
19 | const [name, setName] = useState('');
20 | const [about, setAbout] = useState('');
21 | const [contact, setContact] = useState('');
22 | const [instructions, setInstructions] = useState('');
23 | const [openning_hours, setOpenningHours] = useState('');
24 | const [open_on_weekends, setOpenOnWeekends] = useState(true);
25 |
26 | const [position, setPosition] = useState({ latitude: 0, longitude: 0 });
27 |
28 | const [images, setImages] = useState([]);
29 | const [imagesPreview, setImagesPreview] = useState<{ url: string; id?: number }[]>([]);
30 | const [images_id, setImagesId] = useState('');
31 |
32 | useEffect(() => {
33 | api.get(`/orphanages/${params.id}`)
34 | .then(response => {
35 | const {
36 | name,
37 | about,
38 | instructions,
39 | contact,
40 | openning_hours,
41 | open_on_weekends,
42 | images,
43 | latitude,
44 | longitude,
45 | } = response.data.orphanage;
46 |
47 | setPosition({ latitude, longitude });
48 |
49 | setName(name);
50 | setAbout(about);
51 | setInstructions(instructions);
52 | setContact(contact);
53 | setOpenningHours(openning_hours);
54 | setOpenOnWeekends(open_on_weekends);
55 |
56 | setImagesPreview(images);
57 | });
58 | }, [params.id]);
59 |
60 | async function approveOrphanage() {
61 | const { latitude, longitude } = position;
62 |
63 | const data = new FormData();
64 | data.append('name', name);
65 | data.append('about', about);
66 | data.append('contact', contact);
67 | data.append('instructions', instructions);
68 | data.append('latitude', String(latitude));
69 | data.append('longitude', String(longitude));
70 | data.append('openning_hours', openning_hours);
71 | data.append('open_on_weekends', String(open_on_weekends));
72 | data.append('images_id', images_id);
73 | data.append('approved', String(true));
74 |
75 | images.forEach(image => {
76 | data.append('images', image);
77 | });
78 |
79 | await api.put(`/orphanages/${params.id}`, data)
80 | .then(response => {
81 | history.goBack();
82 | });
83 | };
84 |
85 | async function refuseOrphanage() {
86 | await api.delete(`/orphanages/${params.id}`);
87 |
88 | await history.goBack();
89 | };
90 |
91 | function handleMapClick(e: any) {
92 | const { lat, lng } = e.latlng;
93 | setPosition({ latitude: lat, longitude: lng });
94 | };
95 |
96 | function handleSelectImages(e: React.ChangeEvent) {
97 | if (!e.target.files) return;
98 |
99 | const selectedImages = Array.from(e.target.files);
100 | setImages([...images, ...selectedImages]);
101 |
102 | const selectImagesPreview = selectedImages.map(image => {
103 | return {
104 | url: URL.createObjectURL(image)
105 | }
106 | });
107 |
108 | setImagesPreview([...imagesPreview, ...selectImagesPreview]);
109 | };
110 |
111 | function handleRemoveImage(i: number, imageIndex?: number) {
112 | if (imageIndex) {
113 | const newString = images_id + ' ' + imageIndex
114 | setImagesId(newString);
115 | };
116 |
117 | const currentImages = imagesPreview.filter((image, index) => i === index ? null : image);
118 | setImagesPreview(currentImages);
119 | };
120 |
121 | return (
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
137 | Dados
138 |
139 |
159 |
160 |
161 | Nome
162 | setName(e.target.value)} />
163 |
164 |
165 |
166 |
167 | Sobre
168 | Máximo de 300 caracteres
169 |
170 | setAbout(e.target.value)} />
171 |
172 |
173 |
174 | Número de whatsapp
175 | setContact(e.target.value)} />
176 |
177 |
178 |
179 | Fotos
180 |
181 |
182 | {imagesPreview?.map((image: { id?: number, url: string }, i) => {
183 | return (
184 |
185 |
186 | handleRemoveImage(i, image?.id)}>
187 |
188 |
189 |
190 |
191 |
192 | )
193 | })}
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | Visitação
208 |
209 |
210 | Instruções
211 | setInstructions(e.target.value)} />
212 |
213 |
214 |
215 | Horário de funcionamento
216 | setOpenningHours(e.target.value)} />
217 |
218 |
219 |
220 | Atende fim de semana
221 |
222 |
223 | setOpenOnWeekends(true)}
227 | >
228 | Sim
229 |
230 | setOpenOnWeekends(false)}
234 | >
235 | Não
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 | Recusar
246 |
247 |
248 |
249 | Aceitar
250 |
251 |
252 |
253 |
254 |
255 |
256 | );
257 | };
258 |
259 | export default ApproveOrphanage;
260 |
--------------------------------------------------------------------------------
/web/src/images/icons/deleted.svg:
--------------------------------------------------------------------------------
1 |
31 |
--------------------------------------------------------------------------------