├── mobile ├── src │ ├── @types │ │ └── png.d.ts │ ├── assets │ │ ├── bug.png │ │ ├── idea.png │ │ ├── success.png │ │ └── thought.png │ ├── libs │ │ └── api.ts │ ├── components │ │ ├── Copyright │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Button │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Options │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── ScreenshotButton │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Option │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Widget │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ ├── Success │ │ │ ├── styles.ts │ │ │ └── index.tsx │ │ └── Form │ │ │ ├── styles.ts │ │ │ └── index.tsx │ ├── utils │ │ └── feedbackTypes.ts │ └── theme │ │ └── index.ts ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ └── adaptive-icon.png ├── tsconfig.json ├── .expo-shared │ └── assets.json ├── .gitignore ├── babel.config.js ├── app.json ├── App.tsx └── package.json ├── web ├── src │ ├── vite-env.d.ts │ ├── lib │ │ └── api.ts │ ├── App.tsx │ ├── styles │ │ └── global.css │ ├── main.tsx │ ├── components │ │ ├── Loading.tsx │ │ └── Widget │ │ │ ├── CloseButton.tsx │ │ │ ├── WidgetForm │ │ │ ├── Steps │ │ │ │ ├── FeedbackTypeStep.tsx │ │ │ │ ├── FeedbackSuccessStep.tsx │ │ │ │ └── FeedbackContentStep.tsx │ │ │ ├── ScreenshotButton.tsx │ │ │ └── index.tsx │ │ │ └── index.tsx │ └── assets │ │ ├── thought.svg │ │ ├── idea.svg │ │ └── bug.svg ├── README.md ├── postcss.config.js ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── index.html ├── tsconfig.json ├── package.json └── tailwind.config.js ├── .github └── cover.png ├── server ├── src │ ├── prisma.ts │ ├── adapters │ │ ├── mail-adapter.ts │ │ └── nodemailer │ │ │ └── nodemailer-mail-adapter.ts │ ├── repositories │ │ ├── feedbacks-repository.ts │ │ └── prisma │ │ │ └── prisma-feedbacks-repository.ts │ ├── server.ts │ ├── routes.ts │ └── use-cases │ │ ├── submit-feedback-use-case.ts │ │ └── submit-feedback-use-case.spec.ts ├── .gitignore ├── prisma │ ├── migrations │ │ ├── migration_lock.toml │ │ ├── 20220426175856_create_feedbacks │ │ │ └── migration.sql │ │ └── 20220426181218_add_type_to_feedbacks_table │ │ │ └── migration.sql │ └── schema.prisma ├── package.json ├── jest.config.ts └── tsconfig.json └── README.md /mobile/src/@types/png.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png"; -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | - TypeScript 2 | - Vite 3 | - Tailwind 4 | - Headless UI -------------------------------------------------------------------------------- /.github/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/.github/cover.png -------------------------------------------------------------------------------- /mobile/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/assets/icon.png -------------------------------------------------------------------------------- /mobile/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/assets/favicon.png -------------------------------------------------------------------------------- /mobile/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/assets/splash.png -------------------------------------------------------------------------------- /mobile/src/assets/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/src/assets/bug.png -------------------------------------------------------------------------------- /mobile/src/assets/idea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/src/assets/idea.png -------------------------------------------------------------------------------- /mobile/src/assets/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/src/assets/success.png -------------------------------------------------------------------------------- /mobile/src/assets/thought.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/src/assets/thought.png -------------------------------------------------------------------------------- /mobile/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orodrigogo/feedback-widget/HEAD/mobile/assets/adaptive-icon.png -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /web/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const api = axios.create({ 4 | baseURL: 'http://localhost:3333', 5 | }) -------------------------------------------------------------------------------- /mobile/src/libs/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const api = axios.create({ 4 | baseURL: 'http://192.168.0.117:3333' 5 | }); -------------------------------------------------------------------------------- /server/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const prisma = new PrismaClient({ 4 | log: ['query'], 5 | }); -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Widget } from "./components/Widget"; 2 | 3 | export function App() { 4 | return ( 5 | 6 | ) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | coverage 5 | build 6 | prisma/dev.db 7 | prisma/dev.db-journal -------------------------------------------------------------------------------- /server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /web/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply bg-[#09090A] text-zinc-100; 8 | } 9 | } -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /mobile/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /mobile/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /mobile/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['react-native-reanimated/plugin'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /server/src/adapters/mail-adapter.ts: -------------------------------------------------------------------------------- 1 | export interface SendMailData { 2 | subject: string; 3 | content: string; 4 | } 5 | 6 | export interface MailAdapter { 7 | sendMail(data: SendMailData): Promise; 8 | } -------------------------------------------------------------------------------- /server/src/repositories/feedbacks-repository.ts: -------------------------------------------------------------------------------- 1 | export interface CreateFeedbackData { 2 | type: string; 3 | screenshot?: string; 4 | comment: string; 5 | } 6 | 7 | export interface FeedbacksRepository { 8 | create(data: CreateFeedbackData): Promise; 9 | } -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { App } from './App' 4 | 5 | import './styles/global.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /mobile/src/components/Copyright/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | text: { 6 | fontSize: 12, 7 | color: theme.colors.text_secondary, 8 | fontFamily: theme.fonts.medium 9 | } 10 | }); -------------------------------------------------------------------------------- /web/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { CircleNotch } from "phosphor-react"; 2 | 3 | export function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /server/prisma/migrations/20220426175856_create_feedbacks/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Feedback" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "screenshot" TEXT, 5 | "comment" TEXT NOT NULL, 6 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" DATETIME NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import { routes } from './routes'; 4 | 5 | const app = express(); 6 | 7 | app.use(express.json()); 8 | app.use(cors()); 9 | 10 | app.use(routes); 11 | 12 | app.listen(3333, () => { 13 | console.log('HTTP server running!'); 14 | }); 15 | -------------------------------------------------------------------------------- /mobile/src/components/Copyright/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | 4 | import { styles } from './styles'; 5 | 6 | export function Copyright() { 7 | return ( 8 | 9 | 10 | Feito com ♥ pela Rocketseat 11 | 12 | 13 | ); 14 | } -------------------------------------------------------------------------------- /mobile/src/utils/feedbackTypes.ts: -------------------------------------------------------------------------------- 1 | export const feedbackTypes = { 2 | 'BUG': { 3 | title: 'Problema', 4 | image: require('../assets/bug.png') 5 | }, 6 | 'IDEA': { 7 | title: 'Ideia', 8 | image: require('../assets/idea.png'), 9 | }, 10 | 'OTHER': { 11 | title: 'Outro', 12 | image: require('../assets/thought.png'), 13 | }, 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .vercel 26 | -------------------------------------------------------------------------------- /server/src/repositories/prisma/prisma-feedbacks-repository.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../../prisma"; 2 | import { CreateFeedbackData, FeedbacksRepository } from "../feedbacks-repository"; 3 | 4 | export class PrismaFeedbacksRepository implements FeedbacksRepository { 5 | async create(data: CreateFeedbackData): Promise { 6 | await prisma.feedback.create({ 7 | data, 8 | }) 9 | } 10 | } -------------------------------------------------------------------------------- /server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = "file:./dev.db" 8 | } 9 | 10 | model Feedback { 11 | id String @id @default(uuid()) 12 | 13 | type String 14 | screenshot String? 15 | comment String 16 | 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @updatedAt 19 | 20 | @@index([type]) 21 | } 22 | -------------------------------------------------------------------------------- /mobile/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | colors: { 3 | brand: '#8257E5', 4 | background: '#09090A', 5 | 6 | surface_primary: '#18181B', 7 | surface_secondary: '#27272A', 8 | 9 | text_primary: '#F4F4F5', 10 | text_secondary: '#A1A1AA', 11 | text_on_brand_color: '#FFFFFF', 12 | 13 | stroke: '#52525B' 14 | }, 15 | 16 | fonts: { 17 | regular: 'Inter_400Regular', 18 | medium: 'Inter_500Medium', 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /mobile/src/components/Button/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | height: 40, 8 | backgroundColor: theme.colors.brand, 9 | alignItems: 'center', 10 | justifyContent: 'center', 11 | borderRadius: 4, 12 | }, 13 | title: { 14 | fontSize: 14, 15 | fontFamily: theme.fonts.medium, 16 | color: theme.colors.text_on_brand_color 17 | } 18 | }); -------------------------------------------------------------------------------- /mobile/src/components/Options/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | alignItems: 'center', 7 | }, 8 | options: { 9 | width: '100%', 10 | marginBottom: 48, 11 | flexDirection: 'row', 12 | justifyContent: 'center' 13 | }, 14 | title: { 15 | fontSize: 20, 16 | marginBottom: 32, 17 | fontFamily: theme.fonts.medium, 18 | color: theme.colors.text_primary 19 | } 20 | }); -------------------------------------------------------------------------------- /web/src/components/Widget/CloseButton.tsx: -------------------------------------------------------------------------------- 1 | import { Popover } from "@headlessui/react"; 2 | import { X } from "phosphor-react"; 3 | import { ButtonHTMLAttributes } from "react"; 4 | 5 | interface CloseButtonProps extends ButtonHTMLAttributes {} 6 | 7 | export function CloseButton(props: CloseButtonProps) { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /mobile/src/components/ScreenshotButton/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | width: 40, 7 | height: 40, 8 | borderRadius: 4, 9 | backgroundColor: theme.colors.surface_secondary, 10 | justifyContent: 'center', 11 | alignItems: 'center', 12 | marginRight: 8, 13 | position: 'relative' 14 | }, 15 | removeIcon: { 16 | position: 'absolute', 17 | bottom: 0, 18 | right: 0, 19 | }, 20 | image: { 21 | width: 40, 22 | height: 40, 23 | } 24 | }); -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Feedget 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /mobile/src/components/Option/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | width: 104, 7 | height: 112, 8 | justifyContent: 'center', 9 | alignItems: 'center', 10 | padding: 8, 11 | borderRadius: 8, 12 | marginHorizontal: 8, 13 | backgroundColor: theme.colors.surface_secondary 14 | }, 15 | image: { 16 | width: 40, 17 | height: 40, 18 | }, 19 | title: { 20 | fontSize: 14, 21 | marginTop: 8, 22 | fontFamily: theme.fonts.medium, 23 | color: theme.colors.text_primary 24 | } 25 | }); -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /mobile/src/components/Option/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | TouchableOpacity, 4 | TouchableOpacityProps, 5 | Image, 6 | ImageProps, 7 | Text 8 | } from 'react-native'; 9 | 10 | import { styles } from './styles'; 11 | 12 | interface Props extends TouchableOpacityProps { 13 | title: string; 14 | image: ImageProps; 15 | } 16 | 17 | export function Option({ title, image, ...rest }: Props) { 18 | return ( 19 | 23 | 27 | 28 | 29 | {title} 30 | 31 | 32 | ); 33 | } -------------------------------------------------------------------------------- /web/src/assets/thought.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /mobile/src/components/Widget/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { getBottomSpace } from 'react-native-iphone-x-helper'; 3 | 4 | import { theme } from '../../theme'; 5 | 6 | export const styles = StyleSheet.create({ 7 | button: { 8 | width: 48, 9 | height: 48, 10 | borderRadius: 24, 11 | backgroundColor: theme.colors.brand, 12 | justifyContent: 'center', 13 | alignItems: 'center', 14 | 15 | position: 'absolute', 16 | right: 16, 17 | bottom: getBottomSpace() + 16 18 | }, 19 | modal: { 20 | backgroundColor: theme.colors.surface_primary, 21 | paddingBottom: getBottomSpace() + 16 22 | }, 23 | indicator: { 24 | backgroundColor: theme.colors.text_primary, 25 | width: 56, 26 | } 27 | }); -------------------------------------------------------------------------------- /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 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/adapters/nodemailer/nodemailer-mail-adapter.ts: -------------------------------------------------------------------------------- 1 | import nodemailer, { Transporter } from 'nodemailer'; 2 | 3 | import { MailAdapter, SendMailData } from "../mail-adapter"; 4 | 5 | export class NodemailerMailAdapter implements MailAdapter { 6 | private transport: Transporter; 7 | 8 | constructor() { 9 | this.transport = nodemailer.createTransport({ 10 | host: "smtp.mailtrap.io", 11 | port: 2525, 12 | auth: { 13 | user: "9ff8ef80e2979d", 14 | pass: "9774321aea08f1" 15 | } 16 | }); 17 | } 18 | 19 | async sendMail({ subject, content }: SendMailData) { 20 | await this.transport.sendMail({ 21 | from: "Equipe Feedget ", 22 | to: "Diego Fernandes ", 23 | subject, 24 | html: content, 25 | }) 26 | } 27 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@headlessui/react": "^1.6.0", 12 | "axios": "^0.27.1", 13 | "html2canvas": "^1.4.1", 14 | "phosphor-react": "^1.4.1", 15 | "react": "^18.0.0", 16 | "react-dom": "^18.0.0" 17 | }, 18 | "devDependencies": { 19 | "@tailwindcss/forms": "^0.5.0", 20 | "@types/react": "^18.0.0", 21 | "@types/react-dom": "^18.0.0", 22 | "@vitejs/plugin-react": "^1.3.0", 23 | "autoprefixer": "^10.4.5", 24 | "postcss": "^8.4.12", 25 | "tailwind-scrollbar": "^1.3.1", 26 | "tailwindcss": "^3.0.24", 27 | "typescript": "^4.6.3", 28 | "vite": "^2.9.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![App Screenshot](.github/cover.png) 2 | 3 | Widget Web, Mobile and Back-end to send Feedback e-mail with screenshot. This application was created for the purpose of studies. 4 | 5 | 6 | ## Stack 7 | - ReactJs 8 | - Tailwind 9 | - NodeJs 10 | - Prism 11 | - React Native 12 | - Typescript 13 | - Expo 14 | - Moti 15 | - React Native SVG 16 | - And more... 17 | 18 | 19 | ## Feedback 20 | 21 | Would you like to speak with me? I find myself on Linkedin: [linkedin.com/in/rodrigo-goncalves-santana/](https://www.linkedin.com/in/rodrigo-goncalves-santana/) 22 | 23 | [![Linkedin Badge](https://img.shields.io/badge/-Rodrigo%20Gonçalves%20Santana-6633cc?style=flat-square&logo=Linkedin&logoColor=white&link=https://www.linkedin.com/in/rodrigo-gon%C3%A7alves-santana/)](https://www.linkedin.com/in/rodrigo-gon%C3%A7alves-santana/) 24 | 25 | -------------------------------------------------------------------------------- /mobile/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Text, 4 | TouchableOpacity, 5 | TouchableOpacityProps, 6 | ActivityIndicator 7 | } from 'react-native'; 8 | import { theme } from '../../theme'; 9 | 10 | import { styles } from './styles'; 11 | 12 | interface Props extends TouchableOpacityProps { 13 | isLoading: boolean; 14 | } 15 | 16 | export function Button({ isLoading, ...rest }: Props) { 17 | return ( 18 | 22 | { 23 | isLoading 24 | ? 25 | 28 | : 29 | 30 | Enviar Feedback 31 | 32 | } 33 | 34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /mobile/src/components/Success/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | alignItems: 'center', 7 | }, 8 | image: { 9 | width: 36, 10 | height: 36, 11 | marginTop: 42, 12 | marginBottom: 10 13 | }, 14 | title: { 15 | fontSize: 20, 16 | marginBottom: 24, 17 | fontFamily: theme.fonts.medium, 18 | color: theme.colors.text_primary, 19 | }, 20 | button: { 21 | height: 40, 22 | backgroundColor: theme.colors.surface_secondary, 23 | borderRadius: 4, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | paddingHorizontal: 24, 27 | marginBottom: 56, 28 | }, 29 | buttonTitle: { 30 | fontSize: 14, 31 | fontFamily: theme.fonts.medium, 32 | color: theme.colors.text_primary 33 | } 34 | }); -------------------------------------------------------------------------------- /server/prisma/migrations/20220426181218_add_type_to_feedbacks_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `type` to the `Feedback` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- RedefineTables 8 | PRAGMA foreign_keys=OFF; 9 | CREATE TABLE "new_Feedback" ( 10 | "id" TEXT NOT NULL PRIMARY KEY, 11 | "type" TEXT NOT NULL, 12 | "screenshot" TEXT, 13 | "comment" TEXT NOT NULL, 14 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "updatedAt" DATETIME NOT NULL 16 | ); 17 | INSERT INTO "new_Feedback" ("comment", "createdAt", "id", "screenshot", "updatedAt") SELECT "comment", "createdAt", "id", "screenshot", "updatedAt" FROM "Feedback"; 18 | DROP TABLE "Feedback"; 19 | ALTER TABLE "new_Feedback" RENAME TO "Feedback"; 20 | CREATE INDEX "Feedback_type_idx" ON "Feedback"("type"); 21 | PRAGMA foreign_key_check; 22 | PRAGMA foreign_keys=ON; 23 | -------------------------------------------------------------------------------- /mobile/App.tsx: -------------------------------------------------------------------------------- 1 | import 'react-native-gesture-handler'; 2 | 3 | import { StatusBar } from 'expo-status-bar'; 4 | import { View } from 'react-native'; 5 | import AppLoading from 'expo-app-loading'; 6 | 7 | import { 8 | useFonts, 9 | Inter_400Regular, 10 | Inter_500Medium 11 | } from '@expo-google-fonts/inter'; 12 | 13 | import { theme } from './src/theme'; 14 | import Widget from './src/components/Widget'; 15 | 16 | export default function App() { 17 | const [fontsLoaded] = useFonts({ 18 | Inter_400Regular, 19 | Inter_500Medium 20 | }); 21 | 22 | if (!fontsLoaded) { 23 | return ; 24 | } 25 | 26 | return ( 27 | 32 | 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | /** 4 | * @type { import('tailwindcss/tailwind-config').TailwindConfig } 5 | */ 6 | module.exports = { 7 | mode: 'jit', 8 | content: [ 9 | './src/components/**/*.tsx', 10 | './src/pages/**/*.tsx', 11 | './src/*.tsx', 12 | ], 13 | theme: { 14 | extend: { 15 | fontFamily: { 16 | 'sans': ['Inter', ...defaultTheme.fontFamily.sans], 17 | }, 18 | colors: { 19 | brand: { 20 | 300: '#996DFF', 21 | 500: '#8257e6', 22 | 900: '#271A45', 23 | } 24 | }, 25 | borderRadius: { 26 | 'md': '4px', 27 | }, 28 | transitionDelay: { 29 | '0': '0ms', 30 | }, 31 | transitionProperty: { 32 | 'width': 'max-width, width' 33 | }, 34 | }, 35 | }, 36 | plugins: [ 37 | require('@tailwindcss/forms'), 38 | require('tailwind-scrollbar'), 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "author": "Diego Fernandes ", 6 | "description": "build/server.js", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "jest", 10 | "build": "tsc", 11 | "start:dev": "ts-node-dev --respawn --transpile-only src/server.ts", 12 | "start": "node build/server.js" 13 | }, 14 | "dependencies": { 15 | "@prisma/client": "^3.13.0", 16 | "cors": "^2.8.5", 17 | "express": "^4.18.0", 18 | "nodemailer": "^6.7.3" 19 | }, 20 | "devDependencies": { 21 | "@swc/jest": "^0.2.20", 22 | "@types/cors": "^2.8.12", 23 | "@types/express": "^4.17.13", 24 | "@types/jest": "^27.4.1", 25 | "@types/node": "^17.0.27", 26 | "@types/nodemailer": "^6.4.4", 27 | "jest": "^28.0.1", 28 | "prisma": "^3.13.0", 29 | "ts-node": "^10.7.0", 30 | "ts-node-dev": "^1.1.8", 31 | "typescript": "^4.6.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { NodemailerMailAdapter } from './adapters/nodemailer/nodemailer-mail-adapter'; 3 | import { PrismaFeedbacksRepository } from './repositories/prisma/prisma-feedbacks-repository'; 4 | import { SubmitFeedbackUseCase } from './use-cases/submit-feedback-use-case'; 5 | 6 | export const routes = Router(); 7 | 8 | routes.post('/feedbacks', async (req, res) => { 9 | const prismaFeedbacksRepository = new PrismaFeedbacksRepository(); 10 | const nodemailerMailAdapter = new NodemailerMailAdapter() 11 | 12 | const submitFeedback = new SubmitFeedbackUseCase( 13 | prismaFeedbacksRepository, 14 | nodemailerMailAdapter, 15 | ) 16 | 17 | try { 18 | const { type, screenshot, comment } = req.body; 19 | 20 | await submitFeedback.execute({ type, screenshot, comment }); 21 | 22 | return res.status(201).send() 23 | } catch (err) { 24 | return res.status(400).json({ message: 'Error submitting feedback.' }) 25 | } 26 | }); -------------------------------------------------------------------------------- /mobile/src/components/Success/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Image, Text, TouchableOpacity } from 'react-native'; 3 | 4 | import successImg from '../../assets/success.png'; 5 | 6 | import { Copyright } from '../Copyright'; 7 | 8 | import { styles } from './styles'; 9 | 10 | interface Props { 11 | onSendAnotherFeedback: () => void; 12 | } 13 | 14 | export function Success({ onSendAnotherFeedback }: Props) { 15 | return ( 16 | 17 | 21 | 22 | 23 | Agradecemos o feedback 24 | 25 | 26 | 30 | 31 | Quero enviar outro 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } -------------------------------------------------------------------------------- /mobile/src/components/Form/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | import { theme } from '../../theme'; 3 | 4 | export const styles = StyleSheet.create({ 5 | container: { 6 | paddingHorizontal: 24, 7 | alignItems: 'center' 8 | }, 9 | header: { 10 | flexDirection: 'row', 11 | marginVertical: 16 12 | }, 13 | titleContainer: { 14 | flex: 1, 15 | flexDirection: 'row', 16 | justifyContent: 'center', 17 | alignItems: 'center', 18 | paddingRight: 24 19 | }, 20 | titleText: { 21 | fontSize: 20, 22 | color: theme.colors.text_primary, 23 | fontFamily: theme.fonts.medium 24 | }, 25 | image: { 26 | width: 24, 27 | height: 24, 28 | marginRight: 8 29 | }, 30 | input: { 31 | height: 112, 32 | padding: 12, 33 | marginBottom: 8, 34 | borderRadius: 4, 35 | borderWidth: 1, 36 | borderColor: theme.colors.stroke, 37 | color: theme.colors.text_primary, 38 | fontFamily: theme.fonts.regular 39 | }, 40 | footer: { 41 | flexDirection: 'row', 42 | marginBottom: 16 43 | } 44 | }); -------------------------------------------------------------------------------- /mobile/src/components/Options/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | 4 | import { Copyright } from '../Copyright'; 5 | import { Option } from '../Option'; 6 | import { FeedbackType } from '../Widget'; 7 | 8 | import { feedbackTypes } from '../../utils/feedbackTypes'; 9 | import { styles } from './styles'; 10 | 11 | interface Props { 12 | onFeedbackTypeChanged: (feedbackType: FeedbackType) => void; 13 | } 14 | 15 | export function Options({ onFeedbackTypeChanged }: Props) { 16 | return ( 17 | 18 | 19 | Deixe seu feedback 20 | 21 | 22 | 23 | 24 | { 25 | Object 26 | .entries(feedbackTypes) 27 | .map(([key, value]) => ( 28 | 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /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/inter": "^0.2.2", 12 | "@gorhom/bottom-sheet": "^4", 13 | "axios": "^0.27.2", 14 | "expo": "~44.0.0", 15 | "expo-app-loading": "~1.3.0", 16 | "expo-font": "~10.0.4", 17 | "expo-status-bar": "~1.2.0", 18 | "phosphor-react-native": "^1.1.1", 19 | "react": "17.0.1", 20 | "react-dom": "17.0.1", 21 | "react-native": "0.64.3", 22 | "react-native-gesture-handler": "~2.1.0", 23 | "react-native-iphone-x-helper": "^1.3.1", 24 | "react-native-reanimated": "~2.3.1", 25 | "react-native-svg": "12.1.1", 26 | "react-native-view-shot": "3.1.2", 27 | "react-native-web": "0.17.1", 28 | "expo-file-system": "~13.1.4" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.12.9", 32 | "@types/react": "~17.0.21", 33 | "@types/react-native": "~0.64.12", 34 | "typescript": "~4.3.5" 35 | }, 36 | "private": true 37 | } 38 | -------------------------------------------------------------------------------- /server/src/use-cases/submit-feedback-use-case.ts: -------------------------------------------------------------------------------- 1 | import { MailAdapter } from "../adapters/mail-adapter"; 2 | import { FeedbacksRepository } from "../repositories/feedbacks-repository"; 3 | 4 | interface SubmitFeedbackUseCaseRequest { 5 | type: string; 6 | screenshot?: string; 7 | comment: string; 8 | } 9 | 10 | export class SubmitFeedbackUseCase { 11 | constructor( 12 | private feedbacksRepository: FeedbacksRepository, 13 | private mailAdapter: MailAdapter, 14 | ) {} 15 | 16 | async execute({ type, screenshot, comment }: SubmitFeedbackUseCaseRequest) { 17 | if (!type) { 18 | throw new Error('Feedback type must be provided.'); 19 | } 20 | 21 | if (!comment) { 22 | throw new Error('Feedback comment must be provided.'); 23 | } 24 | 25 | await this.feedbacksRepository.create({ 26 | type, 27 | screenshot, 28 | comment, 29 | }) 30 | 31 | await this.mailAdapter.sendMail({ 32 | subject: `[${type}] Novo feedback`, 33 | content: [ 34 | `

Tipo de feedback: ${type}

`, 35 | `

Comentário: ${comment}

`, 36 | screenshot ? `` : false, 37 | ].filter(Boolean).join('') 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /server/src/use-cases/submit-feedback-use-case.spec.ts: -------------------------------------------------------------------------------- 1 | import { SubmitFeedbackUseCase } from "./submit-feedback-use-case"; 2 | 3 | const createFeedbackMock = jest.fn(); 4 | const sendMailMock = jest.fn() 5 | 6 | const submitFeedback = new SubmitFeedbackUseCase( 7 | { create: createFeedbackMock }, 8 | { sendMail: sendMailMock } 9 | ) 10 | 11 | describe('Submit feedback', () => { 12 | it('should not be able to submit a feedback without type', async () => { 13 | await expect(submitFeedback.execute({ 14 | type: '', 15 | comment: 'example comment' 16 | })).rejects.toThrow() 17 | }); 18 | 19 | it('should not be able to submit a feedback without comment', async () => { 20 | await expect(submitFeedback.execute({ 21 | type: 'bug', 22 | comment: '' 23 | })).rejects.toThrow() 24 | }); 25 | 26 | it('should be able to submit a feedback', async () => { 27 | await expect(submitFeedback.execute({ 28 | type: 'bug', 29 | comment: 'example comment', 30 | screenshot: 'test.jpg', 31 | })).resolves.not.toThrow(); 32 | 33 | expect(sendMailMock).toBeCalled() 34 | expect(createFeedbackMock).toBeCalledWith(expect.objectContaining({ 35 | type: 'bug', 36 | comment: 'example comment' 37 | })) 38 | }); 39 | }) -------------------------------------------------------------------------------- /mobile/src/components/ScreenshotButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, TouchableOpacity, Image } from 'react-native'; 3 | import { Trash, Camera } from 'phosphor-react-native'; 4 | 5 | import { styles } from './styles'; 6 | import { theme } from '../../theme'; 7 | 8 | interface Props { 9 | screenshot: string | null; 10 | onTakeShot: () => void; 11 | onRemoveShot: () => void; 12 | } 13 | 14 | export function ScreenshotButton({ screenshot, onTakeShot, onRemoveShot }: Props) { 15 | return ( 16 | 20 | { 21 | screenshot 22 | ? 23 | 24 | 28 | 29 | 35 | 36 | : 37 | 42 | } 43 | 44 | ); 45 | } -------------------------------------------------------------------------------- /web/src/components/Widget/WidgetForm/Steps/FeedbackTypeStep.tsx: -------------------------------------------------------------------------------- 1 | import { FeedbackType, feedbackTypes } from ".."; 2 | import { CloseButton } from "../../CloseButton"; 3 | 4 | interface FeedbackTypeStepProps { 5 | onFeedbackTypeChanged: (feedbackType: FeedbackType) => void; 6 | } 7 | 8 | export function FeedbackTypeStep({ 9 | onFeedbackTypeChanged, 10 | }: FeedbackTypeStepProps) { 11 | return ( 12 | <> 13 |
14 | Deixe seu feedback 15 | 16 | 17 |
18 | 19 |
20 | { Object.entries(feedbackTypes).map(([ key, value ]) => { 21 | return ( 22 | 31 | ) 32 | }) } 33 |
34 | 35 | ); 36 | } -------------------------------------------------------------------------------- /web/src/components/Widget/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChatTeardropDots } from "phosphor-react"; 2 | import { Popover, Transition } from '@headlessui/react' 3 | import { WidgetForm } from "./WidgetForm"; 4 | import { Fragment } from "react"; 5 | 6 | export function Widget() { 7 | return ( 8 | 9 | {/* */} 18 | 19 | 20 | 21 | {/* */} 22 | 23 | 26 | 27 | 28 | 29 | Feedback 30 | 31 | 32 | 33 | ); 34 | } -------------------------------------------------------------------------------- /web/src/assets/idea.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /web/src/components/Widget/WidgetForm/Steps/FeedbackSuccessStep.tsx: -------------------------------------------------------------------------------- 1 | import { CloseButton } from "../../CloseButton"; 2 | 3 | interface FeedbackSuccessStepProps { 4 | onSendAnotherFeedbackRequest: () => void; 5 | } 6 | 7 | export function FeedbackSuccessStep({ 8 | onSendAnotherFeedbackRequest, 9 | }: FeedbackSuccessStepProps) { 10 | return ( 11 | <> 12 |
13 | 14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | Agradecemos o feedback! 23 | 24 | 30 |
31 | 32 | ); 33 | } -------------------------------------------------------------------------------- /web/src/components/Widget/WidgetForm/ScreenshotButton.tsx: -------------------------------------------------------------------------------- 1 | import html2canvas from "html2canvas"; 2 | import { Camera, Trash } from "phosphor-react"; 3 | import { useState } from "react"; 4 | import { Loading } from "../../Loading"; 5 | 6 | interface ScreenshotButtonProps { 7 | screenshot: string | null; 8 | onScreenshotTook: (screenshot: string | null) => void; 9 | } 10 | 11 | export function ScreenshotButton({ 12 | screenshot, 13 | onScreenshotTook 14 | }: ScreenshotButtonProps) { 15 | const [isTakingScreenshot, setIsTakingScreenshot] = useState(false); 16 | 17 | async function handleScreenshot() { 18 | setIsTakingScreenshot(true) 19 | 20 | const canvas = await html2canvas(document.querySelector('html')!) 21 | 22 | const base64image = canvas.toDataURL("image/png"); 23 | 24 | onScreenshotTook(base64image) 25 | setIsTakingScreenshot(false) 26 | } 27 | 28 | function handleDeleteScreenshot() { 29 | onScreenshotTook(null) 30 | } 31 | 32 | if (screenshot) { 33 | return ( 34 | 46 | ); 47 | } 48 | 49 | return ( 50 | 60 | ); 61 | } -------------------------------------------------------------------------------- /mobile/src/components/Widget/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { TouchableOpacity } from 'react-native'; 3 | import { ChatTeardropDots } from 'phosphor-react-native'; 4 | import BottomSheet from '@gorhom/bottom-sheet'; 5 | import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; 6 | 7 | import { Options } from '../Options'; 8 | import { Success } from '../Success'; 9 | import { Form } from '../Form'; 10 | 11 | import { styles } from './styles'; 12 | import { theme } from '../../theme'; 13 | import { feedbackTypes } from '../../utils/feedbackTypes'; 14 | 15 | export type FeedbackType = keyof typeof feedbackTypes; 16 | 17 | function Widget() { 18 | const [feedbackType, setFeedbackType] = useState(null); 19 | const [feedbackSent, setFeedbackSent] = useState(false); 20 | 21 | const bottomSheetRef = useRef(null); 22 | 23 | function handleOpen() { 24 | bottomSheetRef.current?.expand(); 25 | } 26 | 27 | function handleRestartFeedback() { 28 | setFeedbackType(null); 29 | setFeedbackSent(false); 30 | } 31 | 32 | function handleFeedbackSent() { 33 | setFeedbackSent(true); 34 | } 35 | 36 | return ( 37 | <> 38 | 42 | 47 | 48 | 49 | 55 | { 56 | feedbackSent ? 57 | 58 | : 59 | <> 60 | { 61 | feedbackType ? 62 |
67 | : 68 | 69 | } 70 | 71 | } 72 | 73 | 74 | ); 75 | } 76 | 77 | export default gestureHandlerRootHOC(Widget); -------------------------------------------------------------------------------- /web/src/components/Widget/WidgetForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { FeedbackContentStep } from './Steps/FeedbackContentStep'; 3 | import { FeedbackSuccessStep } from './Steps/FeedbackSuccessStep'; 4 | import { FeedbackTypeStep } from './Steps/FeedbackTypeStep'; 5 | 6 | import bugImageUrl from '../../../assets/bug.svg'; 7 | import ideaImageUrl from '../../../assets/idea.svg'; 8 | import thoughtImageUrl from '../../../assets/thought.svg'; 9 | 10 | export const feedbackTypes = { 11 | 'BUG': { 12 | image: { 13 | source: bugImageUrl, 14 | alt: "Imagem de um inseto", 15 | }, 16 | title: 'Problema' 17 | }, 18 | 'IDEA': { 19 | image: { 20 | source: ideaImageUrl, 21 | alt: "Imagem de uma lâmpada", 22 | }, 23 | title: 'Ideia' 24 | }, 25 | 'OTHER': { 26 | image: { 27 | source: thoughtImageUrl, 28 | alt: "Imagem de uma nuvem de pensamento", 29 | }, 30 | title: 'Outro' 31 | }, 32 | }; 33 | 34 | export type FeedbackType = keyof typeof feedbackTypes; 35 | 36 | export function WidgetForm() { 37 | const [feedbackType, setFeedbackType] = useState(null) 38 | const [feedbackSent, setFeedbackSent] = useState(false) 39 | 40 | function handleFeedbackSent() { 41 | setFeedbackSent(true); 42 | } 43 | 44 | function handleRestartFeedback() { 45 | setFeedbackType(null); 46 | setFeedbackSent(false); 47 | } 48 | 49 | return ( 50 |
51 | { feedbackSent ? ( 52 | 55 | ) : ( 56 | <> 57 | { !feedbackType ? ( 58 | 61 | ) : ( 62 | setFeedbackType(null)} 65 | onFeedbackSent={handleFeedbackSent} 66 | /> 67 | )} 68 | 69 | )} 70 | 71 | 74 |
75 | ); 76 | } -------------------------------------------------------------------------------- /web/src/components/Widget/WidgetForm/Steps/FeedbackContentStep.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeft } from "phosphor-react"; 2 | import { FormEvent, useState } from "react"; 3 | 4 | import { FeedbackType, feedbackTypes } from ".."; 5 | import { api } from "../../../../lib/api"; 6 | 7 | import { Loading } from "../../../Loading"; 8 | import { CloseButton } from "../../CloseButton"; 9 | import { ScreenshotButton } from "../ScreenshotButton"; 10 | 11 | interface FeedbackContentStepProps { 12 | onFeedbackCanceled: () => void; 13 | onFeedbackSent: () => void; 14 | feedbackType: FeedbackType; 15 | } 16 | 17 | export function FeedbackContentStep({ 18 | onFeedbackCanceled, 19 | onFeedbackSent, 20 | feedbackType 21 | }: FeedbackContentStepProps) { 22 | const [isSendingFeedback, setIsSendingFeedback] = useState(false); 23 | 24 | const [screenshot, setScreenshot] = useState(null) 25 | const [comment, setComment] = useState('') 26 | 27 | async function handleSendFeedback(event: FormEvent) { 28 | event.preventDefault(); 29 | 30 | if (isSendingFeedback) { 31 | return; 32 | } 33 | 34 | setIsSendingFeedback(true); 35 | 36 | await api.post('/feedbacks', { 37 | type: feedbackType, 38 | screenshot, 39 | comment, 40 | }) 41 | 42 | setIsSendingFeedback(false); 43 | onFeedbackSent(); 44 | } 45 | 46 | const feedbackTypeInfo = feedbackTypes[feedbackType]; 47 | 48 | return ( 49 | <> 50 |
51 | 54 | 55 | 56 | {feedbackTypeInfo.image.alt} 57 | {feedbackTypeInfo.title} 58 | 59 | 60 | 61 |
62 | 63 | 64 |