├── .gitignore
├── .storybook
├── main.js
├── manager.js
├── preview-head.html
└── preview.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── mockServiceWorker.js
├── src
├── App.tsx
├── assets
│ ├── bug.svg
│ ├── idea.svg
│ └── thought.svg
├── components
│ ├── CloseButton.tsx
│ ├── Loading.stories.tsx
│ ├── Loading.tsx
│ ├── Widget.stories.tsx
│ ├── Widget.tsx
│ └── WidgetForm
│ │ ├── ScreenshotButton.tsx
│ │ ├── Steps
│ │ ├── FeedbackContentStep.stories.tsx
│ │ ├── FeedbackContentStep.tsx
│ │ ├── FeedbackSuccessStep.stories.tsx
│ │ ├── FeedbackSuccessStep.tsx
│ │ ├── FeedbackTypeStep.stories.tsx
│ │ └── FeedbackTypeStep.tsx
│ │ └── index.tsx
├── global.css
├── lib
│ └── api.ts
├── main.tsx
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.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 |
26 | # Env
27 | .env.local
28 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/**/*.stories.mdx",
4 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/addon-interactions"
10 | ],
11 | "framework": "@storybook/react",
12 | "core": {
13 | "builder": "@storybook/builder-vite"
14 | },
15 | "features": {
16 | "storyStoreV7": true
17 | }
18 | }
--------------------------------------------------------------------------------
/.storybook/manager.js:
--------------------------------------------------------------------------------
1 | import { addons } from '@storybook/addons';
2 | import { themes } from '@storybook/theming';
3 |
4 | addons.setConfig({
5 | theme: themes.dark,
6 | })
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { themes } from '@storybook/theming'
2 | import { initialize, mswDecorator } from 'msw-storybook-addon';
3 | import '../src/global.css'
4 |
5 | initialize();
6 |
7 | export const decorators = [mswDecorator];
8 |
9 | export const parameters = {
10 | actions: { argTypesRegex: "^on[A-Z].*" },
11 | controls: {
12 | matchers: {
13 | color: /(background|color)$/i,
14 | date: /Date$/,
15 | },
16 | },
17 | docs: {
18 | theme: themes.dark,
19 | },
20 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vite App
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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 | "storybook": "start-storybook -p 6006",
10 | "build-storybook": "build-storybook"
11 | },
12 | "dependencies": {
13 | "@headlessui/react": "^1.6.0",
14 | "@tailwindcss/forms": "^0.5.0",
15 | "axios": "^0.27.2",
16 | "html2canvas": "^1.4.1",
17 | "phosphor-react": "^1.4.1",
18 | "react": "^18.0.0",
19 | "react-dom": "^18.0.0",
20 | "tailwind-scrollbar": "^1.3.1"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.19.3",
24 | "@storybook/addon-actions": "^6.5.12",
25 | "@storybook/addon-essentials": "^6.5.12",
26 | "@storybook/addon-interactions": "^6.5.12",
27 | "@storybook/addon-links": "^6.5.12",
28 | "@storybook/builder-vite": "^0.2.3",
29 | "@storybook/react": "^6.5.12",
30 | "@storybook/testing-library": "^0.0.13",
31 | "@types/react": "^18.0.0",
32 | "@types/react-dom": "^18.0.0",
33 | "@vitejs/plugin-react": "^1.3.0",
34 | "autoprefixer": "^10.4.6",
35 | "babel-loader": "^8.2.5",
36 | "msw": "^0.47.4",
37 | "msw-storybook-addon": "^1.6.3",
38 | "postcss": "^8.4.13",
39 | "tailwindcss": "^3.0.24",
40 | "typescript": "^4.6.3",
41 | "vite": "^3.1.6"
42 | },
43 | "msw": {
44 | "workerDirectory": "public"
45 | }
46 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/mockServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* tslint:disable */
3 |
4 | /**
5 | * Mock Service Worker (0.47.4).
6 | * @see https://github.com/mswjs/msw
7 | * - Please do NOT modify this file.
8 | * - Please do NOT serve this file on production.
9 | */
10 |
11 | const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995'
12 | const activeClientIds = new Set()
13 |
14 | self.addEventListener('install', function () {
15 | self.skipWaiting()
16 | })
17 |
18 | self.addEventListener('activate', function (event) {
19 | event.waitUntil(self.clients.claim())
20 | })
21 |
22 | self.addEventListener('message', async function (event) {
23 | const clientId = event.source.id
24 |
25 | if (!clientId || !self.clients) {
26 | return
27 | }
28 |
29 | const client = await self.clients.get(clientId)
30 |
31 | if (!client) {
32 | return
33 | }
34 |
35 | const allClients = await self.clients.matchAll({
36 | type: 'window',
37 | })
38 |
39 | switch (event.data) {
40 | case 'KEEPALIVE_REQUEST': {
41 | sendToClient(client, {
42 | type: 'KEEPALIVE_RESPONSE',
43 | })
44 | break
45 | }
46 |
47 | case 'INTEGRITY_CHECK_REQUEST': {
48 | sendToClient(client, {
49 | type: 'INTEGRITY_CHECK_RESPONSE',
50 | payload: INTEGRITY_CHECKSUM,
51 | })
52 | break
53 | }
54 |
55 | case 'MOCK_ACTIVATE': {
56 | activeClientIds.add(clientId)
57 |
58 | sendToClient(client, {
59 | type: 'MOCKING_ENABLED',
60 | payload: true,
61 | })
62 | break
63 | }
64 |
65 | case 'MOCK_DEACTIVATE': {
66 | activeClientIds.delete(clientId)
67 | break
68 | }
69 |
70 | case 'CLIENT_CLOSED': {
71 | activeClientIds.delete(clientId)
72 |
73 | const remainingClients = allClients.filter((client) => {
74 | return client.id !== clientId
75 | })
76 |
77 | // Unregister itself when there are no more clients
78 | if (remainingClients.length === 0) {
79 | self.registration.unregister()
80 | }
81 |
82 | break
83 | }
84 | }
85 | })
86 |
87 | self.addEventListener('fetch', function (event) {
88 | const { request } = event
89 | const accept = request.headers.get('accept') || ''
90 |
91 | // Bypass server-sent events.
92 | if (accept.includes('text/event-stream')) {
93 | return
94 | }
95 |
96 | // Bypass navigation requests.
97 | if (request.mode === 'navigate') {
98 | return
99 | }
100 |
101 | // Opening the DevTools triggers the "only-if-cached" request
102 | // that cannot be handled by the worker. Bypass such requests.
103 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
104 | return
105 | }
106 |
107 | // Bypass all requests when there are no active clients.
108 | // Prevents the self-unregistered worked from handling requests
109 | // after it's been deleted (still remains active until the next reload).
110 | if (activeClientIds.size === 0) {
111 | return
112 | }
113 |
114 | // Generate unique request ID.
115 | const requestId = Math.random().toString(16).slice(2)
116 |
117 | event.respondWith(
118 | handleRequest(event, requestId).catch((error) => {
119 | if (error.name === 'NetworkError') {
120 | console.warn(
121 | '[MSW] Successfully emulated a network error for the "%s %s" request.',
122 | request.method,
123 | request.url,
124 | )
125 | return
126 | }
127 |
128 | // At this point, any exception indicates an issue with the original request/response.
129 | console.error(
130 | `\
131 | [MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
132 | request.method,
133 | request.url,
134 | `${error.name}: ${error.message}`,
135 | )
136 | }),
137 | )
138 | })
139 |
140 | async function handleRequest(event, requestId) {
141 | const client = await resolveMainClient(event)
142 | const response = await getResponse(event, client, requestId)
143 |
144 | // Send back the response clone for the "response:*" life-cycle events.
145 | // Ensure MSW is active and ready to handle the message, otherwise
146 | // this message will pend indefinitely.
147 | if (client && activeClientIds.has(client.id)) {
148 | ;(async function () {
149 | const clonedResponse = response.clone()
150 | sendToClient(client, {
151 | type: 'RESPONSE',
152 | payload: {
153 | requestId,
154 | type: clonedResponse.type,
155 | ok: clonedResponse.ok,
156 | status: clonedResponse.status,
157 | statusText: clonedResponse.statusText,
158 | body:
159 | clonedResponse.body === null ? null : await clonedResponse.text(),
160 | headers: Object.fromEntries(clonedResponse.headers.entries()),
161 | redirected: clonedResponse.redirected,
162 | },
163 | })
164 | })()
165 | }
166 |
167 | return response
168 | }
169 |
170 | // Resolve the main client for the given event.
171 | // Client that issues a request doesn't necessarily equal the client
172 | // that registered the worker. It's with the latter the worker should
173 | // communicate with during the response resolving phase.
174 | async function resolveMainClient(event) {
175 | const client = await self.clients.get(event.clientId)
176 |
177 | if (client.frameType === 'top-level') {
178 | return client
179 | }
180 |
181 | const allClients = await self.clients.matchAll({
182 | type: 'window',
183 | })
184 |
185 | return allClients
186 | .filter((client) => {
187 | // Get only those clients that are currently visible.
188 | return client.visibilityState === 'visible'
189 | })
190 | .find((client) => {
191 | // Find the client ID that's recorded in the
192 | // set of clients that have registered the worker.
193 | return activeClientIds.has(client.id)
194 | })
195 | }
196 |
197 | async function getResponse(event, client, requestId) {
198 | const { request } = event
199 | const clonedRequest = request.clone()
200 |
201 | function passthrough() {
202 | // Clone the request because it might've been already used
203 | // (i.e. its body has been read and sent to the client).
204 | const headers = Object.fromEntries(clonedRequest.headers.entries())
205 |
206 | // Remove MSW-specific request headers so the bypassed requests
207 | // comply with the server's CORS preflight check.
208 | // Operate with the headers as an object because request "Headers"
209 | // are immutable.
210 | delete headers['x-msw-bypass']
211 |
212 | return fetch(clonedRequest, { headers })
213 | }
214 |
215 | // Bypass mocking when the client is not active.
216 | if (!client) {
217 | return passthrough()
218 | }
219 |
220 | // Bypass initial page load requests (i.e. static assets).
221 | // The absence of the immediate/parent client in the map of the active clients
222 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
223 | // and is not ready to handle requests.
224 | if (!activeClientIds.has(client.id)) {
225 | return passthrough()
226 | }
227 |
228 | // Bypass requests with the explicit bypass header.
229 | // Such requests can be issued by "ctx.fetch()".
230 | if (request.headers.get('x-msw-bypass') === 'true') {
231 | return passthrough()
232 | }
233 |
234 | // Notify the client that a request has been intercepted.
235 | const clientMessage = await sendToClient(client, {
236 | type: 'REQUEST',
237 | payload: {
238 | id: requestId,
239 | url: request.url,
240 | method: request.method,
241 | headers: Object.fromEntries(request.headers.entries()),
242 | cache: request.cache,
243 | mode: request.mode,
244 | credentials: request.credentials,
245 | destination: request.destination,
246 | integrity: request.integrity,
247 | redirect: request.redirect,
248 | referrer: request.referrer,
249 | referrerPolicy: request.referrerPolicy,
250 | body: await request.text(),
251 | bodyUsed: request.bodyUsed,
252 | keepalive: request.keepalive,
253 | },
254 | })
255 |
256 | switch (clientMessage.type) {
257 | case 'MOCK_RESPONSE': {
258 | return respondWithMock(clientMessage.data)
259 | }
260 |
261 | case 'MOCK_NOT_FOUND': {
262 | return passthrough()
263 | }
264 |
265 | case 'NETWORK_ERROR': {
266 | const { name, message } = clientMessage.data
267 | const networkError = new Error(message)
268 | networkError.name = name
269 |
270 | // Rejecting a "respondWith" promise emulates a network error.
271 | throw networkError
272 | }
273 | }
274 |
275 | return passthrough()
276 | }
277 |
278 | function sendToClient(client, message) {
279 | return new Promise((resolve, reject) => {
280 | const channel = new MessageChannel()
281 |
282 | channel.port1.onmessage = (event) => {
283 | if (event.data && event.data.error) {
284 | return reject(event.data.error)
285 | }
286 |
287 | resolve(event.data)
288 | }
289 |
290 | client.postMessage(message, [channel.port2])
291 | })
292 | }
293 |
294 | function sleep(timeMs) {
295 | return new Promise((resolve) => {
296 | setTimeout(resolve, timeMs)
297 | })
298 | }
299 |
300 | async function respondWithMock(response) {
301 | await sleep(response.delay)
302 | return new Response(response.body, response)
303 | }
304 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Widget } from "./components/Widget"
2 |
3 | export function App() {
4 | return (
5 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/src/assets/bug.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/idea.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/assets/thought.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/CloseButton.tsx:
--------------------------------------------------------------------------------
1 | import { Popover } from '@headlessui/react'
2 | import { X } from 'phosphor-react'
3 |
4 | export function CloseButton() {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
--------------------------------------------------------------------------------
/src/components/Loading.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from '@storybook/react'
2 | import { Loading } from './Loading'
3 |
4 | export default {
5 | title: 'Components/Loading',
6 | component: Loading,
7 | } as Meta
8 |
9 | export const Default: StoryObj = {}
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { CircleNotch } from "phosphor-react";
2 |
3 | export function Loading() {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/src/components/Widget.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Meta, StoryObj } from '@storybook/react'
2 | import { Widget } from './Widget'
3 |
4 | export default {
5 | title: 'Widget',
6 | component: Widget,
7 | } as Meta
8 |
9 | export const Default: StoryObj = {}
--------------------------------------------------------------------------------
/src/components/Widget.tsx:
--------------------------------------------------------------------------------
1 | import { ChatTeardropDots } from 'phosphor-react'
2 | import { Popover } from '@headlessui/react'
3 | import { WidgetForm } from './WidgetForm'
4 |
5 | export function Widget() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 | Feedback
20 |
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/src/components/WidgetForm/ScreenshotButton.tsx:
--------------------------------------------------------------------------------
1 | import { Camera, Trash } from "phosphor-react";
2 | import html2canvas from 'html2canvas'
3 | import { useState } from "react";
4 | import { Loading } from "../Loading";
5 |
6 | interface FeedbackTypeStepProps {
7 | onScreenshotTaken: (screenshot: string | null) => void;
8 | screenshot: string | null;
9 | }
10 |
11 | export function ScreenshotButton({ onScreenshotTaken, screenshot }: FeedbackTypeStepProps) {
12 | const [isTakingScreenshot, setIsTakingScreenshot] = useState(false)
13 |
14 | async function handleTakeScreenshot() {
15 | setIsTakingScreenshot(true);
16 |
17 | const canvas = await html2canvas(document.querySelector('html')!)
18 | const base64image = canvas.toDataURL('image/png')
19 |
20 | onScreenshotTaken(base64image)
21 | setIsTakingScreenshot(false)
22 | }
23 |
24 | if (screenshot) {
25 | return (
26 |
38 | )
39 | }
40 |
41 | return (
42 |
49 | )
50 | }
--------------------------------------------------------------------------------
/src/components/WidgetForm/Steps/FeedbackContentStep.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Popover } from '@headlessui/react'
2 | import { Meta, StoryObj } from '@storybook/react'
3 | import { rest } from 'msw'
4 | import { feedbackTypes } from '..'
5 | import { FeedbackContentStep, FeedbackContentStepProps } from './FeedbackContentStep'
6 |
7 | export default {
8 | title: 'Widget UI/FeedbackContentStep',
9 | component: FeedbackContentStep,
10 | args: {
11 | feedbackType: 'IDEA'
12 | },
13 | parameters: {
14 | msw: {
15 | handlers: [
16 | rest.post('/feedbacks', (req, res) => {
17 | return res()
18 | })
19 | ],
20 | }
21 | },
22 | argTypes: {
23 | feedbackType: {
24 | options: Object.keys(feedbackTypes),
25 | control: {
26 | type: 'inline-radio'
27 | }
28 | }
29 | },
30 | decorators: [
31 | (Story) => {
32 | return (
33 |
34 |
35 |
36 | {Story()}
37 |
38 |
39 |
40 | )
41 | }
42 | ],
43 | } as Meta
44 |
45 | export const Bug: StoryObj = {
46 | args: {
47 | feedbackType: 'BUG',
48 | },
49 | }
50 |
51 | export const Idea: StoryObj = {
52 | args: {
53 | feedbackType: 'IDEA',
54 | },
55 | }
56 |
57 | export const Other: StoryObj = {
58 | args: {
59 | feedbackType: 'OTHER',
60 | },
61 | }
--------------------------------------------------------------------------------
/src/components/WidgetForm/Steps/FeedbackContentStep.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeft } from "phosphor-react";
2 | import { FormEvent, useState } from "react";
3 | import { FeedbackType, feedbackTypes } from "..";
4 | import { api } from "../../../lib/api";
5 | import { CloseButton } from "../../CloseButton"
6 | import { Loading } from "../../Loading";
7 | import { ScreenshotButton } from "../ScreenshotButton";
8 |
9 | export interface FeedbackContentStepProps {
10 | feedbackType: FeedbackType;
11 | onFeedbackRestartRequested: () => void;
12 | onFeedbackSent: () => void;
13 | }
14 |
15 | export function FeedbackContentStep({
16 | feedbackType,
17 | onFeedbackRestartRequested,
18 | onFeedbackSent,
19 | }: FeedbackContentStepProps) {
20 | const [screenshot, setScreenshot] = useState(null)
21 | const [comment, setComment] = useState('')
22 | const [isSendingFeedback, setIsSendingFeedback] = useState(false);
23 |
24 | const feedbackTypeInfo = feedbackTypes[feedbackType];
25 |
26 | async function handleSubmitFeedback(event: FormEvent) {
27 | event.preventDefault();
28 |
29 | setIsSendingFeedback(true);
30 |
31 | // console.log({
32 | // screenshot,
33 | // comment,
34 | // })
35 |
36 | await api.post('/feedbacks', {
37 | type: feedbackType,
38 | comment,
39 | screenshot,
40 | });
41 |
42 | setIsSendingFeedback(false);
43 | onFeedbackSent();
44 | }
45 |
46 | return (
47 | <>
48 |
49 |
57 |
58 |
59 | {feedbackTypeInfo.title}
60 |
61 |
62 |
63 |
64 |
65 |
88 | >
89 | )
90 | }
--------------------------------------------------------------------------------
/src/components/WidgetForm/Steps/FeedbackSuccessStep.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Popover } from '@headlessui/react'
2 | import { Meta, StoryObj } from '@storybook/react'
3 | import { FeedbackSuccessStep } from './FeedbackSuccessStep'
4 |
5 | export default {
6 | title: 'Widget UI/FeedbackSuccessStep',
7 | component: FeedbackSuccessStep,
8 | decorators: [
9 | (Story) => {
10 | return (
11 |
12 |
13 |
14 | {Story()}
15 |
16 |
17 |
18 | )
19 | }
20 | ],
21 | } as Meta
22 |
23 | export const Default: StoryObj = {}
--------------------------------------------------------------------------------
/src/components/WidgetForm/Steps/FeedbackSuccessStep.tsx:
--------------------------------------------------------------------------------
1 | import { CloseButton } from "../../CloseButton";
2 |
3 | interface FeedbackSuccessStepProps {
4 | onFeedbackRestartRequested: () => void
5 | }
6 |
7 | export function FeedbackSuccessStep({ onFeedbackRestartRequested }: FeedbackSuccessStepProps) {
8 | return (
9 | <>
10 |
13 |
14 |
15 |
19 |
20 |
Agradecemos o feedback!
21 |
22 |
29 |
30 |
31 | >
32 | )
33 | }
--------------------------------------------------------------------------------
/src/components/WidgetForm/Steps/FeedbackTypeStep.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Popover } from '@headlessui/react'
2 | import { Meta, StoryObj } from '@storybook/react'
3 | import { FeedbackTypeStep } from './FeedbackTypeStep'
4 |
5 | export default {
6 | title: 'Widget UI/FeedbackTypeStep',
7 | component: FeedbackTypeStep,
8 | decorators: [
9 | (Story) => {
10 | return (
11 |
12 |
13 |
14 | {Story()}
15 |
16 |
17 |
18 | )
19 | }
20 | ],
21 | } as Meta
22 |
23 | export const Default: StoryObj = {}
--------------------------------------------------------------------------------
/src/components/WidgetForm/Steps/FeedbackTypeStep.tsx:
--------------------------------------------------------------------------------
1 | import { FeedbackType, feedbackTypes } from ".."
2 | import { CloseButton } from "../../CloseButton"
3 |
4 | interface FeedbackTypeStepProps {
5 | onFeedbackTypeChanged: (type: FeedbackType) => void
6 | }
7 |
8 | export function FeedbackTypeStep({ onFeedbackTypeChanged }: FeedbackTypeStepProps) {
9 | return (
10 | <>
11 |
12 | Deixe seu feedback
13 |
14 |
15 |
16 |
17 | {Object.entries(feedbackTypes).map(([key, value]) => {
18 | return (
19 |
28 | )
29 | })}
30 |
31 | >
32 | )
33 | }
--------------------------------------------------------------------------------
/src/components/WidgetForm/index.tsx:
--------------------------------------------------------------------------------
1 | import { CloseButton } from "../CloseButton";
2 |
3 | import bugImageUrl from '../../assets/bug.svg'
4 | import ideaImageUrl from '../../assets/idea.svg'
5 | import thoughtImageUrl from '../../assets/thought.svg'
6 | import { useState } from "react";
7 | import { FeedbackTypeStep } from "./Steps/FeedbackTypeStep";
8 | import { FeedbackContentStep } from "./Steps/FeedbackContentStep";
9 | import { FeedbackSuccessStep } from "./Steps/FeedbackSuccessStep";
10 |
11 | export const feedbackTypes = {
12 | BUG: {
13 | title: 'Problema',
14 | image: {
15 | source: bugImageUrl,
16 | alt: 'Ilustração de um inseto roxo'
17 | }
18 | },
19 | IDEA: {
20 | title: 'Ideia',
21 | image: {
22 | source: ideaImageUrl,
23 | alt: 'Lâmpada acesa'
24 | }
25 | },
26 | OTHER: {
27 | title: 'Outro',
28 | image: {
29 | source: thoughtImageUrl,
30 | alt: 'Núvem de pensamento'
31 | }
32 | },
33 | }
34 |
35 | export type FeedbackType = keyof typeof feedbackTypes;
36 |
37 | export function WidgetForm() {
38 | const [feedbackType, setFeedbackType] = useState(null);
39 | const [feedbackSent, setFeedbackSent] = useState(false)
40 |
41 | function handleRestartFeedback() {
42 | setFeedbackSent(false)
43 | setFeedbackType(null)
44 | }
45 |
46 | return (
47 |
48 | {feedbackSent ? (
49 |
50 | ): (
51 | <>
52 | {!feedbackType ? (
53 |
54 | ): (
55 |
setFeedbackSent(true)}
59 | />
60 | )}
61 | >
62 | )}
63 |
66 |
67 | )
68 | }
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | @apply bg-[#09090A] text-zinc-100;
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/api.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const api = axios.create({
4 | baseURL: import.meta.env.VITE_API_URL,
5 | });
6 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import { App } from './App'
4 |
5 | import './global.css'
6 |
7 | ReactDOM.createRoot(document.getElementById('root')!).render(
8 |
9 |
10 |
11 | )
12 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./src/**/*.tsx"],
3 | theme: {
4 | extend: {
5 | colors: {
6 | brand: {
7 | 300: '#996dff',
8 | 500: '#8257e6',
9 | }
10 | },
11 | borderRadius: {
12 | md: '4px'
13 | }
14 | },
15 | },
16 | plugins: [
17 | require('@tailwindcss/forms'),
18 | require('tailwind-scrollbar'),
19 | ],
20 | }
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------