diff --git a/app/gitea_client.py b/app/gitea_client.py index 3bf5c6c..d9f64f8 100644 --- a/app/gitea_client.py +++ b/app/gitea_client.py @@ -317,6 +317,26 @@ class GiteaClient: self._cache_put(key, milestones) return milestones + async def list_starred(self) -> list[dict]: + """Repos starred by the token user (issue #20: pinning rides + on Gitea stars — the forge stays the source of truth).""" + key = "user:starred" + cached = self._cache_get(key) + if cached is not None: + return cached + repos = await self._get_all_pages("/user/starred") + self._cache_put(key, repos) + return repos + + async def set_starred(self, owner: str, repo: str, starred: bool) -> None: + """Star / unstar a repo for the token user.""" + if starred: + r = await self.client.put(f"/user/starred/{owner}/{repo}") + else: + r = await self.client.delete(f"/user/starred/{owner}/{repo}") + r.raise_for_status() + self._cache_clear() + async def create_issue( self, owner: str, repo: str, title: str, body: str = "" ) -> dict: diff --git a/static/js/stars.js b/static/js/stars.js new file mode 100644 index 0000000..97762ff --- /dev/null +++ b/static/js/stars.js @@ -0,0 +1,56 @@ +/** + * Kanbante — pin projects via Gitea stars (issue #20). + * The star lives in Gitea (source of truth); this only toggles it + * through the API and moves the card between the pinned section + * and its home tab grid, without a page reload. + */ +(function () { + var pinnedSection = document.getElementById('pinned-section'); + var pinnedGrid = document.getElementById('pinned-grid'); + + function refreshPinnedVisibility() { + if (!pinnedSection || !pinnedGrid) return; + pinnedSection.hidden = pinnedGrid.querySelectorAll('.project-card').length === 0; + } + + document.querySelectorAll('.star-btn').forEach(function (btn) { + btn.addEventListener('click', async function (e) { + e.preventDefault(); + e.stopPropagation(); + var full = btn.getAttribute('data-full'); + var parts = full.split('/'); + var want = btn.getAttribute('data-starred') !== '1'; + btn.disabled = true; + try { + var fd = new FormData(); + fd.append('starred', want ? '1' : '0'); + fd.append('csrf_token', window.KANBANTE_CSRF || ''); + var res = await fetch('/api/projects/' + parts[0] + '/' + parts[1] + '/star', { + method: 'POST', body: fd, + }); + var data = await res.json(); + if (!data.ok) throw new Error(data.error || 'échec'); + btn.setAttribute('data-starred', want ? '1' : '0'); + btn.textContent = want ? '⭐' : '☆'; + btn.title = want ? 'Désépingler (étoile Gitea)' : 'Épingler (étoile Gitea)'; + var card = btn.closest('.project-card'); + if (card) { + if (want && pinnedGrid) { + pinnedGrid.appendChild(card); + } else { + var home = document.getElementById('grid-' + (card.getAttribute('data-home') || 'active')); + if (home) home.appendChild(card); + } + refreshPinnedVisibility(); + } + } catch (err) { + btn.textContent = '⚠️'; + setTimeout(function () { + btn.textContent = btn.getAttribute('data-starred') === '1' ? '⭐' : '☆'; + }, 1600); + } finally { + btn.disabled = false; + } + }); + }); +})(); diff --git a/tests/conftest.py b/tests/conftest.py index 348b4ed..b8247f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,6 +68,7 @@ class FakeGitea: def __init__(self) -> None: now = datetime.now(timezone.utc) + self.starred = set() self.repos = [ _repo("demo", now - timedelta(days=1)), _repo("vieux", now - timedelta(days=800)), @@ -270,6 +271,15 @@ class FakeGitea: }, ] + async def list_starred(self): + return [r for r in self.repos if r["full_name"] in self.starred] + + async def set_starred(self, owner, repo, starred): + if starred: + self.starred.add(f"{owner}/{repo}") + else: + self.starred.discard(f"{owner}/{repo}") + async def get_milestones(self, owner, repo, state="open"): return [ {