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]`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||