✨ feat: implement new feature for enhanced user experience
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"_id": { "$oid": "700000000000000000000001" },
|
||||
"active": true,
|
||||
"contentId": "6821c0a10000000000000001",
|
||||
"author": "Maximilian Müller",
|
||||
"message": "Ein großartiges Demo-Projekt! Die Umsetzung mit Svelte 5 sieht wirklich extrem aufgeräumt und performant aus. Freue mich schon, darauf aufzubauen."
|
||||
},
|
||||
{
|
||||
"_id": { "$oid": "700000000000000000000002" },
|
||||
"active": true,
|
||||
"contentId": "6821c0a10000000000000001",
|
||||
"author": "Sarah Jenkins",
|
||||
"message": "Awesome work on the SSR cache mechanism. It clarifies so many edge cases regarding dependent collections."
|
||||
},
|
||||
{
|
||||
"_id": { "$oid": "700000000000000000000003" },
|
||||
"active": true,
|
||||
"contentId": "6821c0a10000000000000001",
|
||||
"author": "Thomas W.",
|
||||
"message": "Gibt es zufällig auch Beispiele für den kombinierten Einsatz von aggregate mit einem nested array? Ansonsten aber Top-Starter!"
|
||||
},
|
||||
{
|
||||
"_id": { "$oid": "700000000000000000000004" },
|
||||
"active": true,
|
||||
"contentId": "6821c0a10000000000000003",
|
||||
"author": "Lisa Schmidt",
|
||||
"message": "Das 'Über uns' sieht sehr clean aus. Ich denke, das Grid-Layout passt hier perfekt."
|
||||
},
|
||||
{
|
||||
"_id": { "$oid": "700000000000000000000005" },
|
||||
"active": true,
|
||||
"contentId": "6821c0a10000000000000003",
|
||||
"author": "David C.",
|
||||
"message": "Looking forward to seeing more blocks added to the generic block renderer in the future."
|
||||
}
|
||||
]
|
||||
+33
-1
@@ -131,6 +131,7 @@
|
||||
}
|
||||
let loading = $state(true)
|
||||
let notFound = $state(false)
|
||||
let comments = $state<any[]>([])
|
||||
|
||||
// Header scroll detection
|
||||
let scrolled = $state(false)
|
||||
@@ -194,7 +195,7 @@
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ aggregate: "comments:contentId:count" },
|
||||
CONTENT_MEDIA_LOOKUP
|
||||
)
|
||||
|
||||
@@ -205,6 +206,17 @@
|
||||
lang: contentEntry.lang,
|
||||
path: contentEntry.path,
|
||||
}
|
||||
|
||||
try {
|
||||
comments = await getCachedEntries(
|
||||
"comments",
|
||||
{ active: true, contentId: contentEntry.id as string },
|
||||
"sort"
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Failed to load comments", e)
|
||||
comments = []
|
||||
}
|
||||
} else {
|
||||
notFound = true
|
||||
}
|
||||
@@ -392,6 +404,26 @@
|
||||
{:else if contentEntry?.blocks}
|
||||
<div class="page-enter">
|
||||
<BlockRenderer blocks={contentEntry.blocks} />
|
||||
|
||||
{#if (contentEntry as any)?._aggregate?.commentsCount !== undefined}
|
||||
<div class="max-w-6xl mx-auto px-6 py-8 border-t border-gray-100 my-12">
|
||||
<h3 class="text-xl font-bold mb-6">
|
||||
Kommentare ({(contentEntry as any)._aggregate.commentsCount})
|
||||
</h3>
|
||||
{#if comments && comments.length > 0}
|
||||
<div class="space-y-6">
|
||||
{#each comments as comment}
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
<div class="font-bold text-gray-900 mb-2">{comment.author}</div>
|
||||
<p class="text-gray-700 whitespace-pre-wrap">{comment.message}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-gray-500 italic">Noch keine Kommentare vorhanden.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{#if hasImage}
|
||||
<div class="absolute inset-0 z-0">
|
||||
<MedialibImage
|
||||
id={block.heroImage?.image || resolvedHeroImage?.id || resolvedHeroImage?._id || ""}
|
||||
id={block.heroImage?.image || resolvedHeroImage?.id || ""}
|
||||
entry={resolvedHeroImage}
|
||||
noPlaceholder
|
||||
style="width:100%;height:100%;object-fit:cover;"
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="rounded-2xl overflow-hidden shadow-xl shadow-brand-900/10">
|
||||
{#if block.image || resolvedImage}
|
||||
<MedialibImage
|
||||
id={block.image || resolvedImage?.id || resolvedImage?._id || ""}
|
||||
id={block.image || resolvedImage?.id || ""}
|
||||
entry={resolvedImage}
|
||||
noPlaceholder
|
||||
style="width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import contentData from "../../mocking/content.json"
|
||||
import medialibData from "../../mocking/medialib.json"
|
||||
import navigationData from "../../mocking/navigation.json"
|
||||
import commentsData from "../../mocking/comments.json"
|
||||
|
||||
type EJsonObjectId = {
|
||||
$oid: string
|
||||
@@ -25,6 +26,7 @@ const mockRegistry: Record<string, Record<string, unknown>[]> = {
|
||||
content: normalizeMockCollection(contentData as Record<string, unknown>[]),
|
||||
medialib: normalizeMockCollection(medialibData as Record<string, unknown>[]),
|
||||
navigation: normalizeMockCollection(navigationData as Record<string, unknown>[]),
|
||||
comments: normalizeMockCollection(commentsData as Record<string, unknown>[]),
|
||||
}
|
||||
|
||||
function isEJsonObjectId(value: unknown): value is EJsonObjectId {
|
||||
@@ -88,7 +90,11 @@ export function mockApiRequest(endpoint: string, options?: ApiOptions, _body?: u
|
||||
// --- Single-item retrieval ---
|
||||
if (itemId) {
|
||||
const item = sourceData.find((e) => e.id === itemId || e._id === itemId)
|
||||
const resultItem = item ? applyLookups(cloneEntry(item), options) : null
|
||||
let resultItem = item ? cloneEntry(item) : null
|
||||
if (resultItem) {
|
||||
resultItem = applyAggregate(resultItem, options)
|
||||
resultItem = applyLookups(resultItem, options)
|
||||
}
|
||||
return {
|
||||
data: resultItem,
|
||||
count: resultItem ? 1 : 0,
|
||||
@@ -119,7 +125,12 @@ export function mockApiRequest(endpoint: string, options?: ApiOptions, _body?: u
|
||||
results = results.slice(0, options.limit)
|
||||
}
|
||||
|
||||
results = results.map((entry) => applyLookups(cloneEntry(entry), options))
|
||||
results = results.map((entry) => {
|
||||
let e = cloneEntry(entry)
|
||||
e = applyAggregate(e, options)
|
||||
e = applyLookups(e, options)
|
||||
return e
|
||||
})
|
||||
|
||||
// Projection
|
||||
if (options?.projection) {
|
||||
@@ -230,6 +241,63 @@ function cloneEntry<T>(entry: T): T {
|
||||
return JSON.parse(JSON.stringify(entry)) as T
|
||||
}
|
||||
|
||||
function applyAggregate(entry: Record<string, unknown>, options?: ApiOptions): Record<string, unknown> {
|
||||
const rawAggregate = options?.params?.aggregate
|
||||
if (!rawAggregate) return entry
|
||||
|
||||
const aggregates = Array.isArray(rawAggregate)
|
||||
? rawAggregate
|
||||
: typeof rawAggregate === "string"
|
||||
? rawAggregate.split(",")
|
||||
: []
|
||||
|
||||
if (!entry._aggregate) {
|
||||
entry._aggregate = {}
|
||||
}
|
||||
|
||||
for (const spec of aggregates) {
|
||||
try {
|
||||
// Check for json object first
|
||||
if (spec.startsWith("{")) {
|
||||
// Not supported in this simple mock
|
||||
continue
|
||||
}
|
||||
|
||||
// "comments:contentId:count"
|
||||
const parts = spec.split(":")
|
||||
if (parts.length < 3) continue
|
||||
const targetCollection = parts[0]
|
||||
const foreignField = parts[1]
|
||||
const operator = parts[2]
|
||||
|
||||
const lookupSource = mockRegistry[targetCollection]
|
||||
if (!lookupSource) continue
|
||||
|
||||
const localId = entry._id
|
||||
? typeof (entry._id as any).$oid === "string"
|
||||
? (entry._id as any).$oid
|
||||
: entry._id.toString()
|
||||
: null
|
||||
|
||||
if (operator === "count") {
|
||||
const count = lookupSource.filter((item) => {
|
||||
const itemForeignId = item[foreignField]
|
||||
? typeof (item[foreignField] as any).$oid === "string"
|
||||
? (item[foreignField] as any).$oid
|
||||
: (item[foreignField] as any).toString()
|
||||
: null
|
||||
return itemForeignId === localId && item.active !== false // basic mock rule
|
||||
}).length
|
||||
|
||||
;(entry._aggregate as any)[`${targetCollection}Count`] = count
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Mock aggregate error", e)
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
function applyLookups(entry: Record<string, unknown>, options?: ApiOptions): Record<string, unknown> {
|
||||
const lookupSpecs = parseLookupSpecs(options)
|
||||
if (!lookupSpecs.length) return entry
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
|
||||
let imgEl = $state<HTMLImageElement | null>(null)
|
||||
let currentFilter = $state<string>("l-webp")
|
||||
const effectiveId = $derived(entry?.id || entry?._id || id || "")
|
||||
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || entry?._id || effectiveId))
|
||||
const effectiveId = $derived(entry?.id || id || "")
|
||||
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || effectiveId))
|
||||
const placeholderSrc = $derived(
|
||||
resolveApiAssetUrl("/assets/img/placeholder-image.svg") || "/assets/img/placeholder-image.svg"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user