Initial LinkLog implementation

This commit is contained in:
Olaf
2026-08-24 14:30:30 +02:00
commit 1c827956c4
50 changed files with 3436 additions and 0 deletions
+22
View File
@@ -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
}
}
+71
View File
@@ -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;
}
+36
View File
@@ -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>
+61
View File
@@ -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();
+88
View File
@@ -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;
}
+41
View File
@@ -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>
+96
View File
@@ -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();