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
================================================
================================================
FILE: conceitos/diretivas/App.vue
================================================
================================================
FILE: conceitos/eventos-e-metodos/App.vue
================================================
================================================
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
================================================
<%= htmlWebpackPlugin.options.title %>
================================================
FILE: conceitos/lifecycle-hooks/src/App.vue
================================================
Lifecycle hooks
================================================
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
================================================
<%= htmlWebpackPlugin.options.title %>
================================================
FILE: conceitos/nova-syntax-e-antiga/src/App.vue
================================================
================================================
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
================================================
Hello World
================================================
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
================================================
Feedbacker
================================================
FILE: dashboard/src/App.vue
================================================
================================================
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
================================================
================================================
FILE: dashboard/src/components/FeedbackCard/Badge.vue
================================================
{{ label }}
================================================
FILE: dashboard/src/components/FeedbackCard/Loading.vue
================================================
================================================
FILE: dashboard/src/components/FeedbackCard/index.vue
================================================
{{ getDiffTimeBetweenCurrentDate(feedback.createdAt) }}
{{ feedback.text }}
Página
{{ feedback.page }}
Dispositivo
{{ feedback.device }}
Usuário
{{ feedback.fingerprint }}
================================================
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('', () => {
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[` should render header logged correctly 1`] = `
- Credenciais
- Feedbacks
- ...
`;
================================================
FILE: dashboard/src/components/HeaderLogged/index.vue
================================================
- router.push({ name: 'Credencials' })"
class="px-6 py-2 mr-2 font-bold text-white rounded-full cursor-pointer focus:outline-none"
>
Credenciais
- router.push({ name: 'Feedbacks' })"
class="px-6 py-2 mr-2 font-bold text-white rounded-full cursor-pointer focus:outline-none"
>
Feedbacks
-
{{ logoutLabel }}
================================================
FILE: dashboard/src/components/Icon/ChevronDown.vue
================================================
================================================
FILE: dashboard/src/components/Icon/Copy.vue
================================================
================================================
FILE: dashboard/src/components/Icon/Loading.vue
================================================
================================================
FILE: dashboard/src/components/Icon/index.vue
================================================
================================================
FILE: dashboard/src/components/ModalCreateAccount/index.vue
================================================
Crie uma conta
================================================
FILE: dashboard/src/components/ModalFactory/index.vue
================================================
================================================
FILE: dashboard/src/components/ModalLogin/index.vue
================================================
Entre na sua conta
================================================
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
================================================
Credenciais
Guia de instalação e geração de suas credenciais
Instalação e configuração
Este aqui é a sua chave de api
Erro ao carregar a apikey
{{ store.User.currentUser.apiKey }}
Coloque o script abaixo no seu site para começar a receber feedbacks
================================================
FILE: dashboard/src/views/Feedbacks/Filters.vue
================================================
Filtros
- handleSelect(filter)"
class="flex items-center justify-between px-4 py-1 rounded cursor-pointer"
>
{{ filter.label }}
{{ filter.amount }}
================================================
FILE: dashboard/src/views/Feedbacks/FiltersLoading.vue
================================================
================================================
FILE: dashboard/src/views/Feedbacks/index.vue
================================================
Feedbacks
Detalhes de todos os feedbacks recebidos.
Listagem
Aconteceu um erro ao carregar os feedbacks 🥺
Ainda nenhum feedback recebido 🤓
================================================
FILE: dashboard/src/views/Home/Contact.vue
================================================
Alguma dúvida?
Quer saber melhor como funciona e quais são os preços?
================================================
FILE: dashboard/src/views/Home/CustomHeader.vue
================================================
================================================
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('', () => {
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[` should render home correctly 1`] = `
`;
================================================
FILE: dashboard/src/views/Home/index.vue
================================================
================================================
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
================================================
Minha loja de biscoitos
Meu site de venda de biscoitos
================================================
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
================================================
<%= htmlWebpackPlugin.options.title %>
================================================
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
================================================
================================================
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
================================================
================================================
FILE: widget/src/components/Icon/Atention.vue
================================================
================================================
FILE: widget/src/components/Icon/Chat.vue
================================================
================================================
FILE: widget/src/components/Icon/Check.vue
================================================
================================================
FILE: widget/src/components/Icon/ChevronDown.vue
================================================
================================================
FILE: widget/src/components/Icon/Close.vue
================================================
================================================
FILE: widget/src/components/Icon/Copy.vue
================================================
================================================
FILE: widget/src/components/Icon/Loading.vue
================================================
================================================
FILE: widget/src/components/Icon/index.vue
================================================
================================================
FILE: widget/src/components/Wizard/Error.vue
================================================
Droga! Aconteceu algum erro.
================================================
FILE: widget/src/components/Wizard/SelectFeedbackType.vue
================================================
================================================
FILE: widget/src/components/Wizard/Success.vue
================================================
Obrigado! Recebemos o seu feedback.
================================================
FILE: widget/src/components/Wizard/WriteAFeedback.vue
================================================
================================================
FILE: widget/src/components/Wizard/index.vue
================================================
================================================
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;
}
function FeedbacksService (httpClient: AxiosInstance): FeedbackServiceInterface {
async function create (payload: CreatePayload): Promise {
const response = await httpClient.post('/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({ ...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('', () => {
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[` should component render correctly 1`] = `
VueWrapper {
"__app": Object {
"_component": Object {
"__emits": Object {},
"__props": Array [
Object {},
Array [],
],
"name": "VTU_ROOT",
"render": [Function],
},
"_container":
Playground
Este é o playground, use para testar o widget.
🚀
👀
🚨
,
"_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
================================================
Playground
Este é o playground, use para testar o widget.
🚀
👀
🚨
================================================
FILE: widget/src/views/Widget/Box.vue
================================================
{{ label }}
widget by
feedbacker
================================================
FILE: widget/src/views/Widget/Standby.vue
================================================
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
">
Deixe um feedback
================================================
FILE: widget/src/views/Widget/index.vue
================================================
================================================
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',
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: widget/tests/e2e/.eslintrc.js
================================================
module.exports = {
plugins: [
'cypress'
],
env: {
mocha: true,
'cypress/globals': true
},
rules: {
strict: 'off'
}
}
================================================
FILE: widget/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: widget/tests/e2e/specs/widget.js
================================================
const APP_URL = process.env.APP_URL || 'http://localhost:8080'
describe('Widget', () => {
it('Check if widget button are shown', () => {
cy.visit(APP_URL)
cy.wait(2000)
cy.get('#widget-open-button')
})
})
================================================
FILE: widget/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: widget/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: widget/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": false,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}