Make the invite copyable and widen what feedback invites

Three fixes from using the admin panel and the site:

- The invite link was an anchor, but an invite is something to send, not to
  follow — clicking it opened the join form in the admin's own browser. It
  is now the URL beside a Kopioi button. The handler reads the text out of
  the sibling element rather than interpolating the URL into JS, so there is
  nothing to escape, and where the clipboard API is missing (it needs a
  secure context, which the documented SSH tunnel to localhost provides) it
  selects the text instead of leaving a button that does nothing.
- "Ilmoita ongelmasta" framed the feedback form as a bug tracker when it is
  meant to take ideas and general feedback too. The footer now asks
  "Ongelmia? Ideoita? Palautetta?", and the page it leads to answers all
  three: the ingress covers ideas explicitly and the placeholder suggests a
  feature rather than a fault.
- "Kuuntele YouTubessa" opens in a new tab. Leaving the page mid-review
  would lose whatever is already typed into the review form.
This commit is contained in:
Esa Kataja
2026-08-02 20:57:34 +03:00
parent 1fe5211ae6
commit 60660849c7
5 changed files with 40 additions and 7 deletions
+4
View File
@@ -743,6 +743,10 @@ td.break { word-break: break-all; font-size: 0.8rem; }
code { background: var(--surface-raised); padding: 0.1rem var(--space-1); code { background: var(--surface-raised); padding: 0.1rem var(--space-1);
border-radius: var(--radius); font-size: 0.85rem; } border-radius: var(--radius); font-size: 0.85rem; }
/* The link is long and the button must stay reachable next to it on a narrow admin window. */
.invitecell { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.invitecell code { word-break: break-all; }
/* --- toasts --- */ /* --- toasts --- */
.toasts { position: fixed; right: var(--space-4); bottom: var(--space-4); z-index: 1000; .toasts { position: fixed; right: var(--space-4); bottom: var(--space-4); z-index: 1000;
+30 -2
View File
@@ -13,8 +13,11 @@
<tbody> <tbody>
{{range .Data.Invites}} {{range .Data.Invites}}
<tr> <tr>
<td> <!-- Not a link: an invite is something to send, never to follow. A click used to open the
<a href="{{.Link}}">{{.Link}}</a> join form in the admin's own browser, which is never what was wanted. -->
<td class="invitecell">
<code>{{.Link}}</code>
<button type="button" class="ghost" onclick="copyInvite(this)">Kopioi</button>
</td> </td>
<td class="nowrap"><span class="dot on"></span> käyttämätön</td> <td class="nowrap"><span class="dot on"></span> käyttämätön</td>
<td>{{fidate .CreatedAt}}</td> <td>{{fidate .CreatedAt}}</td>
@@ -87,4 +90,29 @@
</div> </div>
</section> </section>
<script>
// The clipboard API needs a secure context. Over the documented SSH tunnel the origin is
// localhost, which qualifies; reached any other way it is missing, so selecting the text is the
// fallback — the admin presses Ctrl+C instead of being left with a button that does nothing.
function copyInvite(button) {
const link = button.previousElementSibling;
const done = () => {
button.textContent = 'Kopioitu';
setTimeout(() => { button.textContent = 'Kopioi'; }, 1500);
};
if (navigator.clipboard) {
navigator.clipboard.writeText(link.textContent).then(done, () => selectText(link));
} else {
selectText(link);
}
}
function selectText(el) {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
</script>
{{end}} {{end}}
+1 -1
View File
@@ -62,7 +62,7 @@
<footer class="sitefooter"> <footer class="sitefooter">
{{if .Member}} {{if .Member}}
<!-- The server already knows where they were, so the path travels in the link — no JS. --> <!-- The server already knows where they were, so the path travels in the link — no JS. -->
<a href="/report?from={{.Path}}">Ilmoita ongelmasta</a> · <a href="/report?from={{.Path}}">Ongelmia? Ideoita? Palautetta?</a> ·
{{end}} {{end}}
<span class="slogan">We know good music, baby!</span> <span class="slogan">We know good music, baby!</span>
<span class="copyright">© Kessinen</span> <span class="copyright">© Kessinen</span>
+3 -3
View File
@@ -1,13 +1,13 @@
{{define "content"}} {{define "content"}}
<h1>Palaute</h1> <h1>Palaute</h1>
<p class="muted">Kerro mikä on rikki tai ärsyttää. Ei kategorioita eikä prioriteetteja — yksi <p class="muted">Ongelmat, ideat ja kaikki muu palaute samaan paikkaan. Ei kategorioita eikä
virke riittää.</p> prioriteetteja — yksi virke riittää.</p>
<form method="post" action="/report" class="stack"> <form method="post" action="/report" class="stack">
<input type="hidden" name="from" value="{{.Data.From}}"> <input type="hidden" name="from" value="{{.Data.From}}">
<label>Palaute <label>Palaute
<textarea name="body" rows="6" maxlength="2000" required autofocus <textarea name="body" rows="6" maxlength="2000" required autofocus
placeholder="Esim. soitin ei toimi puhelimella."></textarea> placeholder="Esim. soittimeen kaipaisi kelausta."></textarea>
</label> </label>
<button type="submit">Lähetä palaute</button> <button type="submit">Lähetä palaute</button>
</form> </form>
+2 -1
View File
@@ -74,7 +74,8 @@
{{with $s.Description}}<p class="intro">{{.}}</p>{{end}} {{with $s.Description}}<p class="intro">{{.}}</p>{{end}}
{{end}} {{end}}
{{with $s.SourceURL}}<p class="muted small"><a href="{{.}}" rel="noreferrer">Kuuntele YouTubessa</a></p>{{end}} {{/* New tab: leaving the page mid-review would lose whatever is already typed in the form. */}}
{{with $s.SourceURL}}<p class="muted small"><a href="{{.}}" target="_blank" rel="noreferrer">Kuuntele YouTubessa</a></p>{{end}}
{{if and (not $s.CanReview) (or $s.Lyrics $s.Own)}} {{if and (not $s.CanReview) (or $s.Lyrics $s.Own)}}
<!-- Only when the review strip is not already showing them: while reviewing, the lyrics live in <!-- Only when the review strip is not already showing them: while reviewing, the lyrics live in