- ReviewsSection (Server Component) mit Durchschnitt, zugaenglichen Sternen, Initialen statt Profilbildern, relativer Zeit, Links zur Rezension und zum Maps-Eintrag sowie Hinweis "Rezensionen von Google" - lange Texte optisch gekuerzt, "Weiterlesen" mit aria-expanded - ohne Daten ausgeblendet; in Entwicklung/Staging ein Einrichtungshinweis - Laden parallel zu loadPublicPage, Fehler blenden nur den Abschnitt aus - Links aus der API-Antwort nur zu HTTPS-Google-Hosts (safeGoogleUrl) - Linkstile und Browser-Oberflaechen (Scrollbar, Caret, Unterstreichung) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
/**
|
||
* Anzeigehilfen fuer Google-Rezensionen. Reine Funktionen ohne
|
||
* Seiteneffekte, damit sie ohne Netzwerk testbar sind.
|
||
*/
|
||
|
||
/** Ab dieser Laenge wird eine Rezension zunaechst gekuerzt angezeigt. */
|
||
export const REVIEW_CLAMP_CHARS = 320;
|
||
|
||
/** Initialen statt Profilbild: bis zu zwei Buchstaben aus dem Anzeigenamen. */
|
||
export function initialsOf(name: string): string {
|
||
const letters = name
|
||
.trim()
|
||
.split(/\s+/)
|
||
.map((part) => Array.from(part.replace(/[^\p{L}\p{N}]/gu, ""))[0] ?? "")
|
||
.filter(Boolean);
|
||
if (letters.length === 0) return "?";
|
||
const pick = letters.length === 1 ? [letters[0]] : [letters[0], letters[letters.length - 1]];
|
||
return pick.join("").toLocaleUpperCase("de-DE");
|
||
}
|
||
|
||
/** "4,8" – Durchschnitt mit einer Nachkommastelle im deutschen Format. */
|
||
export function formatRating(rating: number): string {
|
||
return rating.toLocaleString("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 1 });
|
||
}
|
||
|
||
/** Zugaenglicher Text fuer eine Sternebewertung, z. B. "4 von 5 Sternen". */
|
||
export function starsLabel(rating: number): string {
|
||
const value = Number.isInteger(rating) ? String(rating) : formatRating(rating);
|
||
return `${value} von 5 Sternen`;
|
||
}
|
||
|
||
/** "1 Rezension" bzw. "12 Rezensionen". */
|
||
export function reviewCountLabel(count: number): string {
|
||
return `${count.toLocaleString("de-DE")} ${count === 1 ? "Rezension" : "Rezensionen"}`;
|
||
}
|
||
|
||
export function needsClamp(text: string): boolean {
|
||
return text.length > REVIEW_CLAMP_CHARS;
|
||
}
|