@duffcloudservices/cms 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,92 +1,150 @@
1
- /**
2
- * Composable for reading curated review selections from DCS content.
3
- * Reviews are stored in content.yaml by the visual editor's ReviewPickerSheet.
4
- */
5
- import { computed, type ComputedRef } from 'vue'
6
-
7
- export interface ReviewItem {
8
- id: string
9
- platform: 'google' | 'meta' | string
10
- rating: number
11
- authorName: string
12
- authorPhotoUrl?: string
13
- text?: string
14
- date?: string
15
- replyText?: string
16
- locationName?: string
17
- sourceUrl?: string
18
- }
19
-
20
- export interface UseReviewContentConfig {
21
- /** The section key matching the data-dcs-reviews attribute value */
22
- sectionKey: string
23
- /** Page slug for page-specific content lookup (defaults to current page) */
24
- pageSlug?: string
25
- /** Fallback reviews when no content is available */
26
- defaults?: ReviewItem[]
27
- }
28
-
29
- export interface UseReviewContentReturn {
30
- /** The curated review items from content */
31
- reviews: ComputedRef<ReviewItem[]>
32
- /** Whether any reviews are available */
33
- hasReviews: ComputedRef<boolean>
34
- /** Number of reviews */
35
- count: ComputedRef<number>
36
- }
37
-
38
- // Declare the global content variable injected by dcsContentPlugin
39
- declare const __DCS_CONTENT__: {
40
- global?: Record<string, unknown>
41
- pages?: Record<string, Record<string, unknown>>
42
- } | undefined
43
-
44
- export function useReviewContent(config: UseReviewContentConfig): UseReviewContentReturn {
45
- const { sectionKey, pageSlug, defaults = [] } = config
46
-
47
- const reviews = computed<ReviewItem[]>(() => {
48
- if (typeof __DCS_CONTENT__ === 'undefined' || __DCS_CONTENT__ == null) {
49
- return defaults
50
- }
51
-
52
- let reviewData: unknown = null
53
-
54
- if (pageSlug && __DCS_CONTENT__.pages?.[pageSlug]) {
55
- reviewData = __DCS_CONTENT__.pages[pageSlug][`reviews.${sectionKey}.items`]
56
- ?? __DCS_CONTENT__.pages[pageSlug][`reviews.${sectionKey}`]
57
- }
58
-
59
- if (!reviewData && __DCS_CONTENT__.global) {
60
- reviewData = __DCS_CONTENT__.global[`reviews.${sectionKey}.items`]
61
- ?? __DCS_CONTENT__.global[`reviews.${sectionKey}`]
62
- }
63
-
64
- if (!reviewData || !Array.isArray(reviewData)) {
65
- return defaults
66
- }
67
-
68
- return reviewData
69
- .filter((item): item is Record<string, unknown> => (
70
- item != null &&
71
- typeof item === 'object' &&
72
- 'id' in item
73
- ))
74
- .map((item) => ({
75
- id: String(item.id ?? ''),
76
- platform: String(item.platform ?? 'google'),
77
- rating: Number(item.rating ?? 5),
78
- authorName: String(item.authorName ?? 'Anonymous'),
79
- authorPhotoUrl: item.authorPhotoUrl ? String(item.authorPhotoUrl) : undefined,
80
- text: item.text ? String(item.text) : undefined,
81
- date: item.date ? String(item.date) : undefined,
82
- replyText: item.replyText ? String(item.replyText) : undefined,
83
- locationName: item.locationName ? String(item.locationName) : undefined,
84
- sourceUrl: item.sourceUrl ? String(item.sourceUrl) : undefined,
85
- }))
86
- })
87
-
88
- const hasReviews = computed(() => reviews.value.length > 0)
89
- const count = computed(() => reviews.value.length)
90
-
91
- return { reviews, hasReviews, count }
92
- }
1
+ /**
2
+ * Composable for reading curated review selections from DCS content.
3
+ * Reviews are stored in content.yaml by the visual editor's ReviewPickerSheet.
4
+ */
5
+ import { computed, onMounted, onUnmounted, shallowRef, type ComputedRef } from 'vue'
6
+
7
+ export interface ReviewItem {
8
+ id: string
9
+ platform: 'google' | 'meta' | string
10
+ rating: number
11
+ authorName: string
12
+ authorPhotoUrl?: string
13
+ text?: string
14
+ date?: string
15
+ replyText?: string
16
+ locationName?: string
17
+ sourceUrl?: string
18
+ }
19
+
20
+ export interface UseReviewContentConfig {
21
+ /** The section key matching the data-dcs-reviews attribute value */
22
+ sectionKey: string
23
+ /** Page slug for page-specific content lookup (defaults to current page) */
24
+ pageSlug?: string
25
+ /** Fallback reviews when no content is available */
26
+ defaults?: ReviewItem[]
27
+ }
28
+
29
+ export interface UseReviewContentReturn {
30
+ /** The curated review items from content */
31
+ reviews: ComputedRef<ReviewItem[]>
32
+ /** Whether any reviews are available */
33
+ hasReviews: ComputedRef<boolean>
34
+ /** Number of reviews */
35
+ count: ComputedRef<number>
36
+ }
37
+
38
+ const previewReviewOverrides = shallowRef<Record<string, ReviewItem[]>>({})
39
+ let activePreviewReviewConsumers = 0
40
+
41
+ // Declare the global content variable injected by dcsContentPlugin
42
+ declare const __DCS_CONTENT__: {
43
+ global?: Record<string, unknown>
44
+ pages?: Record<string, Record<string, unknown>>
45
+ } | undefined
46
+
47
+ function normalizeReviewItem(item: Record<string, unknown>): ReviewItem | null {
48
+ const id = String(item.id ?? '').trim()
49
+ if (!id) {
50
+ return null
51
+ }
52
+
53
+ const rawRating = Number(item.rating ?? 5)
54
+ const rating = Number.isFinite(rawRating)
55
+ ? Math.min(5, Math.max(1, Math.round(rawRating)))
56
+ : 5
57
+
58
+ return {
59
+ id,
60
+ platform: String(item.platform ?? 'google'),
61
+ rating,
62
+ authorName: String(item.authorName ?? 'Anonymous'),
63
+ authorPhotoUrl: item.authorPhotoUrl ? String(item.authorPhotoUrl) : undefined,
64
+ text: item.text ? String(item.text) : undefined,
65
+ date: item.date ? String(item.date) : undefined,
66
+ replyText: item.replyText ? String(item.replyText) : undefined,
67
+ locationName: item.locationName ? String(item.locationName) : undefined,
68
+ sourceUrl: item.sourceUrl ? String(item.sourceUrl) : undefined,
69
+ }
70
+ }
71
+
72
+ function normalizeReviewList(value: unknown): ReviewItem[] {
73
+ if (!Array.isArray(value)) {
74
+ return []
75
+ }
76
+
77
+ return value
78
+ .filter((item): item is Record<string, unknown> => item != null && typeof item === 'object')
79
+ .map(normalizeReviewItem)
80
+ .filter((item): item is ReviewItem => item != null)
81
+ }
82
+
83
+ function handlePreviewReviewUpdate(event: Event) {
84
+ const detail = event instanceof CustomEvent && event.detail != null && typeof event.detail === 'object'
85
+ ? event.detail as { key?: unknown; reviews?: unknown }
86
+ : null
87
+ const key = typeof detail?.key === 'string' ? detail.key.trim() : ''
88
+ if (!key) {
89
+ return
90
+ }
91
+
92
+ const next = { ...previewReviewOverrides.value }
93
+ if (Array.isArray(detail?.reviews)) {
94
+ next[key] = normalizeReviewList(detail.reviews)
95
+ } else {
96
+ delete next[key]
97
+ }
98
+ previewReviewOverrides.value = next
99
+ }
100
+
101
+ export function useReviewContent(config: UseReviewContentConfig): UseReviewContentReturn {
102
+ const { sectionKey, pageSlug, defaults = [] } = config
103
+
104
+ onMounted(() => {
105
+ activePreviewReviewConsumers += 1
106
+ if (activePreviewReviewConsumers === 1) {
107
+ window.addEventListener('dcs:reviews-updated', handlePreviewReviewUpdate)
108
+ }
109
+ })
110
+
111
+ onUnmounted(() => {
112
+ activePreviewReviewConsumers = Math.max(0, activePreviewReviewConsumers - 1)
113
+ if (activePreviewReviewConsumers === 0) {
114
+ window.removeEventListener('dcs:reviews-updated', handlePreviewReviewUpdate)
115
+ }
116
+ })
117
+
118
+ const reviews = computed<ReviewItem[]>(() => {
119
+ if (Object.prototype.hasOwnProperty.call(previewReviewOverrides.value, sectionKey)) {
120
+ return previewReviewOverrides.value[sectionKey] ?? []
121
+ }
122
+
123
+ if (typeof __DCS_CONTENT__ === 'undefined' || __DCS_CONTENT__ == null) {
124
+ return defaults
125
+ }
126
+
127
+ let reviewData: unknown = null
128
+
129
+ if (pageSlug && __DCS_CONTENT__.pages?.[pageSlug]) {
130
+ reviewData = __DCS_CONTENT__.pages[pageSlug][`reviews.${sectionKey}.items`]
131
+ ?? __DCS_CONTENT__.pages[pageSlug][`reviews.${sectionKey}`]
132
+ }
133
+
134
+ if (!reviewData && __DCS_CONTENT__.global) {
135
+ reviewData = __DCS_CONTENT__.global[`reviews.${sectionKey}.items`]
136
+ ?? __DCS_CONTENT__.global[`reviews.${sectionKey}`]
137
+ }
138
+
139
+ if (!reviewData || !Array.isArray(reviewData)) {
140
+ return defaults
141
+ }
142
+
143
+ return normalizeReviewList(reviewData)
144
+ })
145
+
146
+ const hasReviews = computed(() => reviews.value.length > 0)
147
+ const count = computed(() => reviews.value.length)
148
+
149
+ return { reviews, hasReviews, count }
150
+ }