Initial LinkLog implementation
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "LinkLog",
|
||||
"version": "0.1.0",
|
||||
"description": "Capture and submit page links to LinkLog.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"tabs"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"action": {
|
||||
"default_title": "LinkLog",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.options-shell {
|
||||
max-width: 440px;
|
||||
margin: 40px auto;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-top: 6px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>LinkLog Settings</title>
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="options-shell">
|
||||
<h1>LinkLog Settings</h1>
|
||||
|
||||
<div id="status" class="status hidden" aria-live="polite"></div>
|
||||
|
||||
<form id="settings-form">
|
||||
<label>
|
||||
Backend URL
|
||||
<input id="backend-url" type="url" placeholder="https://example.com" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Username
|
||||
<input id="username" type="text" placeholder="alice" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Password
|
||||
<input id="password" type="password" placeholder="********" />
|
||||
</label>
|
||||
|
||||
<button type="submit">Save and log in</button>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
const DEFAULT_BACKEND = 'http://localhost:8000';
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const form = document.getElementById('settings-form');
|
||||
const backendUrlInput = document.getElementById('backend-url');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
function setStatus(message, isError = false) {
|
||||
statusEl.textContent = message;
|
||||
statusEl.classList.remove('hidden');
|
||||
statusEl.classList.toggle('error', isError);
|
||||
statusEl.classList.toggle('success', !isError);
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const settings = await browser.storage.local.get(['backendUrl', 'username']);
|
||||
backendUrlInput.value = settings.backendUrl || DEFAULT_BACKEND;
|
||||
usernameInput.value = settings.username || '';
|
||||
}
|
||||
|
||||
async function saveSettingsAndLogin(event) {
|
||||
event.preventDefault();
|
||||
const backendUrl = backendUrlInput.value.trim();
|
||||
const username = usernameInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!backendUrl || !username || !password) {
|
||||
setStatus('Please fill in all fields', true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${backendUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
await browser.storage.local.set({
|
||||
backendUrl,
|
||||
username,
|
||||
accessToken: data.access_token,
|
||||
tokenType: data.token_type,
|
||||
tokenExpiresAt: data.expires_at,
|
||||
refreshToken: data.refresh_token,
|
||||
});
|
||||
|
||||
setStatus('Logged in successfully');
|
||||
} catch (error) {
|
||||
setStatus('Unable to log in. Check backend URL and credentials.', true);
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', saveSettingsAndLogin);
|
||||
loadSettings();
|
||||
@@ -0,0 +1,88 @@
|
||||
body {
|
||||
width: 360px;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background: #f5f7fb;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.popup-shell {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
margin-top: 6px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#submit-link {
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: #e2e8f0;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>LinkLog</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup-shell">
|
||||
<header>
|
||||
<h1>LinkLog</h1>
|
||||
</header>
|
||||
|
||||
<div id="status" class="status hidden" aria-live="polite"></div>
|
||||
|
||||
<form id="link-form">
|
||||
<label>
|
||||
Title
|
||||
<input id="title" name="title" type="text" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
URL
|
||||
<input id="url" name="url" type="url" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Comment
|
||||
<textarea id="comment" name="comment" rows="3" placeholder="Your comment"></textarea>
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" id="submit-link">Save link</button>
|
||||
<button type="button" id="open-settings" class="secondary">Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
const statusEl = document.getElementById('status');
|
||||
const form = document.getElementById('link-form');
|
||||
const titleInput = document.getElementById('title');
|
||||
const urlInput = document.getElementById('url');
|
||||
const commentInput = document.getElementById('comment');
|
||||
const openSettingsButton = document.getElementById('open-settings');
|
||||
|
||||
function setStatus(message, isError = false) {
|
||||
statusEl.textContent = message;
|
||||
statusEl.classList.remove('hidden');
|
||||
statusEl.classList.toggle('error', isError);
|
||||
statusEl.classList.toggle('success', !isError);
|
||||
}
|
||||
|
||||
async function getSettings() {
|
||||
const result = await browser.storage.local.get([
|
||||
'backendUrl',
|
||||
'accessToken',
|
||||
'tokenExpiresAt',
|
||||
]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function removeKnownTrackingParams(urlString) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const known = new Set([
|
||||
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
|
||||
'utm_id', 'utm_name', 'gclid', 'fbclid', 'dclid', 'msclkid'
|
||||
]);
|
||||
|
||||
for (const key of known) {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
return url.toString();
|
||||
} catch (error) {
|
||||
return urlString;
|
||||
}
|
||||
}
|
||||
|
||||
async function populateCurrentTab() {
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab) return;
|
||||
|
||||
titleInput.value = tab.title || '';
|
||||
urlInput.value = tab.url || '';
|
||||
}
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
setStatus('Submitting...', false);
|
||||
|
||||
const settings = await getSettings();
|
||||
const token = settings.accessToken;
|
||||
const backendUrl = settings.backendUrl;
|
||||
|
||||
if (!token || !backendUrl) {
|
||||
setStatus('Please configure the backend URL and log in first.', true);
|
||||
browser.runtime.openOptionsPage();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${backendUrl}/api/links`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: titleInput.value,
|
||||
url: removeKnownTrackingParams(urlInput.value),
|
||||
comment: commentInput.value,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
setStatus('Session expired. Re-authenticate in settings.', true);
|
||||
browser.runtime.openOptionsPage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Submission failed');
|
||||
}
|
||||
|
||||
setStatus('Link saved successfully');
|
||||
} catch (error) {
|
||||
setStatus('Submission failed. Check your backend connection.', true);
|
||||
}
|
||||
}
|
||||
|
||||
openSettingsButton.addEventListener('click', () => browser.runtime.openOptionsPage());
|
||||
form.addEventListener('submit', handleSubmit);
|
||||
populateCurrentTab();
|
||||
Reference in New Issue
Block a user