Sortierung nach Titel und haengendes Overlay beheben
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 2m3s

Sortierung: Erklaerungen erscheinen im Admin wie im oeffentlichen
Bereich alphabetisch nach Titel. Bewusst in JavaScript statt ueber die
Datenbank - SQLite vergleicht binaer, dort landeten "Aenderungs-
mitteilung" und "Uebernahme" hinter "Zaehlerstandsablesung". Intl mit
deutscher Kollation sortiert Ae bei A. Gemeinsame Regel in
lib/policy-sort.ts, benutzt von der Server-Komponente, der API und der
Admin-Tabelle; die Spaltenkoepfe bleiben umschaltbar.

Overlay: @radix-ui/react-select verlangte react-dismissable-layer
1.1.11, alle anderen Radix-Pakete 1.1.19. Dieses Paket setzt und
entfernt pointer-events auf dem body und fuehrt dazu eine Liste offener
Layer im Modulzustand - bei zwei Kopien zwei getrennte Listen. Ein
Select im Dialog liess deshalb beim Schliessen das Overlay des Dialogs
verschwinden und pointer-events: none stehen. Die Seite war danach bis
zum Neuladen tot.

Belegt im Browser mit einer Wegwerfseite (Select im Dialog):
  zwei Versionen -> Dialog nach Select-Schliessen nicht mehr bedienbar,
                    body pointer-events bleibt "none"
  eine Version   -> body wieder leer, Seite klickbar

react-select, -label, -slot und -tabs auf aktuelle Versionen gehoben;
damit liegt jedes Radix-Kernpaket nur noch einmal im Baum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Weapie 2026-09-08 13:37:58 +02:00
parent 150fd3878c
commit c951a4123a
6 changed files with 156 additions and 2451 deletions

View File

@ -65,6 +65,7 @@ import { deletePolicy, usePrivacyPolicies } from "@/hooks/use-privacy-policies"
import { usePolicyDefaults } from "@/hooks/use-policy-defaults"
import { describeMissing, missingRequiredFields } from "@/lib/policy-completeness"
import { formatDateTime, initials } from "@/lib/format"
import { compareByTitle } from "@/lib/policy-sort"
import { PrivacyPolicy } from "@/types/privacy-policy"
type SortKey = "title" | "createdAt" | "updatedAt"
@ -75,8 +76,9 @@ export default function AdminDashboard() {
const { defaults } = usePolicyDefaults()
const [searchTerm, setSearchTerm] = useState("")
const [sortBy, setSortBy] = useState<SortKey>("createdAt")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc")
// Vorgabe: alphabetisch nach Titel. Die Spaltenköpfe bleiben umschaltbar.
const [sortBy, setSortBy] = useState<SortKey>("title")
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc")
const [isFormOpen, setIsFormOpen] = useState(false)
const [editingPolicy, setEditingPolicy] = useState<PrivacyPolicy | null>(null)
const [previewPolicy, setPreviewPolicy] = useState<PrivacyPolicy | null>(null)
@ -99,9 +101,7 @@ export default function AdminDashboard() {
)
.sort((a, b) => {
if (sortBy === "title") {
return sortOrder === "asc"
? a.title.localeCompare(b.title, "de")
: b.title.localeCompare(a.title, "de")
return sortOrder === "asc" ? compareByTitle(a, b) : compareByTitle(b, a)
}
const aDate = new Date(a[sortBy]).getTime()
const bDate = new Date(b[sortBy]).getTime()

View File

@ -7,6 +7,7 @@ import {
import { readPolicyDefaults, toApiPolicy } from '@/lib/policy-defaults.server'
import { describeMissing, missingRequiredFields } from '@/lib/policy-completeness'
import { resolveSlug } from '@/lib/policy-slug.server'
import { sortByTitle } from '@/lib/policy-sort'
import { PolicyDefaults } from '@/types/privacy-policy'
// Prisma client instance
@ -70,16 +71,14 @@ export async function GET(request: NextRequest) {
where: isAdmin ? {} : {
isPublic: true
},
orderBy: {
createdAt: 'desc'
}
}),
readPolicyDefaults(prisma),
])
// Standardtexte einsetzen, damit Konsumenten immer den geltenden Text sehen
// Standardtexte einsetzen, damit Konsumenten immer den geltenden Text sehen.
// Sortiert nach Titel – nicht über die Datenbank, siehe lib/policy-sort.
return NextResponse.json(
policies.map((policy) => toApiPolicy(policy, defaults))
sortByTitle(policies.map((policy) => toApiPolicy(policy, defaults)))
)
} catch (error) {
console.error('Error fetching privacy policies:', error)

View File

@ -4,6 +4,7 @@ import { PolicyList } from "@/components/policy-list"
import { PublicHeader } from "@/components/public-header"
import { SiteFooter } from "@/components/site-footer"
import { readPolicyDefaults, toApiPolicy } from "@/lib/policy-defaults.server"
import { sortByTitle } from "@/lib/policy-sort"
// Prisma client instance
const prisma = new PrismaClient()
@ -22,7 +23,6 @@ export default async function PublicDashboard() {
const [policies, defaults] = await Promise.all([
prisma.privacyPolicy.findMany({
where: { isPublic: true },
orderBy: { createdAt: "desc" },
}),
readPolicyDefaults(prisma),
])
@ -30,7 +30,9 @@ export default async function PublicDashboard() {
return (
<div className="flex min-h-screen flex-col bg-background">
<PublicHeader subtitle="Datenschutz" />
<PolicyList initial={policies.map((policy) => toApiPolicy(policy, defaults))} />
<PolicyList
initial={sortByTitle(policies.map((policy) => toApiPolicy(policy, defaults)))}
/>
<SiteFooter />
</div>
)

19
lib/policy-sort.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* Einheitliche Sortierung der Erklärungen nach Titel.
*
* Bewusst nicht über die Datenbank: SQLite vergleicht Zeichenketten binär,
* dort landete „Änderungsmitteilung“ hinter „Zählerstandsablesung“. Intl
* sortiert nach deutschen Regeln, Ä also bei A.
*
* `numeric` sorgt außerdem für „Nr. 2“ vor „Nr. 10“.
*/
const collator = new Intl.Collator("de", { numeric: true })
export function compareByTitle(a: { title: string }, b: { title: string }) {
return collator.compare(a.title, b.title)
}
/** Gibt eine nach Titel aufsteigend sortierte Kopie zurück. */
export function sortByTitle<T extends { title: string }>(policies: T[]): T[] {
return [...policies].sort(compareByTitle)
}

2555
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,13 +29,13 @@
"@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.24",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-scroll-area": "^1.2.18",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-separator": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.3.3",
"@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tabs": "^1.1.21",
"@radix-ui/react-tooltip": "^1.2.16",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/typography": "^0.5.16",