initial commit
181
background.js
Normal file
@ -0,0 +1,181 @@
|
||||
let currentPostData = {};
|
||||
let authToken = null;
|
||||
let negocios_id = null;
|
||||
let negocios_nome = null;
|
||||
let user_id = null;
|
||||
let expires = null;
|
||||
|
||||
// URL base da API do Bubble no Launchr
|
||||
const API_URL = 'https://launchr.com.br/version-test/api/1.1/wf';
|
||||
|
||||
// Recuperar o token e informações armazenadas ao iniciar a extensão
|
||||
chrome.storage.local.get(['authToken', 'negocios_id', 'negocios_nome', 'user_id', 'expires'], (result) => {
|
||||
if (result.authToken) {
|
||||
authToken = result.authToken;
|
||||
negocios_id = result.negocios_id;
|
||||
negocios_nome = result.negocios_nome;
|
||||
user_id = result.user_id;
|
||||
expires = result.expires;
|
||||
}
|
||||
});
|
||||
|
||||
// Função para lidar com erros de autenticação
|
||||
function handleAuthError() {
|
||||
// Remover o token e informações armazenadas
|
||||
authToken = null;
|
||||
chrome.storage.local.remove(['authToken', 'negocios_id', 'negocios_nome', 'user_id', 'expires'], () => {
|
||||
console.log('Token de autenticação removido.');
|
||||
// Opcional: Notificar o usuário sobre a necessidade de login
|
||||
});
|
||||
}
|
||||
|
||||
// Listener para mensagens
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'storePostData') {
|
||||
currentPostData = request.data || {};
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === 'getPostData') {
|
||||
sendResponse({ data: currentPostData });
|
||||
} else if (request.action === 'savePost') {
|
||||
const postDataToSave = request.data;
|
||||
let responseSent = false;
|
||||
|
||||
if (!postDataToSave) {
|
||||
sendResponse({ success: false, error: 'Dados do post não fornecidos.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authToken) {
|
||||
handleAuthError();
|
||||
sendResponse({ success: false, error: 'Usuário não autenticado. Faça login novamente.' });
|
||||
return true;
|
||||
}
|
||||
|
||||
const dataToSend = {
|
||||
content: postDataToSave.content,
|
||||
author: postDataToSave.author,
|
||||
authorProfileLink: postDataToSave.authorProfileLink,
|
||||
date: postDataToSave.date,
|
||||
postLink: postDataToSave.postLink,
|
||||
category: postDataToSave.category,
|
||||
user_id: user_id,
|
||||
negocios_id: negocios_id
|
||||
};
|
||||
|
||||
fetch(`${API_URL}/novo_swipe_file`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': authToken
|
||||
},
|
||||
body: JSON.stringify(dataToSend)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else if (response.status === 401) {
|
||||
handleAuthError();
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: false, error: 'Token expirado ou inválido. Faça login novamente.' });
|
||||
responseSent = true;
|
||||
}
|
||||
throw new Error('Token inválido ou expirado.');
|
||||
} else {
|
||||
return response.json().then(data => {
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: false, error: data.message || 'Erro ao salvar o post.' });
|
||||
responseSent = true;
|
||||
}
|
||||
throw new Error(data.message || 'Erro na requisição.');
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Post salvo com sucesso:', data);
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: true });
|
||||
responseSent = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erro ao salvar o post:', error);
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: false, error: 'Erro ao salvar o post.' });
|
||||
responseSent = true;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} else if (request.action === 'login') {
|
||||
console.log('Iniciando o processo de login...');
|
||||
const { email, password } = request.data || {};
|
||||
let responseSent = false;
|
||||
|
||||
if (!email || !password) {
|
||||
sendResponse({ success: false, error: 'E-mail ou senha não fornecidos.' });
|
||||
console.log('Email ou senha não fornecidos');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${API_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
password: password
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: false, error: 'Erro na autenticação. Verifique suas credenciais.' });
|
||||
responseSent = true;
|
||||
}
|
||||
throw new Error('Erro na autenticação.');
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.response && data.response.token) {
|
||||
authToken = 'Bearer ' + data.response.token;
|
||||
negocios_id = data.response.negocios_id;
|
||||
negocios_nome = data.response.negocios_nome;
|
||||
user_id = data.response.user_id;
|
||||
expires = data.response.expires;
|
||||
|
||||
chrome.storage.local.set({ authToken, negocios_id, negocios_nome, user_id, expires }, () => {
|
||||
console.log('Usuário autenticado com sucesso.');
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: true });
|
||||
responseSent = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Erro na autenticação:', data);
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: false, error: 'Falha na autenticação. Verifique suas credenciais.' });
|
||||
responseSent = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erro na autenticação:', error);
|
||||
if (!responseSent) {
|
||||
sendResponse({ success: false, error: 'Erro ao realizar o login. Por favor, tente novamente.' });
|
||||
responseSent = true;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
} else if (request.action === 'logout') {
|
||||
authToken = null;
|
||||
chrome.storage.local.remove(['authToken', 'negocios_id', 'negocios_nome', 'user_id', 'expires'], () => {
|
||||
console.log('Usuário deslogado com sucesso.');
|
||||
sendResponse({ success: true });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
60
content.css
Normal file
@ -0,0 +1,60 @@
|
||||
/* Estilos do botão */
|
||||
.launchr-save-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #4F46E5;
|
||||
color: white;
|
||||
border: none;
|
||||
margin-left: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.launchr-save-button:hover:not(:disabled) {
|
||||
background-color: #4338CA;
|
||||
}
|
||||
|
||||
.launchr-save-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.launchr-save-button.loading {
|
||||
background-color: #6B7280;
|
||||
}
|
||||
|
||||
.launchr-save-button.success {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.launchr-save-button.error {
|
||||
background-color: #EF4444;
|
||||
}
|
||||
|
||||
.launchr-save-button.auth {
|
||||
background-color: #F59E0B;
|
||||
}
|
||||
|
||||
/* Animação de loading */
|
||||
.launchr-save-button.loading::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
74
content.js
Normal file
@ -0,0 +1,74 @@
|
||||
// Função para extrair dados do post atual
|
||||
function extractPostData() {
|
||||
// Selecionar o post atual
|
||||
const post = document.querySelector('.occludable-update');
|
||||
|
||||
if (!post) {
|
||||
console.error('Post não encontrado.');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extrair informações do post
|
||||
const contentElement = post.querySelector('.feed-shared-update-v2__description, .break-words, [data-test-id="feed-update-message"]');
|
||||
const authorElement = post.querySelector('.feed-shared-actor__name, .update-components-actor__name');
|
||||
const dateElement = post.querySelector('span.feed-shared-actor__sub-description span[aria-hidden="true"], span.update-components-actor__sub-description time');
|
||||
|
||||
const postContent = contentElement ? contentElement.innerText.trim() : '';
|
||||
const postAuthor = authorElement ? authorElement.innerText.trim() : '';
|
||||
|
||||
// Obter o link do perfil do autor
|
||||
const authorProfileLinkElement = authorElement ? authorElement.closest('a') : null;
|
||||
const baseUrl = 'https://www.linkedin.com';
|
||||
const authorProfileLink = authorProfileLinkElement ? new URL(authorProfileLinkElement.getAttribute('href'), baseUrl).href : '';
|
||||
|
||||
// Obter a data do post
|
||||
const postDate = dateElement ? dateElement.innerText.trim() : '';
|
||||
|
||||
// Obter o link do post
|
||||
const postLinkElement = post.querySelector('a[href*="/feed/update/"], a[data-control-name="share_link"], a[href*="/posts/"]');
|
||||
const postLink = postLinkElement ? postLinkElement.href : window.location.href;
|
||||
|
||||
const postData = {
|
||||
content: postContent,
|
||||
author: postAuthor,
|
||||
authorProfileLink: authorProfileLink,
|
||||
date: postDate,
|
||||
postLink: postLink
|
||||
};
|
||||
|
||||
return postData;
|
||||
}
|
||||
|
||||
// Adicionar um botão na página para salvar o post
|
||||
function addSavePostButton() {
|
||||
const button = document.createElement('button');
|
||||
button.innerText = 'Salvar Post';
|
||||
button.id = 'savePostButton';
|
||||
button.style.position = 'fixed';
|
||||
button.style.top = '10px';
|
||||
button.style.right = '10px';
|
||||
button.style.zIndex = '9999';
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
const postData = extractPostData();
|
||||
|
||||
if (postData) {
|
||||
// Enviar os dados do post para o background script
|
||||
chrome.runtime.sendMessage({ action: 'storePostData', data: postData }, (response) => {
|
||||
if (response && response.success) {
|
||||
console.log('Dados do post enviados para o background script.');
|
||||
alert('Dados do post prontos para serem salvos. Clique no ícone da extensão para prosseguir.');
|
||||
} else {
|
||||
console.error('Erro ao enviar dados do post:', response.error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Não foi possível extrair os dados do post.');
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
// Chamar a função para adicionar o botão
|
||||
addSavePostButton();
|
||||
BIN
icon128.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
icon16.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
icon32.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
icon48.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
images/icon128.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
images/icon16.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
images/icon32.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
images/icon48.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
images/logo.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
12
index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Launchr</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/popup/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
manifest.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Sua Extensão",
|
||||
"version": "1.0",
|
||||
"description": "Extensão para salvar posts do LinkedIn e gerar mensagens personalizadas.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"tabs",
|
||||
"https://launchr.com.br/*",
|
||||
"https://api.openai.com/*"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": false
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Minha Extensão",
|
||||
"default_icon": {
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png",
|
||||
"128": "icon128.png"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://www.linkedin.com/*"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png",
|
||||
"128": "icon128.png"
|
||||
}
|
||||
}
|
||||
24
menu.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Login</h2>
|
||||
<form id="loginForm">
|
||||
<label for="emailInput">E-mail:</label>
|
||||
<input type="email" id="emailInput" name="email" required>
|
||||
|
||||
<label for="passwordInput">Senha:</label>
|
||||
<input type="password" id="passwordInput" name="password" required>
|
||||
|
||||
<button type="submit">Entrar</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="menu.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
menu.js
Normal file
@ -0,0 +1,23 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
||||
loginForm.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const email = document.getElementById('emailInput').value.trim();
|
||||
const password = document.getElementById('passwordInput').value.trim();
|
||||
|
||||
if (email && password) {
|
||||
chrome.runtime.sendMessage({ action: 'login', data: { email: email, password: password } }, function(response) {
|
||||
if (response && response.success) {
|
||||
alert('Login realizado com sucesso!');
|
||||
window.close();
|
||||
} else {
|
||||
alert('Falha no login: ' + (response.error || 'Erro desconhecido.'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Por favor, preencha os campos de e-mail e senha.');
|
||||
}
|
||||
});
|
||||
});
|
||||
8
package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"watch": "vite build --watch"
|
||||
}
|
||||
}
|
||||
53
popup.html
Normal file
@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Salvar Post</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Salvar Post</h2>
|
||||
<form id="postForm">
|
||||
<label for="author">Autor:</label>
|
||||
<input type="text" id="author" name="author" readonly>
|
||||
|
||||
<label for="authorProfileLink">Link do Perfil do Autor:</label>
|
||||
<input type="text" id="authorProfileLink" name="authorProfileLink" readonly>
|
||||
|
||||
<label for="postLink">Link do Post:</label>
|
||||
<input type="text" id="postLink" name="postLink" readonly>
|
||||
|
||||
<label for="date">Data:</label>
|
||||
<input type="text" id="date" name="date" readonly>
|
||||
|
||||
<label for="content">Conteúdo:</label>
|
||||
<textarea id="content" name="content" rows="4" readonly></textarea>
|
||||
|
||||
<label for="category">Categoria:</label>
|
||||
<select id="category" name="category">
|
||||
<option value="">Selecione uma categoria</option>
|
||||
<option value="Marketing">Marketing</option>
|
||||
<option value="Vendas">Vendas</option>
|
||||
<option value="Tecnologia">Tecnologia</option>
|
||||
<!-- Adicione outras categorias conforme necessário -->
|
||||
</select>
|
||||
|
||||
<button type="button" id="saveButton">Salvar</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Popup de confirmação -->
|
||||
<div id="successPopup" class="popup" style="display: none;">
|
||||
<div class="popup-content">
|
||||
<p>O post foi enviado com sucesso!</p>
|
||||
<div class="popup-buttons">
|
||||
<button id="cancelButton">Fechar</button>
|
||||
<button id="viewButton">Visualizar Swipe File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
130
popup.js
Normal file
@ -0,0 +1,130 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Recuperar os dados do post do background script
|
||||
chrome.runtime.sendMessage({ action: 'getPostData' }, (response) => {
|
||||
if (response && response.data) {
|
||||
const postData = response.data;
|
||||
|
||||
// Preencher os campos com os dados do post
|
||||
document.getElementById('author').value = postData.author || '';
|
||||
document.getElementById('authorProfileLink').value = postData.authorProfileLink || '';
|
||||
document.getElementById('postLink').value = postData.postLink || '';
|
||||
document.getElementById('date').value = postData.date || '';
|
||||
document.getElementById('content').value = postData.content || '';
|
||||
} else {
|
||||
console.log('Nenhum dado do post disponível.');
|
||||
}
|
||||
});
|
||||
|
||||
// Referência ao popup e aos botões
|
||||
const successPopup = document.getElementById('successPopup');
|
||||
const cancelButton = document.getElementById('cancelButton');
|
||||
const viewButton = document.getElementById('viewButton');
|
||||
|
||||
let negocios_nome = '';
|
||||
let negocios_id = '';
|
||||
|
||||
// Recuperar negocios_nome e negocios_id do storage
|
||||
chrome.storage.local.get(['negocios_nome', 'negocios_id'], (result) => {
|
||||
if (result.negocios_nome && result.negocios_id) {
|
||||
negocios_nome = result.negocios_nome;
|
||||
negocios_id = result.negocios_id;
|
||||
} else {
|
||||
console.error('Não foi possível obter as informações do negócio.');
|
||||
}
|
||||
});
|
||||
|
||||
// Função para exibir o popup de sucesso
|
||||
function showSuccessPopup() {
|
||||
successPopup.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Evento para o botão de cancelar e fechar o popup
|
||||
cancelButton.addEventListener('click', () => {
|
||||
successPopup.style.display = 'none';
|
||||
// Fechar o popup principal
|
||||
window.close();
|
||||
});
|
||||
|
||||
// Evento para o botão de visualizar o Swipe File
|
||||
viewButton.addEventListener('click', () => {
|
||||
if (negocios_nome && negocios_id) {
|
||||
// Construir o link para visualizar o Swipe File
|
||||
const baseUrl = 'https://launchr.com.br';
|
||||
const encodedNegociosNome = encodeURIComponent(negocios_nome);
|
||||
const swipeFileUrl = `${baseUrl}/posts/${encodedNegociosNome}-${negocios_id}?tab=Swipe%20File`;
|
||||
|
||||
// Abrir o link em uma nova aba
|
||||
chrome.tabs.create({ url: swipeFileUrl }, () => {
|
||||
// Fechar o popup após abrir a nova aba
|
||||
window.close();
|
||||
});
|
||||
} else {
|
||||
alert('Informações do negócio não disponíveis.');
|
||||
}
|
||||
});
|
||||
|
||||
// Lidar com o clique no botão de salvar
|
||||
const saveButton = document.getElementById('saveButton');
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', () => {
|
||||
try {
|
||||
// Obter dados atualizados do formulário
|
||||
const updatedData = {
|
||||
author: document.getElementById('author')?.value || '',
|
||||
authorProfileLink: document.getElementById('authorProfileLink')?.value || '',
|
||||
postLink: document.getElementById('postLink')?.value || '',
|
||||
date: document.getElementById('date')?.value || '',
|
||||
content: document.getElementById('content')?.value || '',
|
||||
category: document.getElementById('category')?.value || ''
|
||||
};
|
||||
|
||||
if (!updatedData.category) {
|
||||
alert('Por favor, selecione uma categoria.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Desabilitar o botão para evitar múltiplos cliques
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerText = 'Salvando...';
|
||||
|
||||
// Enviar dados para o background script para salvar
|
||||
chrome.runtime.sendMessage({ action: 'savePost', data: updatedData }, (response) => {
|
||||
if (response && response.success) {
|
||||
// Exibir o popup de sucesso
|
||||
showSuccessPopup();
|
||||
} else {
|
||||
// Exibir mensagem de erro
|
||||
alert(response.error || 'Houve um erro ao enviar o post. Por favor, tente novamente.');
|
||||
// Reativar o botão de salvar
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerText = 'Salvar';
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao preparar os dados para salvar:', error);
|
||||
alert('Ocorreu um erro ao preparar os dados. Por favor, tente novamente.');
|
||||
// Reativar o botão de salvar
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerText = 'Salvar';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Elemento saveButton não encontrado.');
|
||||
}
|
||||
|
||||
// Lidar com o clique no botão de logout (se aplicável)
|
||||
const logoutButton = document.getElementById('logoutButton');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
if (confirm('Você realmente deseja fazer logout?')) {
|
||||
chrome.runtime.sendMessage({ action: 'logout' }, (response) => {
|
||||
if (response && response.success) {
|
||||
window.close();
|
||||
} else {
|
||||
alert('Erro ao realizar logout.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
34
scripts/copy-assets.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { copyFileSync, mkdirSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const distDir = resolve(__dirname, '../dist');
|
||||
const assetsDir = resolve(distDir, 'src/assets');
|
||||
|
||||
// Criar diretórios necessários
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
mkdirSync(assetsDir, { recursive: true });
|
||||
|
||||
// Lista de arquivos para copiar
|
||||
const files = [
|
||||
{ src: '../manifest.json', dest: '../dist/manifest.json' },
|
||||
{ src: '../src/assets/icon16.png', dest: '../dist/src/assets/icon16.png' },
|
||||
{ src: '../src/assets/icon32.png', dest: '../dist/src/assets/icon32.png' },
|
||||
{ src: '../src/assets/icon48.png', dest: '../dist/src/assets/icon48.png' },
|
||||
{ src: '../src/assets/icon128.png', dest: '../dist/src/assets/icon128.png' },
|
||||
{ src: '../src/content/index.css', dest: '../dist/content.css' }
|
||||
];
|
||||
|
||||
// Copiar arquivos
|
||||
files.forEach(file => {
|
||||
try {
|
||||
copyFileSync(
|
||||
resolve(__dirname, file.src),
|
||||
resolve(__dirname, file.dest)
|
||||
);
|
||||
console.log(`Copiado: ${file.src} -> ${file.dest}`);
|
||||
} catch (err) {
|
||||
console.error(`Erro ao copiar ${file.src}:`, err);
|
||||
}
|
||||
});
|
||||
19
src/assets/createDefaultIcon.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import sharp from 'sharp';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// Criar um ícone SVG simples
|
||||
const svgIcon = `
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" fill="#4F46E5"/>
|
||||
<text x="64" y="64" font-family="Arial" font-size="80" fill="white" text-anchor="middle" dominant-baseline="middle">
|
||||
L
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Salvar como PNG
|
||||
sharp(Buffer.from(svgIcon))
|
||||
.png()
|
||||
.toFile(resolve(__dirname, 'icon.png'))
|
||||
.then(() => console.log('Ícone padrão criado'))
|
||||
.catch(err => console.error('Erro ao criar ícone padrão:', err));
|
||||
19
src/assets/generateIcons.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import sharp from 'sharp';
|
||||
import { mkdirSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const sizes = [16, 32, 48, 128];
|
||||
const inputIcon = resolve(__dirname, 'icon.png');
|
||||
const outputDir = resolve(__dirname, '../../dist/src/assets');
|
||||
|
||||
// Criar diretório de saída
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// Gerar ícones em diferentes tamanhos
|
||||
sizes.forEach(size => {
|
||||
sharp(inputIcon)
|
||||
.resize(size, size)
|
||||
.toFile(resolve(outputDir, `icon${size}.png`))
|
||||
.then(() => console.log(`Ícone ${size}x${size} gerado com sucesso`))
|
||||
.catch(err => console.error(`Erro ao gerar ícone ${size}x${size}:`, err));
|
||||
});
|
||||
BIN
src/assets/icon128.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/assets/icon16.png
Normal file
|
After Width: | Height: | Size: 229 B |
BIN
src/assets/icon32.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
src/assets/icon48.png
Normal file
|
After Width: | Height: | Size: 742 B |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
123
src/background/index.ts
Normal file
@ -0,0 +1,123 @@
|
||||
let currentPostData = {};
|
||||
let authToken: string | null = null;
|
||||
let negocios_id: string | null = null;
|
||||
let negocios_nome: string | null = null;
|
||||
let user_id: string | null = null;
|
||||
|
||||
// URL base da API do Bubble no Launchr
|
||||
const API_URL = 'https://launchr.com.br/api/1.1/wf';
|
||||
|
||||
// Variável para controlar a janela de login
|
||||
let loginWindowId: number | null = null;
|
||||
|
||||
// Função para verificar autenticação
|
||||
const checkAuth = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get(['authToken'], (result) => {
|
||||
authToken = result.authToken;
|
||||
resolve(!!authToken);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Função para abrir popup de login
|
||||
const openLoginPopup = () => {
|
||||
if (loginWindowId === null) {
|
||||
chrome.windows.create({
|
||||
url: chrome.runtime.getURL('index.html#login'),
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
left: Math.round((screen.width - 400) / 2),
|
||||
top: Math.round((screen.height - 600) / 2),
|
||||
focused: true
|
||||
}, (window) => {
|
||||
if (window) {
|
||||
loginWindowId = window.id;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
chrome.windows.update(loginWindowId, { focused: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Listener para quando a janela de login é fechada
|
||||
chrome.windows.onRemoved.addListener((windowId) => {
|
||||
if (windowId === loginWindowId) {
|
||||
loginWindowId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Listener para mensagens do content script e popup
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.type === 'SAVE_POST' || request.type === 'SAVE_IDEA') {
|
||||
checkAuth().then(isAuthenticated => {
|
||||
if (!isAuthenticated) {
|
||||
openLoginPopup();
|
||||
sendResponse({ success: false, error: 'AUTH_REQUIRED' });
|
||||
} else {
|
||||
// Implementar lógica de salvamento
|
||||
handleSaveRequest(request, sendResponse);
|
||||
}
|
||||
});
|
||||
return true; // Indica que a resposta será assíncrona
|
||||
}
|
||||
|
||||
if (request.type === 'CHECK_AUTH') {
|
||||
checkAuth().then(isAuthenticated => {
|
||||
sendResponse({ isAuthenticated });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Listener para o clique no ícone da extensão
|
||||
chrome.action.onClicked.addListener(async () => {
|
||||
const isAuthenticated = await checkAuth();
|
||||
if (!isAuthenticated) {
|
||||
openLoginPopup();
|
||||
} else {
|
||||
// Abrir popup principal da extensão
|
||||
chrome.windows.create({
|
||||
url: chrome.runtime.getURL('index.html'),
|
||||
type: 'popup',
|
||||
width: 300,
|
||||
height: 500,
|
||||
left: Math.round((screen.width - 300) / 2),
|
||||
top: Math.round((screen.height - 500) / 2)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Função para lidar com as requisições de salvamento
|
||||
const handleSaveRequest = async (request: any, sendResponse: (response: any) => void) => {
|
||||
try {
|
||||
const endpoint = request.type === 'SAVE_IDEA' ? 'nova_ideia' : 'novo_swipe_file';
|
||||
const response = await fetch(`${API_URL}/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': authToken || ''
|
||||
},
|
||||
body: JSON.stringify(request.data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// Token inválido ou expirado
|
||||
chrome.storage.local.remove(['authToken']);
|
||||
authToken = null;
|
||||
openLoginPopup();
|
||||
sendResponse({ success: false, error: 'AUTH_REQUIRED' });
|
||||
return;
|
||||
}
|
||||
throw new Error('Erro na requisição');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
sendResponse({ success: true, data });
|
||||
} catch (error) {
|
||||
console.error('Erro:', error);
|
||||
sendResponse({ success: false, error: 'Erro ao processar requisição' });
|
||||
}
|
||||
};
|
||||
97
src/components/LoginForm.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setAuth } from '../store/slices/authSlice';
|
||||
|
||||
const LoginForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://launchr.com.br/api/1.1/wf/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authToken) {
|
||||
// Salvar no chrome.storage
|
||||
chrome.storage.local.set({
|
||||
authToken: data.authToken,
|
||||
negocios_id: data.negocios_id,
|
||||
negocios_nome: data.negocios_nome,
|
||||
user_id: data.user_id,
|
||||
});
|
||||
|
||||
// Atualizar estado
|
||||
dispatch(setAuth({
|
||||
authToken: data.authToken,
|
||||
negocios_id: data.negocios_id,
|
||||
negocios_nome: data.negocios_nome,
|
||||
user_id: data.user_id,
|
||||
isAuthenticated: true,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro no login:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Faça o seu login</h2>
|
||||
<h4 className="text-gray-600 mb-6">Preencha suas informações e faça o login no Launchr</h4>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Senha
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Entrando...' : 'Entrar'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
53
src/components/Menu.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RootState } from '../store';
|
||||
import { setAuth } from '../store/slices/authSlice';
|
||||
import LoginForm from './LoginForm';
|
||||
import Dashboard from './Dashboard';
|
||||
|
||||
const Menu = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
|
||||
|
||||
useEffect(() => {
|
||||
// Verificar autenticação ao carregar
|
||||
chrome.storage.local.get(
|
||||
['authToken', 'negocios_id', 'negocios_nome', 'user_id'],
|
||||
(result) => {
|
||||
if (result.authToken) {
|
||||
dispatch(setAuth({
|
||||
authToken: result.authToken,
|
||||
negocios_id: result.negocios_id,
|
||||
negocios_nome: result.negocios_nome,
|
||||
user_id: result.user_id,
|
||||
isAuthenticated: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<nav className="bg-white shadow-sm fixed w-full top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<img
|
||||
src="/assets/logo.png"
|
||||
alt="Launchr Logo"
|
||||
className="h-10 w-auto self-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="mt-16 p-4">
|
||||
{isAuthenticated ? <Dashboard /> : <LoginForm />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
59
src/content/components/SaveButton.css
Normal file
@ -0,0 +1,59 @@
|
||||
.launchr-save-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #4F46E5;
|
||||
color: white;
|
||||
border: none;
|
||||
margin-left: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.launchr-save-button:hover:not(:disabled) {
|
||||
background-color: #4338CA;
|
||||
}
|
||||
|
||||
.launchr-save-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.launchr-save-button.loading {
|
||||
background-color: #6B7280;
|
||||
}
|
||||
|
||||
.launchr-save-button.success {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.launchr-save-button.error {
|
||||
background-color: #EF4444;
|
||||
}
|
||||
|
||||
.launchr-save-button.auth {
|
||||
background-color: #F59E0B;
|
||||
}
|
||||
|
||||
/* Animação de loading */
|
||||
.launchr-save-button.loading::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
60
src/content/components/SaveButton.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface SaveButtonProps {
|
||||
onSave: () => Promise<{ success: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
const SaveButton = ({ onSave }: SaveButtonProps) => {
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error' | 'auth'>('idle');
|
||||
|
||||
const handleClick = async () => {
|
||||
setStatus('loading');
|
||||
try {
|
||||
const result = await onSave();
|
||||
|
||||
if (result.error === 'AUTH_REQUIRED') {
|
||||
setStatus('auth');
|
||||
} else if (result.success) {
|
||||
setStatus('success');
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setStatus('idle');
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
setStatus('error');
|
||||
setTimeout(() => {
|
||||
setStatus('idle');
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonText = () => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return 'Salvando...';
|
||||
case 'success':
|
||||
return '✓ Salvo!';
|
||||
case 'error':
|
||||
return '✕ Erro ao salvar';
|
||||
case 'auth':
|
||||
return 'Faça login para salvar';
|
||||
default:
|
||||
return 'Salvar no Launchr';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={status !== 'idle'}
|
||||
className={`launchr-save-button ${status}`}
|
||||
>
|
||||
{getButtonText()}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveButton;
|
||||
67
src/content/index.css
Normal file
@ -0,0 +1,67 @@
|
||||
/* Estilos do botão */
|
||||
.launchr-save-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #4F46E5;
|
||||
color: white;
|
||||
border: none;
|
||||
margin-left: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.launchr-save-button:hover:not(:disabled) {
|
||||
background-color: #4338CA;
|
||||
}
|
||||
|
||||
.launchr-save-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.launchr-save-button.loading {
|
||||
background-color: #6B7280;
|
||||
}
|
||||
|
||||
.launchr-save-button.success {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.launchr-save-button.error {
|
||||
background-color: #EF4444;
|
||||
}
|
||||
|
||||
.launchr-save-button.auth {
|
||||
background-color: #F59E0B;
|
||||
}
|
||||
|
||||
/* Animação de loading */
|
||||
.launchr-save-button.loading::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Container do botão */
|
||||
.launchr-save-button-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
0
src/content/index.js
Normal file
72
src/content/index.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import SaveButton from './components/SaveButton';
|
||||
import './index.css';
|
||||
|
||||
function addSaveButtonToPost(post: Element) {
|
||||
if (post.querySelector('.launchr-save-button-container')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Criar container para o botão
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'launchr-save-button-container';
|
||||
|
||||
// Criar root para o React
|
||||
const root = createRoot(buttonContainer);
|
||||
|
||||
// Renderizar o componente SaveButton
|
||||
root.render(
|
||||
<SaveButton
|
||||
onSave={async () => {
|
||||
const contentElement = post.querySelector('.feed-shared-update-v2__description, .break-words');
|
||||
const authorElement = post.querySelector('.feed-shared-actor__name, .update-components-actor__name');
|
||||
const dateElement = post.querySelector('span.feed-shared-actor__sub-description span[aria-hidden="true"], span.update-components-actor__sub-description time');
|
||||
const postLinkElement = post.querySelector('a[href*="/feed/update/"], a[data-control-name="share_link"], a[href*="/posts/"]');
|
||||
|
||||
const content = contentElement?.textContent?.trim() || '';
|
||||
const author = authorElement?.textContent?.trim() || '';
|
||||
const date = dateElement?.textContent?.trim() || '';
|
||||
const postLink = postLinkElement?.getAttribute('href') || window.location.href;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'SAVE_POST',
|
||||
data: {
|
||||
content,
|
||||
author,
|
||||
date,
|
||||
postLink
|
||||
}
|
||||
}, (response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Encontrar onde inserir o botão
|
||||
const actionsContainer = post.querySelector('.feed-shared-social-actions, .social-details-social-counts');
|
||||
if (actionsContainer) {
|
||||
actionsContainer.appendChild(buttonContainer);
|
||||
} else {
|
||||
post.appendChild(buttonContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// Observer para detectar novos posts
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node instanceof Element && node.matches('[data-id]')) {
|
||||
addSaveButtonToPost(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Iniciar observação
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
67
src/content/style.css
Normal file
@ -0,0 +1,67 @@
|
||||
/* Estilos do botão de salvar */
|
||||
.launchr-save-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background-color: #4F46E5;
|
||||
color: white;
|
||||
border: none;
|
||||
margin-left: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.launchr-save-button:hover:not(:disabled) {
|
||||
background-color: #4338CA;
|
||||
}
|
||||
|
||||
.launchr-save-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.launchr-save-button.loading {
|
||||
background-color: #6B7280;
|
||||
}
|
||||
|
||||
.launchr-save-button.success {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.launchr-save-button.error {
|
||||
background-color: #EF4444;
|
||||
}
|
||||
|
||||
.launchr-save-button.auth {
|
||||
background-color: #F59E0B;
|
||||
}
|
||||
|
||||
/* Animação de loading */
|
||||
.launchr-save-button.loading::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Container do botão */
|
||||
.launchr-save-button-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
3
src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
44
src/popup/Router.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import LoginForm from '../components/LoginForm';
|
||||
import Dashboard from '../components/Dashboard';
|
||||
import { setAuth } from '../store/slices/authSlice';
|
||||
|
||||
const Router = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
chrome.runtime.sendMessage({ type: 'CHECK_AUTH' }, (response) => {
|
||||
if (response.isAuthenticated) {
|
||||
chrome.storage.local.get(
|
||||
['authToken', 'negocios_id', 'negocios_nome', 'user_id'],
|
||||
(result) => {
|
||||
dispatch(setAuth({
|
||||
authToken: result.authToken,
|
||||
negocios_id: result.negocios_id,
|
||||
negocios_nome: result.negocios_nome,
|
||||
user_id: result.user_id,
|
||||
isAuthenticated: true,
|
||||
}));
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return isAuthenticated ? <Dashboard /> : <LoginForm />;
|
||||
};
|
||||
|
||||
export default Router;
|
||||
14
src/popup/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from '../store';
|
||||
import Router from './Router';
|
||||
import '../index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<Router />
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
128
src/popup/styles.css
Normal file
@ -0,0 +1,128 @@
|
||||
/* Reset e estilos base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #1f1f1f;
|
||||
color: white;
|
||||
width: 300px;
|
||||
min-height: 400px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Container principal */
|
||||
.popup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #1f1f1f;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.popup-header img {
|
||||
height: 24px;
|
||||
width: auto;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.popup-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Conteúdo principal */
|
||||
.popup-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Formulários */
|
||||
.form-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background-color: #2d2d2d;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
textarea.form-input {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Botão */
|
||||
.button {
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background-color: #4338ca;
|
||||
}
|
||||
|
||||
.button-primary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Scrollbar personalizada */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4f46e5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
11
src/store/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import authReducer from './slices/authSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
auth: authReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
33
src/store/slices/authSlice.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface AuthState {
|
||||
authToken: string | null;
|
||||
negocios_id: string | null;
|
||||
negocios_nome: string | null;
|
||||
user_id: string | null;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
authToken: null,
|
||||
negocios_id: null,
|
||||
negocios_nome: null,
|
||||
user_id: null,
|
||||
isAuthenticated: false,
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setAuth: (state, action: PayloadAction<Partial<AuthState>>) => {
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
logout: (state) => {
|
||||
return initialState;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setAuth, logout } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
20
src/styles.css
Normal file
@ -0,0 +1,20 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Scrollbar personalizada */
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-gray-900;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-indigo-600 rounded-md;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-indigo-700;
|
||||
}
|
||||
103
styles.css
Normal file
@ -0,0 +1,103 @@
|
||||
/* Estilos gerais */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Container principal */
|
||||
.container {
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
/* Títulos */
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Formulários */
|
||||
form label {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
form input[type="text"],
|
||||
form select,
|
||||
form textarea {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form button {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #0073b1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
background-color: #005582;
|
||||
}
|
||||
|
||||
/* Estilos para o popup */
|
||||
.popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.popup-content p {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Botões do popup */
|
||||
.popup-buttons {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.popup-buttons button {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#cancelButton {
|
||||
background-color: #ccc;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#viewButton {
|
||||
background-color: #0073b1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#viewButton:hover {
|
||||
background-color: #005582;
|
||||
}
|
||||
10
tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{html,js,ts,jsx,tsx}",
|
||||
"./*.html"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
}
|
||||
35
vite.config.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
popup: resolve(__dirname, 'index.html'),
|
||||
background: resolve(__dirname, 'src/background/index.ts'),
|
||||
content: resolve(__dirname, 'src/content/index.ts')
|
||||
},
|
||||
output: {
|
||||
entryFileNames: (chunkInfo) => {
|
||||
return chunkInfo.name === 'background' ? 'background.js' :
|
||||
chunkInfo.name === 'content' ? 'content.js' :
|
||||
'[name].js';
|
||||
},
|
||||
assetFileNames: (assetInfo) => {
|
||||
const info = assetInfo.name.split('.');
|
||||
const extType = info[info.length - 1];
|
||||
|
||||
if (extType === 'css') {
|
||||
return 'content.css';
|
||||
}
|
||||
|
||||
return `assets/[name][extname]`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||