This commit is contained in:
@@ -42,7 +42,7 @@ public class SecurityConfig {
|
||||
.csrf(csrf -> csrf
|
||||
// Spiele-APIs werden per fetch (application/json) ohne Token aufgerufen -> ausnehmen.
|
||||
// Reine JSON-Endpunkte sind nicht über klassische HTML-Formulare CSRF-ausnutzbar.
|
||||
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**", "/api/syntaxsaga/**", "/api/devlab/**", "/api/codeescape/**", "/api/bughunter/**", "/api/serviceclash/**", "/api/networker/**")
|
||||
.ignoringRequestMatchers("/api/games/**", "/api/techtacular/**", "/api/mindmatch/**", "/api/syntaxsaga/**", "/api/devlab/**", "/api/codeescape/**", "/api/bughunter/**", "/api/serviceclash/**", "/api/networker/**", "/api/neuralnet/**")
|
||||
);
|
||||
|
||||
return http.build();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.mindflux.controller;
|
||||
import com.example.mindflux.service.NeuralNetService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/neuralnet")
|
||||
public class NeuralNetApiController {
|
||||
private final NeuralNetService service;
|
||||
public NeuralNetApiController(NeuralNetService service) { this.service=service; }
|
||||
@GetMapping("/overview") public NeuralNetService.Overview overview(Principal p) { return service.overview(p.getName()); }
|
||||
@PostMapping("/enable") public void enable(Principal p) { service.enable(p.getName()); }
|
||||
@PostMapping("/requests") public void request(Principal p,@RequestBody Map<String,String> body) { service.request(p.getName(),body.get("username")); }
|
||||
@PostMapping("/requests/{username}/accept") public void accept(Principal p,@PathVariable String username) { service.accept(p.getName(),username); }
|
||||
@PostMapping("/requests/{username}/decline") public void decline(Principal p,@PathVariable String username) { service.decline(p.getName(),username); }
|
||||
@DeleteMapping("/requests/{username}") public void cancel(Principal p,@PathVariable String username) { service.cancel(p.getName(),username); }
|
||||
@GetMapping("/messages/{username}") public Object messages(Principal p,@PathVariable String username) { return service.conversation(p.getName(),username); }
|
||||
@PostMapping("/messages") public Object send(Principal p,@RequestBody Map<String,String> body) { return service.send(p.getName(),body.get("recipient"),body.get("body")); }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "neuralnet_connections")
|
||||
public class NeuralNetConnection {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
private String requester;
|
||||
private String addressee;
|
||||
private String status;
|
||||
@Column(name = "created_at") private LocalDateTime createdAt;
|
||||
@Column(name = "responded_at") private LocalDateTime respondedAt;
|
||||
public NeuralNetConnection() {}
|
||||
public NeuralNetConnection(String requester, String addressee) { this.requester=requester; this.addressee=addressee; this.status="PENDING"; this.createdAt=LocalDateTime.now(); }
|
||||
public Long getId() { return id; } public String getRequester() { return requester; } public String getAddressee() { return addressee; }
|
||||
public String getStatus() { return status; } public void setStatus(String status) { this.status=status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getRespondedAt() { return respondedAt; }
|
||||
public void setRespondedAt(LocalDateTime respondedAt) { this.respondedAt=respondedAt; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "neuralnet_messages")
|
||||
public class NeuralNetMessage {
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
||||
private String sender;
|
||||
private String recipient;
|
||||
private String body;
|
||||
@Column(name = "sent_at") private LocalDateTime sentAt;
|
||||
@Column(name = "read_at") private LocalDateTime readAt;
|
||||
private boolean reported;
|
||||
public NeuralNetMessage() {}
|
||||
public NeuralNetMessage(String sender, String recipient, String body) { this.sender=sender; this.recipient=recipient; this.body=body; this.sentAt=LocalDateTime.now(); }
|
||||
public Long getId() { return id; } public String getSender() { return sender; } public String getRecipient() { return recipient; }
|
||||
public String getBody() { return body; } public LocalDateTime getSentAt() { return sentAt; } public LocalDateTime getReadAt() { return readAt; }
|
||||
public void setReadAt(LocalDateTime readAt) { this.readAt=readAt; } public boolean isReported() { return reported; } public void setReported(boolean reported) { this.reported=reported; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.mindflux.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "neuralnet_privacy")
|
||||
public class NeuralNetPrivacy {
|
||||
@Id private String username;
|
||||
private boolean enabled;
|
||||
public NeuralNetPrivacy() {}
|
||||
public NeuralNetPrivacy(String username, boolean enabled) { this.username = username; this.enabled = enabled; }
|
||||
public String getUsername() { return username; }
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.mindflux.repository;
|
||||
import com.example.mindflux.model.NeuralNetConnection;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import java.util.*;
|
||||
public interface NeuralNetConnectionRepository extends JpaRepository<NeuralNetConnection, Long> {
|
||||
Optional<NeuralNetConnection> findByRequesterAndAddressee(String requester, String addressee);
|
||||
List<NeuralNetConnection> findByAddresseeAndStatusOrderByCreatedAtAsc(String addressee, String status);
|
||||
List<NeuralNetConnection> findByRequesterAndStatusOrderByCreatedAtAsc(String requester, String status);
|
||||
@Query("select c from NeuralNetConnection c where c.status='ACCEPTED' and ((c.requester=:username) or (c.addressee=:username)) order by c.respondedAt desc")
|
||||
List<NeuralNetConnection> acceptedFor(@Param("username") String username);
|
||||
@Query("select c from NeuralNetConnection c where c.status='ACCEPTED' and ((c.requester=:a and c.addressee=:b) or (c.requester=:b and c.addressee=:a))")
|
||||
Optional<NeuralNetConnection> acceptedBetween(@Param("a") String a, @Param("b") String b);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.mindflux.repository;
|
||||
import com.example.mindflux.model.NeuralNetMessage;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import java.util.*;
|
||||
public interface NeuralNetMessageRepository extends JpaRepository<NeuralNetMessage, Long> {
|
||||
@Query("select m from NeuralNetMessage m where (m.sender=:a and m.recipient=:b) or (m.sender=:b and m.recipient=:a) order by m.sentAt asc")
|
||||
List<NeuralNetMessage> conversation(@Param("a") String a, @Param("b") String b);
|
||||
long countByRecipientAndReadAtIsNull(String recipient);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.example.mindflux.repository;
|
||||
import com.example.mindflux.model.NeuralNetPrivacy;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
public interface NeuralNetPrivacyRepository extends JpaRepository<NeuralNetPrivacy, String> {}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.example.mindflux.service;
|
||||
|
||||
import com.example.mindflux.model.*;
|
||||
import com.example.mindflux.repository.*;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class NeuralNetService {
|
||||
private final NeuralNetPrivacyRepository privacy;
|
||||
private final NeuralNetConnectionRepository connections;
|
||||
private final NeuralNetMessageRepository messages;
|
||||
private final UserRepository users;
|
||||
public NeuralNetService(NeuralNetPrivacyRepository privacy, NeuralNetConnectionRepository connections, NeuralNetMessageRepository messages, UserRepository users) {
|
||||
this.privacy=privacy; this.connections=connections; this.messages=messages; this.users=users;
|
||||
}
|
||||
public record Overview(String username, boolean enabled, List<String> connections, List<String> incoming, List<String> outgoing, long unread) {}
|
||||
public record ChatMessage(long id, String sender, String body, LocalDateTime sentAt) {}
|
||||
private boolean enabled(String username) { return privacy.findById(username).map(NeuralNetPrivacy::isEnabled).orElse(false); }
|
||||
private void requireEnabled(String username) { if (!enabled(username)) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "NeuralNet ist für dieses Konto nicht aktiviert."); }
|
||||
private void requireConnection(String a, String b) { if (connections.acceptedBetween(a,b).isEmpty()) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Private Nachrichten sind erst nach bestätigter Verbindung möglich."); }
|
||||
public void enable(String username) { privacy.save(new NeuralNetPrivacy(username, true)); }
|
||||
@Transactional(readOnly = true)
|
||||
public Overview overview(String username) {
|
||||
if (!enabled(username)) return new Overview(username, false, List.of(), List.of(), List.of(), 0);
|
||||
List<String> accepted=connections.acceptedFor(username).stream().map(c -> c.getRequester().equals(username) ? c.getAddressee() : c.getRequester()).toList();
|
||||
List<String> incoming=connections.findByAddresseeAndStatusOrderByCreatedAtAsc(username,"PENDING").stream().map(NeuralNetConnection::getRequester).toList();
|
||||
List<String> outgoing=connections.findByRequesterAndStatusOrderByCreatedAtAsc(username,"PENDING").stream().map(NeuralNetConnection::getAddressee).toList();
|
||||
return new Overview(username, true, accepted, incoming, outgoing, messages.countByRecipientAndReadAtIsNull(username));
|
||||
}
|
||||
public void request(String username, String target) {
|
||||
requireEnabled(username); target=cleanUser(target);
|
||||
var targetUser=users.findByUsernameIgnoreCase(target).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Dieses NeuralNet-Mitglied ist nicht verfügbar."));
|
||||
target=targetUser.getUsername();
|
||||
if (username.equalsIgnoreCase(target) || !enabled(target)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dieses NeuralNet-Mitglied ist nicht verfügbar.");
|
||||
if (connections.findByRequesterAndAddressee(username,target).isPresent() || connections.findByRequesterAndAddressee(target,username).isPresent()) throw new ResponseStatusException(HttpStatus.CONFLICT, "Zwischen euch besteht bereits eine Anfrage oder Verbindung.");
|
||||
connections.save(new NeuralNetConnection(username,target));
|
||||
}
|
||||
public void accept(String username, String requester) { var c=pendingFor(username,requester); c.setStatus("ACCEPTED"); c.setRespondedAt(LocalDateTime.now()); connections.save(c); }
|
||||
public void decline(String username, String requester) { connections.delete(pendingFor(username,requester)); }
|
||||
public void cancel(String username, String target) { connections.delete(connections.findByRequesterAndAddressee(username,cleanUser(target)).filter(c -> c.getStatus().equals("PENDING")).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Anfrage nicht gefunden."))); }
|
||||
@Transactional
|
||||
public List<ChatMessage> conversation(String username, String other) {
|
||||
requireEnabled(username); other=cleanUser(other); requireConnection(username,other);
|
||||
var result=messages.conversation(username,other);
|
||||
result.stream().filter(m -> m.getRecipient().equals(username) && m.getReadAt()==null).forEach(m -> m.setReadAt(LocalDateTime.now()));
|
||||
return result.stream().map(m -> new ChatMessage(m.getId(),m.getSender(),m.getBody(),m.getSentAt())).toList();
|
||||
}
|
||||
public ChatMessage send(String username, String recipient, String body) {
|
||||
requireEnabled(username); recipient=cleanUser(recipient); requireConnection(username,recipient);
|
||||
String text=body==null ? "" : body.trim();
|
||||
if (text.isBlank() || text.length()>2000) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Eine Nachricht benötigt 1 bis 2000 Zeichen.");
|
||||
var m=messages.save(new NeuralNetMessage(username,recipient,text));
|
||||
return new ChatMessage(m.getId(),m.getSender(),m.getBody(),m.getSentAt());
|
||||
}
|
||||
private NeuralNetConnection pendingFor(String addressee,String requester) { return connections.findByRequesterAndAddressee(cleanUser(requester),addressee).filter(c -> c.getStatus().equals("PENDING")).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,"Anfrage nicht gefunden.")); }
|
||||
private String cleanUser(String username) { if (username==null || username.trim().isBlank() || username.trim().length()>100) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"Ungültiger Benutzername."); return username.trim(); }
|
||||
}
|
||||
@@ -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