diff --git a/shaarli-pro/css/custom_views.css b/shaarli-pro/css/custom_views.css
new file mode 100644
index 0000000..d20a03d
--- /dev/null
+++ b/shaarli-pro/css/custom_views.css
@@ -0,0 +1,787 @@
+/* =========================================
+ Special Views for Todos and Notes
+ ========================================= */
+
+/* --- Layout Wrapper (injected by JS) --- */
+.special-view-wrapper {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ min-height: calc(100vh - 60px);
+ /* Adjust for header */
+}
+
+/* --- TODO VIEW --- */
+body.view-todo .content-container {
+ max-width: 100%;
+ padding: 0;
+ margin: 0;
+}
+
+body.view-todo #linklist {
+ display: none;
+ /* Hide default list when wrapper is active */
+}
+
+/* Sidebar */
+.todo-sidebar {
+ width: 280px;
+ background-color: var(--background-secondary, #f8f9fa);
+ border-right: 1px solid var(--border-color, #e2e8f0);
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+[data-theme="dark"] .todo-sidebar {
+ background-color: #1e293b;
+ border-color: #334155;
+}
+
+.create-task-btn {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ background-color: var(--primary-color, #2563eb);
+ color: white;
+ border: none;
+ border-radius: 2rem;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s, background-color 0.2s;
+ margin-bottom: 1.5rem;
+}
+
+.create-task-btn:hover {
+ background-color: var(--primary-dark, #1d4ed8);
+ transform: translateY(-1px);
+}
+
+.todo-list-item {
+ padding: 0.75rem 1rem;
+ margin-bottom: 0.25rem;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ color: var(--text-color, #334155);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ transition: background-color 0.2s;
+}
+
+[data-theme="dark"] .todo-list-item {
+ color: #cbd5e1;
+}
+
+.todo-list-item:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+[data-theme="dark"] .todo-list-item:hover {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+.todo-list-item.active {
+ background-color: rgba(37, 99, 235, 0.1);
+ color: var(--primary-color, #2563eb);
+ font-weight: 600;
+}
+
+/* Main Task Area */
+.todo-main {
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+ background-color: var(--bg-body);
+}
+
+[data-theme="dark"] .todo-main {
+ background-color: #0f172a;
+}
+
+.todo-main-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+}
+
+.todo-main-header h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0;
+}
+
+/* Todo Item */
+.todo-item {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color, #e2e8f0);
+ display: flex;
+ align-items: center;
+ /* keep aligned */
+ gap: 1rem;
+ transition: background-color 0.2s;
+}
+
+[data-theme="dark"] .todo-item {
+ border-bottom-color: #334155;
+}
+
+.todo-item:hover {
+ background-color: rgba(0, 0, 0, 0.02);
+}
+
+.todo-checkbox {
+ width: 20px;
+ height: 20px;
+ border: 2px solid var(--text-light, #94a3b8);
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.todo-checkbox.checked {
+ background-color: var(--primary-color, #2563eb);
+ border-color: var(--primary-color, #2563eb);
+ color: white;
+}
+
+.todo-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.todo-title {
+ font-size: 1rem;
+ color: var(--text-color, #0f172a);
+ font-weight: 400;
+}
+
+[data-theme="dark"] .todo-title {
+ color: #f1f5f9;
+}
+
+.todo-item.completed .todo-title {
+ text-decoration: line-through;
+ color: var(--text-light, #94a3b8);
+}
+
+.todo-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.75rem;
+ color: var(--text-light, #64748b);
+}
+
+.todo-badge {
+ padding: 0.125rem 0.5rem;
+ border-radius: 1rem;
+ background-color: rgba(0, 0, 0, 0.05);
+ font-size: 0.7rem;
+}
+
+[data-theme="dark"] .todo-badge {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.due-date {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.due-date.overdue {
+ color: var(--danger-color, #ef4444);
+}
+
+/* --- NOTES VIEW --- */
+
+/* Wrapper */
+body.view-notes .content-container {
+ padding: 2rem;
+ background-color: var(--bg-body);
+ min-height: 100vh;
+}
+
+[data-theme="dark"] body.view-notes .content-container {
+ background-color: var(--bg-body);
+}
+
+/* Tool Bar / Input Area */
+.notes-top-bar {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ margin-bottom: 2rem;
+ position: relative;
+ padding-right: 60px;
+ /* Space for toggle */
+}
+
+.note-input-container {
+ width: 600px;
+ max-width: 100%;
+ background-color: var(--background-secondary, #ffffff);
+ border-radius: 8px;
+ box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 2px 6px 2px rgba(60, 64, 67, 0.15);
+ transition: box-shadow 0.2s;
+ overflow: hidden;
+}
+
+[data-theme="dark"] .note-input-container {
+ background-color: var(--bg-sidebar);
+ border: 1px solid var(--border);
+ box-shadow: none;
+}
+
+.note-input-collapsed {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ cursor: text;
+ color: var(--text-light, #80868b);
+ font-weight: 500;
+ font-size: 1rem;
+}
+
+.note-input-placeholder {
+ flex: 1;
+}
+
+.note-input-actions {
+ display: flex;
+ gap: 16px;
+}
+
+.note-input-actions button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-light, #80868b);
+ padding: 4px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.2s, color 0.2s;
+}
+
+.note-input-actions button:hover {
+ background-color: rgba(136, 136, 136, 0.1);
+ color: var(--text-color);
+}
+
+/* Tools (View Toggle) */
+.notes-tools {
+ position: absolute;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ gap: 0.5rem;
+}
+
+.icon-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--text-light, #5f6368);
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.2s;
+}
+
+.icon-btn:hover {
+ background-color: rgba(136, 136, 136, 0.1);
+}
+
+.icon-btn.active {
+ color: var(--primary-color, #202124);
+ /* or specific active color */
+ background-color: rgba(136, 136, 136, 0.1);
+ /* Keep highlight style */
+}
+
+[data-theme="dark"] .icon-btn {
+ color: #9aa0a6;
+}
+
+[data-theme="dark"] .icon-btn.active {
+ color: #e8eaed;
+}
+
+
+/* --- LOGIC: Masonry vs List --- */
+
+/* Masonry Grid */
+.notes-masonry {
+ column-count: 4;
+ column-gap: 16px;
+ width: 100%;
+}
+
+@media (max-width: 1200px) {
+ .notes-masonry {
+ column-count: 3;
+ }
+}
+
+@media (max-width: 800px) {
+ .notes-masonry {
+ column-count: 2;
+ }
+}
+
+@media (max-width: 500px) {
+ .notes-masonry {
+ column-count: 1;
+ }
+}
+
+/* List View */
+.notes-list-view {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ width: 600px;
+ max-width: 100%;
+ margin: 0 auto;
+}
+
+.notes-list-view .note-card {
+ width: 100%;
+}
+
+
+/* --- CARD STYLING --- */
+.note-card {
+ background-color: var(--background-secondary, #ffffff);
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ /* spacing for masonry */
+ break-inside: avoid;
+ /* Prevent split */
+ position: relative;
+ transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s;
+ overflow: hidden;
+ /* for cover image */
+}
+
+/* Dark Mode Card */
+[data-theme="dark"] .note-card {
+ background-color: #202124;
+ border: 1px solid #5f6368;
+ color: #e8eaed;
+}
+
+.note-card:hover {
+ box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);
+}
+
+[data-theme="dark"] .note-card:hover {
+ background-color: #202124;
+ /* Keep lightens on hover? usually same but controls appear */
+}
+
+/* Cover Image */
+.note-cover img {
+ width: 100%;
+ height: auto;
+ display: block;
+ object-fit: cover;
+}
+
+/* Inner Content */
+.note-inner {
+ padding: 12px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+/* Title */
+.note-title {
+ font-size: 1rem;
+ font-weight: 500;
+ margin: 0;
+ line-height: 1.5rem;
+ color: var(--text-color, #202124);
+}
+
+[data-theme="dark"] .note-title {
+ color: #e8eaed;
+}
+
+/* Body (Truncated) */
+.note-body {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ color: var(--text-color, #202124);
+ word-wrap: break-word;
+ /* Limit to ~12 lines */
+ display: -webkit-box;
+ -webkit-line-clamp: 12;
+ line-clamp: 12;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ max-height: 300px;
+ /* Fallback */
+}
+
+[data-theme="dark"] .note-body {
+ color: #e8eaed;
+}
+
+/* Tags */
+.note-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 4px;
+}
+
+.note-tag {
+ background: rgba(0, 0, 0, 0.06);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.7rem;
+ color: var(--text-light);
+}
+
+[data-theme="dark"] .note-tag {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* Hover Actions */
+.note-hover-actions {
+ display: flex;
+ align-items: center;
+ gap: 0px;
+ /* evenly spaced */
+ margin-top: 8px;
+ margin-left: -8px;
+ /* Alignment fix */
+ opacity: 0;
+ /* Hidden by default */
+ transition: opacity 0.2s;
+ position: relative;
+ /* For palette popup */
+}
+
+/* Show actions on hover */
+.note-card:hover .note-hover-actions {
+ opacity: 1;
+}
+
+/* Always show actions on touch devices? (Can't detect easily here, but opacity 1 is safer for UX if no hover) */
+@media (hover: none) {
+ .note-hover-actions {
+ opacity: 1;
+ }
+}
+
+.note-hover-actions button,
+.note-hover-actions a {
+ background: none;
+ border: none;
+ width: 34px;
+ /* Touch target */
+ height: 34px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-light, #5f6368);
+ cursor: pointer;
+ font-size: 1.1rem;
+ transition: background-color 0.2s, color 0.2s;
+ text-decoration: none;
+}
+
+[data-theme="dark"] .note-hover-actions button,
+[data-theme="dark"] .note-hover-actions a {
+ color: #9aa0a6;
+}
+
+.note-hover-actions button:hover,
+.note-hover-actions a:hover {
+ background-color: rgba(136, 136, 136, 0.2);
+ color: var(--text-color);
+}
+
+[data-theme="dark"] .note-hover-actions button:hover,
+[data-theme="dark"] .note-hover-actions a:hover {
+ color: #e8eaed;
+}
+
+.spacer {
+ flex: 1;
+}
+
+/* Pushes action buttons to left/right if needed, currently all left? Keep aligns left mostly. */
+
+.note-body img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+ margin-top: 4px;
+ /* If we extracted cover, hide first img? */
+}
+
+/* --- COLORS --- */
+/* Reference: Keep Colors */
+/* Default */
+.note-card.note-color-default {
+ background-color: var(--background-secondary, #ffffff);
+}
+
+[data-theme="dark"] .note-card.note-color-default {
+ background-color: #202124;
+ border-color: #5f6368;
+}
+
+/* Red */
+.note-card.note-color-red {
+ background-color: #f28b82;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-red {
+ background-color: #5c2b29;
+ border-color: #5c2b29;
+}
+
+/* Orange */
+.note-card.note-color-orange {
+ background-color: #fbbc04;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-orange {
+ background-color: #614a19;
+ border-color: #614a19;
+}
+
+/* Yellow */
+.note-card.note-color-yellow {
+ background-color: #fff475;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-yellow {
+ background-color: #635d19;
+ border-color: #635d19;
+}
+
+/* Green */
+.note-card.note-color-green {
+ background-color: #ccff90;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-green {
+ background-color: #345920;
+ border-color: #345920;
+}
+
+/* Teal */
+.note-card.note-color-teal {
+ background-color: #a7ffeb;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-teal {
+ background-color: #16504b;
+ border-color: #16504b;
+}
+
+/* Blue */
+.note-card.note-color-blue {
+ background-color: #cbf0f8;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-blue {
+ background-color: #2d555e;
+ border-color: #2d555e;
+}
+
+/* Dark Blue */
+.note-card.note-color-darkblue {
+ background-color: #aecbfa;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-darkblue {
+ background-color: #1e3a5f;
+ border-color: #1e3a5f;
+}
+
+/* Purple */
+.note-card.note-color-purple {
+ background-color: #d7aefb;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-purple {
+ background-color: #42275e;
+ border-color: #42275e;
+}
+
+/* Pink */
+.note-card.note-color-pink {
+ background-color: #fdcfe8;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-pink {
+ background-color: #5b2245;
+ border-color: #5b2245;
+}
+
+/* Brown */
+.note-card.note-color-brown {
+ background-color: #e6c9a8;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-brown {
+ background-color: #442f19;
+ border-color: #442f19;
+}
+
+/* Grey */
+.note-card.note-color-grey {
+ background-color: #e8eaed;
+ border-color: transparent;
+}
+
+[data-theme="dark"] .note-card.note-color-grey {
+ background-color: #3c3f43;
+ border-color: #3c3f43;
+}
+
+
+/* --- PALETTE POPUP --- */
+.palette-popup {
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ width: 320px;
+ background: white;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+ border-radius: 4px;
+ padding: 8px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ z-index: 100;
+ display: none;
+ /* JS toggles this */
+}
+
+[data-theme="dark"] .palette-popup {
+ background: #202124;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
+ border: 1px solid #5f6368;
+}
+
+.palette-popup.open {
+ display: flex;
+}
+
+.palette-btn {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ position: relative;
+}
+
+.palette-btn:hover {
+ border-color: #999;
+}
+
+[data-theme="dark"] .palette-btn {
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+/* --- MODAL FOR NOTE VIEW --- */
+.note-modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(2px);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.2s;
+}
+
+.note-modal-overlay.open {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.note-modal {
+ background: var(--background-secondary, #fff);
+ width: 600px;
+ max-width: 90%;
+ max-height: 90vh;
+ border-radius: 8px;
+ overflow-y: auto;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ padding: 24px;
+ position: relative;
+}
+
+[data-theme="dark"] .note-modal {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+}
+
+.note-modal .note-title {
+ font-size: 1.3rem;
+ line-height: 1.5;
+ margin-bottom: 16px;
+}
+
+.note-modal .note-body {
+ font-size: 1rem;
+ line-height: 1.5;
+ -webkit-line-clamp: unset;
+ line-clamp: unset;
+ max-height: none;
+ overflow: visible;
+}
+
+/* Modal actions at bottom? */
+.note-modal-actions {
+ margin-top: 24px;
+ display: flex;
+ justify-content: flex-end;
+}
\ No newline at end of file
diff --git a/shaarli-pro/includes.html b/shaarli-pro/includes.html
index 20a06df..14e3e9e 100644
--- a/shaarli-pro/includes.html
+++ b/shaarli-pro/includes.html
@@ -23,6 +23,7 @@
+
{if="$pageName=='editlink' || $pageName=='addlink' || $pageName=='editlinkbatch'"}
@@ -56,6 +57,7 @@ untaggedonly: {if="$untaggedonly"}true{else}false{/if}
};
+
{if="file_exists('tpl/shaarli-pro/extra.html')"}
{include="extra"}
diff --git a/shaarli-pro/js/custom_views.js b/shaarli-pro/js/custom_views.js
new file mode 100644
index 0000000..0d9e8dd
--- /dev/null
+++ b/shaarli-pro/js/custom_views.js
@@ -0,0 +1,884 @@
+document.addEventListener('DOMContentLoaded', function () {
+ // Check URL parameters for custom views
+ const urlParams = new URLSearchParams(window.location.search);
+ const searchTags = urlParams.get('searchtags');
+
+ const linkList = document.getElementById('links-list');
+ const container = document.querySelector('.content-container');
+
+ // Always init Pinned Items logic (sorting and listeners)
+ // This function is defined at the end of the file
+ if (typeof initPinnedItems === 'function') {
+ initPinnedItems();
+ }
+
+ if (!linkList || !container) return;
+
+ if (searchTags === 'todo') {
+ initTodoView(linkList, container);
+ } else if (searchTags === 'note') {
+ initNoteView(linkList, container);
+ }
+});
+
+/**
+ * Initialize the Google Tasks-like view
+ */
+function initTodoView(linkList, container) {
+ document.body.classList.add('view-todo');
+
+ // Extract task data from existing DOM
+ const rawLinks = Array.from(linkList.querySelectorAll('.link-outer'));
+ const tasks = rawLinks.map(link => parseTaskFromLink(link)).filter(t => t !== null);
+
+ // Create new Layout
+ const wrapper = document.createElement('div');
+ wrapper.className = 'special-view-wrapper';
+
+ // 1. Sidebar
+ const sidebar = document.createElement('div');
+ sidebar.className = 'todo-sidebar';
+
+ // Extract unique groups for the sidebar
+ const groups = new Set();
+ tasks.forEach(t => {
+ if (t.group) groups.add(t.group);
+ });
+
+ const groupsList = Array.from(groups).map(g =>
+ `
${g}
`
+ ).join('');
+
+ sidebar.innerHTML = `
+
+
+ Mes tâches
+ ${tasks.length}
+
+ ${groups.size > 0 ? `${groupsList}` : ''}
+ `;
+
+ // 2. Main Content
+ const main = document.createElement('div');
+ main.className = 'todo-main';
+
+ const mainHeader = document.createElement('div');
+ mainHeader.className = 'todo-main-header';
+ mainHeader.innerHTML = `
+ Mes tâches
+
+
+
+ `;
+
+ const itemsContainer = document.createElement('div');
+ itemsContainer.className = 'todo-items-container';
+
+ // Sort Tasks: Pinned items first
+ tasks.sort((a, b) => {
+ const aPinned = a.tags && a.tags.includes('shaarli-pin');
+ const bPinned = b.tags && b.tags.includes('shaarli-pin');
+ return bPinned - aPinned;
+ });
+
+ // Render Tasks
+ tasks.forEach(task => {
+ itemsContainer.appendChild(renderTaskItem(task));
+ });
+
+ main.appendChild(mainHeader);
+ main.appendChild(itemsContainer);
+
+ wrapper.appendChild(sidebar);
+ wrapper.appendChild(main);
+
+ // Inject and Hide original
+ linkList.style.display = 'none';
+
+ // Remove pagination/toolbar if present to clean up view
+ const toolbar = document.querySelector('.content-toolbar');
+ if (toolbar) toolbar.style.display = 'none';
+
+ if (linkList.parentNode) {
+ linkList.parentNode.insertBefore(wrapper, linkList);
+ } else {
+ container.appendChild(wrapper);
+ }
+
+ // Global filter function
+ window.filterTasksByGroup = function (group) {
+ const title = document.getElementById('todo-list-title');
+ const items = document.querySelectorAll('.todo-item');
+
+ // Update Sidebar Active State
+ document.querySelectorAll('.todo-list-item').forEach(el => el.classList.remove('active'));
+ if (event && event.currentTarget) event.currentTarget.classList.add('active');
+
+ if (group === 'all') {
+ title.textContent = 'Mes tâches';
+ items.forEach(item => item.style.display = 'flex');
+ } else {
+ title.textContent = group;
+ items.forEach(item => {
+ if (item.dataset.group === group) item.style.display = 'flex';
+ else item.style.display = 'none';
+ });
+ }
+ };
+}
+
+
+function parseTaskFromLink(linkEl) {
+ const id = linkEl.dataset.id;
+ const titleEl = linkEl.querySelector('.link-title'); // "Title" normally
+ // For Todos, the Bookmark Title is the Task Title?
+ // Or is the title inside the description?
+ // Mental model says: "Todo: reste un bookmark privé... LinkEntity.title" is used.
+
+ const title = titleEl ? titleEl.textContent.trim() : 'Task';
+ const descEl = linkEl.querySelector('.link-description');
+ const rawDesc = descEl ? descEl.innerHTML : '';
+ const textDesc = descEl ? descEl.textContent : '';
+
+ // Check if it's really a todo (should be if we are in ?searchtags=todo, but double check)
+ // We assume yes.
+
+ // Parse Metadata from Description text
+ // Format: 📅 **Échéance :**
+ // Format: 🏷️ **Groupe :**
+ // Format: - [ ] Subtask or Main task status?
+
+ // Status
+ // If [x] is found in the first few lines, maybe completed?
+ // User says: "Puis une checkbox markdown: - [ ] Titre"
+ // Wait, if the Description contains the checkbox and title, then Bookmark Title is ignored?
+ // Let's assume Bookmark Title is the master Display Title.
+ // And "Checkbox status" can be parsed from description.
+
+ let isCompleted = false;
+ if (textDesc.includes('[x]')) isCompleted = true;
+
+ // Due Date
+ let dueDate = null;
+ const dateMatch = textDesc.match(/Échéance\s*:\s*\*+([^*]+)\*+/); // varies by markdown regex
+ // Text might be "📅 **Échéance :** 2023-10-10"
+ // Regex: Échéance\s*:\s*(.*?)(\n|$)
+ const dueMatch = textDesc.match(/Échéance\s*[:]\s*(.*?)(\n|$)/);
+ if (dueMatch) dueDate = dueMatch[1].trim();
+
+ // Group
+ let group = null;
+ const groupMatch = textDesc.match(/Groupe\s*[:]\s*(.*?)(\n|$)/);
+ if (groupMatch) group = groupMatch[1].trim();
+
+ return {
+ id,
+ title,
+ isCompleted,
+ dueDate,
+ group,
+ originalUrl: linkEl.querySelector('.link-url') ? linkEl.querySelector('.link-url').textContent : '',
+ editUrl: linkEl.querySelector('a[href*="admin/shaare/"]') ? linkEl.querySelector('a[href*="admin/shaare/"]').href : '#'
+ };
+}
+
+function renderTaskItem(task) {
+ const el = document.createElement('div');
+ el.className = `todo-item ${task.isCompleted ? 'completed' : ''}`;
+ el.dataset.group = task.group || '';
+
+ /*
+ We cannot easily toggle state (AJAX) without a backend API that supports partial updates efficiently.
+ But we can provide a link to Edit.
+ Or simulate it.
+ */
+
+ el.innerHTML = `
+
+ ${task.isCompleted ? '' : ''}
+
+
+ `;
+
+ // Simple click handler to open edit (since we can't sync state easily)
+ el.querySelector('.todo-checkbox').addEventListener('click', (e) => {
+ e.stopPropagation();
+ // Open edit page or toggle visually
+ // window.location.href = task.editUrl;
+ });
+
+ return el;
+}
+
+function isOverdue(dateStr) {
+ try {
+ return new Date(dateStr) < new Date();
+ } catch (e) { return false; }
+}
+
+function formatDate(dateStr) {
+ try {
+ const d = new Date(dateStr);
+ return d.toLocaleDateString();
+ } catch (e) { return dateStr; }
+}
+
+/**
+ * Initialize the Google Keep-like view
+ */
+
+/**
+ * Initialize the Google Keep-like view
+ */
+function initNoteView(linkList, container) {
+ document.body.classList.add('view-notes');
+
+ // Hide standard toolbar
+ const toolbar = document.querySelector('.content-toolbar');
+ if (toolbar) toolbar.style.display = 'none';
+
+ // 1. Create Layout Wrapper
+ const wrapper = document.createElement('div');
+ wrapper.className = 'notes-wrapper';
+
+ // 2. Create Search/Input Area (Top)
+ const topBar = document.createElement('div');
+ topBar.className = 'notes-top-bar';
+
+ // Custom Input "Take a note..."
+ const inputContainer = document.createElement('div');
+ inputContainer.className = 'note-input-container';
+ inputContainer.innerHTML = `
+
+ `;
+ topBar.appendChild(inputContainer);
+
+ // View Toggle and other tools
+ const tools = document.createElement('div');
+ tools.className = 'notes-tools';
+ tools.innerHTML = `
+
+
+ `;
+ topBar.appendChild(tools);
+
+ wrapper.appendChild(topBar);
+
+ // 3. Content Area
+ const contentArea = document.createElement('div');
+ contentArea.className = 'notes-content-area';
+
+ const links = Array.from(linkList.querySelectorAll('.link-outer'));
+ const notes = links.map(link => parseNoteFromLink(link));
+
+ // Initial Render (Grid)
+ renderNotes(contentArea, notes, 'grid');
+
+ wrapper.appendChild(contentArea);
+
+ // Replace original list
+ linkList.style.display = 'none';
+ if (linkList.parentNode) {
+ linkList.parentNode.insertBefore(wrapper, linkList);
+ } else {
+ container.appendChild(wrapper);
+ }
+
+ // Modal Container
+ const modalOverlay = document.createElement('div');
+ modalOverlay.className = 'note-modal-overlay';
+ modalOverlay.innerHTML = `
+
+ `;
+ document.body.appendChild(modalOverlay);
+
+ // Event Listeners for Toggles
+ const btnGrid = wrapper.querySelector('#btn-view-grid');
+ const btnList = wrapper.querySelector('#btn-view-list');
+
+ btnGrid.addEventListener('click', () => {
+ btnGrid.classList.add('active');
+ btnList.classList.remove('active');
+ renderNotes(contentArea, notes, 'grid');
+ });
+
+ btnList.addEventListener('click', () => {
+ btnList.classList.add('active');
+ btnGrid.classList.remove('active');
+ renderNotes(contentArea, notes, 'list');
+ });
+
+ // Close Modal
+ modalOverlay.querySelector('#note-modal-close').addEventListener('click', () => {
+ modalOverlay.classList.remove('open');
+ });
+ modalOverlay.addEventListener('click', (e) => {
+ if (e.target === modalOverlay) modalOverlay.classList.remove('open');
+ });
+}
+
+function parseNoteFromLink(linkEl) {
+ const id = linkEl.dataset.id;
+ const titleEl = linkEl.querySelector('.link-title');
+ const title = titleEl ? titleEl.textContent.trim() : '';
+
+ const descEl = linkEl.querySelector('.link-description');
+ const descHtml = descEl ? descEl.innerHTML : '';
+ const descText = descEl ? descEl.textContent : '';
+
+ // Extract Image from Description (First image as cover)
+ let coverImage = null;
+ if (descEl) {
+ const img = descEl.querySelector('img');
+ if (img) {
+ coverImage = img.src;
+ // Optionally remove img from body text if it's purely a cover
+ // But usually we keep it or hide it via CSS if we construct a custom card
+ }
+ }
+
+ const urlEl = linkEl.querySelector('.link-url');
+ const url = urlEl ? urlEl.textContent.trim() : '';
+
+ const tags = [];
+ let color = 'default';
+ linkEl.querySelectorAll('.link-tag-list a').forEach(tag => {
+ const t = tag.textContent.trim();
+ // Check for color tag
+ if (t.startsWith('note-')) {
+ const potentialColor = t.substring(5);
+ color = potentialColor;
+ } else {
+ tags.push(t);
+ }
+ });
+
+ const isPinnedByTag = tags.includes('shaarli-pin');
+
+ const actionsEl = linkEl.querySelector('.link-actions');
+ const editUrl = actionsEl && actionsEl.querySelector('a[href*="admin/shaare"]') ? actionsEl.querySelector('a[href*="admin/shaare"]').href : '#';
+ const deleteUrl = actionsEl && actionsEl.querySelector('a[href*="delete"]') ? actionsEl.querySelector('a[href*="delete"]').href : '#';
+ const pinUrl = actionsEl && actionsEl.querySelector('a[href*="pin"]') ? actionsEl.querySelector('a[href*="pin"]').href : '#';
+
+ // User requested "availability of the tag 'shaarli-pin' as the main source"
+ const isPinned = tags.includes('shaarli-pin');
+
+ return { id, title, descHtml, descText, coverImage, url, tags, color, editUrl, deleteUrl, pinUrl, isPinned };
+}
+
+function renderNotes(container, notes, viewMode) {
+ container.innerHTML = '';
+ container.className = viewMode === 'grid' ? 'notes-masonry' : 'notes-list-view';
+
+ // Sort: Pinned items first
+ notes.sort((a, b) => {
+ const aPinned = a.tags.includes('shaarli-pin');
+ const bPinned = b.tags.includes('shaarli-pin');
+ return bPinned - aPinned;
+ });
+
+ notes.forEach(note => {
+ const card = document.createElement('div');
+ card.className = `note-card note-color-${note.color}`;
+ card.dataset.id = note.id;
+ if (viewMode === 'list') card.classList.add('list-mode');
+
+ // Main Click to Open Modal
+ card.addEventListener('click', (e) => {
+ // Prevent if clicking buttons
+ if (e.target.closest('button') || e.target.closest('a') || e.target.closest('.note-hover-actions')) return;
+ openNoteModal(note);
+ });
+
+ // Cover Image
+ if (note.coverImage && viewMode === 'grid') { // Show cover mainly in grid
+ const imgContainer = document.createElement('div');
+ imgContainer.className = 'note-cover';
+ imgContainer.innerHTML = `
`;
+ card.appendChild(imgContainer);
+ }
+
+ // Inner Content
+ const inner = document.createElement('div');
+ inner.className = 'note-inner';
+
+ // Title
+ if (note.title) {
+ const h3 = document.createElement('h3');
+ h3.className = 'note-title';
+ h3.textContent = note.title;
+ inner.appendChild(h3);
+ }
+
+ // Body (truncated in grid, maybe?)
+ if (note.descHtml) {
+ const body = document.createElement('div');
+ body.className = 'note-body';
+ // Start simple: use innerHTML but maybe strip big images if we used cover?
+ // For now, let's just dump it and style images to fit or hide if first child
+ body.innerHTML = note.descHtml;
+ inner.appendChild(body);
+ }
+
+ // Tags (Labels)
+ if (note.tags.length > 0) {
+ const tagContainer = document.createElement('div');
+ tagContainer.className = 'note-tags';
+ note.tags.forEach(t => {
+ if (t !== 'note') {
+ const span = document.createElement('span');
+ span.className = 'note-tag';
+ span.textContent = t;
+ tagContainer.appendChild(span);
+ }
+ });
+ inner.appendChild(tagContainer);
+ }
+
+ // Hover Actions (Keep style: at bottom, visible on hover)
+ const actions = document.createElement('div');
+ actions.className = 'note-hover-actions';
+
+ // Palette Button Logic
+ const paletteBtnId = `palette-${note.id}`;
+
+ actions.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Palette Toggle
+ const paletteBtn = actions.querySelector(`#${paletteBtnId}`);
+ const palettePopup = actions.querySelector(`#popup-${paletteBtnId}`);
+
+ paletteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ // Close others?
+ document.querySelectorAll('.palette-popup.open').forEach(p => {
+ if (p !== palettePopup) p.classList.remove('open');
+ });
+ palettePopup.classList.toggle('open');
+ });
+
+ // Close palette when clicking outside
+ // (Handled globally or card based? simple: card mouseleave?)
+ card.addEventListener('mouseleave', () => {
+ palettePopup.classList.remove('open');
+ });
+
+
+ inner.appendChild(actions);
+ card.appendChild(inner);
+
+ container.appendChild(card);
+ });
+}
+
+function openNoteModal(note) {
+ const modal = document.querySelector('.note-modal-overlay');
+ const content = modal.querySelector('.note-modal-content');
+
+ // Build full content
+ let html = `
+ ${note.title}
+ ${note.descHtml}
+ `;
+ // Add images if not in desc? (desc usually has it)
+
+ content.innerHTML = html;
+ modal.classList.add('open');
+}
+
+function generatePaletteButtons(note) {
+ const colors = [
+ 'default', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'darkblue', 'purple', 'pink', 'brown', 'grey'
+ ];
+ // Map to hex for the button background
+ const colorMap = {
+ 'default': '#ffffff', 'red': '#f28b82', 'orange': '#fbbc04', 'yellow': '#fff475',
+ 'green': '#ccff90', 'teal': '#a7ffeb', 'blue': '#cbf0f8', 'darkblue': '#aecbfa',
+ 'purple': '#d7aefb', 'pink': '#fdcfe8', 'brown': '#e6c9a8', 'grey': '#e8eaed'
+ };
+ // Dark mode mapping could be handled via CSS classes on buttons but inline styles are easier for the picker circles
+ // We will just use class names and let CSS handle the preview color if possible, or set style.
+
+ return colors.map(c => {
+ // We use style for the button background roughly.
+ // Actually, let's use the class on the button itself to pick up the color from CSS variables if defined,
+ // OR just hardcode the light mode preview for simplicity as the picker is usually on white/dark background.
+ return ``;
+ }).join('');
+}
+
+window.setNoteColor = function (noteId, color, editUrl) {
+ // 1. Visual Update (Immediate feedback)
+ const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
+ if (card) {
+ // Remove all color classes
+ card.classList.forEach(cls => {
+ if (cls.startsWith('note-color-')) card.classList.remove(cls);
+ });
+ card.classList.add(`note-color-${color}`);
+ }
+
+ // 2. Persistence via AJAX Form Submission
+ fetch(editUrl)
+ .then(response => response.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const form = doc.querySelector('form[name="linkform"]');
+
+ if (!form) throw new Error('Could not find edit form');
+
+ // Extract all necessary fields
+ const formData = new URLSearchParams();
+ const inputs = form.querySelectorAll('input, textarea');
+
+ inputs.forEach(input => {
+ if (input.type === 'checkbox') {
+ if (input.checked) formData.append(input.name, input.value || 'on');
+ } else if (input.name) {
+ formData.append(input.name, input.value);
+ }
+ });
+
+ // Update Tags
+ let currentTags = formData.get('lf_tags') || '';
+ let tagsArray = currentTags.split(/[\s,]+/).filter(t => t.trim() !== '');
+
+ // Remove existing color tags
+ tagsArray = tagsArray.filter(t => !t.startsWith('note-'));
+
+ // Add new color tag (unless default)
+ if (color !== 'default') {
+ tagsArray.push(`note-${color}`);
+ }
+
+ formData.set('lf_tags', tagsArray.join(' '));
+ formData.append('save_edit', '1'); // Trigger save action
+
+ // POST back to Shaarli
+ return fetch(form.action, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: formData.toString()
+ });
+ })
+ .then(response => {
+ if (response.ok) {
+ console.log(`Color ${color} saved for note ${noteId}`);
+ } else {
+ throw new Error('Failed to save color');
+ }
+ })
+ .catch(err => {
+ console.error('Error saving note color:', err);
+ alert("Erreur lors de la sauvegarde de la couleur. Veuillez rafraîchir la page.");
+ });
+};
+
+/* ==========================================================
+ PINNED ITEMS LOGIC (Tag: shaarli-pin)
+ ========================================================== */
+function initPinnedItems() {
+ const container = document.querySelector('.links-list, .notes-masonry, .notes-list-view');
+ if (!container) return; // Exit if no container found (e.g. empty page or other view)
+
+ const items = Array.from(container.children);
+ const pinnedItems = [];
+
+ items.forEach(item => {
+ // Support both Standard Link items and Note items
+ if (item.classList.contains('link-outer') || item.classList.contains('note-card')) {
+ let isPinned = false;
+
+ // 1. Check for Tag 'shaarli-pin'
+ // In note-card, we might need to check dataset or re-parse tags if not visible?
+ // But usually renderNotes puts tags in DOM or we rely on data attribute if we saved it?
+ // Let's rely on finding the tag text in the DOM for consistency with Standard View.
+ // For Note View, tags are in .note-tags > .note-tag
+ // For Standard View, tags are in .link-tag-list > a
+
+ const itemHtml = item.innerHTML; // Simple search in content (quick & dirty but effective for tag presence)
+ // Better: Select text content of tags specifically to avoid false positives in description
+ const tagElements = item.querySelectorAll('.link-tag-list a, .note-tag');
+ for (let t of tagElements) {
+ if (t.textContent.trim() === 'shaarli-pin') {
+ isPinned = true;
+ break;
+ }
+ }
+
+ // 2. Enforce Visual State based on Tag Presence
+ const pinBtnIcon = item.querySelector('.mdi-pin-outline, .mdi-pin');
+ const titleArea = item.querySelector('.link-title, .note-title');
+ const titleIcon = titleArea ? titleArea.querySelector('i.mdi-pin') : null;
+
+ if (isPinned) {
+ // It IS Pinned: Ensure UI reflects this
+ pinnedItems.push(item);
+ item.classList.add('is-pinned-tag');
+
+ // Button -> Filled Pin
+ if (pinBtnIcon) {
+ pinBtnIcon.classList.remove('mdi-pin-outline');
+ pinBtnIcon.classList.add('mdi-pin');
+ if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.add('active');
+ }
+
+ // Title -> Add Icon if missing
+ if (titleArea && !titleIcon) {
+ const newIcon = document.createElement('i');
+ newIcon.className = 'mdi mdi-pin';
+ newIcon.style.color = 'var(--primary)';
+ newIcon.style.marginRight = '8px';
+ titleArea.prepend(newIcon);
+ }
+ } else {
+ // It is NOT Pinned: Ensure UI reflects this (Clean up native sticky or mismatches)
+ item.classList.remove('is-pinned-tag');
+
+ // Button -> Outline Pin
+ if (pinBtnIcon) {
+ pinBtnIcon.classList.remove('mdi-pin');
+ pinBtnIcon.classList.add('mdi-pin-outline');
+ if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.remove('active');
+ }
+
+ // Title -> Remove Icon if present
+ if (titleIcon) {
+ titleIcon.remove();
+ }
+ }
+ }
+ });
+
+ // 3. Move Pinned Items to Top (Only for standard list, renderNotes already sorts itself)
+ if (container.classList.contains('links-list')) {
+ for (let i = pinnedItems.length - 1; i >= 0; i--) {
+ container.prepend(pinnedItems[i]);
+ }
+ }
+
+ // 4. Click Listener for all Pin Buttons (Event Delegation)
+ // Avoid adding multiple listeners if init called multiple times?
+ // We'll rely on one global listener on document, but here we add it inside this function which is called once on load.
+ // To be safe, let's remove old one if we could, but anonymous function makes it hard.
+ // Better: Allow this to run, but ensure we don't duplicate.
+ // Since initPinnedItems is called on DOMContentLoaded, it runs once.
+
+ // Note: We already have the listener attached in previous version.
+ // We will just keep the listener logic here in the replacement.
+ document.addEventListener('click', function (e) {
+ const btn = e.target.closest('a[href*="do=pin"], .note-hover-actions a[href*="pin"], .link-actions a[href*="pin"]');
+ if (btn) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const card = btn.closest('.link-outer, .note-card');
+ const id = card ? card.dataset.id : null;
+
+ // Re-derive edit URL if needed
+ let editUrl = btn.href.replace('do=pin', 'do=editlink').replace('pin', 'editlink');
+ if (card) {
+ const editBtn = card.querySelector('a[href*="edit_link"], a[href*="admin/shaare"]');
+ if (editBtn) editUrl = editBtn.href;
+ }
+
+ if (id && editUrl) {
+ togglePinTag(id, editUrl, btn);
+ }
+ }
+ }, { once: false }); // Listener is permanent
+}
+
+function togglePinTag(id, editUrl, btn) {
+ const icon = btn.querySelector('i');
+ let isPinning = false;
+
+ if (icon) {
+ if (icon.classList.contains('mdi-pin-outline')) {
+ icon.classList.remove('mdi-pin-outline');
+ icon.classList.add('mdi-pin');
+ btn.classList.add('active');
+ isPinning = true;
+ } else {
+ icon.classList.remove('mdi-pin');
+ icon.classList.add('mdi-pin-outline');
+ btn.classList.remove('active');
+ isPinning = false;
+ }
+ }
+
+ // Update Title Icon (The one "devant le titre")
+ const card = btn.closest('.link-outer, .note-card');
+ if (card) {
+ // Update Title Icon
+ const titleArea = card.querySelector('.link-title, .note-title');
+ if (titleArea) {
+ let titleIcon = titleArea.querySelector('i.mdi-pin');
+ if (isPinning) {
+ if (!titleIcon) {
+ const newIcon = document.createElement('i');
+ newIcon.className = 'mdi mdi-pin';
+ newIcon.style.color = 'var(--primary)';
+ newIcon.style.marginRight = '8px';
+ titleArea.prepend(newIcon);
+ }
+ } else {
+ if (titleIcon) titleIcon.remove();
+ }
+ }
+
+ // Update Tag List Visualization
+ // We need to handle both Note Cards (.note-tags) and Standard Links (.link-tag-list)
+ let tagContainer = card.querySelector('.note-tags');
+ if (!tagContainer) tagContainer = card.querySelector('.link-tag-list');
+
+ if (tagContainer) {
+ // Check if tag exists already
+ let existingTagElement = null;
+
+ // Search in children
+ // Notes: .note-tag
+ // Links: .label-tag > a OR just a
+ const allCandidates = tagContainer.querySelectorAll('*');
+ for (let el of allCandidates) {
+ if (el.textContent.trim() === 'shaarli-pin') {
+ // We found the text.
+ // If note, it's the span.note-tag
+ if (el.classList.contains('note-tag')) {
+ existingTagElement = el;
+ break;
+ }
+ // If link, we want the anchor or its wrapper
+ if (el.tagName === 'A') {
+ // Check if wrapped in .label-tag
+ if (el.parentElement.classList.contains('label-tag')) {
+ existingTagElement = el.parentElement;
+ } else {
+ existingTagElement = el;
+ }
+ break;
+ }
+ }
+ }
+
+ if (isPinning) {
+ if (!existingTagElement) {
+ if (card.classList.contains('note-card')) {
+ // Add Note Tag
+ const span = document.createElement('span');
+ span.className = 'note-tag';
+ span.textContent = 'shaarli-pin';
+ tagContainer.appendChild(span);
+ } else {
+ // Add Link Tag (Standard View)
+ // Structure: shaarli-pin
+ const wrapper = document.createElement('span');
+ wrapper.className = 'link-tag';
+
+ const link = document.createElement('a');
+ link.href = '?searchtags=shaarli-pin';
+ link.textContent = 'shaarli-pin';
+
+ wrapper.appendChild(link);
+
+ // Append space first for separation if there are other tags
+ if (tagContainer.children.length > 0) {
+ tagContainer.appendChild(document.createTextNode(' '));
+ }
+ tagContainer.appendChild(wrapper);
+ }
+ }
+ } else {
+ if (existingTagElement) {
+ // Remove the element
+ const prev = existingTagElement.previousSibling;
+ existingTagElement.remove();
+ // Clean up trailing space/text if it was the last one or between tags
+ if (prev && prev.nodeType === Node.TEXT_NODE && !prev.textContent.trim()) {
+ prev.remove();
+ }
+ }
+ }
+ }
+ }
+
+ fetch(editUrl)
+ .then(response => response.text())
+ .then(html => {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ const form = doc.querySelector('form[name="linkform"]');
+ if (!form) throw new Error('Could not find edit form');
+
+ const formData = new URLSearchParams();
+ const inputs = form.querySelectorAll('input, textarea');
+ inputs.forEach(input => {
+ if (input.type === 'checkbox') {
+ if (input.checked) formData.append(input.name, input.value || 'on');
+ } else if (input.name) {
+ formData.append(input.name, input.value);
+ }
+ });
+
+ let currentTags = formData.get('lf_tags') || '';
+ let tagsArray = currentTags.split(/[\s,]+/).filter(t => t.trim() !== '');
+ const pinTag = 'shaarli-pin';
+
+ if (isPinning) {
+ if (!tagsArray.includes(pinTag)) tagsArray.push(pinTag);
+ } else {
+ tagsArray = tagsArray.filter(t => t !== pinTag);
+ }
+
+ formData.set('lf_tags', tagsArray.join(' '));
+ formData.append('save_edit', '1');
+
+ return fetch(form.action, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: formData.toString()
+ });
+ })
+ .then(res => {
+ if (res.ok) console.log("Pin toggled successfully");
+ })
+ .catch(err => console.error(err));
+}
diff --git a/shaarli-pro/page.header.html b/shaarli-pro/page.header.html
index fc6f037..ff7612d 100644
--- a/shaarli-pro/page.header.html
+++ b/shaarli-pro/page.header.html
@@ -17,33 +17,44 @@