- Freunde (freunde.py, Tabelle friendships): suchen, anfragen, annehmen, ablehnen, entfernen; neue Seite "Freunde" im Menü - Vorratslisten und Kanban-Projekte immer privat: sichtbar für Ersteller und Mitglieder; teilen nur mit Freunden (Admins: mit allen); Kanban-Mitglieder (Tabelle kanban_project_members), Bearbeiter nur aus dem Projekt, ausgetretene Mitglieder verlieren ihre Zuteilung - Artikel: lesen alle; bearbeiten Autor, Mitarbeitende verknüpfter Kanban-Projekte und Admins; verknüpfen nur bearbeitbare Artikel (sonst ließen sich Rechte erschleichen); Board-Liste auf der Artikelseite zeigt nur sichtbare Boards - Administratoren sehen und bearbeiten alles - Reisen: Städte nur von Freunden (Admins: alle); "Reiserouten zeigen" zeichnet die Strecken der Kilometer-Rechnung als Bögen - Migration Schritt 6: bisher gemeinsame Listen und alle Projekte bekommen die vorhandenen Konten als Mitglieder, ohne Ersteller → erster Admin; erste Liste einer neuen Installation gehört dem ersten Konto - Tests: test_freunde.py, test_zugriff.py, bestehende auf Privat-Modell umgestellt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.7 KiB
React
102 lines
4.7 KiB
React
import { useEffect, useRef, useState } from 'react';
|
|
import { api } from '../api.js';
|
|
import useLiveData from '../useLiveData.js';
|
|
|
|
// Freunde: Grundlage fürs Teilen von Listen und Kanban-Projekten und für fremde Punkte auf der Reise-Karte
|
|
export default function Freunde() {
|
|
const { data, error, busy, run } = useLiveData('/api/freunde', { refresh: 30 });
|
|
const [q, setQ] = useState('');
|
|
const [results, setResults] = useState([]);
|
|
const timer = useRef(null);
|
|
|
|
useEffect(() => {
|
|
clearTimeout(timer.current);
|
|
if (q.trim().length < 2) { setResults([]); return undefined; }
|
|
timer.current = setTimeout(() => {
|
|
api('GET', `/api/freunde/suche?q=${encodeURIComponent(q.trim())}`).then(setResults).catch(() => setResults([]));
|
|
}, 250);
|
|
return () => clearTimeout(timer.current);
|
|
}, [q, data]);
|
|
|
|
const act = (method, person) => run(method, `/api/freunde/${person.id}`);
|
|
const label = { freund: '✓ Freund', angefragt: 'Angefragt', eingeladen: 'Hat dich angefragt' };
|
|
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<h1>Freunde</h1>
|
|
<p className="lead">Mit Freunden teilst du Vorratslisten und Kanban-Projekte und siehst ihre Reise-Punkte. Eine Freundschaft gilt erst, wenn beide zugestimmt haben.</p>
|
|
</div>
|
|
</div>
|
|
{error && <p className="error" role="alert">{error}</p>}
|
|
|
|
<section className="card friends-search" aria-labelledby="freunde-suche">
|
|
<header className="household-head"><h2 id="freunde-suche">Freund hinzufügen</h2></header>
|
|
<label className="sr-only" htmlFor="friend-q">Name oder Benutzername</label>
|
|
<input id="friend-q" className="friends-input" value={q} onChange={(e) => setQ(e.target.value)} placeholder="Name oder Benutzername …" autoComplete="off" />
|
|
{results.length > 0 && (
|
|
<ul className="friends-list">
|
|
{results.map((p) => (
|
|
<li key={p.id}>
|
|
<span><strong>{p.name}</strong> <small>@{p.username}</small></span>
|
|
{p.status === 'freund' || p.status === 'angefragt'
|
|
? <span className="hint">{label[p.status]}</span>
|
|
: <button className="btn small" type="button" disabled={busy} onClick={() => act('POST', p)}>{p.status === 'eingeladen' ? 'Annehmen' : 'Anfragen'}</button>}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
{q.trim().length >= 2 && results.length === 0 && <p className="hint">Kein Konto gefunden.</p>}
|
|
</section>
|
|
|
|
{data?.incoming.length > 0 && (
|
|
<section className="friends-section" aria-labelledby="freunde-anfragen">
|
|
<h2 className="section-title" id="freunde-anfragen">Anfragen an dich</h2>
|
|
<ul className="friends-list">
|
|
{data.incoming.map((p) => (
|
|
<li key={p.id}>
|
|
<span><strong>{p.name}</strong> <small>@{p.username}</small></span>
|
|
<span className="friends-actions">
|
|
<button className="btn small" type="button" disabled={busy} onClick={() => act('POST', p)}>Annehmen</button>
|
|
<button className="link-btn danger" type="button" disabled={busy} onClick={() => act('DELETE', p)}>Ablehnen</button>
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
|
|
<section className="friends-section" aria-labelledby="freunde-liste">
|
|
<h2 className="section-title" id="freunde-liste">Deine Freunde</h2>
|
|
{data && !data.friends.length && <p className="hint">Noch keine Freunde. Oben suchen und anfragen.</p>}
|
|
<ul className="friends-list">
|
|
{data?.friends.map((p) => (
|
|
<li key={p.id}>
|
|
<span><strong>{p.name}</strong> <small>@{p.username}</small></span>
|
|
<button className="link-btn danger" type="button" disabled={busy}
|
|
onClick={() => window.confirm(`Freundschaft mit ${p.name} beenden? Geteilte Listen und Projekte bleiben geteilt, bis du sie dort entfernst.`) && act('DELETE', p)}>
|
|
Entfernen
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
{data?.outgoing.length > 0 && (
|
|
<section className="friends-section" aria-labelledby="freunde-offen">
|
|
<h2 className="section-title" id="freunde-offen">Deine offenen Anfragen</h2>
|
|
<ul className="friends-list">
|
|
{data.outgoing.map((p) => (
|
|
<li key={p.id}>
|
|
<span><strong>{p.name}</strong> <small>@{p.username}</small></span>
|
|
<button className="link-btn" type="button" disabled={busy} onClick={() => act('DELETE', p)}>Zurückziehen</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
)}
|
|
</>
|
|
);
|
|
}
|