master a54695dea941 cached
159 files
184.4 KB
61.7k tokens
50 symbols
1 requests
Download .txt
Showing preview only (219K chars total). Download the full file or copy to clipboard to get everything.
Repository: vuejs-br/treinamento-vue3-code
Branch: master
Commit: a54695dea941
Files: 159
Total size: 184.4 KB

Directory structure:
gitextract_3dqmdq3z/

├── .github/
│   └── workflows/
│       ├── ci-dashboard-e2e.yml
│       ├── ci-dashboard-unit.yml
│       ├── ci-widget-e2e.yml
│       └── ci-widget-unit.yml
├── .gitignore
├── backend/
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── Dockerfile
│   ├── README.md
│   ├── database/
│   │   ├── index.js
│   │   └── mock.js
│   ├── handlers/
│   │   ├── apikey.js
│   │   ├── auth.js
│   │   ├── feedbacks.js
│   │   └── users.js
│   ├── index.js
│   ├── package.json
│   ├── vercel.json
│   └── vuejs_brasil_feedbacker.json
├── conceitos/
│   ├── data-binding/
│   │   └── App.vue
│   ├── diretivas/
│   │   └── App.vue
│   ├── eventos-e-metodos/
│   │   └── App.vue
│   ├── lifecycle-hooks/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── babel.config.js
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── index.html
│   │   └── src/
│   │       ├── App.vue
│   │       └── main.js
│   ├── nova-syntax-e-antiga/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── babel.config.js
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── index.html
│   │   └── src/
│   │       ├── App.vue
│   │       └── main.js
│   └── single-file-components/
│       └── App.vue
├── dashboard/
│   ├── .browserslistrc
│   ├── .editorconfig
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── Dockerfile
│   ├── README.md
│   ├── babel.config.js
│   ├── cypress.json
│   ├── jest.config.js
│   ├── package.json
│   ├── palette.js
│   ├── postcss.config.js
│   ├── public/
│   │   ├── _redirects
│   │   └── index.html
│   ├── src/
│   │   ├── App.vue
│   │   ├── assets/
│   │   │   └── css/
│   │   │       ├── fonts.css
│   │   │       └── tailwind.css
│   │   ├── components/
│   │   │   ├── ContentLoader/
│   │   │   │   └── index.vue
│   │   │   ├── FeedbackCard/
│   │   │   │   ├── Badge.vue
│   │   │   │   ├── Loading.vue
│   │   │   │   └── index.vue
│   │   │   ├── HeaderLogged/
│   │   │   │   ├── HeaderLogged.spec.js
│   │   │   │   ├── __snapshots__/
│   │   │   │   │   └── HeaderLogged.spec.js.snap
│   │   │   │   └── index.vue
│   │   │   ├── Icon/
│   │   │   │   ├── ChevronDown.vue
│   │   │   │   ├── Copy.vue
│   │   │   │   ├── Loading.vue
│   │   │   │   └── index.vue
│   │   │   ├── ModalCreateAccount/
│   │   │   │   └── index.vue
│   │   │   ├── ModalFactory/
│   │   │   │   └── index.vue
│   │   │   └── ModalLogin/
│   │   │       └── index.vue
│   │   ├── hooks/
│   │   │   ├── useModal.js
│   │   │   └── useStore.js
│   │   ├── main.js
│   │   ├── router/
│   │   │   └── index.js
│   │   ├── services/
│   │   │   ├── __snapshots__/
│   │   │   │   └── auth.spec.js.snap
│   │   │   ├── auth.js
│   │   │   ├── auth.spec.js
│   │   │   ├── feedbacks.js
│   │   │   ├── index.js
│   │   │   └── users.js
│   │   ├── store/
│   │   │   ├── global.js
│   │   │   ├── index.js
│   │   │   ├── user.js
│   │   │   └── user.spec.js
│   │   ├── utils/
│   │   │   ├── bus.js
│   │   │   ├── date.js
│   │   │   ├── timeout.js
│   │   │   ├── validators.js
│   │   │   └── validators.spec.js
│   │   └── views/
│   │       ├── Credencials/
│   │       │   └── index.vue
│   │       ├── Feedbacks/
│   │       │   ├── Filters.vue
│   │       │   ├── FiltersLoading.vue
│   │       │   └── index.vue
│   │       └── Home/
│   │           ├── Contact.vue
│   │           ├── CustomHeader.vue
│   │           ├── Home.spec.js
│   │           ├── __snapshots__/
│   │           │   └── Home.spec.js.snap
│   │           └── index.vue
│   ├── tailwind.config.js
│   └── tests/
│       └── e2e/
│           ├── .eslintrc.js
│           ├── plugins/
│           │   └── index.js
│           ├── specs/
│           │   ├── credencials.js
│           │   └── home.js
│           └── support/
│               ├── commands.js
│               └── index.js
├── try-widget/
│   └── index.html
└── widget/
    ├── .browserslistrc
    ├── .editorconfig
    ├── .eslintrc.js
    ├── .gitignore
    ├── Dockerfile
    ├── README.md
    ├── babel.config.js
    ├── cypress.json
    ├── jest.config.js
    ├── package.json
    ├── palette.js
    ├── postcss.config.js
    ├── public/
    │   ├── index.html
    │   └── init.js
    ├── src/
    │   ├── App.vue
    │   ├── assets/
    │   │   └── css/
    │   │       ├── fonts.css
    │   │       └── tailwind.css
    │   ├── components/
    │   │   ├── Icon/
    │   │   │   ├── ArrowRight.vue
    │   │   │   ├── Atention.vue
    │   │   │   ├── Chat.vue
    │   │   │   ├── Check.vue
    │   │   │   ├── ChevronDown.vue
    │   │   │   ├── Close.vue
    │   │   │   ├── Copy.vue
    │   │   │   ├── Loading.vue
    │   │   │   └── index.vue
    │   │   └── Wizard/
    │   │       ├── Error.vue
    │   │       ├── SelectFeedbackType.vue
    │   │       ├── Success.vue
    │   │       ├── WriteAFeedback.vue
    │   │       └── index.vue
    │   ├── hooks/
    │   │   ├── iframe.ts
    │   │   ├── navigation.ts
    │   │   └── store.ts
    │   ├── main.ts
    │   ├── services/
    │   │   ├── feedbacks.ts
    │   │   └── index.ts
    │   ├── shims-vue.d.ts
    │   ├── store/
    │   │   └── index.ts
    │   ├── types/
    │   │   ├── error.ts
    │   │   └── feedback.ts
    │   ├── utils/
    │   │   └── bootstrap.ts
    │   └── views/
    │       ├── Playground/
    │       │   ├── Playground.spec.js
    │       │   ├── __snapshots__/
    │       │   │   └── Playground.spec.js.snap
    │       │   └── index.vue
    │       └── Widget/
    │           ├── Box.vue
    │           ├── Standby.vue
    │           └── index.vue
    ├── tailwind.config.js
    ├── tests/
    │   └── e2e/
    │       ├── .eslintrc.js
    │       ├── plugins/
    │       │   └── index.js
    │       ├── specs/
    │       │   └── widget.js
    │       └── support/
    │           ├── commands.js
    │           └── index.js
    └── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci-dashboard-e2e.yml
================================================
name: Dashboard e2e testing

on:
  workflow_dispatch:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

jobs:
  cypress:
    defaults:
      run:
        working-directory: dashboard

    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v2

      - run: npm install

      - run: npm run build
        env:
          NODE_ENV: production

      - name: Run project locally
        run: |
          npm install serve
          $(npm bin)/serve dist -s -p 8080 &

      - name: Run tests
        uses: cypress-io/github-action@v2
        with:	
          working-directory: dashboard
          browser: chrome
          headless: true


================================================
FILE: .github/workflows/ci-dashboard-unit.yml
================================================
name: Dashboard unit testing

on:
  workflow_dispatch:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

defaults:
  run:
    working-directory: dashboard

jobs:
  jest:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [13.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm install
    - run: npm run test:unit --if-present


================================================
FILE: .github/workflows/ci-widget-e2e.yml
================================================
name: Widget e2e testing

on:
  workflow_dispatch:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

jobs:
  cypress:
    defaults:
      run:
        working-directory: widget

    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v2

      - run: npm install

      - run: npm run build
        env:
          NODE_ENV: production

      - name: Run project locally
        run: |
          npm install serve
          $(npm bin)/serve dist -s -p 8080 &

      - name: Run tests
        uses: cypress-io/github-action@v2
        with:	
          working-directory: widget
          browser: chrome
          headless: true


================================================
FILE: .github/workflows/ci-widget-unit.yml
================================================
name: Widget unit testing

on:
  workflow_dispatch:
  push:
    branches: [ $default-branch ]
  pull_request:
    branches: [ $default-branch ]

defaults:
  run:
    working-directory: widget

jobs:
  jest:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [13.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm install
    - run: npm run test:unit --if-present


================================================
FILE: .gitignore
================================================
.DS_Store
node_modules


================================================
FILE: backend/.eslintrc.js
================================================
module.exports = {
    "env": {
        "commonjs": true,
        "es6": true,
        "node": true
    },
    "extends": [
        "standard"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaVersion": 2018
    },
    "rules": {
    }
};

================================================
FILE: backend/.gitignore
================================================
.now
.vercel


================================================
FILE: backend/Dockerfile
================================================
FROM node:10-alpine

RUN mkdir -p /src

COPY package.json src/package.json

WORKDIR /src

RUN npm install --only=production --silent

COPY . /src

CMD npm start


================================================
FILE: backend/README.md
================================================
## Backend do curso treinamento de Vue.js 3

Backend pré-pronto do curso treinamento de Vue.js 3

### Comandos

```
# Buildar o backend em um container do docker
npm run build

# Rodar o container que foi buildado
npm run container

# O backend estará disponível na porta 3000
```

_Este backend existe para auxiliar o curso de front-end com [Vue.js 3 do Vue.js Brasil](https://treinamento.vuejsbrasil.org/)_



================================================
FILE: backend/database/index.js
================================================
const database = require('./mock')

function wait (timeMs) {
  return new Promise(resolve => {
    setTimeout(resolve, timeMs)
  })
}

async function update (col, id, data) {
  if (!database[col]) {
    return false
  }

  database[col] = database[col].map(item => {
    if (item.id === id) {
      return { ...item, ...data }
    }

    return item
  })

  await wait(500)
  return true
}

async function readAll (col) {
  await wait(2500)
  if (!database[col]) {
    return []
  }

  return database[col].sort((a, b) => b.createdAt - a.createdAt)
}

async function insert (col, data) {
  if (!database[col]) {
    database[col] = []
  }

  database[col].push(data)
  await wait(500)
  return true
}

async function readOneById (col, id) {
  if (!database[col]) return
  const res = database[col].find(item => String(item.id) === String(id))

  await wait(500)
  return res
}

async function readOneByEmail (col, email) {
  if (!database[col]) return
  const res = database[col].find(item => String(item.email) === String(email))

  await wait(500)
  return res
}

module.exports = {
  update,
  insert,
  readAll,
  readOneById,
  readOneByEmail
}


================================================
FILE: backend/database/mock.js
================================================
module.exports = {
  users: [
    {
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      name: 'Igor Halfeld',
      email: 'igor@igor.me',
      password: '1234',
      apiKey: ['fcd5015c-10d3-4e9c-b395-ec7ed8850165'],
      createdAt: new Date('2020-09-05').getTime()
    }
  ],
  feedbacks: [
    {
      text: 'Muito bom!',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'OTHER',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-11-13').getTime()
    },
    {
      text: 'Muitos erros slkkkkkkk',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'ISSUE',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-10-23').getTime()
    },
    {
      text: 'Podia ter um botão de solicitar demo',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'IDEA',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-09-23').getTime()
    },
    {
      text: 'Podia ter um botão de solicitar demo 1',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'IDEA',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-12-23').getTime()
    },
    {
      text: 'Podia ter um botão de solicitar demo 2',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'IDEA',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-08-23').getTime()
    },
    {
      text: 'Muitos erros slkkkkkkk 2',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'ISSUE',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-05-23').getTime()
    },
    {
      text: 'Tava bom, agora parece que piorou',
      fingerprint: '490135491',
      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',
      apiKey: 'fcd5015c-10d3-4e9c-b395-ec7ed8850165',
      type: 'ISSUE',
      device: 'Chrome 85.0, macOS 10.14',
      page: 'https://feedbacker.com/pricing',
      createdAt: new Date('2020-05-23').getTime()
    }
  ]
}


================================================
FILE: backend/handlers/apikey.js
================================================
function CreateApiKeyHandler (db) {
  async function checkIfApiKeyExists (ctx) {
    const { apikey } = ctx.query
    if (!apikey) {
      ctx.status = 400
      ctx.body = { error: 'apikey query param not provided' }
      return
    }
    const users = await db.readAll('users')

    const apiKeyExists = users.map((user) => {
      return user.apiKey.includes(apikey)
    })

    if (apiKeyExists.includes(true)) {
      ctx.status = 200
      return
    }

    ctx.status = 404
  }

  return {
    checkIfApiKeyExists
  }
}

module.exports = CreateApiKeyHandler


================================================
FILE: backend/handlers/auth.js
================================================
const jwt = require('jsonwebtoken')

function CreateAuthHandler (db) {
  async function login (ctx) {
    const { email, password } = ctx.request.body
    const user = await db.readOneByEmail('users', email)

    if (!user) {
      ctx.status = 404
      ctx.body = { error: 'Not found' }
      return
    }

    const canLogin = () => (
      user.email === email &&
      user.password === password
    )

    if (!canLogin()) {
      ctx.status = 401
      ctx.body = { error: 'Unauthorized' }
      return
    }

    const token = jwt.sign({
      id: user.id,
      email: user.email,
      name: user.name
    }, process.env.JWT_SECRET)

    ctx.status = 200
    ctx.body = { token }
  }

  return { login }
}

module.exports = CreateAuthHandler


================================================
FILE: backend/handlers/feedbacks.js
================================================
const { v4: uuidv4 } = require('uuid')

const FEEDBACK_TYPES = {
  ISSUE: 'ISSUE',
  IDEA: 'IDEA',
  OTHER: 'OTHER'
}

function CreateFeedbackHandler (db) {
  async function create (ctx) {
    const {
      type,
      text,
      apiKey,
      fingerprint,
      device,
      page
    } = ctx.request.body

    if (!type) {
      ctx.status = 400
      ctx.body = { error: 'type is empty' }
    }
    if (!text) {
      ctx.status = 400
      ctx.body = { error: 'text is empty' }
    }
    if (!fingerprint) {
      ctx.status = 400
      ctx.body = { error: 'fingerprint is empty' }
    }
    if (!device) {
      ctx.status = 400
      ctx.body = { error: 'device is empty' }
    }
    if (!page) {
      ctx.status = 400
      ctx.body = { error: 'page is empty' }
    }
    if (!apiKey) {
      ctx.status = 400
      ctx.body = { error: 'apiKey is empty' }
    }

    if (!FEEDBACK_TYPES[String(type).toUpperCase()]) {
      ctx.status = 422
      ctx.body = { error: 'Unknown feedback type' }
      return
    }

    // @TODO: for this, I don't validate if apikey is valid.
    // Just for study purposes.

    const feedback = {
      text,
      fingerprint,
      id: uuidv4(),
      apiKey,
      type: String(type).toUpperCase(),
      device,
      page,
      createdAt: new Date().getTime()
    }

    const inserted = await db.insert('feedbacks', feedback)
    if (inserted) {
      ctx.status = 201
      ctx.body = feedback
      return
    }

    ctx.status = 422
    ctx.body = { error: 'Feedback not created' }
  }

  async function getFeedbacks (ctx) {
    const { type } = ctx.query
    let offset = ctx.query.offset ? Number(ctx.query.offset) : 0
    let limit = ctx.query.limit ? Number(ctx.query.limit) : 5

    let [
      user,
      feedbacks
    ] = await Promise.all([
      db.readOneById('users', ctx.state.user.id),
      db.readAll('feedbacks')
    ])

    if (!user) {
      ctx.status = 401
      ctx.body = { error: 'Unauthorized' }
      return
    }

    feedbacks = feedbacks.filter((feedback) => {
      return user.apiKey.includes(feedback.apiKey)
    })

    if (type) {
      feedbacks = feedbacks.filter((feedback) => {
        return feedback.type === String(type).toUpperCase()
      })
    }

    const total = feedbacks.length

    if (limit > 10) {
      limit = 5
    }
    if (offset > limit) {
      offset = limit
    }

    feedbacks = feedbacks.slice(offset, feedbacks.length).slice(0, limit)

    ctx.status = 200
    ctx.body = {
      results: feedbacks || [],
      pagination: { offset, limit, total }
    }
  }

  async function getSummary (ctx) {
    const { type } = ctx.query
    let [
      user,
      feedbacks
    ] = await Promise.all([
      db.readOneById('users', ctx.state.user.id),
      db.readAll('feedbacks')
    ])

    if (!user) {
      ctx.status = 401
      ctx.body = { error: 'Unauthorized. User not found with this token' }
      return
    }

    feedbacks = feedbacks.filter((feedback) => {
      return user.apiKey.includes(feedback.apiKey)
    })

    if (type) {
      feedbacks = feedbacks.filter((feedback) => {
        return feedback.type === String(type).toUpperCase()
      })
    }

    let all = 0
    let issue = 0
    let idea = 0
    let other = 0

    feedbacks.forEach((feedback) => {
      all++

      if (feedback.type === 'ISSUE') {
        issue++
      }
      if (feedback.type === 'IDEA') {
        idea++
      }
      if (feedback.type === 'OTHER') {
        other++
      }
    })

    ctx.status = 200
    ctx.body = { all, issue, idea, other }
  }

  return {
    create,
    getFeedbacks,
    getSummary
  }
}

module.exports = CreateFeedbackHandler


================================================
FILE: backend/handlers/users.js
================================================
const { v4: uuidv4 } = require('uuid')

function CreateUserHandler (db) {
  async function getLoggerUser (ctx) {
    const { id } = ctx.state.user
    const user = await db.readOneById('users', id)
    if (!user) {
      ctx.status = 404
      ctx.body = { error: 'Not found' }
      return
    }

    const userResponse = {
      ...user,
      apiKey: user.apiKey[user.apiKey.length - 1]
    }

    delete userResponse.password
    ctx.status = 200
    ctx.body = userResponse
  }

  async function generateApiKey (ctx) {
    const apiKey = uuidv4()
    const { id } = ctx.state.user

    const user = await db.readOneById('users', id)
    const updated = await db.update('users', id, {
      apiKey: [...user.apiKey, apiKey]
    })
    if (updated) {
      ctx.status = 202
      ctx.body = { apiKey }
      return
    }
    ctx.status = 422
    ctx.body = { error: 'User not updated' }
  }

  async function create (ctx) {
    const { email, password, name } = ctx.request.body

    if (!email) {
      ctx.status = 400
      ctx.body = { error: 'email is empty' }
      return
    }
    if (!password) {
      ctx.status = 400
      ctx.body = { error: 'password is empty' }
      return
    }
    if (!name) {
      ctx.status = 400
      ctx.body = { error: 'name is empty' }
      return
    }

    const user = {
      id: uuidv4(),
      name,
      email,
      password,
      apiKey: [uuidv4()],
      createdAt: new Date().getTime()
    }

    const inserted = await db.insert('users', user)
    if (inserted) {
      ctx.status = 201
      ctx.body = user
      return
    }

    ctx.status = 422
    ctx.body = { error: 'User not created' }
  }

  return {
    create,
    generateApiKey,
    getLoggerUser
  }
}

module.exports = CreateUserHandler


================================================
FILE: backend/index.js
================================================
const Koa = require('koa')
const Router = require('koa-router')
const jwt = require('koa-jwt')
const cors = require('@koa/cors')
const bodyParser = require('koa-bodyparser')

const database = require('./database')
const CreateUserHandler = require('./handlers/users')
const CreateAuthHandler = require('./handlers/auth')
const CreateFeedbackHandler = require('./handlers/feedbacks')
const CreateApiKeyHandler = require('./handlers/apikey')

const app = new Koa()
const router = new Router()

const {
  JWT_SECRET = '',
  PORT = 3000
} = process.env
const authMiddleware = jwt({ secret: JWT_SECRET })
app.use(bodyParser())
app.use(cors())

const feedbacksHandler = CreateFeedbackHandler(database)
const usersHandler = CreateUserHandler(database)
const authHandler = CreateAuthHandler(database)
const apiKeyHandler = CreateApiKeyHandler(database)

router.get('/', (ctx) => {
  ctx.status = 200
  ctx.body = { message: new Date() }
})
router.head('/apikey/exists', apiKeyHandler.checkIfApiKeyExists)
router.post('/auth/register', usersHandler.create)
router.post('/auth/login', authHandler.login)
router.get('/users/me', authMiddleware, usersHandler.getLoggerUser)
router.post('/users/me/apikey', authMiddleware, usersHandler.generateApiKey)
router.get('/feedbacks', authMiddleware, feedbacksHandler.getFeedbacks)
router.post('/feedbacks', feedbacksHandler.create)
router.get('/feedbacks/summary', authMiddleware, feedbacksHandler.getSummary)
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(PORT, () => {
  console.log(`Server running http://localhost:${PORT}`)
})

module.exports = app


================================================
FILE: backend/package.json
================================================
{
  "name": "backend",
  "version": "1.0.0",
  "description": "Backend pré-pronto do curso treinamento de Vue.js 3",
  "main": "index.js",
  "scripts": {
    "start": "JWT_SECRET=sssshhhh node index.js",
    "build": "docker build -t vuejs-treinamento-backend .",
    "container": "docker run -d -p 3000:3000 vuejs-treinamento-backend",
    "dev": "DEBUG=* JWT_SECRET=sssshhhh nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@koa/cors": "^3.1.0",
    "@vercel/node": "^1.8.5",
    "jsonwebtoken": "^8.5.1",
    "koa": "^2.13.0",
    "koa-bodyparser": "^4.3.0",
    "koa-jwt": "^4.0.0",
    "koa-router": "^10.0.0",
    "uuid": "^8.3.1"
  },
  "devDependencies": {
    "eslint": "^7.13.0",
    "eslint-config-standard": "^16.0.1",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^4.2.1",
    "nodemon": "^2.0.6"
  }
}


================================================
FILE: backend/vercel.json
================================================
{
  "version": 2,
  "builds": [
    {
      "src": "index.js",
      "use": "@vercel/node"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/"
    }
  ]
}


================================================
FILE: backend/vuejs_brasil_feedbacker.json
================================================
{
	"info": {
		"_postman_id": "03caccb8-c176-4e3f-ae31-751901560b3c",
		"name": "Vue.js Brasil - Feedbacker",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
	},
	"item": [
		{
			"name": "Status",
			"item": [
				{
					"name": "Pegar o status",
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "",
								"value": "",
								"type": "text"
							}
						],
						"url": {
							"raw": "http://localhost:3000/?",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								""
							],
							"query": [
								{
									"key": "Authorization",
									"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVhYjc1OWY4LWYyMzgtNGZmOS1hZTkxLWVlMTU1ODk4MjMyOSIsImVtYWlsIjoiaWd1aW5AaWd1aW4ubWUiLCJuYW1lIjoiSWdvciBIYWxmZWxkIiwiaWF0IjoxNjA4NTA2ODAwfQ.AXBHWYY1hioeBXQfhxpI9uBGDH3shKqGgWE2JuTOsh4",
									"disabled": true
								}
							]
						}
					},
					"response": []
				}
			],
			"protocolProfileBehavior": {}
		},
		{
			"name": "Apikey",
			"item": [
				{
					"name": "Checar se a apikey existe",
					"request": {
						"method": "HEAD",
						"header": [],
						"url": {
							"raw": "http://localhost:3000/apikey/exists?apikey=fcd5015c-10d3-4e9c-b395-ec7ed8850165",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"apikey",
								"exists"
							],
							"query": [
								{
									"key": "apikey",
									"value": "fcd5015c-10d3-4e9c-b395-ec7ed8850165"
								}
							]
						}
					},
					"response": []
				}
			],
			"protocolProfileBehavior": {}
		},
		{
			"name": "Auth",
			"item": [
				{
					"name": "Fazer login",
					"request": {
						"method": "POST",
						"header": [],
						"body": {
							"mode": "raw",
							"raw": "{\n    \"email\": \"igor@igor.me\",\n    \"password\": \"1234\"\n}",
							"options": {
								"raw": {
									"language": "json"
								}
							}
						},
						"url": {
							"raw": "http://localhost:3000/auth/login",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"auth",
								"login"
							]
						}
					},
					"response": []
				},
				{
					"name": "Criar um novo usuário",
					"request": {
						"method": "POST",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijc4NzRiNzYwLTIyZTItNDE2NS05NDJhLWE4MDcwODJmNDNhNCIsImVtYWlsIjoiZXZhbi55b3VAZ21haWwuY29tIiwibmFtZSI6IkV2YW4gWW91IiwiaWF0IjoxNjA5ODExOTc4fQ.4M4u5n7n8NMfsNDWNAeLw8q20PBQFFfRHicbwT8r8W8",
								"type": "text"
							}
						],
						"body": {
							"mode": "raw",
							"raw": "{\n\t\"name\": \"Evan You\",\n    \"email\": \"evan.you@gmail.com\",\n    \"password\": \"1234\"\n}",
							"options": {
								"raw": {
									"language": "json"
								}
							}
						},
						"url": {
							"raw": "http://localhost:3000/auth/register",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"auth",
								"register"
							]
						}
					},
					"response": []
				}
			],
			"protocolProfileBehavior": {}
		},
		{
			"name": "Users",
			"item": [
				{
					"name": "Pegar os dados do usuário logado",
					"protocolProfileBehavior": {
						"disableBodyPruning": true
					},
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijc4NzRiNzYwLTIyZTItNDE2NS05NDJhLWE4MDcwODJmNDNhNCIsImVtYWlsIjoiZXZhbi55b3VAZ21haWwuY29tIiwibmFtZSI6IkV2YW4gWW91IiwiaWF0IjoxNjA5ODExOTc4fQ.4M4u5n7n8NMfsNDWNAeLw8q20PBQFFfRHicbwT8r8W8",
								"type": "text"
							}
						],
						"body": {
							"mode": "raw",
							"raw": ""
						},
						"url": {
							"raw": "http://localhost:3000/users/me",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"users",
								"me"
							]
						}
					},
					"response": []
				},
				{
					"name": "Gerar uma nova apiKey",
					"request": {
						"method": "POST",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijc4NzRiNzYwLTIyZTItNDE2NS05NDJhLWE4MDcwODJmNDNhNCIsImVtYWlsIjoiZXZhbi55b3VAZ21haWwuY29tIiwibmFtZSI6IkV2YW4gWW91IiwiaWF0IjoxNjA5ODExOTc4fQ.4M4u5n7n8NMfsNDWNAeLw8q20PBQFFfRHicbwT8r8W8",
								"type": "text"
							}
						],
						"url": {
							"raw": "http://localhost:3000/users/me/apikey",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"users",
								"me",
								"apikey"
							]
						}
					},
					"response": []
				}
			],
			"protocolProfileBehavior": {}
		},
		{
			"name": "Feedbacks",
			"item": [
				{
					"name": "Pegar índice de feedbacks",
					"protocolProfileBehavior": {
						"disableBodyPruning": true
					},
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVhYjc1OWY4LWYyMzgtNGZmOS1hZTkxLWVlMTU1ODk4MjMyOSIsImVtYWlsIjoiaWdvckBpZ29yLm1lIiwibmFtZSI6Iklnb3IgSGFsZmVsZCIsImlhdCI6MTYxMDQyNjg4OH0.88S5YLssZhC_TgotUZFDlcw5Cc3xlQTB0mqsQcQu1dY",
								"type": "text"
							}
						],
						"body": {
							"mode": "raw",
							"raw": "{\n    \"type\": \"ISSUE\",\n    \"text\": \"Tem um problema aqui\",\n    \"fingerprint\": \"10wdjas0da93r0jf\",\n    \"device\": \"Chrome 34, Mac OS\",\n    \"page\": \"https://feedbacker.com.br/ajuda\"\n}",
							"options": {
								"raw": {
									"language": "json"
								}
							}
						},
						"url": {
							"raw": "http://localhost:3000/feedbacks/summary",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks",
								"summary"
							]
						}
					},
					"response": []
				},
				{
					"name": "Pegar todos os feedbacks",
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijc4NzRiNzYwLTIyZTItNDE2NS05NDJhLWE4MDcwODJmNDNhNCIsImVtYWlsIjoiZXZhbi55b3VAZ21haWwuY29tIiwibmFtZSI6IkV2YW4gWW91IiwiaWF0IjoxNjA5ODExOTc4fQ.4M4u5n7n8NMfsNDWNAeLw8q20PBQFFfRHicbwT8r8W8",
								"type": "text"
							}
						],
						"url": {
							"raw": "http://localhost:3000/feedbacks",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							]
						}
					},
					"response": []
				},
				{
					"name": "Pegar todos os feedbacks do tipo IDEA",
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVhYjc1OWY4LWYyMzgtNGZmOS1hZTkxLWVlMTU1ODk4MjMyOSIsImVtYWlsIjoiaWdvckBpZ29yLm1lIiwibmFtZSI6Iklnb3IgSGFsZmVsZCIsImlhdCI6MTYxMDc0MzgyNn0.2R-hm8yCSAtpcvniI1R9CNF_ZzguRaMZoU2pTrwijds",
								"type": "text"
							}
						],
						"url": {
							"raw": "http://localhost:3000/feedbacks?type=idea",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							],
							"query": [
								{
									"key": "type",
									"value": "idea"
								}
							]
						}
					},
					"response": []
				},
				{
					"name": "Pegar todos os feedbacks do tipo ISSUE",
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVhYjc1OWY4LWYyMzgtNGZmOS1hZTkxLWVlMTU1ODk4MjMyOSIsImVtYWlsIjoiaWdvckBpZ29yLm1lIiwibmFtZSI6Iklnb3IgSGFsZmVsZCIsImlhdCI6MTYxMDc0MzgyNn0.2R-hm8yCSAtpcvniI1R9CNF_ZzguRaMZoU2pTrwijds",
								"type": "text"
							}
						],
						"url": {
							"raw": "http://localhost:3000/feedbacks?type=issue",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							],
							"query": [
								{
									"key": "type",
									"value": "issue"
								}
							]
						}
					},
					"response": []
				},
				{
					"name": "Pegar todos os feedbacks do tipo OTHER",
					"request": {
						"method": "GET",
						"header": [
							{
								"key": "Authorization",
								"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImVhYjc1OWY4LWYyMzgtNGZmOS1hZTkxLWVlMTU1ODk4MjMyOSIsImVtYWlsIjoiaWdvckBpZ29yLm1lIiwibmFtZSI6Iklnb3IgSGFsZmVsZCIsImlhdCI6MTYxMDc0MzgyNn0.2R-hm8yCSAtpcvniI1R9CNF_ZzguRaMZoU2pTrwijds",
								"type": "text"
							}
						],
						"url": {
							"raw": "http://localhost:3000/feedbacks?type=other",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							],
							"query": [
								{
									"key": "type",
									"value": "other"
								}
							]
						}
					},
					"response": []
				},
				{
					"name": "Criar um feedback ISSUE",
					"request": {
						"method": "POST",
						"header": [],
						"body": {
							"mode": "raw",
							"raw": "{\n    \"type\": \"ISSUE\",\n    \"text\": \"Tem um problema aqui\",\n    \"fingerprint\": \"10wdjas0da93r0jf\",\n    \"apiKey\": \"fcd5015c-10d3-4e9c-b395-ec7ed8850165\",\n    \"device\": \"Chrome 34, Mac OS\",\n    \"page\": \"https://feedbacker.com.br/ajuda\"\n}",
							"options": {
								"raw": {
									"language": "json"
								}
							}
						},
						"url": {
							"raw": "http://localhost:3000/feedbacks",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							]
						}
					},
					"response": []
				},
				{
					"name": "Criar um feedback IDEA",
					"request": {
						"method": "POST",
						"header": [],
						"body": {
							"mode": "raw",
							"raw": "{\n    \"type\": \"IDEA\",\n    \"text\": \"E se mudar a cor dessa página?\",\n    \"fingerprint\": \"10wdjas0da93r0jf\",\n    \"apiKey\": \"fcd5015c-10d3-4e9c-b395-ec7ed8850165\",\n    \"device\": \"Chrome 34, Mac OS\",\n    \"page\": \"https://feedbacker.com.br/ajuda\"\n}",
							"options": {
								"raw": {
									"language": "json"
								}
							}
						},
						"url": {
							"raw": "http://localhost:3000/feedbacks",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							]
						}
					},
					"response": []
				},
				{
					"name": "Criar um feedback OTHER",
					"request": {
						"method": "POST",
						"header": [],
						"body": {
							"mode": "raw",
							"raw": "{\n    \"type\": \"OTHER\",\n    \"text\": \"Muito show!\",\n    \"fingerprint\": \"10wdjas0da93r0jf\",\n    \"apiKey\": \"fcd5015c-10d3-4e9c-b395-ec7ed8850165\",\n    \"device\": \"Chrome 34, Mac OS\",\n    \"page\": \"https://feedbacker.com.br/ajuda\"\n}",
							"options": {
								"raw": {
									"language": "json"
								}
							}
						},
						"url": {
							"raw": "http://localhost:3000/feedbacks",
							"protocol": "http",
							"host": [
								"localhost"
							],
							"port": "3000",
							"path": [
								"feedbacks"
							]
						}
					},
					"response": []
				}
			],
			"protocolProfileBehavior": {}
		}
	],
	"protocolProfileBehavior": {}
}

================================================
FILE: conceitos/data-binding/App.vue
================================================
<template>
  <div>
    <h1 :style="{ textDecoration: decoration }">Hello {{ name }}!</h1>
    <input type="text" v-model="name">
    <br>
    <a :href="link">Link pro curso!</a>
  </div>
</template>

<script>
export default {
  data: () => ({
    name: 'Igor',
    link: 'https://treinamento.vuejsbrasil.org',
    decoration: 'underline'
  })
}
</script>



================================================
FILE: conceitos/diretivas/App.vue
================================================
<template>
  <div>
    <h1>Minha lista de tarefas!</h1>
    <button @click="() => showList = !showList">
      Ver a lista!
    </button>
    <br>
    <input type="text" v-focus> 

    <ul v-if="showList">
      <li
        v-for="(task, index) in tasks"
        :key="`${task}-${index}`"
      >
        {{ task.name }}
      </li>
    </ul>
    <p v-else>Lista de tarefas escondidas</p>
  </div>
</template>

<script>
const focus = {
  inserted: (el) => {
    el.focus()
  }
}

export default {
  directives: {
    focus
  },
  data: () => ({
    showList: false,
    tasks: [
      { name: 'Fazer o curso', isDone: false }
    ]
  })
}
</script>



================================================
FILE: conceitos/eventos-e-metodos/App.vue
================================================
<template>
  <div>
    <h1>Minha lista de tarefas!</h1>
    <button @click="handleShowHideList">
      Ver a lista!
    </button>
    <br>
    <input
      type="text"
      @keyup.enter="addTask"
      v-focus
      v-model="currentTask"> 

    <ul v-if="showList">
      <li
        v-for="(task, index) in tasks"
        @dblclick="complete(task)"
        :key="`${task}-${index}`"
        class="task-item"
        :class="{
          'line-through': task.isDone
        }"
      >
        {{ task.name }}
        <button
          @click="remove(task)"
        >&times;</button>
      </li>
    </ul>
    <p v-else>Lista de tarefas escondidas</p>
  </div>
</template>

<script>
const focus = {
  inserted: (el) => {
    el.focus()
  }
}

export default {
  directives: {
    focus
  },
  data: () => ({
    currentTask: '',
    showList: false,
    tasks: [
      { name: 'Fazer o curso', isDone: false }
    ]
  }),
  methods: {
    handleShowHideList () {
      this.showList = !this.showList
    },
    addTask () {
      this.tasks.push({
        name: this.currentTask,
        isDone: false
      })
      this.currentTask = ''
    },
    complete (task) {
      this.tasks = this.tasks.map(t => {
        if (t.name === task.name) {
          return { ...t, isDone: !t.isDone }
        }
        return { ...t }
      })
    },
    remove (task) {
      this.tasks = this.tasks.filter(t => t.name !== task.name)
    }
  }
}
</script>

<style scoped>
.line-through {
  text-decoration: line-through;
}
.task-item {
  cursor: pointer;
}
</style>


================================================
FILE: conceitos/lifecycle-hooks/.gitignore
================================================
.DS_Store
node_modules
/dist


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: conceitos/lifecycle-hooks/README.md
================================================
# nova-syntax-e-antiga

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).


================================================
FILE: conceitos/lifecycle-hooks/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}


================================================
FILE: conceitos/lifecycle-hooks/package.json
================================================
{
  "name": "nova-syntax-e-antiga",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}


================================================
FILE: conceitos/lifecycle-hooks/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: conceitos/lifecycle-hooks/src/App.vue
================================================
<template>
  <h1>Lifecycle hooks</h1>
</template>

<script>
import { onMounted } from 'vue'

export default {
  setup () {
    onMounted(() => {
      console.log('Foi montado composition API')
    })
  }
}
/*
export default {
  mounted () {
    console.log('Foi montado options API')
  }
}
*/
</script>


================================================
FILE: conceitos/lifecycle-hooks/src/main.js
================================================
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')


================================================
FILE: conceitos/nova-syntax-e-antiga/.gitignore
================================================
.DS_Store
node_modules
/dist


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: conceitos/nova-syntax-e-antiga/README.md
================================================
# nova-syntax-e-antiga

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).


================================================
FILE: conceitos/nova-syntax-e-antiga/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}


================================================
FILE: conceitos/nova-syntax-e-antiga/package.json
================================================
{
  "name": "nova-syntax-e-antiga",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}


================================================
FILE: conceitos/nova-syntax-e-antiga/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: conceitos/nova-syntax-e-antiga/src/App.vue
================================================
<template>
  <div>
    <h1>Minha lista de tarefas!</h1>
    <button @click="handleShowHideList">
      Ver a lista!
    </button>
    <br>
    <input
      type="text"
      @keyup.enter="addTask"
      v-focus
      v-model="state.currentTask"> 

    <ul v-if="state.showList">
      <li
        v-for="(task, index) in state.tasks"
        @dblclick="complete(task)"
        :key="`${task}-${index}`"
        class="task-item"
        :class="{
          'line-through': task.isDone
        }"
      >
        {{ task.name }}
        <button
          @click="remove(task)"
        >&times;</button>
      </li>
    </ul>
    <p v-else>Lista de tarefas escondidas</p>
  </div>
</template>

<script>
import { reactive } from 'vue'

const focus = {
  inserted: (el) => {
    el.focus()
  }
}

export default {
  directives: {
    focus
  },
  setup () {
    const state = reactive({
      currentTask: '',
      showList: false,
      tasks: [
        { name: 'Fazer o curso', isDone: false }
      ]
    })

    function handleShowHideList () {
      state.showList = !state.showList
    }

    function addTask () {
      state.tasks.push({
        name: state.currentTask,
        isDone: false
      })
      state.currentTask = ''
    }

    function complete (task) {
      state.tasks = state.tasks.map(t => {
        if (t.name === task.name) {
          return { ...t, isDone: !t.isDone }
        }
        return { ...t }
      })
    }

    function remove (task) {
      state.tasks = state.tasks.filter(t => t.name !== task.name)
    }

    return {
      state,
      handleShowHideList,
      addTask,
      complete,
      remove
    }
  }
}
</script>

<style scoped>
.line-through {
  text-decoration: line-through;
}
.task-item {
  cursor: pointer;
}
</style>


================================================
FILE: conceitos/nova-syntax-e-antiga/src/main.js
================================================
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')


================================================
FILE: conceitos/single-file-components/App.vue
================================================
<template>  
  <h1>Hello World</h1>
</template>

<script lang="ts">
export default {}
</script>

<style lang="scss">
</style>


================================================
FILE: dashboard/.browserslistrc
================================================
> 1%
last 2 versions
not dead


================================================
FILE: dashboard/.editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: dashboard/.eslintrc.js
================================================
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    '@vue/standard'
  ],
  parserOptions: {
    parser: 'babel-eslint'
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  },
  overrides: [
    {
      files: [
        '**/*.spec.js'
      ],
      env: {
        jest: true
      }
    }
  ]
}


================================================
FILE: dashboard/.gitignore
================================================
.DS_Store
node_modules
/dist

/tests/e2e/videos/
/tests/e2e/screenshots/


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: dashboard/Dockerfile
================================================
FROM node:13-alpine as build

WORKDIR /

COPY . .

ENV NODE_ENV=production
RUN npm install --production
RUN npm run build

FROM nginx:1.18.0-alpine as final

WORKDIR /
COPY --from=build ./dist /usr/share/nginx/html


================================================
FILE: dashboard/README.md
================================================
# dashboard

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Run your unit tests
```
npm run test:unit
```

### Run your end-to-end tests
```
npm run test:e2e
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).


================================================
FILE: dashboard/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}


================================================
FILE: dashboard/cypress.json
================================================
{
  "pluginsFile": "tests/e2e/plugins/index.js"
}


================================================
FILE: dashboard/jest.config.js
================================================
module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  testMatch: [
    '**/*.spec.js'
  ],
  transform: {
    '^.+\\.vue$': 'vue-jest'
  }
}


================================================
FILE: dashboard/package.json
================================================
{
  "name": "dashboard",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "test:e2e": "vue-cli-service test:e2e",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@tailwindcss/postcss7-compat": "^2.0.2",
    "animate.css": "^4.1.1",
    "autoprefixer": "^9.8.6",
    "axios": "^0.21.1",
    "core-js": "^3.6.5",
    "postcss": "^7.0.35",
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
    "tiny-emitter": "^2.1.0",
    "vee-validate": "^4.1.9",
    "vue": "^3.0.0",
    "vue-router": "^4.0.0-0",
    "vue-toastification": "^2.0.0-beta.9"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-e2e-cypress": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-unit-jest": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "@vue/eslint-config-standard": "^5.1.2",
    "@vue/test-utils": "^2.0.0-beta.14",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.0",
    "eslint-plugin-vue": "^7.0.0-0",
    "lint-staged": "^9.5.0",
    "typescript": "~3.9.3",
    "vue-jest": "^5.0.0-0"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,vue}": [
      "vue-cli-service lint",
      "git add"
    ]
  }
}


================================================
FILE: dashboard/palette.js
================================================
module.exports = {
  brand: {
    main: '#EF4983',
    gray: '#F9F9F9',
    info: '#8296FB',
    graydark: '#C0BCB0',
    warning: '#E4B52E',
    danger: '#F88676'
  },
  mediumslateblue: {
    50: '#f6f9fd',
    100: '#e8f3fd',
    200: '#cbdefb',
    300: '#abc4fb',
    400: '#8296fb',
    500: '#5667fb',
    600: '#3d46f7',
    700: '#3137e5',
    800: '#272cb9',
    900: '#202591'
  },
  slateblue: {
    50: '#f5f8fc',
    100: '#eaf0fc',
    200: '#d3d8fa',
    300: '#babcf9',
    400: '#9c8ff9',
    500: '#7a61f9',
    600: '#5d41f4',
    700: '#4933e1',
    800: '#382ab5',
    900: '#2d248f'
  },
  mediumorchid: {
    50: '#f9f7fa',
    100: '#f7edf9',
    200: '#efd1f5',
    300: '#e8aef1',
    400: '#e47cec',
    500: '#e04fe7',
    600: '#c932d7',
    700: '#9a27b5',
    800: '#712186',
    900: '#571c66'
  },
  deeppink: {
    50: '#fcf9f9',
    100: '#fcf0f3',
    200: '#fad3e6',
    300: '#f9acd1',
    400: '#fa73ab',
    500: '#fb4783',
    600: '#f42a5c',
    700: '#d6214a',
    800: '#a71b3a',
    900: '#83172f'
  },
  tomato: {
    50: '#fcf9f6',
    100: '#fcf0ed',
    200: '#fadad6',
    300: '#f8b9b0',
    400: '#f88676',
    500: '#f85b47',
    600: '#ee392e',
    700: '#cc2b2b',
    800: '#9e2328',
    900: '#7c1d23'
  },
  chocolate: {
    50: '#fbf8f2',
    100: '#fbf1e1',
    200: '#f8e1bb',
    300: '#f5c782',
    400: '#f39d41',
    500: '#f1741e',
    600: '#e24f13',
    700: '#bc3b16',
    800: '#922e1a',
    900: '#732619'
  },
  goldenrod: {
    50: '#fbfaf4',
    100: '#faf6e0',
    200: '#f5ebb0',
    300: '#efd86f',
    400: '#e4b52e',
    500: '#d69111',
    600: '#b76b0a',
    700: '#8b500e',
    800: '#663d12',
    900: '#4e3012'
  },
  darkgoldenrod: {
    50: '#fbfaf6',
    100: '#f9f8e7',
    200: '#f2efbc',
    300: '#e8de7f',
    400: '#d3bd3a',
    500: '#b79b17',
    600: '#8e750d',
    700: '#685910',
    800: '#4b4213',
    900: '#393414'
  },
  lightseagreen: {
    50: '#f5fafa',
    100: '#eaf7f5',
    200: '#ceeee8',
    300: '#a7dfd9',
    400: '#63c3be',
    500: '#30a29c',
    600: '#237f79',
    700: '#236460',
    800: '#204c4a',
    900: '#1b3d3c'
  },
  cornflowerblue: {
    50: '#f4fafc',
    100: '#e2f7fb',
    200: '#bdeaf7',
    300: '#8fd8f5',
    400: '#4fb6f1',
    500: '#238fed',
    600: '#1a6bdf',
    700: '#1b54bd',
    800: '#19418c',
    900: '#15346b'
  }
}


================================================
FILE: dashboard/postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}


================================================
FILE: dashboard/public/_redirects
================================================
/* /index.html 200



================================================
FILE: dashboard/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>Feedbacker</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: dashboard/src/App.vue
================================================
<template>
  <modal-factory />
  <router-view />
</template>

<script>
import { watch } from 'vue'
import ModalFactory from './components/ModalFactory'
import { useRouter, useRoute } from 'vue-router'
import services from './services'
import { setCurrentUser } from './store/user'

export default {
  components: { ModalFactory },
  setup () {
    const router = useRouter()
    const route = useRoute()

    watch(() => route.path, async () => {
      if (route.meta.hasAuth) {
        const token = window.localStorage.getItem('token')
        if (!token) {
          router.push({ name: 'Home' })
          return
        }

        const { data } = await services.users.getMe()
        setCurrentUser(data)
      }
    })
  }
}
</script>


================================================
FILE: dashboard/src/assets/css/fonts.css
================================================
@font-face {
  font-family: "RobotoRegular";
  src: local("Roboto Regular"), local("Roboto-Regular"),
    url("../fonts/Roboto-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: "RobotoMedium";
  src: local("Roboto Medium"), local("Roboto-Medium"),
    url("../fonts/Roboto-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
}

@font-face {
  font-family: "RobotoBold";
  src: local("Roboto Bold"), local("Roboto-Bold"),
    url("../fonts/Roboto-Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
}

@font-face {
  font-family: "RobotoBlack";
  src: local("Roboto Black"), local("Roboto-Black"),
    url("../fonts/Roboto-Black.ttf") format("truetype");
  font-weight: 900;
  font-style: normal;
}


================================================
FILE: dashboard/src/assets/css/tailwind.css
================================================
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body,
#app {
  width: 100%;
  height: 100%;
}


================================================
FILE: dashboard/src/components/ContentLoader/index.vue
================================================
<template>
  <div
    :style="{
      width: computedWidth,
      height
    }"
    class="opacity-75 content-loader"
  >
    <span :style="{ animationDuration }" class="content-loader--fx"/>
    <slot />
  </div>
</template>

<script>
import { computed } from 'vue'

export default {
  props: {
    maxWidth: {
      default: 100,
      type: Number
    },
    minWidth: {
      default: 80,
      type: Number
    },
    animationDuration: {
      type: String,
      default: '1.6s'
    },
    height: {
      default: '1rem',
      type: String
    },
    width: {
      default: '1rem',
      type: String
    }
  },
  setup (props) {
    const computedWidth = computed(() => {
      const value = Math.random() * (props.width - props.minWidth)
      return props.width ?? `${Math.floor(value + props.minWidth)}%`
    })

    return { computedWidth }
  }
}
</script>

<style lang="postcss" scoped>
@keyframes shimmer {
  100% {
    transform: translateX(100%);
  }
}

.content-loader {
  position: relative;
  vertical-align: middle;
  overflow: hidden;
  background: #f6f7f8;
}
.content-loader--fx {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transform: translateX(-100%);
  background-image: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%);
  background-position: 0 0;
  background-size: 1000 100;
  animation: shimmer infinite alternate ease-in-out;
}
</style>


================================================
FILE: dashboard/src/components/FeedbackCard/Badge.vue
================================================
<template>
  <span
    :class="`bg-${classColor}`"
    class="p-2 text-xs font-black text-white uppercase rounded-full">
    {{ label }}
  </span>
</template>

<script>
import { computed } from 'vue'
export default {
  props: {
    type: { type: String, required: true }
  },
  setup (props) {
    const label = computed(() => {
      if (props.type === 'ISSUE') {
        return 'problema'
      }

      if (props.type === 'IDEA') {
        return 'ideia'
      }

      return 'outros'
    })

    const classColor = computed(() => {
      if (props.type === 'ISSUE') {
        return 'brand-danger'
      }

      if (props.type === 'IDEA') {
        return 'brand-warning'
      }

      return 'brand-graydark'
    })

    return {
      label,
      classColor
    }
  }
}
</script>


================================================
FILE: dashboard/src/components/FeedbackCard/Loading.vue
================================================
<template>
  <content-loader
    class="flex flex-col items-center rounded"
    width="100%"
    height="300px"
  >
    <content-loader
      class="mt-3 rounded"
      width="90%"
      height="90px"
      animation-duration="2s"
    />

    <content-loader
      class="mt-3 rounded"
      width="90%"
      height="90px"
      animation-duration="2.3s"
    />

    <content-loader
      class="mt-3 rounded"
      width="90%"
      height="90px"
      animation-duration="2.7s"
    />
  </content-loader>
</template>

<script>
import ContentLoader from '../ContentLoader'

export default {
  components: { ContentLoader }
}
</script>


================================================
FILE: dashboard/src/components/FeedbackCard/index.vue
================================================
<template>
  <div
    @click="handleToggle"
    class="flex flex-col px-8 py-6 rounded cursor-pointer bg-brand-gray">
    <div class="flex items-center justify-between w-full mb-8">
      <badge :type="feedback.type" />

      <span class="font-regular text-brand-graydark">
        {{ getDiffTimeBetweenCurrentDate(feedback.createdAt) }}
      </span>
    </div>

    <div class="text-lg font-medium text-gray-800">
      {{ feedback.text }}
    </div>

    <div
      :class="{
        animate__fadeOutUp: state.isClosing
      }"
      class="flex mt-8 animate__animated animate__fadeInUp animate__faster"
      v-if="state.isOpen"
    >
      <div class="flex flex-col w-1/2">
        <div class="flex flex-col">
          <span class="font-bold text-gray-400 uppercase select-none">Página</span>
          <span class="font-medium text-gray-700">{{ feedback.page }}</span>
        </div>
        <div class="flex flex-col">
          <span class="font-bold text-gray-400 uppercase select-none">Dispositivo</span>
          <span class="font-medium text-gray-700">{{ feedback.device }}</span>
        </div>
      </div>

      <div class="flex flex-col w-1/2">
        <div class="flex flex-col">
          <span class="font-bold text-gray-400 uppercase select-none">Usuário</span>
          <span class="font-medium text-gray-700">{{ feedback.fingerprint }}</span>
        </div>
      </div>
    </div>

    <div class="flex justify-end mt-8" v-if="!state.isOpen">
      <icon name="chevron-down" size="24" :color="brandColors.graydark" />
    </div>

  </div>
</template>

<script>
import Icon from '../Icon'
import Badge from './Badge'
import { getDiffTimeBetweenCurrentDate } from '../../utils/date'
import palette from '../../../palette'
import { reactive } from 'vue'
import { wait } from '../../utils/timeout'

export default {
  components: { Badge, Icon },
  props: {
    isOpened: { type: Boolean, default: false },
    feedback: { type: Object, required: true }
  },
  setup (props) {
    const state = reactive({
      isOpen: props.isOpened,
      isClosing: !props.isOpened
    })

    async function handleToggle () {
      state.isClosing = true

      await wait(250)
      state.isOpen = !state.isOpen
      state.isClosing = false
    }

    return {
      state,
      handleToggle,
      getDiffTimeBetweenCurrentDate,
      brandColors: palette.brand
    }
  }
}
</script>


================================================
FILE: dashboard/src/components/HeaderLogged/HeaderLogged.spec.js
================================================
import { shallowMount } from '@vue/test-utils'
import HeaderLogged from '.'
import { routes } from '../../router'

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory('/'),
  routes
})

const mockStore = { currentUser: {} }
jest.mock('../../hooks/useStore', () => {
  return () => {
    return mockStore
  }
})

describe('<HeaderLogged />', () => {
  it('should render header logged correctly', async () => {
    router.push('/')
    await router.isReady()
    const wrapper = shallowMount(HeaderLogged, {
      global: {
        plugins: [router]
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should render 3 dots when there\'s not user logged', async () => {
    router.push('/')
    await router.isReady()
    const wrapper = shallowMount(HeaderLogged, {
      global: {
        plugins: [router]
      }
    })

    const buttonLogout = wrapper.find('#logout-button')
    expect(buttonLogout.text()).toBe('...')
  })

  it('should render user anem when there\'s user logged', async () => {
    router.push('/')
    await router.isReady()
    mockStore.currentUser.name = 'Igor'
    const wrapper = shallowMount(HeaderLogged, {
      global: {
        plugins: [router]
      }
    })

    const buttonLogout = wrapper.find('#logout-button')
    expect(buttonLogout.text()).toBe('Igor (sair)')
  })
})


================================================
FILE: dashboard/src/components/HeaderLogged/__snapshots__/HeaderLogged.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<HeaderLogged /> should render header logged correctly 1`] = `
<div class="flex items-center justify-between w-4/5 max-w-6xl py-10">
  <div class="w-28 lg:w-36"><img class="w-full" alt="logo"></div>
  <div class="flex">
    <ul class="flex list-none">
      <li class="px-6 py-2 mr-2 font-bold text-white rounded-full cursor-pointer focus:outline-none"> Credenciais </li>
      <li class="px-6 py-2 mr-2 font-bold text-white rounded-full cursor-pointer focus:outline-none"> Feedbacks </li>
      <li id="logout-button" class="px-6 py-2 font-bold bg-white rounded-full cursor-pointer text-brand-main focus:outline-none">...</li>
    </ul>
  </div>
</div>
`;


================================================
FILE: dashboard/src/components/HeaderLogged/index.vue
================================================
<template>
  <div class="flex items-center justify-between w-4/5 max-w-6xl py-10">
    <div class="w-28 lg:w-36">
      <img class="w-full" src="../../assets/images/logo_white.png" alt="logo">
    </div>

    <div class="flex">
      <ul class="flex list-none">
        <li
          @click="() => router.push({ name: 'Credencials' })"
          class="px-6 py-2 mr-2 font-bold text-white rounded-full cursor-pointer focus:outline-none"
        >
          Credenciais
        </li>
        <li
          @click="() => router.push({ name: 'Feedbacks' })"
          class="px-6 py-2 mr-2 font-bold text-white rounded-full cursor-pointer focus:outline-none"
        >
          Feedbacks
        </li>
        <li
          id="logout-button"
          @click="handleLogout"
          class="px-6 py-2 font-bold bg-white rounded-full cursor-pointer text-brand-main focus:outline-none"
        >
          {{ logoutLabel }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import useStore from '../../hooks/useStore'
import { cleanCurrentUser } from '../../store/user'

export default {
  setup () {
    const router = useRouter()
    const store = useStore('User')

    const logoutLabel = computed(() => {
      if (!store.currentUser.name) {
        return '...'
      }
      return `${store.currentUser.name} (sair)`
    })

    function handleLogout () {
      window.localStorage.removeItem('token')
      cleanCurrentUser()
      router.push({ name: 'Home' })
    }

    return {
      router,
      logoutLabel,
      handleLogout
    }
  }
}
</script>


================================================
FILE: dashboard/src/components/Icon/ChevronDown.vue
================================================
<template>
  <svg :width="size" :height="size" viewBox="0 0 17 10" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M1.9975 0L8.5 6.18084L15.0025 0L17 1.90283L8.5 10L0 1.90283L1.9975 0Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: dashboard/src/components/Icon/Copy.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 19 22" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M14 0H2C0.9 0 0 0.9 0 2V16H2V2H14V0ZM13 4L19 10V20C19 21.1 18.1 22 17 22H5.99C4.89 22 4 21.1 4 20L4.01 6C4.01 4.9 4.9 4 6 4H13ZM12 11H17.5L12 5.5V11Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: dashboard/src/components/Icon/Loading.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M11 6.81818V10.9091L16.5 5.45455L11 0V4.09091C4.9225 4.09091 0 8.97273 0 15C0 17.1409 0.6325 19.1318 1.705 20.8091L3.7125 18.8182C3.09375 17.6864 2.75 16.3773 2.75 15C2.75 10.4864 6.44875 6.81818 11 6.81818ZM20.295 9.19091L18.2875 11.1818C18.8925 12.3273 19.25 13.6227 19.25 15C19.25 19.5136 15.5512 23.1818 11 23.1818V19.0909L5.5 24.5455L11 30V25.9091C17.0775 25.9091 22 21.0273 22 15C22 12.8591 21.3675 10.8682 20.295 9.19091Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: dashboard/src/components/Icon/index.vue
================================================
<template>
  <component :is="name" v-bind="$props"/>
</template>

<script>
import Loading from './Loading.vue'
import Copy from './Copy.vue'
import ChevronDown from './ChevronDown.vue'

export default {
  components: { Loading, Copy, ChevronDown },
  props: {
    name: { type: String, required: true }
  }
}
</script>


================================================
FILE: dashboard/src/components/ModalCreateAccount/index.vue
================================================
<template>
  <div class="flex justify-between" id="modal-create-account">
    <h1 class="text-4xl font-black text-gray-800">
      Crie uma conta
    </h1>

    <button
      @click="close"
      class="text-4xl text-gray-600 focus:outline-none"
    >
      &times;
    </button>
  </div>

  <div class="mt-16">
    <form @submit.prevent="handleSubmit">
      <label class="block">
        <span class="text-lg font-medium text-gray-800">Nome</span>
        <input
          v-model="state.name.value"
          type="text"
          :class="{
            'border-brand-danger': !!state.name.errorMessage
          }"
          class="block w-full px-4 py-3 mt-1 text-lg bg-gray-100 border-2 border-transparent rounded"
          placeholder="Jone Doe"
        >
        <span
          v-if="!!state.name.errorMessage"
          class="block font-medium text-brand-danger"
        >
          {{ state.name.errorMessage }}
        </span>
      </label>

      <label class="block mt-9">
        <span class="text-lg font-medium text-gray-800">E-mail</span>
        <input
          v-model="state.email.value"
          type="email"
          :class="{
            'border-brand-danger': !!state.email.errorMessage
          }"
          class="block w-full px-4 py-3 mt-1 text-lg bg-gray-100 border-2 border-transparent rounded"
          placeholder="jane.dae@gmail.com"
        >
        <span
          v-if="!!state.email.errorMessage"
          class="block font-medium text-brand-danger"
        >
          {{ state.email.errorMessage }}
        </span>
      </label>

      <label class="block mt-9">
        <span class="text-lg font-medium text-gray-800">Senha</span>
        <input
          v-model="state.password.value"
          type="password"
          :class="{
            'border-brand-danger': !!state.password.errorMessage
          }"
          class="block w-full px-4 py-3 mt-1 text-lg bg-gray-100 border-2 border-transparent rounded"
          placeholder="jane.dae@gmail.com"
        >
        <span
          v-if="!!state.password.errorMessage"
          class="block font-medium text-brand-danger"
        >
          {{ state.password.errorMessage }}
        </span>
      </label>

      <button
        :disabled="state.isLoading"
        type="submit"
        :class="{
          'opacity-50': state.isLoading
        }"
        class="px-8 py-3 mt-10 text-2xl font-bold text-white rounded-full bg-brand-main focus:outline-none transition-all duration-150"
      >
        <icon v-if="state.isLoading" name="loading" class="animate-spin" />
        <span v-else>Entrar</span>
      </button>
    </form>
  </div>
</template>

<script>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useField } from 'vee-validate'
import { useToast } from 'vue-toastification'
import useModal from '../../hooks/useModal'
import Icon from '../Icon'
import { validateEmptyAndLength3, validateEmptyAndEmail } from '../../utils/validators'
import services from '../../services'

export default {
  components: { Icon },
  setup () {
    const router = useRouter()
    const modal = useModal()
    const toast = useToast()

    const {
      value: nameValue,
      errorMessage: nameErrorMessage
    } = useField('name', validateEmptyAndLength3)

    const {
      value: emailValue,
      errorMessage: emailErrorMessage
    } = useField('email', validateEmptyAndEmail)

    const {
      value: passwordValue,
      errorMessage: passwordErrorMessage
    } = useField('password', validateEmptyAndLength3)

    const state = reactive({
      hasErrors: false,
      isLoading: false,
      name: {
        value: nameValue,
        errorMessage: nameErrorMessage
      },
      email: {
        value: emailValue,
        errorMessage: emailErrorMessage
      },
      password: {
        value: passwordValue,
        errorMessage: passwordErrorMessage
      }
    })

    async function login ({ email, password }) {
      const { data, errors } = await services.auth.login({ email, password })
      if (!errors) {
        window.localStorage.setItem('token', data.token)
        router.push({ name: 'Feedbacks' })
        modal.close()
      }

      state.isLoading = false
    }

    async function handleSubmit () {
      try {
        toast.clear()
        state.isLoading = true

        const { errors } = await services.auth.register({
          name: state.name.value,
          email: state.email.value,
          password: state.password.value
        })

        if (!errors) {
          await login({
            email: state.email.value,
            password: state.password.value
          })
          return
        }

        if (errors.status === 400) {
          toast.error('Ocorreu um erro ao criar a conta')
        }

        state.isLoading = false
      } catch (error) {
        state.isLoading = false
        state.hasErrors = !!error
        toast.error('Ocorreu um erro ao criar a conta')
      }
    }

    return {
      state,
      close: modal.close,
      handleSubmit
    }
  }
}
</script>


================================================
FILE: dashboard/src/components/ModalFactory/index.vue
================================================
<template>
  <teleport to="body">
    <div
      v-if="state.isActive"
      class="fixed top-0 left-0 z-50 flex items-center justify-center w-full h-full bg-black bg-opacity-50"
      @click="handleModalToogle({ status: false })"
    >
      <div
        class="fixed mx-10"
        :class="state.width"
        @click.stop
      >
        <div class="flex flex-col overflow-hidden bg-white rounded-lg animate__animated animate__fadeInDown animate__faster">
          <div class="flex flex-col px-12 py-10 bg-white">
            <component :is="state.component" />
          </div>
        </div>

      </div>
    </div>
  </teleport>
</template>

<script>
import { reactive, onMounted, onBeforeUnmount, defineAsyncComponent } from 'vue'
import useModal from '../../hooks/useModal'

const ModalLogin = defineAsyncComponent(() => import('../ModalLogin'))
const ModalAccountCreate = defineAsyncComponent(() => import('../ModalCreateAccount'))

const DEFAULT_WIDTH = 'w-3/4 lg:w-1/3'

export default {
  components: {
    ModalLogin,
    ModalAccountCreate
  },
  setup () {
    const modal = useModal()
    const state = reactive({
      isActive: false,
      component: {},
      props: {},
      width: DEFAULT_WIDTH
    })

    onMounted(() => {
      modal.listen(handleModalToogle)
    })

    onBeforeUnmount(() => {
      modal.off(handleModalToogle)
    })

    function handleModalToogle (payload) {
      if (payload.status) {
        state.component = payload.component
        state.props = payload.props
        state.width = payload.width ?? DEFAULT_WIDTH
      } else {
        state.component = {}
        state.props = {}
        state.width = DEFAULT_WIDTH
      }

      state.isActive = payload.status
    }

    return {
      state,
      handleModalToogle
    }
  }
}
</script>


================================================
FILE: dashboard/src/components/ModalLogin/index.vue
================================================
<template>
  <div class="flex justify-between" id="modal-login">
    <h1 class="text-4xl font-black text-gray-800">
      Entre na sua conta
    </h1>

    <button
      @click="close"
      class="text-4xl text-gray-600 focus:outline-none"
    >
      &times;
    </button>
  </div>

  <div class="mt-16">
    <form @submit.prevent="handleSubmit">
      <label class="block">
        <span class="text-lg font-medium text-gray-800">E-mail</span>
        <input
          id="email-field"
          v-model="state.email.value"
          type="email"
          :class="{
            'border-brand-danger': !!state.email.errorMessage
          }"
          class="block w-full px-4 py-3 mt-1 text-lg bg-gray-100 border-2 border-transparent rounded"
          placeholder="jane.dae@gmail.com"
        >
        <span
          id="email-error"
          v-if="!!state.email.errorMessage"
          class="block font-medium text-brand-danger"
        >
          {{ state.email.errorMessage }}
        </span>
      </label>

      <label class="block mt-9">
        <span class="text-lg font-medium text-gray-800">Senha</span>
        <input
          id="password-field"
          v-model="state.password.value"
          type="password"
          :class="{
            'border-brand-danger': !!state.password.errorMessage
          }"
          class="block w-full px-4 py-3 mt-1 text-lg bg-gray-100 border-2 border-transparent rounded"
          placeholder="jane.dae@gmail.com"
        >
        <span
          v-if="!!state.password.errorMessage"
          class="block font-medium text-brand-danger"
        >
          {{ state.password.errorMessage }}
        </span>
      </label>

      <button
        id="submit-button"
        :disabled="state.isLoading"
        type="submit"
        :class="{
          'opacity-50': state.isLoading
        }"
        class="px-8 py-3 mt-10 text-2xl font-bold text-white rounded-full bg-brand-main focus:outline-none transition-all duration-150"
      >
        <icon v-if="state.isLoading" name="loading" class="animate-spin" />
        <span v-else>Entrar</span>
      </button>
    </form>
  </div>
</template>

<script>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useField } from 'vee-validate'
import { useToast } from 'vue-toastification'
import useModal from '../../hooks/useModal'
import Icon from '../Icon'
import { validateEmptyAndLength3, validateEmptyAndEmail } from '../../utils/validators'
import services from '../../services'

export default {
  components: { Icon },
  setup () {
    const router = useRouter()
    const modal = useModal()
    const toast = useToast()

    const {
      value: emailValue,
      errorMessage: emailErrorMessage
    } = useField('email', validateEmptyAndEmail)

    const {
      value: passwordValue,
      errorMessage: passwordErrorMessage
    } = useField('password', validateEmptyAndLength3)

    const state = reactive({
      hasErrors: false,
      isLoading: false,
      email: {
        value: emailValue,
        errorMessage: emailErrorMessage
      },
      password: {
        value: passwordValue,
        errorMessage: passwordErrorMessage
      }
    })

    async function handleSubmit () {
      try {
        toast.clear()
        state.isLoading = true
        const { data, errors } = await services.auth.login({
          email: state.email.value,
          password: state.password.value
        })

        if (!errors) {
          window.localStorage.setItem('token', data.token)
          router.push({ name: 'Feedbacks' })
          state.isLoading = false
          modal.close()
          return
        }

        if (errors.status === 404) {
          toast.error('E-mail não encontrado')
        }
        if (errors.status === 401) {
          toast.error('E-mail/senha inválidos')
        }
        if (errors.status === 400) {
          toast.error('Ocorreu um erro ao fazer o login')
        }

        state.isLoading = false
      } catch (error) {
        state.isLoading = false
        state.hasErrors = !!error
        toast.error('Ocorreu um erro ao fazer o login')
      }
    }

    return {
      state,
      close: modal.close,
      handleSubmit
    }
  }
}
</script>


================================================
FILE: dashboard/src/hooks/useModal.js
================================================
import bus from '../utils/bus'

const EVENT_NAME = 'modal:toggle'

export default function useModal () {
  function open (payload = {}) {
    bus.emit(EVENT_NAME, { status: true, ...payload })
  }

  function close (payload = {}) {
    bus.emit(EVENT_NAME, { status: false, ...payload })
  }

  function listen (fn) {
    bus.on(EVENT_NAME, fn)
  }

  function off (fn) {
    bus.off(EVENT_NAME, fn)
  }

  return { open, close, listen, off }
}


================================================
FILE: dashboard/src/hooks/useStore.js
================================================
import Store from '../store'

export default function useStore (module) {
  if (module) {
    return Store[module]
  }

  return Store
}


================================================
FILE: dashboard/src/main.js
================================================
import { createApp } from 'vue'
import Toast, { POSITION } from 'vue-toastification'
import App from './App.vue'
import router from './router'

import 'animate.css'
import '@/assets/css/tailwind.css'
import '@/assets/css/fonts.css'
import 'vue-toastification/dist/index.css'

const app = createApp(App)
app.use(router)
app.use(Toast, { position: POSITION.BOTTOM_RIGHT })
app.mount('#app')


================================================
FILE: dashboard/src/router/index.js
================================================
import { createRouter, createWebHistory } from 'vue-router'

const Home = () => import('../views/Home/index.vue')
const Feedbacks = () => import('../views/Feedbacks/index.vue')
const Credencials = () => import('../views/Credencials/index.vue')

export const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/feedbacks',
    name: 'Feedbacks',
    component: Feedbacks,
    meta: {
      hasAuth: true
    }
  },
  {
    path: '/credencials',
    name: 'Credencials',
    component: Credencials,
    meta: {
      hasAuth: true
    }
  },
  {
    path: '/:pathMatch(.*)*',
    redirect: { name: 'Home' }
  }
]

const router = createRouter({
  history: createWebHistory('/'),
  routes
})

export default router


================================================
FILE: dashboard/src/services/__snapshots__/auth.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AuthService should return a token when user login 1`] = `
Object {
  "data": Object {
    "token": "123.123.123",
  },
  "errors": null,
}
`;

exports[`AuthService should return an user when user register 1`] = `
Object {
  "data": Object {
    "email": "igor@igor.me",
    "name": "Igor",
    "password": "123",
  },
  "errors": null,
}
`;


================================================
FILE: dashboard/src/services/auth.js
================================================
export default httpClient => ({
  register: async ({ name, email, password }) => {
    const response = await httpClient.post('/auth/register', {
      name,
      email,
      password
    })
    let errors = null

    if (!response.data) {
      errors = {
        status: response.request.status,
        statusText: response.request.statusText
      }
    }

    return {
      data: response.data,
      errors
    }
  },
  login: async ({ email, password }) => {
    const response = await httpClient.post('/auth/login', {
      email,
      password
    })
    let errors = null

    if (!response.data) {
      errors = {
        status: response.request.status,
        statusText: response.request.statusText
      }
    }

    return {
      data: response.data,
      errors
    }
  }
})


================================================
FILE: dashboard/src/services/auth.spec.js
================================================
import mockAxios from 'axios'
import AuthService from './auth'

jest.mock('axios')

describe('AuthService', () => {
  afterEach(() => {
    jest.clearAllMocks()
  })

  it('should return a token when user login', async () => {
    const token = '123.123.123'
    mockAxios.post.mockImplementationOnce(() => {
      return Promise.resolve({ data: { token } })
    })

    const response = await AuthService(mockAxios).login({ email: 'igor@igor.me', password: '123' })
    expect(response.data).toHaveProperty('token')
    expect(response).toMatchSnapshot()
  })

  it('should return an user when user register', async () => {
    const user = {
      name: 'Igor',
      password: '123',
      email: 'igor@igor.me'
    }
    mockAxios.post.mockImplementationOnce(() => {
      return Promise.resolve({ data: user })
    })

    const response = await AuthService(mockAxios).register(user)
    expect(response.data).toHaveProperty('name')
    expect(response.data).toHaveProperty('password')
    expect(response.data).toHaveProperty('email')
    expect(response).toMatchSnapshot()
  })

  it('should throw an error when not found', async () => {
    const errors = { status: 404, statusText: 'Not Found' }
    mockAxios.post.mockImplementationOnce(() => {
      return Promise.resolve({ request: errors })
    })

    const response = await AuthService(mockAxios).login({ email: 'igor@igor.me', password: '123' })
    expect(response.errors).toHaveProperty('status')
    expect(response.errors).toHaveProperty('statusText')
  })
})


================================================
FILE: dashboard/src/services/feedbacks.js
================================================
const defaultPagination = {
  limit: 5,
  offset: 0
}

export default httpClient => ({
  getAll: async ({ type, limit, offset } = defaultPagination) => {
    const query = { limit, offset }
    if (type) {
      query.type = type
    }
    const response = await httpClient.get('/feedbacks', { params: query })

    return { data: response.data }
  },
  getSummary: async () => {
    const response = await httpClient.get('/feedbacks/summary')
    return { data: response.data }
  }
})


================================================
FILE: dashboard/src/services/index.js
================================================
import axios from 'axios'
import router from '../router'
import { setGlobalLoading } from '../store/global'
import AuthService from './auth'
import UsersService from './users'
import FeedbacksService from './feedbacks'

const API_ENVS = {
  production: 'https://backend-treinamento-vue3.vercel.app',
  development: '',
  local: 'http://localhost:3000'
}

const httpClient = axios.create({
  baseURL: API_ENVS[process.env.NODE_ENV] || API_ENVS.local
})

httpClient.interceptors.request.use(config => {
  setGlobalLoading(true)
  const token = window.localStorage.getItem('token')

  if (token) {
    config.headers.common.Authorization = `Bearer ${token}`
  }

  return config
})

httpClient.interceptors.response.use((response) => {
  setGlobalLoading(false)
  return response
}, (error) => {
  const canThrowAnError = error.request.status === 0 ||
    error.request.status === 500

  if (canThrowAnError) {
    setGlobalLoading(false)
    throw new Error(error.message)
  }

  if (error.response.status === 401) {
    router.push({ name: 'Home' })
  }

  setGlobalLoading(false)
  return error
})

export default {
  auth: AuthService(httpClient),
  users: UsersService(httpClient),
  feedbacks: FeedbacksService(httpClient)
}


================================================
FILE: dashboard/src/services/users.js
================================================
export default httpClient => ({
  getMe: async () => {
    const response = await httpClient.get('/users/me')

    return {
      data: response.data
    }
  },
  generateApikey: async () => {
    const response = await httpClient.post('/users/me/apikey')

    return {
      data: response.data
    }
  }
})


================================================
FILE: dashboard/src/store/global.js
================================================
import { reactive } from 'vue'

const state = reactive({
  isLoading: false
})

export default state

export function setGlobalLoading (status) {
  state.isLoading = status
}


================================================
FILE: dashboard/src/store/index.js
================================================
import { readonly } from 'vue'
import UserModule from './user'
import GlobalModule from './global'

export default readonly({
  User: UserModule,
  Global: GlobalModule
})


================================================
FILE: dashboard/src/store/user.js
================================================
import { reactive } from 'vue'

const userInitialState = {
  currentUser: {}
}

let state = reactive(userInitialState)

export default state

export function resetUserStore () {
  state = reactive(userInitialState)
}

export function cleanCurrentUser () {
  state.currentUser = {}
}

export function setCurrentUser (user) {
  state.currentUser = user
}

export function setApiKey (apiKey) {
  const currentUser = { ...state.currentUser, apiKey }
  state.currentUser = currentUser
}


================================================
FILE: dashboard/src/store/user.spec.js
================================================
import useStore from '../hooks/useStore'
import {
  resetUserStore,
  setApiKey,
  cleanCurrentUser,
  setCurrentUser
} from './user'

describe('UserStore', () => {
  afterEach(() => {
    resetUserStore()
  })

  it('should set current user', () => {
    const store = useStore()
    setCurrentUser({ name: 'Igor' })
    expect(store.User.currentUser.name).toBe('Igor')
  })

  it('should set api_key on current user', () => {
    const store = useStore()
    setApiKey('123')
    expect(store.User.currentUser.apiKey).toBe('123')
  })

  it('should clean current user', () => {
    const store = useStore()
    setCurrentUser({ name: 'Igor' })
    expect(store.User.currentUser.name).toBe('Igor')
    cleanCurrentUser()

    expect(store.User.currentUser.name).toBeFalsy()
  })
})


================================================
FILE: dashboard/src/utils/bus.js
================================================
import Emitter from 'tiny-emitter'

export default new Emitter()


================================================
FILE: dashboard/src/utils/date.js
================================================
export function getDiffTimeBetweenCurrentDate (dateString = '', now = new Date()) {
  const dayInMilliseconds = 86400000
  if ([null, undefined, false, true].includes(dateString)) {
    return dateString
  }
  const date = new Date(dateString)
  const isInvalidDate = isNaN(date.getTime())

  if (isInvalidDate) {
    return dateString
  }

  const month = date.getMonth()
  const day = date.getDate()
  const hour = date.getHours()
  const minutes = date.getMinutes()
  const seconds = date.getSeconds()

  const buildMessage = (label, value) => {
    if (value !== 1) {
      return `${value} ${label}s atrás`
    }

    return `${value} ${label} atrás`
  }
  const notZero = value => value !== 0

  if (month !== now.getMonth()) {
    const diff = Math.abs(now - date)
    const days = Math.ceil(diff / dayInMilliseconds)

    return buildMessage('dia', days)
  }

  if (day < now.getDate() && notZero(day)) { return buildMessage('dia', now.getDate() - day) }
  if (hour < now.getHours() && notZero(hour)) { return buildMessage('hora', now.getHours() - hour) }
  if (minutes < now.getMinutes() && notZero(minutes)) { return buildMessage('minuto', now.getMinutes() - minutes) }
  if (seconds < now.getSeconds() && notZero(seconds)) { return buildMessage('segundo', now.getSeconds() - seconds) }

  return buildMessage('segundo', 1)
}


================================================
FILE: dashboard/src/utils/timeout.js
================================================
export function wait (timeMs) {
  return new Promise(resolve => {
    setTimeout(resolve, timeMs)
  })
}


================================================
FILE: dashboard/src/utils/validators.js
================================================
export function validateEmptyAndLength3 (value) {
  if (!value) {
    return '*Este campo é obrigatório'
  }

  if (value.length < 3) {
    return '*Este campo precisa de no mínimo 3 caracteres'
  }

  return true
}

export function validateEmptyAndEmail (value) {
  if (!value) {
    return '*Este campo é obrigatório'
  }

  const isValid = /^[a-z0-9.]+@[a-z0-9]+\.[a-z]+(\.[a-z]+)?$/i.test(value)

  if (!isValid) {
    return '*Este campo precisa ser um e-mail'
  }

  return true
}


================================================
FILE: dashboard/src/utils/validators.spec.js
================================================
import {
  validateEmptyAndEmail,
  validateEmptyAndLength3
} from './validators'

describe('Validators utils', () => {
  it('should give an error with empty payload', () => {
    expect(validateEmptyAndLength3()).toBe('*Este campo é obrigatório')
  })

  it('should give an error with less then 3 character payload', () => {
    expect(validateEmptyAndLength3('12')).toBe('*Este campo precisa de no mínimo 3 caracteres')
  })

  it('should returns true when pass a correct param', () => {
    expect(validateEmptyAndLength3('1234')).toBe(true)
  })

  it('should give an error with empty payload', () => {
    expect(validateEmptyAndEmail()).toBe('*Este campo é obrigatório')
  })

  it('should give an error with a invalid param', () => {
    expect(validateEmptyAndEmail('myemail@')).toBe('*Este campo precisa ser um e-mail')
  })

  it('should returns true when pass a correct param', () => {
    expect(validateEmptyAndEmail('igor@igor.me')).toBe(true)
  })
})


================================================
FILE: dashboard/src/views/Credencials/index.vue
================================================
<template>
  <div class="flex justify-center w-full h-28 bg-brand-main">
    <header-logged />
  </div>

  <div class="flex flex-col items-center justify-center h-64 bg-brand-gray">
    <h1 class="text-4xl font-black text-center text-gray-800">
      Credenciais
    </h1>
    <p class="text-lg text-center text-gray-800 font-regular">
      Guia de instalação e geração de suas credenciais
    </p>
  </div>

  <div class="flex justify-center w-full h-full">
    <div class="flex flex-col w-4/5 max-w-6xl py-10">
      <h1 class="text-3xl font-black text-brand-darkgray">
        Instalação e configuração
      </h1>
      <p class="mt-10 text-lg text-gray-800 font-regular">
        Este aqui é a sua chave de api
      </p>

      <content-loader
        v-if="store.Global.isLoading || state.isLoading"
        class="rounded"
        width="600px"
        height="50px"
      />
      <div
        v-else
        class="flex py-3 pl-5 mt-2 rounded justify-between items-center bg-brand-gray w-full lg:w-1/2"
      >
        <span v-if="state.hasError">Erro ao carregar a apikey</span>
        <span v-else id="apikey">{{ store.User.currentUser.apiKey }}</span>
        <div class="flex ml-20 mr-5" v-if="!state.hasError">
          <icon
            @click="handleCopy"
            name="copy"
            :color="brandColors.graydark"
            size="24"
            class="cursor-pointer"
          />
          <icon
            id="generate-apikey"
            @click="handleGenerateApikey"
            name="loading"
            :color="brandColors.graydark"
            size="24"
            class="cursor-pointer ml-3"
          />
        </div>
      </div>

      <p class="mt-5 text-lg text-gray-800 font-regular">
        Coloque o script abaixo no seu site para começar a receber feedbacks
      </p>

      <content-loader
        v-if="store.Global.isLoading || state.isLoading"
        class="rounded"
        width="600px"
        height="50px"
      />
      <div
        v-else
        class="py-3 pl-5 pr-20 mt-2 rounded bg-brand-gray w-full lg:w-2/3 overflow-x-scroll"
      >
        <span v-if="state.hasError">Erro ao carregar o script</span>
        <pre v-else>
&lt;script
  defer
  async
  onload="init('{{store.User.currentUser.apiKey}}')"
  src="https://igorhalfeld-feedbacker-widget.netlify.app/init.js"
&gt;&lt;/script&gt;
        </pre>
      </div>
    </div>
  </div>
</template>

<script>
import { reactive, watch } from 'vue'
import { useToast } from 'vue-toastification'
import HeaderLogged from '../../components/HeaderLogged'
import ContentLoader from '../../components/ContentLoader'
import Icon from '../../components/Icon'
import useStore from '../../hooks/useStore'
import palette from '../../../palette'
import services from '../../services'
import { setApiKey } from '../../store/user'

export default {
  components: { ContentLoader, HeaderLogged, Icon },
  setup () {
    const store = useStore()
    const toast = useToast()
    const state = reactive({
      hasError: false,
      isLoading: false
    })

    watch(() => store.User.currentUser, () => {
      if (!store.Global.isLoading && !store.User.currentUser.apiKey) {
        handleError(true)
      }
    })

    function handleError (error) {
      state.isLoading = false
      state.hasError = !!error
    }

    async function handleGenerateApikey () {
      try {
        state.isLoading = true
        const { data } = await services.users.generateApikey()

        setApiKey(data.apiKey)
        state.isLoading = false
      } catch (error) {
        handleError(error)
      }
    }

    async function handleCopy () {
      toast.clear()

      try {
        await navigator.clipboard.writeText(store.User.currentUser.apiKey)
        toast.success('Copiado!')
      } catch (error) {
        handleError(error)
      }
    }

    return {
      state,
      store,
      handleGenerateApikey,
      handleCopy,
      brandColors: palette.brand
    }
  }
}
</script>


================================================
FILE: dashboard/src/views/Feedbacks/Filters.vue
================================================
<template>
  <div class="flex flex-col">
    <h1 class="text-2xl font-regular text-brand-darkgray">
      Filtros
    </h1>

    <ul class="flex flex-col mt-3 list-none">
      <li
        v-for="filter in state.filters"
        :key="filter.label"
        :class="{
          'bg-gray-200 bg-opacity-50': filter.active
        }"
        @click="() => handleSelect(filter)"
        class="flex items-center justify-between px-4 py-1 rounded cursor-pointer"
      >
        <div class="flex items-center">
          <span
            :class="filter.color.bg"
            class="inline-block w-2 h-2 mr-2 rounded-full"/> {{ filter.label }}
        </div>
        <span
          :class="filter.active ? filter.color.text : 'text-brand-graydark'"
          class="font-bold"
        >
          {{ filter.amount }}
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
import { reactive } from 'vue'
import services from '../../services'
import useStore from '../../hooks/useStore'

const LABELS = {
  all: 'Todos',
  issue: 'Problemas',
  idea: 'Ideias',
  other: 'Outros'
}

const COLORS = {
  all: { text: 'text-brand-info', bg: 'bg-brand-info' },
  issue: { text: 'text-brand-danger', bg: 'bg-brand-danger' },
  idea: { text: 'text-brand-warning', bg: 'bg-brand-warning' },
  other: { text: 'text-brand-graydark', bg: 'bg-brand-graydark' }
}

function applyFiltersStructure (summary) {
  return Object.keys(summary).reduce((acc, cur) => {
    const currentFilter = {
      label: LABELS[cur],
      color: COLORS[cur],
      amount: summary[cur]
    }

    if (cur === 'all') {
      currentFilter.active = true
    } else {
      currentFilter.type = cur
    }

    return [...acc, currentFilter]
  }, [])
}

export default {
  async setup (_, { emit }) {
    const store = useStore('Global')
    const state = reactive({
      hasError: false,
      filters: [
        { label: null, amount: null }
      ]
    })

    try {
      const { data } = await services.feedbacks.getSummary()
      state.filters = applyFiltersStructure(data)
    } catch (error) {
      state.hasError = !!error
      state.filters = applyFiltersStructure({ all: 0, issue: 0, idea: 0, other: 0 })
    }

    function handleSelect ({ type }) {
      if (store.isLoading) {
        return
      }

      state.filters = state.filters.map((filter) => {
        if (filter.type === type) {
          return { ...filter, active: true }
        }
        return { ...filter, active: false }
      })

      emit('select', type)
    }

    return {
      state,
      handleSelect
    }
  }
}
</script>


================================================
FILE: dashboard/src/views/Feedbacks/FiltersLoading.vue
================================================
<template>
  <content-loader
    class="flex flex-col items-center rounded"
    width="100%"
    height="300px"
  >

    <content-loader
      class="mt-3 rounded"
      width="90%"
      height="50px"
      animation-duration="2s"
    />

    <content-loader
      class="mt-3 rounded"
      width="80%"
      height="50px"
      animation-duration="2.3s"
    />

    <content-loader
      class="mt-3 rounded"
      width="80%"
      height="50px"
      animation-duration="2.7s"
    />

    <content-loader
      class="mt-3 rounded"
      width="80%"
      height="50px"
      animation-duration="2.9s"
    />
  </content-loader>
</template>

<script>
import ContentLoader from '../../components/ContentLoader'

export default {
  components: { ContentLoader }
}
</script>


================================================
FILE: dashboard/src/views/Feedbacks/index.vue
================================================
<template>
  <div class="flex justify-center w-full h-28 bg-brand-main">
    <header-logged />
  </div>

  <div class="flex flex-col items-center justify-center h-64 bg-brand-gray">
    <h1 class="text-4xl font-black text-center text-gray-800">
      Feedbacks
    </h1>
    <p class="text-lg text-center text-gray-800 font-regular">
      Detalhes de todos os feedbacks recebidos.
    </p>
  </div>

  <div class="flex justify-center w-full pb-20">
    <div class="w-4/5 max-w-6xl py-10 grid grid-cols-4 gap-2">
      <div>
        <h1 class="text-3xl font-black text-brand-darkgray">
          Listagem
        </h1>
        <suspense>
          <template #default>
            <filters
              @select="changeFeedbacksType"
              class="mt-8 animate__animated animate__fadeIn animate__faster"
            />
          </template>
          <template #fallback>
            <filters-loading class="mt-8" />
          </template>
        </suspense>

      </div>
      <div class="px-10 pt-20 col-span-3">
        <p
          v-if="state.hasError"
          class="text-lg text-center text-gray-800 font-regular">
          Aconteceu um erro ao carregar os feedbacks 🥺
        </p>
        <p
          v-if="!state.feedbacks.length && !state.isLoading && !state.isLoadingFeedbacks && !state.hasError"
          class="text-lg text-center text-gray-800 font-regular">
          Ainda nenhum feedback recebido 🤓
        </p>

        <feedback-card-loading v-if="state.isLoading || state.isLoadingFeedbacks" />
        <feedback-card
          v-else
          v-for="(feedback, index) in state.feedbacks"
          :key="feedback.id"
          :is-opened="index === 0"
          :feedback="feedback"
          class="mb-8"
        />
        <feedback-card-loading v-if="state.isLoadingMoreFeedbacks" />
      </div>
    </div>
  </div>
</template>

<script>
import { reactive, onMounted, onUnmounted, onErrorCaptured } from 'vue'
import Filters from './Filters'
import FiltersLoading from './FiltersLoading'
import HeaderLogged from '../../components/HeaderLogged'
import FeedbackCard from '../../components/FeedbackCard'
import FeedbackCardLoading from '../../components/FeedbackCard/Loading'
import services from '../../services'

export default {
  components: {
    HeaderLogged,
    Filters,
    FiltersLoading,
    FeedbackCard,
    FeedbackCardLoading
  },
  setup () {
    const state = reactive({
      isLoading: false,
      isLoadingFeedbacks: false,
      isLoadingMoreFeedbacks: false,
      feedbacks: [],
      currentFeedbackType: '',
      pagination: {
        limit: 5,
        offset: 0,
        total: 0
      },
      hasError: false
    })

    onErrorCaptured(handleErrors)

    onMounted(() => {
      fetchFeedbacks()
      window.addEventListener('scroll', handleScroll, false)
    })

    onUnmounted(() => {
      window.removeEventListener('scroll', handleScroll, false)
    })

    function handleErrors (error) {
      state.isLoading = false
      state.isLoadingFeedbacks = false
      state.isLoadingMoreFeedback = false
      state.hasError = !!error
    }

    async function handleScroll () {
      const isBottomOfWindow = Math.ceil(
        document.documentElement.scrollTop + window.innerHeight
      ) >= document.documentElement.scrollHeight

      if (state.isLoading || state.isLoadingMoreFeedbacks) return
      if (!isBottomOfWindow) return
      if (state.feedbacks.length >= state.pagination.total) return

      try {
        state.isLoadingMoreFeedbacks = true
        const { data } = await services.feedbacks.getAll({
          ...state.pagination,
          type: state.currentFeedbackType,
          offset: (state.pagination.offset + 5)
        })

        if (data.results.length) {
          state.feedbacks.push(...data.results)
        }

        state.isLoadingMoreFeedbacks = false
        state.pagination = data.pagination
      } catch (error) {
        state.isLoadingMoreFeedbacks = false
        handleErrors(error)
      }
    }

    async function changeFeedbacksType (type) {
      try {
        state.isLoadingFeedbacks = true
        state.pagination.offset = 0
        state.pagination.limit = 5
        state.currentFeedbackType = type
        const { data } = await services.feedbacks.getAll({
          type,
          ...state.pagination
        })

        state.feedbacks = data.results
        state.pagination = data.pagination
        state.isLoadingFeedbacks = false
      } catch (error) {
        handleErrors(error)
      }
    }

    async function fetchFeedbacks () {
      try {
        state.isLoading = true
        const { data } = await services.feedbacks.getAll({
          ...state.pagination,
          type: state.currentFeedbackType
        })

        state.feedbacks = data.results
        state.pagination = data.pagination
        state.isLoading = false
      } catch (error) {
        handleErrors(error)
      }
    }

    return {
      state,
      changeFeedbacksType
    }
  }
}
</script>


================================================
FILE: dashboard/src/views/Home/Contact.vue
================================================
<template>
  <div class="flex justify-center w-full">
    <div class="flex flex-col items-center w-4/5 max-w-6xl my-16">
      <h1 class="text-4xl font-black text-center text-gray-800">
        Alguma dúvida?
      </h1>
      <p class="text-lg text-center text-gray-800 font-regular">
        Quer saber melhor como funciona e quais são os preços?
      </p>
      <div class="mt-10">
        <a href="mailto:" class="px-6 py-2 mt-10 font-bold text-white rounded-full bg-brand-main focus:outline-none">
          Nos mande um e-mail!
        </a>
      </div>
    </div>
  </div>
</template>

<script>
export default {

}
</script>


================================================
FILE: dashboard/src/views/Home/CustomHeader.vue
================================================
<template>
  <header class="header">
    <div class="header-group">
      <div class="flex items-center justify-between py-10">
        <div class="w-28 lg:w-36">
          <img class="w-full" src="../../assets/images/logo_white.png" alt="logo">
        </div>

        <div class="flex">
          <button
            id="header-create-account-button"
            @click="() => emit('create-account')"
            class="px-6 py-2 font-bold rounded-full text-white focus:outline-none"
          >
            Crie uma conta
          </button>
          <button
            id="header-login-button"
            @click="() => emit('login')"
            class="px-6 py-2 font-bold bg-white rounded-full text-brand-main focus:outline-none"
          >
            Entrar
          </button>
        </div>
      </div>

      <div class="flex flex-col mt-28">
        <h1 class="text-4xl font-black text-white">
          Tenha um feedback. <br>
          E faça seus clientes mais <br class="hidden lg:inline-block">
          felizes!
        </h1>
        <p class="text-lg font-medium text-white">
          Receba ideias, reclamações e feedbacks com um <br class="hidden lg:inline-block">
          simples widget na página.
        </p>
        <div>
          <button
            @click="() => emit('create-account')"
            id="cta-create-account-button"
            class="px-6 py-2 mt-10 font-bold bg-white rounded-full text-brand-main focus:outline-none"
          >
            Crie uma conta grátis
          </button>
        </div>
      </div>

    </div>
  </header>
</template>

<script>
export default {
  setup (_, { emit }) {
    return { emit }
  }
}
</script>

<style lang="postcss" scoped>
.header {
  @apply bg-brand-main w-full flex justify-center;
  height: 700px;
}

.header-group {
  @apply flex flex-col w-4/5 max-w-6xl;
}

@media (min-width: 640px) {
  .header-group {
    background-image: url(../../assets/images/blue_balloons.png);
    background-size: 628px;
    background-position: 90% 100%;
    background-repeat: no-repeat;
  }
}
</style>


================================================
FILE: dashboard/src/views/Home/Home.spec.js
================================================
import Home from '.'
import { shallowMount } from '@vue/test-utils'
import { routes } from '../../router'

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory('/'),
  routes
})

describe('<Home />', () => {
  it('should render home correctly', async () => {
    router.push('/')
    await router.isReady()
    const wrapper = shallowMount(Home, {
      global: {
        plugins: [router]
      }
    })

    expect(wrapper.html()).toMatchSnapshot()
  })
})


================================================
FILE: dashboard/src/views/Home/__snapshots__/Home.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Home /> should render home correctly 1`] = `
<custom-header-stub></custom-header-stub>
<contact-stub></contact-stub>
<div class="flex justify-center py-10 bg-brand-gray">
  <p class="font-medium text-center text-gray-800">feedbacker © 2021</p>
</div>
`;


================================================
FILE: dashboard/src/views/Home/index.vue
================================================
<template>
  <custom-header
    @create-account="handleAccountCreate"
    @login="handleLogin"
  />
  <contact />
  <div class="flex justify-center py-10 bg-brand-gray">
    <p class="font-medium text-center text-gray-800">feedbacker © 2021</p>
  </div>
</template>

<script>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import CustomHeader from './CustomHeader.vue'
import Contact from './Contact.vue'
import useModal from '../../hooks/useModal'

export default {
  components: { CustomHeader, Contact },
  setup () {
    const router = useRouter()
    const modal = useModal()

    onMounted(() => {
      const token = window.localStorage.getItem('token')
      if (token) {
        router.push({ name: 'Feedbacks' })
      }
    })

    function handleLogin () {
      modal.open({
        component: 'ModalLogin'
      })
    }

    function handleAccountCreate () {
      modal.open({
        component: 'ModalAccountCreate'
      })
    }

    return {
      handleLogin,
      handleAccountCreate
    }
  }
}
</script>


================================================
FILE: dashboard/tailwind.config.js
================================================
const colors = require('tailwindcss/colors')
const palette = require('./palette')

module.exports = {
  purge: [
    './src/**/*.html',
    './src/**/*.vue',
    './src/**/*.jsx'
  ],
  presets: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      colors: palette
    },
    screens: {
      sm: '640px',
      md: '768px',
      lg: '1024px',
      xl: '1280px',
      '2xl': '1536px'
    },
    colors: {
      transparent: 'transparent',
      current: 'currentColor',

      black: colors.black,
      white: colors.white,
      gray: colors.coolGray,
      red: colors.red,
      yellow: colors.amber,
      green: colors.emerald,
      blue: colors.blue,
      indigo: colors.indigo,
      purple: colors.violet,
      pink: colors.pink
    },
    spacing: {
      px: '1px',
      0: '0px',
      0.5: '0.125rem',
      1: '0.25rem',
      1.5: '0.375rem',
      2: '0.5rem',
      2.5: '0.625rem',
      3: '0.75rem',
      3.5: '0.875rem',
      4: '1rem',
      5: '1.25rem',
      6: '1.5rem',
      7: '1.75rem',
      8: '2rem',
      9: '2.25rem',
      10: '2.5rem',
      11: '2.75rem',
      12: '3rem',
      14: '3.5rem',
      16: '4rem',
      20: '5rem',
      24: '6rem',
      28: '7rem',
      32: '8rem',
      36: '9rem',
      40: '10rem',
      44: '11rem',
      48: '12rem',
      52: '13rem',
      56: '14rem',
      60: '15rem',
      64: '16rem',
      72: '18rem',
      80: '20rem',
      96: '24rem'
    },
    animation: {
      none: 'none',
      spin: 'spin 1s linear infinite',
      ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
      pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
      bounce: 'bounce 1s infinite'
    },
    backgroundColor: (theme) => theme('colors'),
    backgroundImage: {
      none: 'none',
      'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
      'gradient-to-tr': 'linear-gradient(to top right, var(--tw-gradient-stops))',
      'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
      'gradient-to-br': 'linear-gradient(to bottom right, var(--tw-gradient-stops))',
      'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
      'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))',
      'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
      'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))'
    },
    backgroundOpacity: (theme) => theme('opacity'),
    backgroundPosition: {
      bottom: 'bottom',
      center: 'center',
      left: 'left',
      'left-bottom': 'left bottom',
      'left-top': 'left top',
      right: 'right',
      'right-bottom': 'right bottom',
      'right-top': 'right top',
      top: 'top'
    },
    backgroundSize: {
      auto: 'auto',
      cover: 'cover',
      contain: 'contain'
    },
    borderColor: (theme) => ({
      ...theme('colors'),
      DEFAULT: theme('colors.gray.200', 'currentColor')
    }),
    borderOpacity: (theme) => theme('opacity'),
    borderRadius: {
      none: '0px',
      sm: '0.125rem',
      DEFAULT: '0.25rem',
      md: '0.375rem',
      lg: '0.5rem',
      xl: '0.75rem',
      '2xl': '1rem',
      '3xl': '1.5rem',
      full: '9999px'
    },
    borderWidth: {
      DEFAULT: '1px',
      0: '0px',
      2: '2px',
      4: '4px',
      8: '8px'
    },
    boxShadow: {
      sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
      DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
      md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
      lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
      xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
      '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
      inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
      none: 'none'
    },
    container: {},
    cursor: {
      auto: 'auto',
      default: 'default',
      pointer: 'pointer',
      wait: 'wait',
      text: 'text',
      move: 'move',
      'not-allowed': 'not-allowed'
    },
    divideColor: (theme) => theme('borderColor'),
    divideOpacity: (theme) => theme('borderOpacity'),
    divideWidth: (theme) => theme('borderWidth'),
    fill: { current: 'currentColor' },
    flex: {
      1: '1 1 0%',
      auto: '1 1 auto',
      initial: '0 1 auto',
      none: 'none'
    },
    flexGrow: {
      0: '0',
      DEFAULT: '1'
    },
    flexShrink: {
      0: '0',
      DEFAULT: '1'
    },
    fontFamily: {
      regular: ['RobotoRegular'],
      medium: ['RobotoMedium'],
      bold: ['RobotoBold'],
      black: ['RobotoBlack'],
      sans: [
        'ui-sans-serif',
        'system-ui',
        '-apple-system',
        'BlinkMacSystemFont',
        '"Segoe UI"',
        'Roboto',
        '"Helvetica Neue"',
        'Arial',
        '"Noto Sans"',
        'sans-serif',
        '"Apple Color Emoji"',
        '"Segoe UI Emoji"',
        '"Segoe UI Symbol"',
        '"Noto Color Emoji"'
      ],
      serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
      mono: [
        'ui-monospace',
        'SFMono-Regular',
        'Menlo',
        'Monaco',
        'Consolas',
        '"Liberation Mono"',
        '"Courier New"',
        'monospace'
      ]
    },
    fontSize: {
      xs: ['0.75rem', { lineHeight: '1rem' }],
      sm: ['0.875rem', { lineHeight: '1.25rem' }],
      base: ['1rem', { lineHeight: '1.5rem' }],
      lg: ['1.125rem', { lineHeight: '1.75rem' }],
      xl: ['1.25rem', { lineHeight: '1.75rem' }],
      '2xl': ['1.5rem', { lineHeight: '2rem' }],
      '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
      '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
      '5xl': ['3rem', { lineHeight: '1' }],
      '6xl': ['3.75rem', { lineHeight: '1' }],
      '7xl': ['4.5rem', { lineHeight: '1' }],
      '8xl': ['6rem', { lineHeight: '1' }],
      '9xl': ['8rem', { lineHeight: '1' }]
    },
    fontWeight: {
      thin: '100',
      extralight: '200',
      light: '300',
      normal: '400',
      medium: '500',
      semibold: '600',
      bold: '700',
      extrabold: '800',
      black: '900'
    },
    gap: (theme) => theme('spacing'),
    gradientColorStops: (theme) => theme('colors'),
    gridAutoColumns: {
      auto: 'auto',
      min: 'min-content',
      max: 'max-content',
      fr: 'minmax(0, 1fr)'
    },
    gridAutoRows: {
      auto: 'auto',
      min: 'min-content',
      max: 'max-content',
      fr: 'minmax(0, 1fr)'
    },
    gridColumn: {
      auto: 'auto',
      'span-1': 'span 1 / span 1',
      'span-2': 'span 2 / span 2',
      'span-3': 'span 3 / span 3',
      'span-4': 'span 4 / span 4',
      'span-5': 'span 5 / span 5',
      'span-6': 'span 6 / span 6',
      'span-7': 'span 7 / span 7',
      'span-8': 'span 8 / span 8',
      'span-9': 'span 9 / span 9',
      'span-10': 'span 10 / span 10',
      'span-11': 'span 11 / span 11',
      'span-12': 'span 12 / span 12',
      'span-full': '1 / -1'
    },
    gridColumnEnd: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7',
      8: '8',
      9: '9',
      10: '10',
      11: '11',
      12: '12',
      13: '13'
    },
    gridColumnStart: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7',
      8: '8',
      9: '9',
      10: '10',
      11: '11',
      12: '12',
      13: '13'
    },
    gridRow: {
      auto: 'auto',
      'span-1': 'span 1 / span 1',
      'span-2': 'span 2 / span 2',
      'span-3': 'span 3 / span 3',
      'span-4': 'span 4 / span 4',
      'span-5': 'span 5 / span 5',
      'span-6': 'span 6 / span 6',
      'span-full': '1 / -1'
    },
    gridRowStart: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7'
    },
    gridRowEnd: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7'
    },
    transformOrigin: {
      center: 'center',
      top: 'top',
      'top-right': 'top right',
      right: 'right',
      'bottom-right': 'bottom right',
      bottom: 'bottom',
      'bottom-left': 'bottom left',
      left: 'left',
      'top-left': 'top left'
    },
    gridTemplateColumns: {
      none: 'none',
      1: 'repeat(1, minmax(0, 1fr))',
      2: 'repeat(2, minmax(0, 1fr))',
      3: 'repeat(3, minmax(0, 1fr))',
      4: 'repeat(4, minmax(0, 1fr))',
      5: 'repeat(5, minmax(0, 1fr))',
      6: 'repeat(6, minmax(0, 1fr))',
      7: 'repeat(7, minmax(0, 1fr))',
      8: 'repeat(8, minmax(0, 1fr))',
      9: 'repeat(9, minmax(0, 1fr))',
      10: 'repeat(10, minmax(0, 1fr))',
      11: 'repeat(11, minmax(0, 1fr))',
      12: 'repeat(12, minmax(0, 1fr))'
    },
    gridTemplateRows: {
      none: 'none',
      1: 'repeat(1, minmax(0, 1fr))',
      2: 'repeat(2, minmax(0, 1fr))',
      3: 'repeat(3, minmax(0, 1fr))',
      4: 'repeat(4, minmax(0, 1fr))',
      5: 'repeat(5, minmax(0, 1fr))',
      6: 'repeat(6, minmax(0, 1fr))'
    },
    height: (theme) => ({
      auto: 'auto',
      ...theme('spacing'),
      '1/2': '50%',
      '1/3': '33.333333%',
      '2/3': '66.666667%',
      '1/4': '25%',
      '2/4': '50%',
      '3/4': '75%',
      '1/5': '20%',
      '2/5': '40%',
      '3/5': '60%',
      '4/5': '80%',
      '1/6': '16.666667%',
      '2/6': '33.333333%',
      '3/6': '50%',
      '4/6': '66.666667%',
      '5/6': '83.333333%',
      full: '100%',
      screen: '100vh'
    }),
    inset: (theme, { negative }) => ({
      auto: 'auto',
      ...theme('spacing'),
      ...negative(theme('spacing')),
      '1/2': '50%',
      '1/3': '33.333333%',
      '2/3': '66.666667%',
      '1/4': '25%',
      '2/4': '50%',
      '3/4': '75%',
      full: '100%',
      '-1/2': '-50%',
      '-1/3': '-33.333333%',
      '-2/3': '-66.666667%',
      '-1/4': '-25%',
      '-2/4': '-50%',
      '-3/4': '-75%',
      '-full': '-100%'
    }),
    keyframes: {
      spin: {
        to: {
          transform: 'rotate(360deg)'
        }
      },
      ping: {
        '75%, 100%': {
          transform: 'scale(2)',
          opacity: '0'
        }
      },
      pulse: {
        '50%': {
          opacity: '.5'
        }
      },
      bounce: {
        '0%, 100%': {
          transform: 'translateY(-25%)',
          animationTimingFunction: 'cubic-bezier(0.8,0,1,1)'
        },
        '50%': {
          transform: 'none',
          animationTimingFunction: 'cubic-bezier(0,0,0.2,1)'
        }
      }
    },
    letterSpacing: {
      tighter: '-0.05em',
      tight: '-0.025em',
      normal: '0em',
      wide: '0.025em',
      wider: '0.05em',
      widest: '0.1em'
    },
    lineHeight: {
      none: '1',
      tight: '1.25',
      snug: '1.375',
      normal: '1.5',
      relaxed: '1.625',
      loose: '2',
      3: '.75rem',
      4: '1rem',
      5: '1.25rem',
      6: '1.5rem',
      7: '1.75rem',
      8: '2rem',
      9: '2.25rem',
      10: '2.5rem'
    },
    listStyleType: {
      none: 'none',
      disc: 'disc',
      decimal: 'decimal'
    },
    margin: (theme, { negative }) => ({
      auto: 'auto',
      ...theme('spacing'),
      ...negative(theme('spacing'))
    }),
    maxHeight: (theme) => ({
      ...theme('spacing'),
      full: '100%',
      screen: '100vh'
    }),
    maxWidth: (theme, { breakpoints }) => ({
      none: 'none',
      0: '0rem',
      xs: '20rem',
      sm: '24rem',
      md: '28rem',
      lg: '32rem',
      xl: '36rem',
      '2xl': '42rem',
      '3xl': '48rem',
      '4xl': '56rem',
      '5xl': '64rem',
      '6xl': '72rem',
      '7xl': '80rem',
      full: '100%',
      min: 'min-content',
      max: 'max-content',
      prose: '65ch',
      ...breakpoints(theme('screens'))
    }),
    minHeight: {
      0: '0px',
      full: '100%',
      screen: '100vh'
    },
    minWidth: {
      0: '0px',
      full: '100%',
      min: 'min-content',
      max: 'max-content'
    },
    objectPosition: {
      bottom: 'bottom',
      center: 'center',
      left: 'left',
      'left-bottom': 'left bottom',
      'left-top': 'left top',
      right: 'right',
      'right-bottom': 'right bottom',
      'right-top': 'right top',
      top: 'top'
    },
    opacity: {
      0: '0',
      5: '0.05',
      10: '0.1',
      20: '0.2',
      25: '0.25',
      30: '0.3',
      40: '0.4',
      50: '0.5',
      60: '0.6',
      70: '0.7',
      75: '0.75',
      80: '0.8',
      90: '0.9',
      95: '0.95',
      100: '1'
    },
    order: {
      first: '-9999',
      last: '9999',
      none: '0',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7',
      8: '8',
      9: '9',
      10: '10',
      11: '11',
      12: '12'
    },
    outline: {
      none: ['2px solid transparent', '2px'],
      white: ['2px dotted white', '2px'],
      black: ['2px dotted black', '2px']
    },
    padding: (theme) => theme('spacing'),
    placeholderColor: (theme) => theme('colors'),
    placeholderOpacity: (theme) => theme('opacity'),
    ringColor: (theme) => ({
      DEFAULT: theme('colors.blue.500', '#3b82f6'),
      ...theme('colors')
    }),
    ringOffsetColor: (theme) => theme('colors'),
    ringOffsetWidth: {
      0: '0px',
      1: '1px',
      2: '2px',
      4: '4px',
      8: '8px'
    },
    ringOpacity: (theme) => ({
      DEFAULT: '0.5',
      ...theme('opacity')
    }),
    ringWidth: {
      DEFAULT: '3px',
      0: '0px',
      1: '1px',
      2: '2px',
      4: '4px',
      8: '8px'
    },
    rotate: {
      '-180': '-180deg',
      '-90': '-90deg',
      '-45': '-45deg',
      '-12': '-12deg',
      '-6': '-6deg',
      '-3': '-3deg',
      '-2': '-2deg',
      '-1': '-1deg',
      0: '0deg',
      1: '1deg',
      2: '2deg',
      3: '3deg',
      6: '6deg',
      12: '12deg',
      45: '45deg',
      90: '90deg',
      180: '180deg'
    },
    scale: {
      0: '0',
      50: '.5',
      75: '.75',
      90: '.9',
      95: '.95',
      100: '1',
      105: '1.05',
      110: '1.1',
      125: '1.25',
      150: '1.5'
    },
    skew: {
      '-12': '-12deg',
      '-6': '-6deg',
      '-3': '-3deg',
      '-2': '-2deg',
      '-1': '-1deg',
      0: '0deg',
      1: '1deg',
      2: '2deg',
      3: '3deg',
      6: '6deg',
      12: '12deg'
    },
    space: (theme, { negative }) => ({
      ...theme('spacing'),
      ...negative(theme('spacing'))
    }),
    stroke: {
      current: 'currentColor'
    },
    strokeWidth: {
      0: '0',
      1: '1',
      2: '2'
    },
    textColor: (theme) => theme('colors'),
    textOpacity: (theme) => theme('opacity'),
    transitionDuration: {
      DEFAULT: '150ms',
      75: '75ms',
      100: '100ms',
      150: '150ms',
      200: '200ms',
      300: '300ms',
      500: '500ms',
      700: '700ms',
      1000: '1000ms'
    },
    transitionDelay: {
      75: '75ms',
      100: '100ms',
      150: '150ms',
      200: '200ms',
      300: '300ms',
      500: '500ms',
      700: '700ms',
      1000: '1000ms'
    },
    transitionProperty: {
      none: 'none',
      all: 'all',
      DEFAULT: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
      colors: 'background-color, border-color, color, fill, stroke',
      opacity: 'opacity',
      shadow: 'box-shadow',
      transform: 'transform'
    },
    transitionTimingFunction: {
      DEFAULT: 'cubic-bezier(0.4, 0, 0.2, 1)',
      linear: 'linear',
      in: 'cubic-bezier(0.4, 0, 1, 1)',
      out: 'cubic-bezier(0, 0, 0.2, 1)',
      'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)'
    },
    translate: (theme, { negative }) => ({
      ...theme('spacing'),
      ...negative(theme('spacing')),
      '1/2': '50%',
      '1/3': '33.333333%',
      '2/3': '66.666667%',
      '1/4': '25%',
      '2/4': '50%',
      '3/4': '75%',
      full: '100%',
      '-1/2': '-50%',
      '-1/3': '-33.333333%',
      '-2/3': '-66.666667%',
      '-1/4': '-25%',
      '-2/4': '-50%',
      '-3/4': '-75%',
      '-full': '-100%'
    }),
    width: (theme) => ({
      auto: 'auto',
      ...theme('spacing'),
      '1/2': '50%',
      '1/3': '33.333333%',
      '2/3': '66.666667%',
      '1/4': '25%',
      '2/4': '50%',
      '3/4': '75%',
      '1/5': '20%',
      '2/5': '40%',
      '3/5': '60%',
      '4/5': '80%',
      '1/6': '16.666667%',
      '2/6': '33.333333%',
      '3/6': '50%',
      '4/6': '66.666667%',
      '5/6': '83.333333%',
      '1/12': '8.333333%',
      '2/12': '16.666667%',
      '3/12': '25%',
      '4/12': '33.333333%',
      '5/12': '41.666667%',
      '6/12': '50%',
      '7/12': '58.333333%',
      '8/12': '66.666667%',
      '9/12': '75%',
      '10/12': '83.333333%',
      '11/12': '91.666667%',
      full: '100%',
      screen: '100vw',
      min: 'min-content',
      max: 'max-content'
    }),
    zIndex: {
      auto: 'auto',
      0: '0',
      10: '10',
      20: '20',
      30: '30',
      40: '40',
      50: '50'
    }
  },
  variantOrder: [
    'first',
    'last',
    'odd',
    'even',
    'visited',
    'checked',
    'group-hover',
    'group-focus',
    'focus-within',
    'hover',
    'focus',
    'focus-visible',
    'active',
    'disabled'
  ],
  variants: {
    accessibility: ['responsive', 'focus-within', 'focus'],
    alignContent: ['responsive'],
    alignItems: ['responsive'],
    alignSelf: ['responsive'],
    animation: ['responsive'],
    appearance: ['responsive'],
    backgroundAttachment: ['responsive'],
    backgroundClip: ['responsive'],
    backgroundColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
    backgroundImage: ['responsive'],
    backgroundOpacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
    backgroundPosition: ['responsive'],
    backgroundRepeat: ['responsive'],
    backgroundSize: ['responsive'],
    borderCollapse: ['responsive'],
    borderColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
    borderOpacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
    borderRadius: ['responsive'],
    borderStyle: ['responsive'],
    borderWidth: ['responsive'],
    boxShadow: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
    boxSizing: ['responsive'],
    clear: ['responsive'],
    container: ['responsive'],
    cursor: ['responsive'],
    display: ['responsive'],
    divideColor: ['responsive', 'dark'],
    divideOpacity: ['responsive'],
    divideStyle: ['responsive'],
    divideWidth: ['responsive'],
    fill: ['responsive'],
    flex: ['responsive'],
    flexDirection: ['responsive'],
    flexGrow: ['responsive'],
    flexShrink: ['responsive'],
    flexWrap: ['responsive'],
    float: ['responsive'],
    fontFamily: ['responsive'],
    fontSize: ['responsive'],
    fontSmoothing: ['responsive'],
    fontStyle: ['responsive'],
    fontVariantNumeric: ['responsive'],
    fontWeight: ['responsive'],
    gap: ['responsive'],
    gradientColorStops: ['responsive', 'dark', 'hover', 'focus'],
    gridAutoColumns: ['responsive'],
    gridAutoFlow: ['responsive'],
    gridAutoRows: ['responsive'],
    gridColumn: ['responsive'],
    gridColumnEnd: ['responsive'],
    gridColumnStart: ['responsive'],
    gridRow: ['responsive'],
    gridRowEnd: ['responsive'],
    gridRowStart: ['responsive'],
    gridTemplateColumns: ['responsive'],
    gridTemplateRows: ['responsive'],
    height: ['responsive'],
    inset: ['responsive'],
    justifyContent: ['responsive'],
    justifyItems: ['responsive'],
    justifySelf: ['responsive'],
    letterSpacing: ['responsive'],
    lineHeight: ['responsive'],
    listStylePosition: ['responsive'],
    listStyleType: ['responsive'],
    margin: ['responsive'],
    maxHeight: ['responsive'],
    maxWidth: ['responsive'],
    minHeight: ['responsive'],
    minWidth: ['responsive'],
    objectFit: ['responsive'],
    objectPosition: ['responsive'],
    opacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
    order: ['responsive'],
    outline: ['responsive', 'focus-within', 'focus'],
    overflow: ['responsive'],
    overscrollBehavior: ['responsive'],
    padding: ['responsive'],
    placeContent: ['responsive'],
    placeItems: ['responsive'],
    placeSelf: ['responsive'],
    placeholderColor: ['responsive', 'dark', 'focus'],
    placeholderOpacity: ['responsive', 'focus'],
    pointerEvents: ['responsive'],
    position: ['responsive'],
    resize: ['responsive'],
    ringColor: ['responsive', 'dark', 'focus-within', 'focus'],
    ringOffsetColor: ['responsive', 'dark', 'focus-within', 'focus'],
    ringOffsetWidth: ['responsive', 'focus-within', 'focus'],
    ringOpacity: ['responsive', 'focus-within', 'focus'],
    ringWidth: ['responsive', 'focus-within', 'focus'],
    rotate: ['responsive', 'hover', 'focus'],
    scale: ['responsive', 'hover', 'focus'],
    skew: ['responsive', 'hover', 'focus'],
    space: ['responsive'],
    stroke: ['responsive'],
    strokeWidth: ['responsive'],
    tableLayout: ['responsive'],
    textAlign: ['responsive'],
    textColor: ['responsive', 'dark', 'group-hover', 'focus-within', 'hover', 'focus'],
    textDecoration: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
    textOpacity: ['responsive', 'group-hover', 'focus-within', 'hover', 'focus'],
    textOverflow: ['responsive'],
    textTransform: ['responsive'],
    transform: ['responsive'],
    transformOrigin: ['responsive'],
    transitionDelay: ['responsive'],
    transitionDuration: ['responsive'],
    transitionProperty: ['responsive'],
    transitionTimingFunction: ['responsive'],
    translate: ['responsive', 'hover', 'focus'],
    userSelect: ['responsive'],
    verticalAlign: ['responsive'],
    visibility: ['responsive'],
    whitespace: ['responsive'],
    width: ['responsive'],
    wordBreak: ['responsive'],
    zIndex: ['responsive', 'focus-within', 'focus']
  },
  plugins: []
}


================================================
FILE: dashboard/tests/e2e/.eslintrc.js
================================================
module.exports = {
  plugins: [
    'cypress'
  ],
  env: {
    mocha: true,
    'cypress/globals': true
  },
  rules: {
    strict: 'off'
  }
}


================================================
FILE: dashboard/tests/e2e/plugins/index.js
================================================
/* eslint-disable arrow-body-style */
// https://docs.cypress.io/guides/guides/plugins-guide.html

// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples

// /* eslint-disable import/no-extraneous-dependencies, global-require */
// const webpack = require('@cypress/webpack-preprocessor')

module.exports = (on, config) => {
  // on('file:preprocessor', webpack({
  //  webpackOptions: require('@vue/cli-service/webpack.config'),
  //  watchOptions: {}
  // }))

  return Object.assign({}, config, {
    fixturesFolder: 'tests/e2e/fixtures',
    integrationFolder: 'tests/e2e/specs',
    screenshotsFolder: 'tests/e2e/screenshots',
    videosFolder: 'tests/e2e/videos',
    supportFile: 'tests/e2e/support/index.js'
  })
}


================================================
FILE: dashboard/tests/e2e/specs/credencials.js
================================================
const APP_URL = process.env.APP_URL || 'http://localhost:8080'

describe('Credencials', () => {
  it('should generate an api_key', () => {
    cy.visit(APP_URL)

    cy.get('#header-login-button').click()
    cy.get('#modal-login')

    cy.get('#email-field').type('igor@igor.me')
    cy.get('#password-field').type('1234')
    cy.get('#submit-button').click()

    cy.wait(4000)
    cy.visit(`${APP_URL}/credencials`)
    cy.wait(2000)

    const oldApikey = cy.get('#apikey').invoke('text')
    cy.get('#generate-apikey').click()
    cy.wait(2000)
    const newApikey = cy.get('#apikey').invoke('text')

    expect(oldApikey).to.not.equal(newApikey)
  })
})


================================================
FILE: dashboard/tests/e2e/specs/home.js
================================================
const APP_URL = process.env.APP_URL || 'http://localhost:8080'

describe('Home', () => {
  it('should render create account modal when click on cta create account button', () => {
    cy.visit(APP_URL)

    cy.get('#cta-create-account-button').click()

    cy.get('#modal-create-account')
  })

  it('should render create account modal when click on header create account button', () => {
    cy.visit(APP_URL)

    cy.get('#header-create-account-button').click()

    cy.get('#modal-create-account')
  })

  it('should render login modal when click on header login button', () => {
    cy.visit(APP_URL)

    cy.get('#header-login-button').click()

    cy.get('#modal-login')
  })

  it('should login with an email and password', () => {
    cy.visit(APP_URL)

    cy.get('#header-login-button').click()
    cy.get('#modal-login')

    cy.get('#email-field').type('igor@igor.me')
    cy.get('#password-field').type('1234')
    cy.get('#submit-button').click()

    cy.url().should('include', '/feedbacks')
  })

  it('should show an error with an invalid email', () => {
    cy.visit(APP_URL)

    cy.get('#header-login-button').click()
    cy.get('#modal-login')

    cy.get('#email-field').type('igor@')
    cy.get('#password-field').type('1234')
    cy.get('#submit-button').click()

    cy.get('#email-error')
  })

  it('should logout work correctly', () => {
    cy.visit(APP_URL)

    cy.get('#header-login-button').click()
    cy.get('#modal-login')

    cy.get('#email-field').type('igor@igor.me')
    cy.get('#password-field').type('1234')
    cy.get('#submit-button').click()

    cy.url().should('include', '/feedbacks')
    cy.get('#logout-button').click()
    cy.url().should('include', '/')
  })
})


================================================
FILE: dashboard/tests/e2e/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })


================================================
FILE: dashboard/tests/e2e/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')


================================================
FILE: try-widget/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Minha loja de biscoitos</title>

  <style>

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    html,
    body {
      width: 100%;
      height: 100%;
      overflow-x: hidden;
    }

    .container {
      width: 100%;
      height: 70vh;
      background-color: #f4f4f4;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .container > h1 {
      font-family: Roboto;
      color: #444;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Meu site de venda de biscoitos</h1>
  </div>

  <script
    defer
    async
    onload="init('fcd5015c-10d3-4e9c-b395-ec7ed8850165')"
    src="https://igorhalfeld-feedbacker-widget.netlify.app/init.js"
  >
  </script>
</body>
</html>


================================================
FILE: widget/.browserslistrc
================================================
> 1%
last 2 versions
not dead


================================================
FILE: widget/.editorconfig
================================================
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: widget/.eslintrc.js
================================================
module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    '@vue/standard',
    '@vue/typescript/recommended'
  ],
  parserOptions: {
    ecmaVersion: 2020
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  },
  overrides: [
    {
      files: [
        '**/*.{j,t}s?(x)',
      ],
      env: {
        jest: true
      }
    }
  ]
}


================================================
FILE: widget/.gitignore
================================================
.DS_Store
node_modules
/dist

/tests/e2e/videos/
/tests/e2e/screenshots/


# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: widget/Dockerfile
================================================
FROM node:13-alpine as build

WORKDIR /

COPY . .

ENV NODE_ENV=production
RUN npm install --production
RUN npm run build

FROM nginx:1.18.0-alpine as final

WORKDIR /
COPY --from=build ./dist /usr/share/nginx/html


================================================
FILE: widget/README.md
================================================
# widget

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Run your unit tests
```
npm run test:unit
```

### Run your end-to-end tests
```
npm run test:e2e
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).


================================================
FILE: widget/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/app'
  ]
}


================================================
FILE: widget/cypress.json
================================================
{
  "pluginsFile": "tests/e2e/plugins/index.js"
}


================================================
FILE: widget/jest.config.js
================================================
module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript',
  testMatch: [
    '**/*.spec.js'
  ],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '^.+\\.(ts|tsx)$': 'ts-jest'
  }
}


================================================
FILE: widget/package.json
================================================
{
  "name": "widget",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "test:e2e": "vue-cli-service test:e2e",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@tailwindcss/postcss7-compat": "^2.0.3",
    "animate.css": "^4.1.1",
    "autoprefixer": "^9.8.6",
    "axios": "^0.21.1",
    "postcss": "^7.0.35",
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.3",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@types/jest": "^24.9.1",
    "@typescript-eslint/eslint-plugin": "^2.33.0",
    "@typescript-eslint/parser": "^2.33.0",
    "@vue/babel-preset-app": "^4.5.11",
    "@vue/cli-plugin-e2e-cypress": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-plugin-unit-jest": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "@vue/eslint-config-standard": "^5.1.2",
    "@vue/eslint-config-typescript": "^5.0.2",
    "@vue/test-utils": "^2.0.0-0",
    "babel-jest": "^26.6.3",
    "eslint": "^6.7.2",
    "eslint-plugin-import": "^2.20.2",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-standard": "^4.0.0",
    "eslint-plugin-vue": "^7.0.0-0",
    "jest": "^26.6.3",
    "lint-staged": "^9.5.0",
    "ts-jest": "^26.5.0",
    "typescript": "^3.9.7",
    "vue-jest": "^5.0.0-0"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,vue,ts,tsx}": [
      "vue-cli-service lint",
      "git add"
    ]
  }
}


================================================
FILE: widget/palette.js
================================================
module.exports = {
  brand: {
    main: '#EF4983',
    gray: '#F9F9F9',
    info: '#8296FB',
    success: '#63C3BE',
    graydark: '#C0BCB0',
    warning: '#E4B52E',
    danger: '#F88676'
  },
  mediumslateblue: {
    50: '#f6f9fd',
    100: '#e8f3fd',
    200: '#cbdefb',
    300: '#abc4fb',
    400: '#8296fb',
    500: '#5667fb',
    600: '#3d46f7',
    700: '#3137e5',
    800: '#272cb9',
    900: '#202591'
  },
  slateblue: {
    50: '#f5f8fc',
    100: '#eaf0fc',
    200: '#d3d8fa',
    300: '#babcf9',
    400: '#9c8ff9',
    500: '#7a61f9',
    600: '#5d41f4',
    700: '#4933e1',
    800: '#382ab5',
    900: '#2d248f'
  },
  mediumorchid: {
    50: '#f9f7fa',
    100: '#f7edf9',
    200: '#efd1f5',
    300: '#e8aef1',
    400: '#e47cec',
    500: '#e04fe7',
    600: '#c932d7',
    700: '#9a27b5',
    800: '#712186',
    900: '#571c66'
  },
  deeppink: {
    50: '#fcf9f9',
    100: '#fcf0f3',
    200: '#fad3e6',
    300: '#f9acd1',
    400: '#fa73ab',
    500: '#fb4783',
    600: '#f42a5c',
    700: '#d6214a',
    800: '#a71b3a',
    900: '#83172f'
  },
  tomato: {
    50: '#fcf9f6',
    100: '#fcf0ed',
    200: '#fadad6',
    300: '#f8b9b0',
    400: '#f88676',
    500: '#f85b47',
    600: '#ee392e',
    700: '#cc2b2b',
    800: '#9e2328',
    900: '#7c1d23'
  },
  chocolate: {
    50: '#fbf8f2',
    100: '#fbf1e1',
    200: '#f8e1bb',
    300: '#f5c782',
    400: '#f39d41',
    500: '#f1741e',
    600: '#e24f13',
    700: '#bc3b16',
    800: '#922e1a',
    900: '#732619'
  },
  goldenrod: {
    50: '#fbfaf4',
    100: '#faf6e0',
    200: '#f5ebb0',
    300: '#efd86f',
    400: '#e4b52e',
    500: '#d69111',
    600: '#b76b0a',
    700: '#8b500e',
    800: '#663d12',
    900: '#4e3012'
  },
  darkgoldenrod: {
    50: '#fbfaf6',
    100: '#f9f8e7',
    200: '#f2efbc',
    300: '#e8de7f',
    400: '#d3bd3a',
    500: '#b79b17',
    600: '#8e750d',
    700: '#685910',
    800: '#4b4213',
    900: '#393414'
  },
  lightseagreen: {
    50: '#f5fafa',
    100: '#eaf7f5',
    200: '#ceeee8',
    300: '#a7dfd9',
    400: '#63c3be',
    500: '#30a29c',
    600: '#237f79',
    700: '#236460',
    800: '#204c4a',
    900: '#1b3d3c'
  },
  cornflowerblue: {
    50: '#f4fafc',
    100: '#e2f7fb',
    200: '#bdeaf7',
    300: '#8fd8f5',
    400: '#4fb6f1',
    500: '#238fed',
    600: '#1a6bdf',
    700: '#1b54bd',
    800: '#19418c',
    900: '#15346b'
  }
}


================================================
FILE: widget/postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
}


================================================
FILE: widget/public/index.html
================================================
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: widget/public/init.js
================================================
function init (apiKey) {
  async function handleLoadWidget () {
    const page = `${window.location.origin}${window.location.pathname}`
    const fp = await window.FingerprintJS.load()
    const fingerprint = await fp.get()

    const WIDGET_URL = `https://igorhalfeld-feedbacker-widget.netlify.app?api_key=${apiKey}&page=${page}&fingerprint=${fingerprint.visitorId}`
    const config = { method: 'HEAD' }
    const res = await fetch(`https://backend-treinamento-vue3.vercel.app/apikey/exists?apikey=${apiKey}`, config)

    if (res.status === 200) {
      const iframe = document.createElement('iframe')
      iframe.src = WIDGET_URL
      iframe.id = 'feedbacker-iframe'
      iframe.style.position = 'fixed'
      iframe.style.bottom = '0px'
      iframe.style.right = '0px'
      iframe.style.overflow = 'hidden'
      iframe.style.border = '0px'
      iframe.style.zIndex = '99999'
      document.body.appendChild(iframe)

      window.addEventListener('message', (event) => {
        if (!event.data.isWidget) return

        if (event.data.isOpen) {
          iframe.width = '100%'
          iframe.height = '100%'
        } else {
          iframe.width = '300px'
          iframe.height = '150px'
        }
      })
      return
    }

    console.log('* [feedbacker] was not loaded because apikey does not exists')
  }

  const script = document.createElement('script')
  script.src = '//cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@3/dist/fp.min.js'
  script.async = 'async'
  script.addEventListener('load', handleLoadWidget)

  document.body.appendChild(script)
}

window.init = init


================================================
FILE: widget/src/App.vue
================================================
<template>
  <widget />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Widget from './views/Widget/index.vue'

export default defineComponent({
  components: { Widget }
})
</script>


================================================
FILE: widget/src/assets/css/fonts.css
================================================
@font-face {
  font-family: "RobotoRegular";
  src: local("Roboto Regular"), local("Roboto-Regular"),
    url("../fonts/Roboto-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: "RobotoMedium";
  src: local("Roboto Medium"), local("Roboto-Medium"),
    url("../fonts/Roboto-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
}

@font-face {
  font-family: "RobotoBold";
  src: local("Roboto Bold"), local("Roboto-Bold"),
    url("../fonts/Roboto-Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
}

@font-face {
  font-family: "RobotoBlack";
  src: local("Roboto Black"), local("Roboto-Black"),
    url("../fonts/Roboto-Black.ttf") format("truetype");
  font-weight: 900;
  font-style: normal;
}


================================================
FILE: widget/src/assets/css/tailwind.css
================================================
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body,
#app {
  width: 100%;
  height: 100%;
}


================================================
FILE: widget/src/components/Icon/ArrowRight.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 13 5" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M3.07415 3.21819L12.3158 3.23798L12.3183 2.06329L3.07666 2.04351L3.08044 0.281483L0.00512019 2.62427L3.07038 4.98021L3.07415 3.21819Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/Atention.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M25.9999 4.3335C14.0399 4.3335 4.33325 14.0402 4.33325 26.0002C4.33325 37.9602 14.0399 47.6668 25.9999 47.6668C37.9599 47.6668 47.6666 37.9602 47.6666 26.0002C47.6666 14.0402 37.9599 4.3335 25.9999 4.3335ZM28.1666 36.8335H23.8333V32.5002H28.1666V36.8335ZM28.1666 28.1668H23.8333V15.1668H28.1666V28.1668Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/Chat.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 30 28" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M24.9982 10.6297C24.9982 4.45448 19.0609 0 12.4988 0C5.89808 0 0 4.48874 0 10.6297C0.00581151 12.8132 0.759476 14.9264 2.13112 16.6049C2.18958 18.548 1.06887 21.3402 0.0551531 23.3652C2.76483 22.8677 6.61838 21.7642 8.36453 20.6736C17.9871 23.062 24.9982 17.0564 24.9982 10.6297ZM6.87429 12.3869C6.56533 12.3865 6.2634 12.2929 6.00667 12.1179C5.74993 11.9429 5.54992 11.6943 5.43192 11.4036C5.31391 11.1128 5.2832 10.793 5.34368 10.4845C5.40415 10.176 5.5531 9.89264 5.77168 9.67031C5.99026 9.44798 6.26868 9.29662 6.57172 9.23537C6.87477 9.17411 7.18885 9.20572 7.47426 9.32619C7.75967 9.44665 8.0036 9.65057 8.17522 9.91217C8.34683 10.1738 8.43843 10.4813 8.43843 10.7959C8.43799 11.218 8.27301 11.6227 7.97972 11.921C7.68643 12.2193 7.28884 12.3869 6.87429 12.3869ZM12.4999 12.3869C12.1909 12.3865 11.889 12.2929 11.6323 12.1179C11.3756 11.9429 11.1755 11.6943 11.0575 11.4036C10.9395 11.1128 10.9088 10.793 10.9693 10.4845C11.0298 10.176 11.1787 9.89264 11.3973 9.67031C11.6159 9.44798 11.8943 9.29662 12.1973 9.23537C12.5004 9.17411 12.8145 9.20572 13.0999 9.32619C13.3853 9.44665 13.6292 9.65057 13.8008 9.91217C13.9725 10.1738 14.0641 10.4813 14.0641 10.7959C14.0639 11.005 14.0233 11.2121 13.9446 11.4052C13.8658 11.5983 13.7505 11.7738 13.6051 11.9215C13.4598 12.0693 13.2872 12.1864 13.0974 12.2663C12.9076 12.3461 12.7042 12.3871 12.4988 12.3869H12.4999ZM18.1255 12.3869C17.8165 12.3869 17.5144 12.2935 17.2575 12.1187C17.0005 11.9439 16.8002 11.6954 16.682 11.4047C16.5637 11.114 16.5328 10.7941 16.5931 10.4855C16.6534 10.1769 16.8022 9.8934 17.0207 9.6709C17.2392 9.4484 17.5176 9.29688 17.8207 9.23549C18.1238 9.1741 18.438 9.20561 18.7235 9.32602C19.009 9.44644 19.253 9.65036 19.4247 9.91199C19.5964 10.1736 19.688 10.4812 19.688 10.7959C19.6879 11.005 19.6473 11.212 19.5686 11.4051C19.4899 11.5982 19.3746 11.7736 19.2293 11.9213C19.084 12.0691 18.9115 12.1862 18.7218 12.2661C18.532 12.346 18.3287 12.387 18.1233 12.3869H18.1255ZM26.9202 14.221C26.7193 14.8418 26.4681 15.4446 26.1691 16.0231C28.2649 17.6292 28.9896 20.1288 26.7487 22.8508C26.7228 23.7994 26.6875 24.1857 26.9214 25.2247C25.8062 24.8315 25.6975 24.7254 24.8404 24.1897C22.1815 24.8501 19.5093 25.0758 17.2293 23.1019C16.5152 23.3048 15.7887 23.4596 15.0546 23.5652C16.9458 25.9536 20.4271 27.252 24.5249 26.2378C25.6677 26.9515 28.191 27.672 29.9647 28C29.3029 26.6702 28.5688 24.845 28.6046 23.5725C30.7964 20.9027 30.5311 16.6543 26.918 14.221H26.9202Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/Check.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 43 43" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M21.5 0C9.632 0 0 9.632 0 21.5C0 33.368 9.632 43 21.5 43C33.368 43 43 33.368 43 21.5C43 9.632 33.368 0 21.5 0ZM17.2 32.25L6.45 21.5L9.4815 18.4685L17.2 26.1655L33.5185 9.847L36.55 12.9L17.2 32.25Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/ChevronDown.vue
================================================
<template>
  <svg :width="size" :height="size" viewBox="0 0 17 10" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M1.9975 0L8.5 6.18084L15.0025 0L17 1.90283L8.5 10L0 1.90283L1.9975 0Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/Close.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M8 0.805714L7.19429 0L4 3.19429L0.805714 0L0 0.805714L3.19429 4L0 7.19429L0.805714 8L4 4.80571L7.19429 8L8 7.19429L4.80571 4L8 0.805714Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/Copy.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 19 22" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M14 0H2C0.9 0 0 0.9 0 2V16H2V2H14V0ZM13 4L19 10V20C19 21.1 18.1 22 17 22H5.99C4.89 22 4 21.1 4 20L4.01 6C4.01 4.9 4.9 4 6 4H13ZM12 11H17.5L12 5.5V11Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/Loading.vue
================================================
<template>
  <svg
    :width="size"
    :height="size" viewBox="0 0 22 30" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path
      d="M11 6.81818V10.9091L16.5 5.45455L11 0V4.09091C4.9225 4.09091 0 8.97273 0 15C0 17.1409 0.6325 19.1318 1.705 20.8091L3.7125 18.8182C3.09375 17.6864 2.75 16.3773 2.75 15C2.75 10.4864 6.44875 6.81818 11 6.81818ZM20.295 9.19091L18.2875 11.1818C18.8925 12.3273 19.25 13.6227 19.25 15C19.25 19.5136 15.5512 23.1818 11 23.1818V19.0909L5.5 24.5455L11 30V25.9091C17.0775 25.9091 22 21.0273 22 15C22 12.8591 21.3675 10.8682 20.295 9.19091Z"
      :fill="color"/>
  </svg>
</template>

<script>
export default {
  props: {
    size: { type: [String, Number], default: 22 },
    color: { type: String, default: 'white' }
  }
}
</script>


================================================
FILE: widget/src/components/Icon/index.vue
================================================
<template>
  <component :is="name" v-bind="$props"/>
</template>

<script>
import Loading from './Loading.vue'
import Copy from './Copy.vue'
import ChevronDown from './ChevronDown.vue'
import Chat from './Chat.vue'
import Close from './Close.vue'
import ArrowRight from './ArrowRight.vue'
import Check from './Check.vue'
import Atention from './Atention.vue'

export default {
  components: {
    Loading,
    Copy,
    ChevronDown,
    Chat,
    Close,
    ArrowRight,
    Check,
    Atention
  },
  props: {
    name: { type: String, required: true }
  }
}
</script>


================================================
FILE: widget/src/components/Wizard/Error.vue
================================================
<template>
  <div class="flex flex-col items-center justify-between w-full my-5">
    <icon
      name="atention"
      :color="palette.danger"
      size="70" />

    <p class="text-xl font-black text-center w-full mt-2">
      Droga! Aconteceu algum erro.
    </p>

    <div class="flex justify-center items-center w-full mt-2">
      <button
        @click="goBack"
        class="
          rounded-full font-regular text-sm flex flex-col justify-center bg-brand-gray
          items-center py-2 px-5 cursor-pointer focus:outline-none
        "
      >
        Tente novamente
      </button>
    </div>

  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { resetStore } from '../../store'
import Icon from '../Icon/index.vue'
import palette from '../../../palette.js'

interface SetupReturn {
  goBack(): void;
  palette: Record<string, string>;
}

export default defineComponent({
  components: { Icon },
  setup (): SetupReturn {
    function goBack (): void {
      resetStore()
    }

    return {
      goBack,
      palette: palette.brand
    }
  }
})
</script>


================================================
FILE: widget/src/components/Wizard/SelectFeedbackType.vue
================================================
<template>
  <div class="flex justify-between w-full my-5">
    <button
      @click="() => handleSelect('ISSUE')"
      class="
        rounded-xl hover:bg-gray-100 bg-brand-gray flex flex-col
        justify-center items-center p-5 w-28 cursor-pointer focus:outline-none
      ">
      <div class="w-12">
        <img class="w-full" src="../../assets/images/issue.png" alt="problema">
      </div>
      <p class="font-medium mt-1 text-gray-800">Problema</p>
    </button>

    <button
      @click="() => handleSelect('IDEA')"
      class="
        rounded-xl hover:bg-gray-100 bg-brand-gray flex flex-col
        justify-center items-center p-5 w-28 cursor-pointer focus:outline-none
      ">
      <div class="w-12">
        <img class="w-full" src="../../assets/images/lamp.png" alt="problema">
      </div>
      <p class="font-medium mt-1 text-gray-800">Ideia</p>
    </button>

    <button
      @click="() => handleSelect('OTHER')"
      class="
        rounded-xl hover:bg-gray-100 bg-brand-gray flex flex-col
        justify-center items-center p-5 w-28 cursor-pointer focus:outline-none
      ">
      <div class="w-12">
        <img class="w-full" src="../../assets/images/fire.png" alt="problema">
      </div>
      <p class="font-medium mt-1 text-gray-800">Outro</p>
    </button>
  </div>
</template>

<script lang="ts">
import { defineComponent, SetupContext } from 'vue'

interface SetupReturn {
  handleSelect(type: string): void;
}

export default defineComponent({
  setup (_, { emit }: SetupContext): SetupReturn {
    function handleSelect (type: string): void {
      emit('select-feedback-type', type)
      emit('next')
    }

    return {
      handleSelect
    }
  }
})
</script>


================================================
FILE: widget/src/components/Wizard/Success.vue
================================================
<template>
  <div class="flex flex-col items-center justify-between w-full my-5">
    <icon
      name="check"
      :color="palette.success"
      size="70" />

    <p class="text-xl font-black text-center w-full mt-2">
      Obrigado! Recebemos o seu feedback.
    </p>

    <div class="flex justify-center items-center w-full mt-2">
      <button
        @click="goBack"
        class="
          rounded-full font-regular text-sm flex flex-col justify-center bg-brand-gray
          items-center py-2 px-5 cursor-pointer focus:outline-none
        "
      >
        Envie mais feedbacks
      </button>
    </div>

  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { resetStore } from '../../store'
import Icon from '../Icon/index.vue'
import palette from '../../../palette.js'

interface SetupReturn {
  goBack(): void;
  palette: Record<string, string>;
}

export default defineComponent({
  components: { Icon },
  setup (): SetupReturn {
    function goBack (): void {
      resetStore()
    }

    return {
      goBack,
      palette: palette.brand
    }
  }
})
</script>


================================================
FILE: widget/src/components/Wizard/WriteAFeedback.vue
================================================
<template>
  <div class="felx flex-col items-center justify-center w-full my-5">
    <textarea
      v-model="state.feedback"
      class="w-full rounded-lg border-2 border-gray-300 border-solid h-24 p-2 resize-none focus:outline-none">
    </textarea>
    <button
      :disabled="submitButtonIsDisabled"
      :class="{
        'opacity-50': state.isLoading,
        'opacity-50 bg-gray-100 text-gray-500': submitButtonIsDisabled,
        'bg-brand-main text-white': !submitButtonIsDisabled
      }"
      @click="submitAFeedback"
      class="
        rounded-lg font-black mt-3 flex flex-col
        justify-center items-center py-2 w-full cursor-pointer
        focus:outline-none transition-all duration-200
      ">
      <icon v-if="state.isLoading" name="loading" class="animate-spin" color="white" />
      <template v-else>
        Enviar feedback
      </template>
    </button>
  </div>
</template>

<script lang="ts">
import { ComputedRef, computed, defineComponent, reactive } from 'vue'
import useNavigation from '../../hooks/navigation'
import { setMessage } from '../../store'
import Icon from '../Icon/index.vue'
import useStore from '../../hooks/store'
import services from '../../services'

type State = {
  feedback: string;
  isLoading: boolean;
  hasError: Error | null;
}

interface SetupReturn {
  state: State;
  submitAFeedback(): Promise<void>;
  submitButtonIsDisabled: ComputedRef<boolean>;
}

export default defineComponent({
  components: { Icon },
  setup (): SetupReturn {
    const store = useStore()
    const { setSuccessState, setErrorState } = useNavigation()
    const state = reactive<State>({
      feedback: '',
      isLoading: false,
      hasError: null
    })

    const submitButtonIsDisabled = computed<boolean>(() => {
      return !state.feedback.length
    })

    function handleError (error: Error) {
      state.hasError = error
      state.isLoading = false
      setErrorState()
    }

    async function submitAFeedback (): Promise<void> {
      setMessage(state.feedback)
      state.isLoading = true

      try {
        const response = await services.feedbacks.create({
          type: store.feedbackType,
          text: store.message,
          page: store.currentPage,
          apiKey: store.apiKey,
          device: window.navigator.userAgent,
          fingerprint: store.fingerprint
        })

        if (!response.errors) {
          setSuccessState()
        } else {
          setErrorState()
        }

        state.isLoading = false
      } catch (error) {
        handleError(error)
      }
    }

    return {
      state,
      submitAFeedback,
      submitButtonIsDisabled
    }
  }
})
</script>


================================================
FILE: widget/src/components/Wizard/index.vue
================================================
<template>
  <component
    :is="store.currentComponent"
    @select-feedback-type="handleSelectFeedbackType"
    @next="next"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import SelectFeedbackType from './SelectFeedbackType.vue'
import WriteAFeedback from './WriteAFeedback.vue'
import Success from './Success.vue'
import ErrorState from './Error.vue'
import useStore from '../../hooks/store'
import useNavigation, { Navigation } from '../../hooks/navigation'
import { StoreState, setFeedbackType } from '../../store'

interface SetupReturn {
  store: StoreState;
  next: Navigation['next'];
  handleSelectFeedbackType(type: string): void;
}

export default defineComponent({
  components: { SelectFeedbackType, WriteAFeedback, Success, Error: ErrorState },
  setup (): SetupReturn {
    const store = useStore()
    const { next } = useNavigation()

    function handleSelectFeedbackType (type: string): void {
      setFeedbackType(type)
    }

    return {
      store,
      next,
      handleSelectFeedbackType
    }
  }
})
</script>


================================================
FILE: widget/src/hooks/iframe.ts
================================================
import {
  setApiKey,
  setCurrentPage,
  setFingerprint
} from '../store'

interface IframeControl {
  updateCoreValuesOnStore(): void;
  notifyOpen(): void;
  notifyClose(): void;
}

export default function useIframeControl (): IframeControl {
  function updateCoreValuesOnStore (): void {
    if (process.env.NODE_ENV === 'production') {
      const query = new URLSearchParams(window.location.search)
      const apiKey = query.get('api_key') ?? ''
      const page = query.get('page') ?? ''
      const fingerprint = query.get('fingerprint') ?? ''

      setCurrentPage(page)
      setApiKey(apiKey)
      setFingerprint(fingerprint)
      return
    }

    setCurrentPage('https://playground-url.com')
    setApiKey('fcd5015c-10d3-4e9c-b395-ec7ed8850165')
    setFingerprint('123123123123123')
  }

  function notifyOpen (): void {
    window.parent.postMessage({
      isWidget: true,
      isOpen: true
    }, '*')
  }

  function notifyClose (): void {
    window.parent.postMessage({
      isWidget: true,
      isOpen: false
    }, '*')
  }

  return {
    updateCoreValuesOnStore,
    notifyClose,
    notifyOpen
  }
}


================================================
FILE: widget/src/hooks/navigation.ts
================================================
import useStore from './store'
import {
  setCurrentComponent,
  setFeedbackType
} from '../store'

export interface Navigation {
  next(): void;
  back(): void;
  setErrorState(): void;
  setSuccessState(): void;
}

export default function useNavigation (): Navigation {
  const store = useStore()

  function setErrorState (): void {
    setCurrentComponent('Error')
  }

  function setSuccessState (): void {
    setCurrentComponent('Success')
  }

  function next (): void {
    if (store.currentComponent === 'SelectFeedbackType') {
      setCurrentComponent('WriteAFeedback')
    }
  }

  function back (): void {
    if (store.currentComponent === 'WriteAFeedback') {
      setCurrentComponent('SelectFeedbackType')
      setFeedbackType('')
    }
  }

  return { next, back, setErrorState, setSuccessState }
}


================================================
FILE: widget/src/hooks/store.ts
================================================
import Store, { StoreState } from '../store'

export default function useStore (): StoreState {
  return Store
}


================================================
FILE: widget/src/main.ts
================================================
import { createApp } from 'vue'
import Playground from './views/Playground/index.vue'
import App from './App.vue'
import { setup } from './utils/bootstrap'

import '@/assets/css/tailwind.css'
import '@/assets/css/fonts.css'
import 'animate.css'

setup({
  onProduction: () => {
    createApp(App).mount('#app')
  },
  onDevelopment: () => {
    createApp(Playground).mount('#app')
  }
})


================================================
FILE: widget/src/services/feedbacks.ts
================================================
import { AxiosInstance } from 'axios'
import { Feedback } from '../types/feedback'
import { RequestError } from '../types/error'

type Create = {
  data: Feedback;
  errors: RequestError | null;
}

type CreatePayload = {
     type: string;
      text: string;
      fingerprint: string;
      device: string;
      page: string;
      apiKey: string;
}

export interface FeedbackServiceInterface {
  create(payload: CreatePayload): Promise<Create>;
}
function FeedbacksService (httpClient: AxiosInstance): FeedbackServiceInterface {
  async function create (payload: CreatePayload): Promise<Create> {
    const response = await httpClient.post<Feedback>('/feedbacks', payload)
    let errors: RequestError | null = null

    if (!response.data) {
      errors = {
        status: response.request.status,
        statusText: response.request.statusText
      }
    }

    return {
      data: response.data,
      errors
    }
  }

  return {
    create
  }
}

export default FeedbacksService


================================================
FILE: widget/src/services/index.ts
================================================
import axios from 'axios'
import FeedbacksService from './feedbacks'

const API_ENVS = {
  production: 'https://backend-treinamento-vue3.vercel.app',
  development: '',
  local: 'http://localhost:3000'
}

const httpClient = axios.create({
  baseURL: API_ENVS[process.env.NODE_ENV] || API_ENVS.local
})

httpClient.interceptors.response.use((response) => {
  return response
}, (error) => {
  const canThrowAnError = error.request.status === 0 ||
    error.request.status === 500

  if (canThrowAnError) {
    throw new Error(error.message)
  }

  return error
})

export default {
  feedbacks: FeedbacksService(httpClient)
}


================================================
FILE: widget/src/shims-vue.d.ts
================================================
/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}


================================================
FILE: widget/src/store/index.ts
================================================
import { reactive, readonly } from 'vue'

export type StoreState = {
  currentComponent: string;
  feedbackType: string;
  message: string;
  apiKey: string;
  fingerprint: string;
  currentPage: string;
}

const initialState: StoreState = {
  currentComponent: 'SelectFeedbackType',
  message: '',
  feedbackType: '',
  fingerprint: '',
  apiKey: '',
  currentPage: ''
}

const state = reactive<StoreState>({ ...initialState })

export function setCurrentComponent (component: string): void {
  state.currentComponent = component
}

export function setMessage (message: string): void {
  state.message = message
}

export function setFeedbackType (type: string): void {
  state.feedbackType = type
}

export function setCurrentPage (page: string): void {
  state.currentPage = page
}

export function setApiKey (apiKey: string): void {
  state.apiKey = apiKey
}

export function setFingerprint (fingerprint: string): void {
  state.fingerprint = fingerprint
}

export function resetStore (): void {
  setCurrentComponent(initialState.currentComponent)
  setMessage(initialState.message)
  setFeedbackType(initialState.feedbackType)
  setCurrentPage(initialState.currentPage)
  setApiKey(initialState.apiKey)
  setFingerprint(initialState.fingerprint)
}

export default readonly(state)


================================================
FILE: widget/src/types/error.ts
================================================
export type RequestError = {
  status: number;
  statusText: string;
}


================================================
FILE: widget/src/types/feedback.ts
================================================
export type Feedback = {
  type: string;
  text: string;
  fingerprint: string;
  device: string;
  page: string;
  apiKey: string;
  createdAt: string;
}


================================================
FILE: widget/src/utils/bootstrap.ts
================================================
interface SetupPayload {
  onProduction: () => void;
  onDevelopment: () => void;
}
export function setup ({ onProduction, onDevelopment }: SetupPayload) {
  if (process.env.NODE_ENV !== 'production') {
    onDevelopment()
    return
  }

  onProduction()
}


================================================
FILE: widget/src/views/Playground/Playground.spec.js
================================================
import { shallowMount } from '@vue/test-utils'
import Playground from './index.vue'

describe('<Playground />', () => {
  it('should component render correctly', () => {
    const wrapper = shallowMount(Playground)
    expect(wrapper).toMatchSnapshot()
  })
})


================================================
FILE: widget/src/views/Playground/__snapshots__/Playground.spec.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Playground /> should component render correctly 1`] = `
VueWrapper {
  "__app": Object {
    "_component": Object {
      "__emits": Object {},
      "__props": Array [
        Object {},
        Array [],
      ],
      "name": "VTU_ROOT",
      "render": [Function],
    },
    "_container": <div
      data-v-app=""
    >
      
      <div
        class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-main"
      >
        <h1
          class="text-6xl font-black text-brand-gray"
        >
          Playground
        </h1>
        <p
          class="text-2xl mt-3 font-regular text-brand-gray"
        >
          Este é o playground, use para testar o widget.
        </p>
      </div>
      <div
        class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-gray"
      >
        <h1
          class="text-6xl font-black text-brand-graydark"
        >
          🚀
        </h1>
      </div>
      <div
        class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-main"
      >
        <h1
          class="text-6xl font-black text-brand-gray"
        >
          👀
        </h1>
      </div>
      <div
        class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-gray"
      >
        <h1
          class="text-6xl font-black text-brand-graydark"
        >
          🚨
        </h1>
      </div>
      <widget-stub />
      
    </div>,
    "_context": Object {
      "app": [Circular],
      "components": Object {
        "transition": Object {
          "name": "transition",
          "props": undefined,
          "render": [Function],
        },
        "transition-group": Object {
          "name": "transition-group",
          "props": undefined,
          "render": [Function],
        },
      },
      "config": Object {
        "errorHandler": undefined,
        "globalProperties": Object {},
        "isCustomElement": [Function],
        "isNativeTag": [Function],
        "optionMergeStrategies": Object {},
        "performance": false,
        "warnHandler": undefined,
      },
      "directives": Object {},
      "mixins": Array [
        Object {
          "__emits": null,
          "__props": Array [],
          "beforeCreate": [Function],
        },
      ],
      "provides": Object {},
      "reload": [Function],
    },
    "_props": null,
    "_uid": 0,
    "component": [Function],
    "config": Object {
      "errorHandler": undefined,
      "globalProperties": Object {},
      "isCustomElement": [Function],
      "isNativeTag": [Function],
      "optionMergeStrategies": Object {},
      "performance": false,
      "warnHandler": undefined,
    },
    "directive": [Function],
    "mixin": [Function],
    "mount": [Function],
    "provide": [Function],
    "unmount": [Function],
    "use": [Function],
    "version": "3.0.5",
  },
  "__setProps": [Function],
  "componentVM": Object {},
  "rootVM": Object {},
}
`;


================================================
FILE: widget/src/views/Playground/index.vue
================================================
<template>
  <div class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-main">
    <h1 class="text-6xl font-black text-brand-gray">Playground</h1>
    <p class="text-2xl mt-3 font-regular text-brand-gray">Este é o playground, use para testar o widget.</p>
  </div>
  <div class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-gray">
    <h1 class="text-6xl font-black text-brand-graydark">🚀</h1>
  </div>
  <div class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-main">
    <h1 class="text-6xl font-black text-brand-gray">👀</h1>
  </div>
  <div class="w-full h-3/4 flex flex-col justify-center items-center bg-brand-gray">
    <h1 class="text-6xl font-black text-brand-graydark">🚨</h1>
  </div>
  <widget />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Widget from '../Widget/index.vue'

export default defineComponent({
  components: { Widget }
})
</script>


================================================
FILE: widget/src/views/Widget/Box.vue
================================================
<template>
  <div class="box animate__animated animate__fadeInUp animate__faster">
    <div
      :class="{
        'justify-between': canShowAdditionalControlAndInfo,
        'justify-end': !canShowAdditionalControlAndInfo
      }"
      class="relative w-full flex">
      <button
        v-if="canShowAdditionalControlAndInfo"
        @click="back"
        :disabled="canGoBack"
        :class="{ invisible: canGoBack }"
        class="text-xl text-gray-800 focus:outline-none"
      >
        <icon name="arrow-right" :color="colors.gray['800']" />
      </button>

      <p
        v-if="canShowAdditionalControlAndInfo"
        class="text-xl font-black text-center text-brand-main"
        >
        {{ label }}
      </p>

      <button
        @click="() => emit('close-box')"
        class="text-xl text-gray-800 focus:outline-none"
      >
        <icon size="14" name="close" :color="colors.gray['800']" />
      </button>
    </div>

    <wizard />

    <div class="text-gray-800 text-sm flex" v-if="canShowAdditionalControlAndInfo">
      <icon name="chat" class="mr-1" :color="brandColors.graydark" />
      widget by
      <span class="ml-1 font-bold">feedbacker</span>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed, ComputedRef, SetupContext } from 'vue'
import { brand } from '../../../palette'
import Icon from '../../components/Icon/index.vue'
import Wizard from '../../components/Wizard/index.vue'
import colors from 'tailwindcss/colors'
import useStore from '../../hooks/store'
import useNavigation, { Navigation } from '../../hooks/navigation'

interface SetupReturn {
  emit: SetupContext['emit'];
  back: Navigation['back'];
  canGoBack: ComputedRef<boolean>;
  label: ComputedRef<string>;
  canShowAdditionalControlAndInfo: ComputedRef<boolean>;
  brandColors: Record<string, string>;
  colors: Record<string, string>;
}

export default defineComponent({
  emits: ['close-box'],
  components: { Icon, Wizard },
  setup (_, { emit }: SetupContext): SetupReturn {
    const store = useStore()
    const { back } = useNavigation()

    const label = computed<string>(() => {
      if (store.feedbackType === 'ISSUE') {
        return 'Reporte um problema'
      }

      if (store.feedbackType === 'IDEA') {
        return 'Nos fale a sua ideia'
      }

      if (store.feedbackType === 'OTHER') {
        return 'Abra sua mente'
      }

      return 'O que você tem em mente?'
    })

    const canGoBack = computed<boolean>(() => {
      return store.currentComponent === 'SelectFeedbackType'
    })

    const canShowAdditionalControlAndInfo = computed<boolean>(() => {
      return store.currentComponent !== 'Success' && store.currentComponent !== 'Error'
    })

    return {
      emit,
      colors,
      label,
      back,
      brandColors: brand,
      canGoBack,
      canShowAdditionalControlAndInfo
    }
  }
})
</script>

<style lang="postcss" scoped>
.box {
  @apply fixed z-50 bottom-0 right-0 mb-5 mr-5 bg-white rounded-xl
    py-5 px-5 flex flex-col items-center shadow-xl select-none;
  width: 400px;
}
</style>


================================================
FILE: widget/src/views/Widget/Standby.vue
================================================
<template>
  <div
    @click="() => emit('open-box')"
    id="widget-open-button"
    class="
      fixed z-50 bottom-0 right-0 mb-5 mr-5 bg-brand-main rounded-full
      py-3 px-5 flex items-center shadow-xl cursor-pointer select-none
      animate__animated animate__fadeInUp animate__faster
    ">

    <icon
      name="Chat"
      color="white"
      size="27"
      class="mr-3"
    />
    <span class="font-black text-white text-xl">
      Deixe um feedback
    </span>
  </div>
</template>

<script lang="ts">
import { defineComponent, SetupContext } from 'vue'
import Icon from '../../components/Icon/index.vue'

interface SetupReturn {
  emit: SetupContext['emit'];
}

export default defineComponent({
  components: { Icon },
  emits: ['open-box'],
  setup (_, { emit }: SetupContext): SetupReturn {
    return { emit }
  }
})
</script>


================================================
FILE: widget/src/views/Widget/index.vue
================================================
<template>
  <teleport to="body">
    <component
      @open-box="handleOpenBox"
      @close-box="handleCloseBox"
      :is="state.component"
    />
  </teleport>
</template>

<script lang="ts">
import { defineComponent, reactive, watch } from 'vue'
import Standby from './Standby.vue'
import Box from './Box.vue'
import useIframeControl from '../../hooks/iframe'
import useStore from '../../hooks/store'

type State = {
  component: string;
}

interface SetupReturn {
  state: State;
  handleOpenBox(): void;
  handleCloseBox(): void;
}

export default defineComponent({
  components: { Standby, Box },
  setup (): SetupReturn {
    const store = useStore()
    const iframe = useIframeControl()
    const state = reactive<State>({
      component: 'Standby'
    })

    watch(() => store.currentComponent, () => {
      iframe.updateCoreValuesOnStore()
    })

    function handleOpenBox (): void {
      iframe.notifyOpen()
      state.component = 'Box'
    }

    function handleCloseBox (): void {
      iframe.notifyClose()
      state.component = 'Standby'
    }

    return {
      state,
      handleOpenBox,
      handleCloseBox
    }
  }
})
</script>


================================================
FILE: widget/tailwind.config.js
================================================
const colors = require('tailwindcss/colors')
const palette = require('./palette')

module.exports = {
  purge: [
    './src/**/*.html',
    './src/**/*.vue',
    './src/**/*.jsx'
  ],
  presets: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      colors: palette
    },
    screens: {
      sm: '640px',
      md: '768px',
      lg: '1024px',
      xl: '1280px',
      '2xl': '1536px'
    },
    colors: {
      transparent: 'transparent',
      current: 'currentColor',

      black: colors.black,
      white: colors.white,
      gray: colors.coolGray,
      red: colors.red,
      yellow: colors.amber,
      green: colors.emerald,
      blue: colors.blue,
      indigo: colors.indigo,
      purple: colors.violet,
      pink: colors.pink
    },
    spacing: {
      px: '1px',
      0: '0px',
      0.5: '0.125rem',
      1: '0.25rem',
      1.5: '0.375rem',
      2: '0.5rem',
      2.5: '0.625rem',
      3: '0.75rem',
      3.5: '0.875rem',
      4: '1rem',
      5: '1.25rem',
      6: '1.5rem',
      7: '1.75rem',
      8: '2rem',
      9: '2.25rem',
      10: '2.5rem',
      11: '2.75rem',
      12: '3rem',
      14: '3.5rem',
      16: '4rem',
      20: '5rem',
      24: '6rem',
      28: '7rem',
      32: '8rem',
      36: '9rem',
      40: '10rem',
      44: '11rem',
      48: '12rem',
      52: '13rem',
      56: '14rem',
      60: '15rem',
      64: '16rem',
      72: '18rem',
      80: '20rem',
      96: '24rem'
    },
    animation: {
      none: 'none',
      spin: 'spin 1s linear infinite',
      ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
      pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
      bounce: 'bounce 1s infinite'
    },
    backgroundColor: (theme) => theme('colors'),
    backgroundImage: {
      none: 'none',
      'gradient-to-t': 'linear-gradient(to top, var(--tw-gradient-stops))',
      'gradient-to-tr': 'linear-gradient(to top right, var(--tw-gradient-stops))',
      'gradient-to-r': 'linear-gradient(to right, var(--tw-gradient-stops))',
      'gradient-to-br': 'linear-gradient(to bottom right, var(--tw-gradient-stops))',
      'gradient-to-b': 'linear-gradient(to bottom, var(--tw-gradient-stops))',
      'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))',
      'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
      'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))'
    },
    backgroundOpacity: (theme) => theme('opacity'),
    backgroundPosition: {
      bottom: 'bottom',
      center: 'center',
      left: 'left',
      'left-bottom': 'left bottom',
      'left-top': 'left top',
      right: 'right',
      'right-bottom': 'right bottom',
      'right-top': 'right top',
      top: 'top'
    },
    backgroundSize: {
      auto: 'auto',
      cover: 'cover',
      contain: 'contain'
    },
    borderColor: (theme) => ({
      ...theme('colors'),
      DEFAULT: theme('colors.gray.200', 'currentColor')
    }),
    borderOpacity: (theme) => theme('opacity'),
    borderRadius: {
      none: '0px',
      sm: '0.125rem',
      DEFAULT: '0.25rem',
      md: '0.375rem',
      lg: '0.5rem',
      xl: '0.75rem',
      '2xl': '1rem',
      '3xl': '1.5rem',
      full: '9999px'
    },
    borderWidth: {
      DEFAULT: '1px',
      0: '0px',
      2: '2px',
      4: '4px',
      8: '8px'
    },
    boxShadow: {
      sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
      DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
      md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
      lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
      xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
      '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
      inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
      none: 'none'
    },
    container: {},
    cursor: {
      auto: 'auto',
      default: 'default',
      pointer: 'pointer',
      wait: 'wait',
      text: 'text',
      move: 'move',
      'not-allowed': 'not-allowed'
    },
    divideColor: (theme) => theme('borderColor'),
    divideOpacity: (theme) => theme('borderOpacity'),
    divideWidth: (theme) => theme('borderWidth'),
    fill: { current: 'currentColor' },
    flex: {
      1: '1 1 0%',
      auto: '1 1 auto',
      initial: '0 1 auto',
      none: 'none'
    },
    flexGrow: {
      0: '0',
      DEFAULT: '1'
    },
    flexShrink: {
      0: '0',
      DEFAULT: '1'
    },
    fontFamily: {
      regular: ['RobotoRegular'],
      medium: ['RobotoMedium'],
      bold: ['RobotoBold'],
      black: ['RobotoBlack'],
      sans: [
        'ui-sans-serif',
        'system-ui',
        '-apple-system',
        'BlinkMacSystemFont',
        '"Segoe UI"',
        'Roboto',
        '"Helvetica Neue"',
        'Arial',
        '"Noto Sans"',
        'sans-serif',
        '"Apple Color Emoji"',
        '"Segoe UI Emoji"',
        '"Segoe UI Symbol"',
        '"Noto Color Emoji"'
      ],
      serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
      mono: [
        'ui-monospace',
        'SFMono-Regular',
        'Menlo',
        'Monaco',
        'Consolas',
        '"Liberation Mono"',
        '"Courier New"',
        'monospace'
      ]
    },
    fontSize: {
      xs: ['0.75rem', { lineHeight: '1rem' }],
      sm: ['0.875rem', { lineHeight: '1.25rem' }],
      base: ['1rem', { lineHeight: '1.5rem' }],
      lg: ['1.125rem', { lineHeight: '1.75rem' }],
      xl: ['1.25rem', { lineHeight: '1.75rem' }],
      '2xl': ['1.5rem', { lineHeight: '2rem' }],
      '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
      '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
      '5xl': ['3rem', { lineHeight: '1' }],
      '6xl': ['3.75rem', { lineHeight: '1' }],
      '7xl': ['4.5rem', { lineHeight: '1' }],
      '8xl': ['6rem', { lineHeight: '1' }],
      '9xl': ['8rem', { lineHeight: '1' }]
    },
    fontWeight: {
      thin: '100',
      extralight: '200',
      light: '300',
      normal: '400',
      medium: '500',
      semibold: '600',
      bold: '700',
      extrabold: '800',
      black: '900'
    },
    gap: (theme) => theme('spacing'),
    gradientColorStops: (theme) => theme('colors'),
    gridAutoColumns: {
      auto: 'auto',
      min: 'min-content',
      max: 'max-content',
      fr: 'minmax(0, 1fr)'
    },
    gridAutoRows: {
      auto: 'auto',
      min: 'min-content',
      max: 'max-content',
      fr: 'minmax(0, 1fr)'
    },
    gridColumn: {
      auto: 'auto',
      'span-1': 'span 1 / span 1',
      'span-2': 'span 2 / span 2',
      'span-3': 'span 3 / span 3',
      'span-4': 'span 4 / span 4',
      'span-5': 'span 5 / span 5',
      'span-6': 'span 6 / span 6',
      'span-7': 'span 7 / span 7',
      'span-8': 'span 8 / span 8',
      'span-9': 'span 9 / span 9',
      'span-10': 'span 10 / span 10',
      'span-11': 'span 11 / span 11',
      'span-12': 'span 12 / span 12',
      'span-full': '1 / -1'
    },
    gridColumnEnd: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7',
      8: '8',
      9: '9',
      10: '10',
      11: '11',
      12: '12',
      13: '13'
    },
    gridColumnStart: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7',
      8: '8',
      9: '9',
      10: '10',
      11: '11',
      12: '12',
      13: '13'
    },
    gridRow: {
      auto: 'auto',
      'span-1': 'span 1 / span 1',
      'span-2': 'span 2 / span 2',
      'span-3': 'span 3 / span 3',
      'span-4': 'span 4 / span 4',
      'span-5': 'span 5 / span 5',
      'span-6': 'span 6 / span 6',
      'span-full': '1 / -1'
    },
    gridRowStart: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7'
    },
    gridRowEnd: {
      auto: 'auto',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7'
    },
    transformOrigin: {
      center: 'center',
      top: 'top',
      'top-right': 'top right',
      right: 'right',
      'bottom-right': 'bottom right',
      bottom: 'bottom',
      'bottom-left': 'bottom left',
      left: 'left',
      'top-left': 'top left'
    },
    gridTemplateColumns: {
      none: 'none',
      1: 'repeat(1, minmax(0, 1fr))',
      2: 'repeat(2, minmax(0, 1fr))',
      3: 'repeat(3, minmax(0, 1fr))',
      4: 'repeat(4, minmax(0, 1fr))',
      5: 'repeat(5, minmax(0, 1fr))',
      6: 'repeat(6, minmax(0, 1fr))',
      7: 'repeat(7, minmax(0, 1fr))',
      8: 'repeat(8, minmax(0, 1fr))',
      9: 'repeat(9, minmax(0, 1fr))',
      10: 'repeat(10, minmax(0, 1fr))',
      11: 'repeat(11, minmax(0, 1fr))',
      12: 'repeat(12, minmax(0, 1fr))'
    },
    gridTemplateRows: {
      none: 'none',
      1: 'repeat(1, minmax(0, 1fr))',
      2: 'repeat(2, minmax(0, 1fr))',
      3: 'repeat(3, minmax(0, 1fr))',
      4: 'repeat(4, minmax(0, 1fr))',
      5: 'repeat(5, minmax(0, 1fr))',
      6: 'repeat(6, minmax(0, 1fr))'
    },
    height: (theme) => ({
      auto: 'auto',
      ...theme('spacing'),
      '1/2': '50%',
      '1/3': '33.333333%',
      '2/3': '66.666667%',
      '1/4': '25%',
      '2/4': '50%',
      '3/4': '75%',
      '1/5': '20%',
      '2/5': '40%',
      '3/5': '60%',
      '4/5': '80%',
      '1/6': '16.666667%',
      '2/6': '33.333333%',
      '3/6': '50%',
      '4/6': '66.666667%',
      '5/6': '83.333333%',
      full: '100%',
      screen: '100vh'
    }),
    inset: (theme, { negative }) => ({
      auto: 'auto',
      ...theme('spacing'),
      ...negative(theme('spacing')),
      '1/2': '50%',
      '1/3': '33.333333%',
      '2/3': '66.666667%',
      '1/4': '25%',
      '2/4': '50%',
      '3/4': '75%',
      full: '100%',
      '-1/2': '-50%',
      '-1/3': '-33.333333%',
      '-2/3': '-66.666667%',
      '-1/4': '-25%',
      '-2/4': '-50%',
      '-3/4': '-75%',
      '-full': '-100%'
    }),
    keyframes: {
      spin: {
        to: {
          transform: 'rotate(360deg)'
        }
      },
      ping: {
        '75%, 100%': {
          transform: 'scale(2)',
          opacity: '0'
        }
      },
      pulse: {
        '50%': {
          opacity: '.5'
        }
      },
      bounce: {
        '0%, 100%': {
          transform: 'translateY(-25%)',
          animationTimingFunction: 'cubic-bezier(0.8,0,1,1)'
        },
        '50%': {
          transform: 'none',
          animationTimingFunction: 'cubic-bezier(0,0,0.2,1)'
        }
      }
    },
    letterSpacing: {
      tighter: '-0.05em',
      tight: '-0.025em',
      normal: '0em',
      wide: '0.025em',
      wider: '0.05em',
      widest: '0.1em'
    },
    lineHeight: {
      none: '1',
      tight: '1.25',
      snug: '1.375',
      normal: '1.5',
      relaxed: '1.625',
      loose: '2',
      3: '.75rem',
      4: '1rem',
      5: '1.25rem',
      6: '1.5rem',
      7: '1.75rem',
      8: '2rem',
      9: '2.25rem',
      10: '2.5rem'
    },
    listStyleType: {
      none: 'none',
      disc: 'disc',
      decimal: 'decimal'
    },
    margin: (theme, { negative }) => ({
      auto: 'auto',
      ...theme('spacing'),
      ...negative(theme('spacing'))
    }),
    maxHeight: (theme) => ({
      ...theme('spacing'),
      full: '100%',
      screen: '100vh'
    }),
    maxWidth: (theme, { breakpoints }) => ({
      none: 'none',
      0: '0rem',
      xs: '20rem',
      sm: '24rem',
      md: '28rem',
      lg: '32rem',
      xl: '36rem',
      '2xl': '42rem',
      '3xl': '48rem',
      '4xl': '56rem',
      '5xl': '64rem',
      '6xl': '72rem',
      '7xl': '80rem',
      full: '100%',
      min: 'min-content',
      max: 'max-content',
      prose: '65ch',
      ...breakpoints(theme('screens'))
    }),
    minHeight: {
      0: '0px',
      full: '100%',
      screen: '100vh'
    },
    minWidth: {
      0: '0px',
      full: '100%',
      min: 'min-content',
      max: 'max-content'
    },
    objectPosition: {
      bottom: 'bottom',
      center: 'center',
      left: 'left',
      'left-bottom': 'left bottom',
      'left-top': 'left top',
      right: 'right',
      'right-bottom': 'right bottom',
      'right-top': 'right top',
      top: 'top'
    },
    opacity: {
      0: '0',
      5: '0.05',
      10: '0.1',
      20: '0.2',
      25: '0.25',
      30: '0.3',
      40: '0.4',
      50: '0.5',
      60: '0.6',
      70: '0.7',
      75: '0.75',
      80: '0.8',
      90: '0.9',
      95: '0.95',
      100: '1'
    },
    order: {
      first: '-9999',
      last: '9999',
      none: '0',
      1: '1',
      2: '2',
      3: '3',
      4: '4',
      5: '5',
      6: '6',
      7: '7',
      8: '8',
      9: '9',
      10: '10',
      11: '11',
      12: '12'
    },
    outline: {
      none: ['2px solid transparent', '2px'],
      white: ['2px dotted white', '2px'],
      black: ['2px dotted black', '2px']
    },
    padding: (theme) => theme('spacing'),
    placeholderColor: (theme) => theme('colors'),
    placeholderOpacity: (theme) => theme('opacity'),
    ringColor: (theme) => ({
      DEFAULT: theme('colors.blue.500', '#3b82f6'),
      ...theme('colors')
    }),
    ringOffsetColor: (theme) => theme('colors'),
    ringOffsetWidth: {
      0: '0px',
      1: '1px',
      2: '2px',
      4: '4px',
      8: '8px'
    },
    ringOpacity: (theme) => ({
      DEFAULT: '0.5',
      ...theme('opacity')
    }),
    ringWidth: {
      DEFAULT: '3px',
      0: '0px',
      1: '1px',
      2: '2px',
      4: '4px',
      8: '8px'
    },
    rotate: {
      '-180': '-180deg',
      '-90': '-90deg',
      '-45': '-45deg',
      '-12': '-12deg',
      '-6': '-6deg',
      '-3': '-3deg',
      '-2': '-2deg',
      '-1': '-1deg',
      0: '0deg',
      1: '1deg',
      2: '2deg',
      3: '3deg',
      6: '6deg',
      12: '12deg',
      45: '45deg',
      90: '90deg',
      180: '180deg'
    },
    scale: {
      0: '0',
      50: '.5',
      75: '.75',
      90: '.9',
      95: '.95',
      100: '1',
      105: '1.05',
      110: '1.1',
      125: '1.25',
      150: '1.5'
    },
    skew: {
      '-12': '-12deg',
      '-6': '-6deg',
      '-3': '-3deg',
      '-2': '-2deg',
      '-1': '-1deg',
      0: '0deg',
      1: '1deg',
      2: '2deg',
      3: '3deg',
      6: '6deg',
      12: '12deg'
    },
    space: (theme, { negative }) => ({
      ...theme('spacing'),
      ...negative(theme('spacing'))
    }),
    stroke: {
      current: 'currentColor'
    },
    strokeWidth: {
      0: '0',
      1: '1',
      2: '2'
    },
    textColor: (theme) => theme('colors'),
    textOpacity: (theme) => theme('opacity'),
    transitionDuration: {
      DEFAULT: '150ms',
      75: '75ms',
      100: '100ms',
      150: '150ms',
      200: '200ms',
      300: '300ms',
      500: '500ms',
      700: '700ms',
      1000: '1000ms'
    },
    transitionDelay: {
      75: '75ms',
      100: '100ms',
      150: '150ms',
      200: '200ms',
      300: '300ms',
      500: '500ms',
      700: '700ms',
      1000: '1000ms'
    },
    transitionProperty: {
      none: 'none',
      all: 'all',
      DEFAULT: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
      colors: 'background-color, border-color, color, fill, stroke',
      opacity: 'opacity',
      s
Download .txt
gitextract_3dqmdq3z/

├── .github/
│   └── workflows/
│       ├── ci-dashboard-e2e.yml
│       ├── ci-dashboard-unit.yml
│       ├── ci-widget-e2e.yml
│       └── ci-widget-unit.yml
├── .gitignore
├── backend/
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── Dockerfile
│   ├── README.md
│   ├── database/
│   │   ├── index.js
│   │   └── mock.js
│   ├── handlers/
│   │   ├── apikey.js
│   │   ├── auth.js
│   │   ├── feedbacks.js
│   │   └── users.js
│   ├── index.js
│   ├── package.json
│   ├── vercel.json
│   └── vuejs_brasil_feedbacker.json
├── conceitos/
│   ├── data-binding/
│   │   └── App.vue
│   ├── diretivas/
│   │   └── App.vue
│   ├── eventos-e-metodos/
│   │   └── App.vue
│   ├── lifecycle-hooks/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── babel.config.js
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── index.html
│   │   └── src/
│   │       ├── App.vue
│   │       └── main.js
│   ├── nova-syntax-e-antiga/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── babel.config.js
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── index.html
│   │   └── src/
│   │       ├── App.vue
│   │       └── main.js
│   └── single-file-components/
│       └── App.vue
├── dashboard/
│   ├── .browserslistrc
│   ├── .editorconfig
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── Dockerfile
│   ├── README.md
│   ├── babel.config.js
│   ├── cypress.json
│   ├── jest.config.js
│   ├── package.json
│   ├── palette.js
│   ├── postcss.config.js
│   ├── public/
│   │   ├── _redirects
│   │   └── index.html
│   ├── src/
│   │   ├── App.vue
│   │   ├── assets/
│   │   │   └── css/
│   │   │       ├── fonts.css
│   │   │       └── tailwind.css
│   │   ├── components/
│   │   │   ├── ContentLoader/
│   │   │   │   └── index.vue
│   │   │   ├── FeedbackCard/
│   │   │   │   ├── Badge.vue
│   │   │   │   ├── Loading.vue
│   │   │   │   └── index.vue
│   │   │   ├── HeaderLogged/
│   │   │   │   ├── HeaderLogged.spec.js
│   │   │   │   ├── __snapshots__/
│   │   │   │   │   └── HeaderLogged.spec.js.snap
│   │   │   │   └── index.vue
│   │   │   ├── Icon/
│   │   │   │   ├── ChevronDown.vue
│   │   │   │   ├── Copy.vue
│   │   │   │   ├── Loading.vue
│   │   │   │   └── index.vue
│   │   │   ├── ModalCreateAccount/
│   │   │   │   └── index.vue
│   │   │   ├── ModalFactory/
│   │   │   │   └── index.vue
│   │   │   └── ModalLogin/
│   │   │       └── index.vue
│   │   ├── hooks/
│   │   │   ├── useModal.js
│   │   │   └── useStore.js
│   │   ├── main.js
│   │   ├── router/
│   │   │   └── index.js
│   │   ├── services/
│   │   │   ├── __snapshots__/
│   │   │   │   └── auth.spec.js.snap
│   │   │   ├── auth.js
│   │   │   ├── auth.spec.js
│   │   │   ├── feedbacks.js
│   │   │   ├── index.js
│   │   │   └── users.js
│   │   ├── store/
│   │   │   ├── global.js
│   │   │   ├── index.js
│   │   │   ├── user.js
│   │   │   └── user.spec.js
│   │   ├── utils/
│   │   │   ├── bus.js
│   │   │   ├── date.js
│   │   │   ├── timeout.js
│   │   │   ├── validators.js
│   │   │   └── validators.spec.js
│   │   └── views/
│   │       ├── Credencials/
│   │       │   └── index.vue
│   │       ├── Feedbacks/
│   │       │   ├── Filters.vue
│   │       │   ├── FiltersLoading.vue
│   │       │   └── index.vue
│   │       └── Home/
│   │           ├── Contact.vue
│   │           ├── CustomHeader.vue
│   │           ├── Home.spec.js
│   │           ├── __snapshots__/
│   │           │   └── Home.spec.js.snap
│   │           └── index.vue
│   ├── tailwind.config.js
│   └── tests/
│       └── e2e/
│           ├── .eslintrc.js
│           ├── plugins/
│           │   └── index.js
│           ├── specs/
│           │   ├── credencials.js
│           │   └── home.js
│           └── support/
│               ├── commands.js
│               └── index.js
├── try-widget/
│   └── index.html
└── widget/
    ├── .browserslistrc
    ├── .editorconfig
    ├── .eslintrc.js
    ├── .gitignore
    ├── Dockerfile
    ├── README.md
    ├── babel.config.js
    ├── cypress.json
    ├── jest.config.js
    ├── package.json
    ├── palette.js
    ├── postcss.config.js
    ├── public/
    │   ├── index.html
    │   └── init.js
    ├── src/
    │   ├── App.vue
    │   ├── assets/
    │   │   └── css/
    │   │       ├── fonts.css
    │   │       └── tailwind.css
    │   ├── components/
    │   │   ├── Icon/
    │   │   │   ├── ArrowRight.vue
    │   │   │   ├── Atention.vue
    │   │   │   ├── Chat.vue
    │   │   │   ├── Check.vue
    │   │   │   ├── ChevronDown.vue
    │   │   │   ├── Close.vue
    │   │   │   ├── Copy.vue
    │   │   │   ├── Loading.vue
    │   │   │   └── index.vue
    │   │   └── Wizard/
    │   │       ├── Error.vue
    │   │       ├── SelectFeedbackType.vue
    │   │       ├── Success.vue
    │   │       ├── WriteAFeedback.vue
    │   │       └── index.vue
    │   ├── hooks/
    │   │   ├── iframe.ts
    │   │   ├── navigation.ts
    │   │   └── store.ts
    │   ├── main.ts
    │   ├── services/
    │   │   ├── feedbacks.ts
    │   │   └── index.ts
    │   ├── shims-vue.d.ts
    │   ├── store/
    │   │   └── index.ts
    │   ├── types/
    │   │   ├── error.ts
    │   │   └── feedback.ts
    │   ├── utils/
    │   │   └── bootstrap.ts
    │   └── views/
    │       ├── Playground/
    │       │   ├── Playground.spec.js
    │       │   ├── __snapshots__/
    │       │   │   └── Playground.spec.js.snap
    │       │   └── index.vue
    │       └── Widget/
    │           ├── Box.vue
    │           ├── Standby.vue
    │           └── index.vue
    ├── tailwind.config.js
    ├── tests/
    │   └── e2e/
    │       ├── .eslintrc.js
    │       ├── plugins/
    │       │   └── index.js
    │       ├── specs/
    │       │   └── widget.js
    │       └── support/
    │           ├── commands.js
    │           └── index.js
    └── tsconfig.json
Download .txt
SYMBOL INDEX (50 symbols across 26 files)

FILE: backend/database/index.js
  function wait (line 3) | function wait (timeMs) {
  function update (line 9) | async function update (col, id, data) {
  function readAll (line 26) | async function readAll (col) {
  function insert (line 35) | async function insert (col, data) {
  function readOneById (line 45) | async function readOneById (col, id) {
  function readOneByEmail (line 53) | async function readOneByEmail (col, email) {

FILE: backend/handlers/apikey.js
  function CreateApiKeyHandler (line 1) | function CreateApiKeyHandler (db) {

FILE: backend/handlers/auth.js
  function CreateAuthHandler (line 3) | function CreateAuthHandler (db) {

FILE: backend/handlers/feedbacks.js
  constant FEEDBACK_TYPES (line 3) | const FEEDBACK_TYPES = {
  function CreateFeedbackHandler (line 9) | function CreateFeedbackHandler (db) {

FILE: backend/handlers/users.js
  function CreateUserHandler (line 3) | function CreateUserHandler (db) {

FILE: dashboard/src/hooks/useModal.js
  constant EVENT_NAME (line 3) | const EVENT_NAME = 'modal:toggle'
  function useModal (line 5) | function useModal () {

FILE: dashboard/src/hooks/useStore.js
  function useStore (line 3) | function useStore (module) {

FILE: dashboard/src/services/index.js
  constant API_ENVS (line 8) | const API_ENVS = {

FILE: dashboard/src/store/global.js
  function setGlobalLoading (line 9) | function setGlobalLoading (status) {

FILE: dashboard/src/store/user.js
  function resetUserStore (line 11) | function resetUserStore () {
  function cleanCurrentUser (line 15) | function cleanCurrentUser () {
  function setCurrentUser (line 19) | function setCurrentUser (user) {
  function setApiKey (line 23) | function setApiKey (apiKey) {

FILE: dashboard/src/utils/date.js
  function getDiffTimeBetweenCurrentDate (line 1) | function getDiffTimeBetweenCurrentDate (dateString = '', now = new Date(...

FILE: dashboard/src/utils/timeout.js
  function wait (line 1) | function wait (timeMs) {

FILE: dashboard/src/utils/validators.js
  function validateEmptyAndLength3 (line 1) | function validateEmptyAndLength3 (value) {
  function validateEmptyAndEmail (line 13) | function validateEmptyAndEmail (value) {

FILE: dashboard/tests/e2e/specs/credencials.js
  constant APP_URL (line 1) | const APP_URL = process.env.APP_URL || 'http://localhost:8080'

FILE: dashboard/tests/e2e/specs/home.js
  constant APP_URL (line 1) | const APP_URL = process.env.APP_URL || 'http://localhost:8080'

FILE: widget/public/init.js
  function init (line 1) | function init (apiKey) {

FILE: widget/src/hooks/iframe.ts
  type IframeControl (line 7) | interface IframeControl {
  function useIframeControl (line 13) | function useIframeControl (): IframeControl {

FILE: widget/src/hooks/navigation.ts
  type Navigation (line 7) | interface Navigation {
  function useNavigation (line 14) | function useNavigation (): Navigation {

FILE: widget/src/hooks/store.ts
  function useStore (line 3) | function useStore (): StoreState {

FILE: widget/src/services/feedbacks.ts
  type Create (line 5) | type Create = {
  type CreatePayload (line 10) | type CreatePayload = {
  type FeedbackServiceInterface (line 19) | interface FeedbackServiceInterface {
  function FeedbacksService (line 22) | function FeedbacksService (httpClient: AxiosInstance): FeedbackServiceIn...

FILE: widget/src/services/index.ts
  constant API_ENVS (line 4) | const API_ENVS = {

FILE: widget/src/store/index.ts
  type StoreState (line 3) | type StoreState = {
  function setCurrentComponent (line 23) | function setCurrentComponent (component: string): void {
  function setMessage (line 27) | function setMessage (message: string): void {
  function setFeedbackType (line 31) | function setFeedbackType (type: string): void {
  function setCurrentPage (line 35) | function setCurrentPage (page: string): void {
  function setApiKey (line 39) | function setApiKey (apiKey: string): void {
  function setFingerprint (line 43) | function setFingerprint (fingerprint: string): void {
  function resetStore (line 47) | function resetStore (): void {

FILE: widget/src/types/error.ts
  type RequestError (line 1) | type RequestError = {

FILE: widget/src/types/feedback.ts
  type Feedback (line 1) | type Feedback = {

FILE: widget/src/utils/bootstrap.ts
  type SetupPayload (line 1) | interface SetupPayload {
  function setup (line 5) | function setup ({ onProduction, onDevelopment }: SetupPayload) {

FILE: widget/tests/e2e/specs/widget.js
  constant APP_URL (line 1) | const APP_URL = process.env.APP_URL || 'http://localhost:8080'
Condensed preview — 159 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (213K chars).
[
  {
    "path": ".github/workflows/ci-dashboard-e2e.yml",
    "chars": 717,
    "preview": "name: Dashboard e2e testing\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ $default-branch ]\n  pull_request:\n    bran"
  },
  {
    "path": ".github/workflows/ci-dashboard-unit.yml",
    "chars": 553,
    "preview": "name: Dashboard unit testing\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ $default-branch ]\n  pull_request:\n    bra"
  },
  {
    "path": ".github/workflows/ci-widget-e2e.yml",
    "chars": 708,
    "preview": "name: Widget e2e testing\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ $default-branch ]\n  pull_request:\n    branche"
  },
  {
    "path": ".github/workflows/ci-widget-unit.yml",
    "chars": 547,
    "preview": "name: Widget unit testing\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ $default-branch ]\n  pull_request:\n    branch"
  },
  {
    "path": ".gitignore",
    "chars": 23,
    "preview": ".DS_Store\nnode_modules\n"
  },
  {
    "path": "backend/.eslintrc.js",
    "chars": 326,
    "preview": "module.exports = {\n    \"env\": {\n        \"commonjs\": true,\n        \"es6\": true,\n        \"node\": true\n    },\n    \"extends\""
  },
  {
    "path": "backend/.gitignore",
    "chars": 13,
    "preview": ".now\n.vercel\n"
  },
  {
    "path": "backend/Dockerfile",
    "chars": 161,
    "preview": "FROM node:10-alpine\n\nRUN mkdir -p /src\n\nCOPY package.json src/package.json\n\nWORKDIR /src\n\nRUN npm install --only=product"
  },
  {
    "path": "backend/README.md",
    "chars": 410,
    "preview": "## Backend do curso treinamento de Vue.js 3\n\nBackend pré-pronto do curso treinamento de Vue.js 3\n\n### Comandos\n\n```\n# Bu"
  },
  {
    "path": "backend/database/index.js",
    "chars": 1150,
    "preview": "const database = require('./mock')\n\nfunction wait (timeMs) {\n  return new Promise(resolve => {\n    setTimeout(resolve, t"
  },
  {
    "path": "backend/database/mock.js",
    "chars": 2770,
    "preview": "module.exports = {\n  users: [\n    {\n      id: 'eab759f8-f238-4ff9-ae91-ee1558982329',\n      name: 'Igor Halfeld',\n      "
  },
  {
    "path": "backend/handlers/apikey.js",
    "chars": 566,
    "preview": "function CreateApiKeyHandler (db) {\n  async function checkIfApiKeyExists (ctx) {\n    const { apikey } = ctx.query\n    if"
  },
  {
    "path": "backend/handlers/auth.js",
    "chars": 752,
    "preview": "const jwt = require('jsonwebtoken')\n\nfunction CreateAuthHandler (db) {\n  async function login (ctx) {\n    const { email,"
  },
  {
    "path": "backend/handlers/feedbacks.js",
    "chars": 3671,
    "preview": "const { v4: uuidv4 } = require('uuid')\n\nconst FEEDBACK_TYPES = {\n  ISSUE: 'ISSUE',\n  IDEA: 'IDEA',\n  OTHER: 'OTHER'\n}\n\nf"
  },
  {
    "path": "backend/handlers/users.js",
    "chars": 1765,
    "preview": "const { v4: uuidv4 } = require('uuid')\n\nfunction CreateUserHandler (db) {\n  async function getLoggerUser (ctx) {\n    con"
  },
  {
    "path": "backend/index.js",
    "chars": 1605,
    "preview": "const Koa = require('koa')\nconst Router = require('koa-router')\nconst jwt = require('koa-jwt')\nconst cors = require('@ko"
  },
  {
    "path": "backend/package.json",
    "chars": 929,
    "preview": "{\n  \"name\": \"backend\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Backend pré-pronto do curso treinamento de Vue.js 3\",\n  \""
  },
  {
    "path": "backend/vercel.json",
    "chars": 174,
    "preview": "{\n  \"version\": 2,\n  \"builds\": [\n    {\n      \"src\": \"index.js\",\n      \"use\": \"@vercel/node\"\n    }\n  ],\n  \"routes\": [\n    "
  },
  {
    "path": "backend/vuejs_brasil_feedbacker.json",
    "chars": 11682,
    "preview": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"03caccb8-c176-4e3f-ae31-751901560b3c\",\n\t\t\"name\": \"Vue.js Brasil - Feedbacker\",\n\t\t\"schema\""
  },
  {
    "path": "conceitos/data-binding/App.vue",
    "chars": 356,
    "preview": "<template>\n  <div>\n    <h1 :style=\"{ textDecoration: decoration }\">Hello {{ name }}!</h1>\n    <input type=\"text\" v-model"
  },
  {
    "path": "conceitos/diretivas/App.vue",
    "chars": 650,
    "preview": "<template>\n  <div>\n    <h1>Minha lista de tarefas!</h1>\n    <button @click=\"() => showList = !showList\">\n      Ver a lis"
  },
  {
    "path": "conceitos/eventos-e-metodos/App.vue",
    "chars": 1556,
    "preview": "<template>\n  <div>\n    <h1>Minha lista de tarefas!</h1>\n    <button @click=\"handleShowHideList\">\n      Ver a lista!\n    "
  },
  {
    "path": "conceitos/lifecycle-hooks/.gitignore",
    "chars": 231,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": "conceitos/lifecycle-hooks/README.md",
    "chars": 332,
    "preview": "# nova-syntax-e-antiga\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run s"
  },
  {
    "path": "conceitos/lifecycle-hooks/babel.config.js",
    "chars": 73,
    "preview": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "conceitos/lifecycle-hooks/package.json",
    "chars": 866,
    "preview": "{\n  \"name\": \"nova-syntax-e-antiga\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-servic"
  },
  {
    "path": "conceitos/lifecycle-hooks/public/index.html",
    "chars": 611,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
  },
  {
    "path": "conceitos/lifecycle-hooks/src/App.vue",
    "chars": 304,
    "preview": "<template>\n  <h1>Lifecycle hooks</h1>\n</template>\n\n<script>\nimport { onMounted } from 'vue'\n\nexport default {\n  setup ()"
  },
  {
    "path": "conceitos/lifecycle-hooks/src/main.js",
    "chars": 90,
    "preview": "import { createApp } from 'vue'\nimport App from './App.vue'\n\ncreateApp(App).mount('#app')\n"
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/.gitignore",
    "chars": 231,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/README.md",
    "chars": 332,
    "preview": "# nova-syntax-e-antiga\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run s"
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/babel.config.js",
    "chars": 73,
    "preview": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/package.json",
    "chars": 866,
    "preview": "{\n  \"name\": \"nova-syntax-e-antiga\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-servic"
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/public/index.html",
    "chars": 611,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/src/App.vue",
    "chars": 1778,
    "preview": "<template>\n  <div>\n    <h1>Minha lista de tarefas!</h1>\n    <button @click=\"handleShowHideList\">\n      Ver a lista!\n    "
  },
  {
    "path": "conceitos/nova-syntax-e-antiga/src/main.js",
    "chars": 90,
    "preview": "import { createApp } from 'vue'\nimport App from './App.vue'\n\ncreateApp(App).mount('#app')\n"
  },
  {
    "path": "conceitos/single-file-components/App.vue",
    "chars": 126,
    "preview": "<template>  \n  <h1>Hello World</h1>\n</template>\n\n<script lang=\"ts\">\nexport default {}\n</script>\n\n<style lang=\"scss\">\n</s"
  },
  {
    "path": "dashboard/.browserslistrc",
    "chars": 30,
    "preview": "> 1%\nlast 2 versions\nnot dead\n"
  },
  {
    "path": "dashboard/.editorconfig",
    "chars": 121,
    "preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true"
  },
  {
    "path": "dashboard/.eslintrc.js",
    "chars": 468,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    node: true\n  },\n  extends: [\n    'plugin:vue/vue3-essential',\n    '@vue/st"
  },
  {
    "path": "dashboard/.gitignore",
    "chars": 275,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n/tests/e2e/videos/\n/tests/e2e/screenshots/\n\n\n# local env files\n.env.local\n.env.*.local\n\n# "
  },
  {
    "path": "dashboard/Dockerfile",
    "chars": 215,
    "preview": "FROM node:13-alpine as build\n\nWORKDIR /\n\nCOPY . .\n\nENV NODE_ENV=production\nRUN npm install --production\nRUN npm run buil"
  },
  {
    "path": "dashboard/README.md",
    "chars": 428,
    "preview": "# dashboard\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run serve\n```\n\n#"
  },
  {
    "path": "dashboard/babel.config.js",
    "chars": 73,
    "preview": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "dashboard/cypress.json",
    "chars": 50,
    "preview": "{\n  \"pluginsFile\": \"tests/e2e/plugins/index.js\"\n}\n"
  },
  {
    "path": "dashboard/jest.config.js",
    "chars": 147,
    "preview": "module.exports = {\n  preset: '@vue/cli-plugin-unit-jest',\n  testMatch: [\n    '**/*.spec.js'\n  ],\n  transform: {\n    '^.+"
  },
  {
    "path": "dashboard/package.json",
    "chars": 1582,
    "preview": "{\n  \"name\": \"dashboard\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n "
  },
  {
    "path": "dashboard/palette.js",
    "chars": 2368,
    "preview": "module.exports = {\n  brand: {\n    main: '#EF4983',\n    gray: '#F9F9F9',\n    info: '#8296FB',\n    graydark: '#C0BCB0',\n  "
  },
  {
    "path": "dashboard/postcss.config.js",
    "chars": 81,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  }\n}\n"
  },
  {
    "path": "dashboard/public/_redirects",
    "chars": 20,
    "preview": "/* /index.html 200\n\n"
  },
  {
    "path": "dashboard/public/index.html",
    "chars": 583,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
  },
  {
    "path": "dashboard/src/App.vue",
    "chars": 742,
    "preview": "<template>\n  <modal-factory />\n  <router-view />\n</template>\n\n<script>\nimport { watch } from 'vue'\nimport ModalFactory f"
  },
  {
    "path": "dashboard/src/assets/css/fonts.css",
    "chars": 799,
    "preview": "@font-face {\n  font-family: \"RobotoRegular\";\n  src: local(\"Roboto Regular\"), local(\"Roboto-Regular\"),\n    url(\"../fonts/"
  },
  {
    "path": "dashboard/src/assets/css/tailwind.css",
    "chars": 127,
    "preview": "/*! @import */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody,\n#app {\n  width: 100%;\n  height: 1"
  },
  {
    "path": "dashboard/src/components/ContentLoader/index.vue",
    "chars": 1412,
    "preview": "<template>\n  <div\n    :style=\"{\n      width: computedWidth,\n      height\n    }\"\n    class=\"opacity-75 content-loader\"\n  "
  },
  {
    "path": "dashboard/src/components/FeedbackCard/Badge.vue",
    "chars": 790,
    "preview": "<template>\n  <span\n    :class=\"`bg-${classColor}`\"\n    class=\"p-2 text-xs font-black text-white uppercase rounded-full\">"
  },
  {
    "path": "dashboard/src/components/FeedbackCard/Loading.vue",
    "chars": 637,
    "preview": "<template>\n  <content-loader\n    class=\"flex flex-col items-center rounded\"\n    width=\"100%\"\n    height=\"300px\"\n  >\n    "
  },
  {
    "path": "dashboard/src/components/FeedbackCard/index.vue",
    "chars": 2401,
    "preview": "<template>\n  <div\n    @click=\"handleToggle\"\n    class=\"flex flex-col px-8 py-6 rounded cursor-pointer bg-brand-gray\">\n  "
  },
  {
    "path": "dashboard/src/components/HeaderLogged/HeaderLogged.spec.js",
    "chars": 1399,
    "preview": "import { shallowMount } from '@vue/test-utils'\nimport HeaderLogged from '.'\nimport { routes } from '../../router'\n\nimpor"
  },
  {
    "path": "dashboard/src/components/HeaderLogged/__snapshots__/HeaderLogged.spec.js.snap",
    "chars": 710,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`<HeaderLogged /> should render header logged correctly 1`] = `\n<div"
  },
  {
    "path": "dashboard/src/components/HeaderLogged/index.vue",
    "chars": 1648,
    "preview": "<template>\n  <div class=\"flex items-center justify-between w-4/5 max-w-6xl py-10\">\n    <div class=\"w-28 lg:w-36\">\n      "
  },
  {
    "path": "dashboard/src/components/Icon/ChevronDown.vue",
    "chars": 399,
    "preview": "<template>\n  <svg :width=\"size\" :height=\"size\" viewBox=\"0 0 17 10\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <"
  },
  {
    "path": "dashboard/src/components/Icon/Copy.vue",
    "chars": 487,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 19 22\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "dashboard/src/components/Icon/Loading.vue",
    "chars": 766,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 22 30\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "dashboard/src/components/Icon/index.vue",
    "chars": 319,
    "preview": "<template>\n  <component :is=\"name\" v-bind=\"$props\"/>\n</template>\n\n<script>\nimport Loading from './Loading.vue'\nimport Co"
  },
  {
    "path": "dashboard/src/components/ModalCreateAccount/index.vue",
    "chars": 5080,
    "preview": "<template>\n  <div class=\"flex justify-between\" id=\"modal-create-account\">\n    <h1 class=\"text-4xl font-black text-gray-8"
  },
  {
    "path": "dashboard/src/components/ModalFactory/index.vue",
    "chars": 1802,
    "preview": "<template>\n  <teleport to=\"body\">\n    <div\n      v-if=\"state.isActive\"\n      class=\"fixed top-0 left-0 z-50 flex items-c"
  },
  {
    "path": "dashboard/src/components/ModalLogin/index.vue",
    "chars": 4252,
    "preview": "<template>\n  <div class=\"flex justify-between\" id=\"modal-login\">\n    <h1 class=\"text-4xl font-black text-gray-800\">\n    "
  },
  {
    "path": "dashboard/src/hooks/useModal.js",
    "chars": 445,
    "preview": "import bus from '../utils/bus'\n\nconst EVENT_NAME = 'modal:toggle'\n\nexport default function useModal () {\n  function open"
  },
  {
    "path": "dashboard/src/hooks/useStore.js",
    "chars": 137,
    "preview": "import Store from '../store'\n\nexport default function useStore (module) {\n  if (module) {\n    return Store[module]\n  }\n\n"
  },
  {
    "path": "dashboard/src/main.js",
    "chars": 389,
    "preview": "import { createApp } from 'vue'\nimport Toast, { POSITION } from 'vue-toastification'\nimport App from './App.vue'\nimport "
  },
  {
    "path": "dashboard/src/router/index.js",
    "chars": 748,
    "preview": "import { createRouter, createWebHistory } from 'vue-router'\n\nconst Home = () => import('../views/Home/index.vue')\nconst "
  },
  {
    "path": "dashboard/src/services/__snapshots__/auth.spec.js.snap",
    "chars": 394,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`AuthService should return a token when user login 1`] = `\nObject {\n"
  },
  {
    "path": "dashboard/src/services/auth.js",
    "chars": 800,
    "preview": "export default httpClient => ({\n  register: async ({ name, email, password }) => {\n    const response = await httpClient"
  },
  {
    "path": "dashboard/src/services/auth.spec.js",
    "chars": 1531,
    "preview": "import mockAxios from 'axios'\nimport AuthService from './auth'\n\njest.mock('axios')\n\ndescribe('AuthService', () => {\n  af"
  },
  {
    "path": "dashboard/src/services/feedbacks.js",
    "chars": 486,
    "preview": "const defaultPagination = {\n  limit: 5,\n  offset: 0\n}\n\nexport default httpClient => ({\n  getAll: async ({ type, limit, o"
  },
  {
    "path": "dashboard/src/services/index.js",
    "chars": 1228,
    "preview": "import axios from 'axios'\nimport router from '../router'\nimport { setGlobalLoading } from '../store/global'\nimport AuthS"
  },
  {
    "path": "dashboard/src/services/users.js",
    "chars": 309,
    "preview": "export default httpClient => ({\n  getMe: async () => {\n    const response = await httpClient.get('/users/me')\n\n    retur"
  },
  {
    "path": "dashboard/src/store/global.js",
    "chars": 175,
    "preview": "import { reactive } from 'vue'\n\nconst state = reactive({\n  isLoading: false\n})\n\nexport default state\n\nexport function se"
  },
  {
    "path": "dashboard/src/store/index.js",
    "chars": 172,
    "preview": "import { readonly } from 'vue'\nimport UserModule from './user'\nimport GlobalModule from './global'\n\nexport default reado"
  },
  {
    "path": "dashboard/src/store/user.js",
    "chars": 482,
    "preview": "import { reactive } from 'vue'\n\nconst userInitialState = {\n  currentUser: {}\n}\n\nlet state = reactive(userInitialState)\n\n"
  },
  {
    "path": "dashboard/src/store/user.spec.js",
    "chars": 783,
    "preview": "import useStore from '../hooks/useStore'\nimport {\n  resetUserStore,\n  setApiKey,\n  cleanCurrentUser,\n  setCurrentUser\n} "
  },
  {
    "path": "dashboard/src/utils/bus.js",
    "chars": 65,
    "preview": "import Emitter from 'tiny-emitter'\n\nexport default new Emitter()\n"
  },
  {
    "path": "dashboard/src/utils/date.js",
    "chars": 1336,
    "preview": "export function getDiffTimeBetweenCurrentDate (dateString = '', now = new Date()) {\n  const dayInMilliseconds = 86400000"
  },
  {
    "path": "dashboard/src/utils/timeout.js",
    "chars": 105,
    "preview": "export function wait (timeMs) {\n  return new Promise(resolve => {\n    setTimeout(resolve, timeMs)\n  })\n}\n"
  },
  {
    "path": "dashboard/src/utils/validators.js",
    "chars": 487,
    "preview": "export function validateEmptyAndLength3 (value) {\n  if (!value) {\n    return '*Este campo é obrigatório'\n  }\n\n  if (valu"
  },
  {
    "path": "dashboard/src/utils/validators.spec.js",
    "chars": 966,
    "preview": "import {\n  validateEmptyAndEmail,\n  validateEmptyAndLength3\n} from './validators'\n\ndescribe('Validators utils', () => {\n"
  },
  {
    "path": "dashboard/src/views/Credencials/index.vue",
    "chars": 3992,
    "preview": "<template>\n  <div class=\"flex justify-center w-full h-28 bg-brand-main\">\n    <header-logged />\n  </div>\n\n  <div class=\"f"
  },
  {
    "path": "dashboard/src/views/Feedbacks/Filters.vue",
    "chars": 2592,
    "preview": "<template>\n  <div class=\"flex flex-col\">\n    <h1 class=\"text-2xl font-regular text-brand-darkgray\">\n      Filtros\n    </"
  },
  {
    "path": "dashboard/src/views/Feedbacks/FiltersLoading.vue",
    "chars": 777,
    "preview": "<template>\n  <content-loader\n    class=\"flex flex-col items-center rounded\"\n    width=\"100%\"\n    height=\"300px\"\n  >\n\n   "
  },
  {
    "path": "dashboard/src/views/Feedbacks/index.vue",
    "chars": 5019,
    "preview": "<template>\n  <div class=\"flex justify-center w-full h-28 bg-brand-main\">\n    <header-logged />\n  </div>\n\n  <div class=\"f"
  },
  {
    "path": "dashboard/src/views/Home/Contact.vue",
    "chars": 633,
    "preview": "<template>\n  <div class=\"flex justify-center w-full\">\n    <div class=\"flex flex-col items-center w-4/5 max-w-6xl my-16\">"
  },
  {
    "path": "dashboard/src/views/Home/CustomHeader.vue",
    "chars": 2081,
    "preview": "<template>\n  <header class=\"header\">\n    <div class=\"header-group\">\n      <div class=\"flex items-center justify-between "
  },
  {
    "path": "dashboard/src/views/Home/Home.spec.js",
    "chars": 525,
    "preview": "import Home from '.'\nimport { shallowMount } from '@vue/test-utils'\nimport { routes } from '../../router'\n\nimport { crea"
  },
  {
    "path": "dashboard/src/views/Home/__snapshots__/Home.spec.js.snap",
    "chars": 308,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`<Home /> should render home correctly 1`] = `\n<custom-header-stub><"
  },
  {
    "path": "dashboard/src/views/Home/index.vue",
    "chars": 1053,
    "preview": "<template>\n  <custom-header\n    @create-account=\"handleAccountCreate\"\n    @login=\"handleLogin\"\n  />\n  <contact />\n  <div"
  },
  {
    "path": "dashboard/tailwind.config.js",
    "chars": 22085,
    "preview": "const colors = require('tailwindcss/colors')\nconst palette = require('./palette')\n\nmodule.exports = {\n  purge: [\n    './"
  },
  {
    "path": "dashboard/tests/e2e/.eslintrc.js",
    "chars": 145,
    "preview": "module.exports = {\n  plugins: [\n    'cypress'\n  ],\n  env: {\n    mocha: true,\n    'cypress/globals': true\n  },\n  rules: {"
  },
  {
    "path": "dashboard/tests/e2e/plugins/index.js",
    "chars": 906,
    "preview": "/* eslint-disable arrow-body-style */\n// https://docs.cypress.io/guides/guides/plugins-guide.html\n\n// if you need a cust"
  },
  {
    "path": "dashboard/tests/e2e/specs/credencials.js",
    "chars": 660,
    "preview": "const APP_URL = process.env.APP_URL || 'http://localhost:8080'\n\ndescribe('Credencials', () => {\n  it('should generate an"
  },
  {
    "path": "dashboard/tests/e2e/specs/home.js",
    "chars": 1715,
    "preview": "const APP_URL = process.env.APP_URL || 'http://localhost:8080'\n\ndescribe('Home', () => {\n  it('should render create acco"
  },
  {
    "path": "dashboard/tests/e2e/support/commands.js",
    "chars": 841,
    "preview": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom"
  },
  {
    "path": "dashboard/tests/e2e/support/index.js",
    "chars": 670,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "try-widget/index.html",
    "chars": 832,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Minha loja de biscoitos</title>\n\n  <style>\n\n  "
  },
  {
    "path": "widget/.browserslistrc",
    "chars": 30,
    "preview": "> 1%\nlast 2 versions\nnot dead\n"
  },
  {
    "path": "widget/.editorconfig",
    "chars": 121,
    "preview": "[*.{js,jsx,ts,tsx,vue}]\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\ninsert_final_newline = true"
  },
  {
    "path": "widget/.eslintrc.js",
    "chars": 502,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    node: true\n  },\n  extends: [\n    'plugin:vue/vue3-essential',\n    '@vue/st"
  },
  {
    "path": "widget/.gitignore",
    "chars": 275,
    "preview": ".DS_Store\nnode_modules\n/dist\n\n/tests/e2e/videos/\n/tests/e2e/screenshots/\n\n\n# local env files\n.env.local\n.env.*.local\n\n# "
  },
  {
    "path": "widget/Dockerfile",
    "chars": 215,
    "preview": "FROM node:13-alpine as build\n\nWORKDIR /\n\nCOPY . .\n\nENV NODE_ENV=production\nRUN npm install --production\nRUN npm run buil"
  },
  {
    "path": "widget/README.md",
    "chars": 425,
    "preview": "# widget\n\n## Project setup\n```\nnpm install\n```\n\n### Compiles and hot-reloads for development\n```\nnpm run serve\n```\n\n### "
  },
  {
    "path": "widget/babel.config.js",
    "chars": 53,
    "preview": "module.exports = {\n  presets: [\n    '@vue/app'\n  ]\n}\n"
  },
  {
    "path": "widget/cypress.json",
    "chars": 50,
    "preview": "{\n  \"pluginsFile\": \"tests/e2e/plugins/index.js\"\n}\n"
  },
  {
    "path": "widget/jest.config.js",
    "chars": 200,
    "preview": "module.exports = {\n  preset: '@vue/cli-plugin-unit-jest/presets/typescript',\n  testMatch: [\n    '**/*.spec.js'\n  ],\n  tr"
  },
  {
    "path": "widget/package.json",
    "chars": 1646,
    "preview": "{\n  \"name\": \"widget\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    "
  },
  {
    "path": "widget/palette.js",
    "chars": 2392,
    "preview": "module.exports = {\n  brand: {\n    main: '#EF4983',\n    gray: '#F9F9F9',\n    info: '#8296FB',\n    success: '#63C3BE',\n   "
  },
  {
    "path": "widget/postcss.config.js",
    "chars": 80,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n}\n"
  },
  {
    "path": "widget/public/index.html",
    "chars": 611,
    "preview": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=ed"
  },
  {
    "path": "widget/public/init.js",
    "chars": 1602,
    "preview": "function init (apiKey) {\n  async function handleLoadWidget () {\n    const page = `${window.location.origin}${window.loca"
  },
  {
    "path": "widget/src/App.vue",
    "chars": 212,
    "preview": "<template>\n  <widget />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue'\nimport Widget from './view"
  },
  {
    "path": "widget/src/assets/css/fonts.css",
    "chars": 799,
    "preview": "@font-face {\n  font-family: \"RobotoRegular\";\n  src: local(\"Roboto Regular\"), local(\"Roboto-Regular\"),\n    url(\"../fonts/"
  },
  {
    "path": "widget/src/assets/css/tailwind.css",
    "chars": 127,
    "preview": "/*! @import */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody,\n#app {\n  width: 100%;\n  height: 1"
  },
  {
    "path": "widget/src/components/Icon/ArrowRight.vue",
    "chars": 464,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 13 5\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\""
  },
  {
    "path": "widget/src/components/Icon/Atention.vue",
    "chars": 635,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 52 52\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "widget/src/components/Icon/Chat.vue",
    "chars": 2774,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 30 28\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "widget/src/components/Icon/Check.vue",
    "chars": 528,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 43 43\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "widget/src/components/Icon/ChevronDown.vue",
    "chars": 399,
    "preview": "<template>\n  <svg :width=\"size\" :height=\"size\" viewBox=\"0 0 17 10\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <"
  },
  {
    "path": "widget/src/components/Icon/Close.vue",
    "chars": 466,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 8 8\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">"
  },
  {
    "path": "widget/src/components/Icon/Copy.vue",
    "chars": 487,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 19 22\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "widget/src/components/Icon/Loading.vue",
    "chars": 766,
    "preview": "<template>\n  <svg\n    :width=\"size\"\n    :height=\"size\" viewBox=\"0 0 22 30\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg"
  },
  {
    "path": "widget/src/components/Icon/index.vue",
    "chars": 569,
    "preview": "<template>\n  <component :is=\"name\" v-bind=\"$props\"/>\n</template>\n\n<script>\nimport Loading from './Loading.vue'\nimport Co"
  },
  {
    "path": "widget/src/components/Wizard/Error.vue",
    "chars": 1108,
    "preview": "<template>\n  <div class=\"flex flex-col items-center justify-between w-full my-5\">\n    <icon\n      name=\"atention\"\n      "
  },
  {
    "path": "widget/src/components/Wizard/SelectFeedbackType.vue",
    "chars": 1710,
    "preview": "<template>\n  <div class=\"flex justify-between w-full my-5\">\n    <button\n      @click=\"() => handleSelect('ISSUE')\"\n     "
  },
  {
    "path": "widget/src/components/Wizard/Success.vue",
    "chars": 1118,
    "preview": "<template>\n  <div class=\"flex flex-col items-center justify-between w-full my-5\">\n    <icon\n      name=\"check\"\n      :co"
  },
  {
    "path": "widget/src/components/Wizard/WriteAFeedback.vue",
    "chars": 2679,
    "preview": "<template>\n  <div class=\"felx flex-col items-center justify-center w-full my-5\">\n    <textarea\n      v-model=\"state.feed"
  },
  {
    "path": "widget/src/components/Wizard/index.vue",
    "chars": 1070,
    "preview": "<template>\n  <component\n    :is=\"store.currentComponent\"\n    @select-feedback-type=\"handleSelectFeedbackType\"\n    @next="
  },
  {
    "path": "widget/src/hooks/iframe.ts",
    "chars": 1131,
    "preview": "import {\n  setApiKey,\n  setCurrentPage,\n  setFingerprint\n} from '../store'\n\ninterface IframeControl {\n  updateCoreValues"
  },
  {
    "path": "widget/src/hooks/navigation.ts",
    "chars": 818,
    "preview": "import useStore from './store'\nimport {\n  setCurrentComponent,\n  setFeedbackType\n} from '../store'\n\nexport interface Nav"
  },
  {
    "path": "widget/src/hooks/store.ts",
    "chars": 113,
    "preview": "import Store, { StoreState } from '../store'\n\nexport default function useStore (): StoreState {\n  return Store\n}\n"
  },
  {
    "path": "widget/src/main.ts",
    "chars": 388,
    "preview": "import { createApp } from 'vue'\nimport Playground from './views/Playground/index.vue'\nimport App from './App.vue'\nimport"
  },
  {
    "path": "widget/src/services/feedbacks.ts",
    "chars": 993,
    "preview": "import { AxiosInstance } from 'axios'\nimport { Feedback } from '../types/feedback'\nimport { RequestError } from '../type"
  },
  {
    "path": "widget/src/services/index.ts",
    "chars": 625,
    "preview": "import axios from 'axios'\nimport FeedbacksService from './feedbacks'\n\nconst API_ENVS = {\n  production: 'https://backend-"
  },
  {
    "path": "widget/src/shims-vue.d.ts",
    "chars": 168,
    "preview": "/* eslint-disable */\ndeclare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n  const component: DefineComp"
  },
  {
    "path": "widget/src/store/index.ts",
    "chars": 1286,
    "preview": "import { reactive, readonly } from 'vue'\n\nexport type StoreState = {\n  currentComponent: string;\n  feedbackType: string;"
  },
  {
    "path": "widget/src/types/error.ts",
    "chars": 71,
    "preview": "export type RequestError = {\n  status: number;\n  statusText: string;\n}\n"
  },
  {
    "path": "widget/src/types/feedback.ts",
    "chars": 155,
    "preview": "export type Feedback = {\n  type: string;\n  text: string;\n  fingerprint: string;\n  device: string;\n  page: string;\n  apiK"
  },
  {
    "path": "widget/src/utils/bootstrap.ts",
    "chars": 258,
    "preview": "interface SetupPayload {\n  onProduction: () => void;\n  onDevelopment: () => void;\n}\nexport function setup ({ onProductio"
  },
  {
    "path": "widget/src/views/Playground/Playground.spec.js",
    "chars": 261,
    "preview": "import { shallowMount } from '@vue/test-utils'\nimport Playground from './index.vue'\n\ndescribe('<Playground />', () => {\n"
  },
  {
    "path": "widget/src/views/Playground/__snapshots__/Playground.spec.js.snap",
    "chars": 2991,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`<Playground /> should component render correctly 1`] = `\nVueWrapper"
  },
  {
    "path": "widget/src/views/Playground/index.vue",
    "chars": 945,
    "preview": "<template>\n  <div class=\"w-full h-3/4 flex flex-col justify-center items-center bg-brand-main\">\n    <h1 class=\"text-6xl "
  },
  {
    "path": "widget/src/views/Widget/Box.vue",
    "chars": 3099,
    "preview": "<template>\n  <div class=\"box animate__animated animate__fadeInUp animate__faster\">\n    <div\n      :class=\"{\n        'jus"
  },
  {
    "path": "widget/src/views/Widget/Standby.vue",
    "chars": 847,
    "preview": "<template>\n  <div\n    @click=\"() => emit('open-box')\"\n    id=\"widget-open-button\"\n    class=\"\n      fixed z-50 bottom-0 "
  },
  {
    "path": "widget/src/views/Widget/index.vue",
    "chars": 1163,
    "preview": "<template>\n  <teleport to=\"body\">\n    <component\n      @open-box=\"handleOpenBox\"\n      @close-box=\"handleCloseBox\"\n     "
  },
  {
    "path": "widget/tailwind.config.js",
    "chars": 22085,
    "preview": "const colors = require('tailwindcss/colors')\nconst palette = require('./palette')\n\nmodule.exports = {\n  purge: [\n    './"
  },
  {
    "path": "widget/tests/e2e/.eslintrc.js",
    "chars": 145,
    "preview": "module.exports = {\n  plugins: [\n    'cypress'\n  ],\n  env: {\n    mocha: true,\n    'cypress/globals': true\n  },\n  rules: {"
  },
  {
    "path": "widget/tests/e2e/plugins/index.js",
    "chars": 906,
    "preview": "/* eslint-disable arrow-body-style */\n// https://docs.cypress.io/guides/guides/plugins-guide.html\n\n// if you need a cust"
  },
  {
    "path": "widget/tests/e2e/specs/widget.js",
    "chars": 222,
    "preview": "const APP_URL = process.env.APP_URL || 'http://localhost:8080'\n\ndescribe('Widget', () => {\n  it('Check if widget button "
  },
  {
    "path": "widget/tests/e2e/support/commands.js",
    "chars": 841,
    "preview": "// ***********************************************\n// This example commands.js shows you how to\n// create various custom"
  },
  {
    "path": "widget/tests/e2e/support/index.js",
    "chars": 670,
    "preview": "// ***********************************************************\n// This example support/index.js is processed and\n// load"
  },
  {
    "path": "widget/tsconfig.json",
    "chars": 712,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"module\": \"esnext\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"im"
  }
]

About this extraction

This page contains the full source code of the vuejs-br/treinamento-vue3-code GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 159 files (184.4 KB), approximately 61.7k tokens, and a symbol index with 50 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!