- SQLite-Import, migrate.sh, notes/migrations.md und altes jQuery-Skript entfernt - Backend: scanEntry/queryEntries statt doppelter Scan-Liste, Feed-Queries direkt statt feedWhere, Thread ohne Map-Umweg, gemeinsame Helfer für Cookie-Löschen und Mediennamen, usernameTaken - Frontend: PostForm für Beitrag und Antwort, EntryCard in Deleted/LiveCard aufgeteilt, tote CSS-Regeln und Typen entfernt, Strich-Regeln zusammengelegt - Rechtstexte verlinken /stats; Kommentare ohne Vorgeschichte Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q
35 lines
1.1 KiB
TypeScript
35 lines
1.1 KiB
TypeScript
import type { ReactNode } from "react";
|
|
|
|
// avatarUrl liefert den Bildpfad oder das Platzhalterbild bei leerem Avatar.
|
|
export function avatarUrl(path: string): string {
|
|
return path ? `/${path}` : "/static/assets/no_profile_pic.jpg";
|
|
}
|
|
|
|
export function formatTime(unix: number): string {
|
|
return new Date(unix * 1000).toLocaleString("de-DE");
|
|
}
|
|
|
|
const urlPattern = /https?:\/\/[^\s<]+/g;
|
|
|
|
// linkify macht aus URLs im Text Links und aus Zeilenumbrüchen <br>. Den Text
|
|
// escaped React selbst.
|
|
export function linkify(text: string): ReactNode[] {
|
|
const out: ReactNode[] = [];
|
|
text.split("\n").forEach((line, i) => {
|
|
if (i > 0) out.push(<br key={`br${i}`} />);
|
|
let last = 0;
|
|
for (const m of line.matchAll(urlPattern)) {
|
|
const start = m.index;
|
|
if (start > last) out.push(line.slice(last, start));
|
|
out.push(
|
|
<a key={`a${i}-${start}`} href={m[0]} target="_blank" rel="noopener">
|
|
{m[0]}
|
|
</a>,
|
|
);
|
|
last = start + m[0].length;
|
|
}
|
|
if (last < line.length) out.push(line.slice(last));
|
|
});
|
|
return out;
|
|
}
|