This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE neuralnet_privacy (
|
||||
username VARCHAR(100) PRIMARY KEY REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
enabled BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE neuralnet_connections (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
requester VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
addressee VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
responded_at TIMESTAMP,
|
||||
CONSTRAINT uq_neuralnet_connection UNIQUE (requester, addressee),
|
||||
CONSTRAINT chk_neuralnet_no_self CHECK (requester <> addressee)
|
||||
);
|
||||
CREATE INDEX idx_neuralnet_connection_addressee ON neuralnet_connections(addressee, status);
|
||||
|
||||
CREATE TABLE neuralnet_messages (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
sender VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
recipient VARCHAR(100) NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
body VARCHAR(2000) NOT NULL,
|
||||
sent_at TIMESTAMP NOT NULL,
|
||||
read_at TIMESTAMP,
|
||||
reported BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE INDEX idx_neuralnet_message_pair ON neuralnet_messages(sender, recipient, sent_at);
|
||||
CREATE INDEX idx_neuralnet_message_unread ON neuralnet_messages(recipient, read_at);
|
||||
@@ -0,0 +1 @@
|
||||
.nn-head{margin:1.3rem 0}.nn-head>span,.nn-chat small{color:var(--accent);font:700 .72rem/1 monospace;letter-spacing:.14em}.nn-head h1{margin:.4rem 0;color:var(--heading)}.nn-head p{color:var(--muted)}.nn-optin{max-width:700px}.nn-optin p{color:var(--muted);line-height:1.6}.nn-app{display:grid;grid-template-columns:minmax(270px,340px) 1fr;gap:1rem}.nn-sidebar{display:grid;align-content:start;gap:1rem}.nn-request div{display:flex;gap:.45rem}.nn-request .mf-input{min-width:0}.nn-section{border-top:1px solid var(--border);padding-top:.85rem}.nn-section h2{font-size:.82rem;color:var(--heading);margin:0 0 .5rem}.nn-list{display:grid;gap:.45rem}.nn-item{display:flex;align-items:center;justify-content:space-between;gap:.5rem;border:1px solid var(--border);background:var(--surface-2);padding:.55rem .65rem;border-radius:8px;color:var(--text);font-size:.84rem}.nn-item button{border:0;background:none;color:var(--secondary);font:inherit;cursor:pointer}.nn-item button:hover,.nn-item button.active{color:var(--accent)}.nn-actions{display:flex;gap:.35rem}.nn-actions button{border:1px solid var(--border);padding:.22rem .38rem;border-radius:5px}.nn-chat{min-height:530px}.nn-empty{height:100%;min-height:430px;display:grid;place-content:center;text-align:center;gap:.5rem;color:var(--muted)}.nn-empty strong{color:var(--heading);font-size:1.1rem}.nn-chat header{border-bottom:1px solid var(--border);padding-bottom:.9rem}.nn-chat h2{margin:.25rem 0 0;color:var(--heading)}.nn-messages{display:flex;flex-direction:column;gap:.6rem;height:380px;overflow:auto;padding:1rem 0}.nn-message{align-self:flex-start;max-width:min(80%,550px);padding:.65rem .8rem;border-radius:10px;background:var(--surface-2);border:1px solid var(--border);color:var(--text);line-height:1.45}.nn-message.mine{align-self:flex-end;border-color:var(--border-accent);background:color-mix(in srgb,var(--accent) 12%,var(--surface))}.nn-message small{display:block;margin-top:.35rem;color:var(--muted);font-size:.66rem}.nn-compose{display:grid;grid-template-columns:1fr auto;gap:.6rem;border-top:1px solid var(--border);padding-top:.85rem}.nn-compose textarea{min-height:52px;resize:vertical;background:var(--surface-2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:.65rem;font:inherit}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:800px){.nn-app{grid-template-columns:1fr}.nn-chat{min-height:400px}.nn-messages{height:300px}.nn-request div{flex-wrap:wrap}.nn-request .mf-input{flex:1}}
|
||||
@@ -0,0 +1 @@
|
||||
(function(){'use strict';var selected=null,me='',poll=null;function q(s){return document.querySelector(s)}function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}async function api(url,method,body){var r=await fetch('/api/neuralnet'+url,{method:method||'GET',headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});if(!r.ok){var e=await r.json().catch(()=>({message:'Anfrage fehlgeschlagen.'}));throw new Error(e.message)}return r.status===204?null:r.json()}function item(name,actions){return '<div class="nn-item"><span>'+esc(name)+'</span><span class="nn-actions">'+actions+'</span></div>'}function render(data){q('[data-nn-incoming]').innerHTML=data.incoming.length?data.incoming.map(n=>item(n,'<button data-action="accept" data-user="'+esc(n)+'">Annehmen</button><button data-action="decline" data-user="'+esc(n)+'">Ablehnen</button>')).join(''):'<span class="mf-small-hint">Keine offenen Anfragen.</span>';q('[data-nn-outgoing]').innerHTML=data.outgoing.length?data.outgoing.map(n=>item(n,'<button data-action="cancel" data-user="'+esc(n)+'">Zurückziehen</button>')).join(''):'<span class="mf-small-hint">Keine gesendeten Anfragen.</span>';q('[data-nn-connections]').innerHTML=data.connections.length?data.connections.map(n=>item(n,'<button data-action="chat" data-user="'+esc(n)+'" class="'+(selected===n?'active':'')+'">Chat</button>')).join(''):'<span class="mf-small-hint">Noch keine Verbindungen.</span>'}async function refresh(){var data=await api('/overview');me=data.username;q('#nnOptIn').hidden=data.enabled;q('#nnApp').hidden=!data.enabled;if(data.enabled)render(data)}async function loadChat(){if(!selected)return;var rows=await api('/messages/'+encodeURIComponent(selected));q('[data-nn-empty]').hidden=true;q('[data-nn-conversation]').hidden=false;q('[data-nn-partner]').textContent=selected;q('[data-nn-messages]').innerHTML=rows.map(m=>'<article class="nn-message '+(m.sender===me?'mine':'')+'">'+esc(m.body)+'<small>'+esc(m.sender)+' · '+new Date(m.sentAt).toLocaleString('de-DE',{dateStyle:'short',timeStyle:'short'})+'</small></article>').join('');var box=q('[data-nn-messages]');box.scrollTop=box.scrollHeight}function message(text){var old=q('.nn-head p');old.textContent=text;setTimeout(()=>old.textContent='Erst eine bestätigte Anfrage, dann ein geschützter Nachrichtenkanal.',3500)}function init(){q('[data-nn-enable]').onclick=async()=>{await api('/enable','POST');refresh()};q('[data-nn-request]').onsubmit=async e=>{e.preventDefault();try{await api('/requests','POST',{username:q('#nn-user').value});q('#nn-user').value='';message('Kontaktanfrage gesendet.')}catch(x){message(x.message)}refresh()};q('.nn-sidebar').onclick=async e=>{var b=e.target.closest('[data-action]');if(!b)return;var u=b.dataset.user,a=b.dataset.action;try{if(a==='chat'){selected=u;await refresh();await loadChat();return}if(a==='accept')await api('/requests/'+encodeURIComponent(u)+'/accept','POST');if(a==='decline')await api('/requests/'+encodeURIComponent(u)+'/decline','POST');if(a==='cancel')await api('/requests/'+encodeURIComponent(u),'DELETE');await refresh()}catch(x){message(x.message)}};q('[data-nn-compose]').onsubmit=async e=>{e.preventDefault();try{await api('/messages','POST',{recipient:selected,body:q('#nn-message').value});q('#nn-message').value='';loadChat()}catch(x){message(x.message)}};refresh().catch(x=>message(x.message));poll=setInterval(()=>{refresh();if(selected)loadChat()},5000)}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init()})();
|
||||
@@ -64,7 +64,7 @@
|
||||
</svg>
|
||||
<span class="ft-trigger-dot" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button class="mf-iconbtn" type="button" aria-label="Benachrichtigungen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></button>
|
||||
<a class="mf-iconbtn" href="/soziales" aria-label="NeuralNet öffnen" title="NeuralNet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></a>
|
||||
<div class="mf-search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg><input placeholder="Suche..."></div>
|
||||
<button class="mf-theme"></button>
|
||||
<form th:action="@{/logout}" method="post" style="display:inline">
|
||||
|
||||
@@ -1,51 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="dark" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SysnaptiK Neural Soziales</title>
|
||||
<link rel="stylesheet" href="/css/theme.css?v=3">
|
||||
<script src="/js/theme.js?v=3"></script>
|
||||
<style>
|
||||
.mf-soziales { text-align:center; padding:2.5rem 1.5rem; }
|
||||
.mf-soziales h1 { color:var(--heading); font-size:1.6rem; line-height:1.4; margin-bottom:1rem; }
|
||||
.mf-soziales p { color:var(--text); line-height:1.6; }
|
||||
.mf-time-machine { font-size:3rem; margin:1.6rem 0; filter:drop-shadow(0 0 12px var(--glow-s)); }
|
||||
.mf-small-hint { color:var(--muted); font-size:.86rem; margin-top:1rem; }
|
||||
</style>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NeuralNet – SysnaptiK Neural</title>
|
||||
<link rel="stylesheet" href="/css/theme.css?v=3"><link rel="stylesheet" href="/css/neuralnet.css?v=1">
|
||||
<script src="/js/theme.js?v=3"></script><script src="/js/neuralnet.js?v=1" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="mf-theme-float"><button class="mf-theme"></button></div>
|
||||
|
||||
<div class="mf-container">
|
||||
|
||||
<div class="mf-page-head">
|
||||
<a th:href="@{/dashboard}" class="mf-back">⟵ Zurück zum Dashboard</a>
|
||||
</div>
|
||||
|
||||
<nav th:replace="~{fragments/layout :: profileTabs('soziales')}"></nav>
|
||||
|
||||
<div class="mf-panel accent mf-soziales">
|
||||
<h1>„Straßen? Wo wir hingehen, brauchen wir keine Straßen!“ 🚀</h1>
|
||||
<p>Das soziale Netzwerk für SysnaptiK Neural-User befindet sich noch in Entwicklung.<br>
|
||||
Bald kannst du dich hier mit anderen Neural-Mitgliedern verbinden!</p>
|
||||
<div class="mf-time-machine">
|
||||
🚗💨💥
|
||||
</div>
|
||||
<p class="mf-small-hint">Zurück in die Zukunft... und bald auch ins SysnaptiK Neural-Netzwerk!</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="mf-footer">
|
||||
<a href="#">Support</a>
|
||||
<a href="#">Feedback</a>
|
||||
<a href="/nutzungsbedingungen">Nutzungsbedingungen</a>
|
||||
<a href="/richtlinien">Richtlinien</a>
|
||||
<a href="/datenschutz">Datenschutz</a>
|
||||
<a href="/impressum">Impressum</a>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<main class="mf-container">
|
||||
<div class="mf-page-head"><a th:href="@{/dashboard}" class="mf-back">⟵ Zurück zum Dashboard</a></div>
|
||||
<nav th:replace="~{fragments/layout :: profileTabs('soziales')}"></nav>
|
||||
<section class="nn-head"><span>NEURALNET</span><h1>Private Lernverbindungen</h1><p>Erst eine bestätigte Anfrage, dann ein geschützter Nachrichtenkanal.</p></section>
|
||||
<section id="nnOptIn" class="mf-panel nn-optin" hidden><h2>NeuralNet aktivieren</h2><p>Mit der Aktivierung wirst du für andere Mitglieder auffindbar und kannst Kontaktanfragen senden oder erhalten. Private Nachrichten sind erst nach gegenseitiger Verbindung möglich.</p><button class="mf-btn" data-nn-enable>NeuralNet aktivieren</button></section>
|
||||
<section id="nnApp" class="nn-app" hidden>
|
||||
<aside class="mf-panel nn-sidebar">
|
||||
<form class="nn-request" data-nn-request><label class="mf-label" for="nn-user">Verbindung anfragen</label><div><input class="mf-input" id="nn-user" maxlength="100" placeholder="Benutzername" required><button class="mf-btn" type="submit">Senden</button></div></form>
|
||||
<div class="nn-section"><h2>Offene Anfragen</h2><div data-nn-incoming class="nn-list"></div></div>
|
||||
<div class="nn-section"><h2>Gesendet</h2><div data-nn-outgoing class="nn-list"></div></div>
|
||||
<div class="nn-section"><h2>Verbindungen</h2><div data-nn-connections class="nn-list"></div></div>
|
||||
</aside>
|
||||
<section class="mf-panel nn-chat">
|
||||
<div data-nn-empty class="nn-empty"><strong>Wähle eine Verbindung</strong><span>Private Nachrichten werden erst nach einer angenommenen Anfrage freigeschaltet.</span></div>
|
||||
<div data-nn-conversation hidden><header><div><small>PRIVATE VERBINDUNG</small><h2 data-nn-partner></h2></div></header><div class="nn-messages" data-nn-messages aria-live="polite"></div><form class="nn-compose" data-nn-compose><label class="sr-only" for="nn-message">Nachricht</label><textarea id="nn-message" maxlength="2000" placeholder="Nachricht schreiben …" required></textarea><button class="mf-btn" type="submit">Senden</button></form></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<footer class="mf-footer"><a href="#">Support</a><a href="#">Feedback</a><a href="/nutzungsbedingungen">Nutzungsbedingungen</a><a href="/richtlinien">Richtlinien</a><a href="/datenschutz">Datenschutz</a><a href="/impressum">Impressum</a></footer>
|
||||
</body></html>
|
||||
|
||||
Reference in New Issue
Block a user