@brilo/base-blocks 0.1.57 → 0.1.60

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.
Files changed (149) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/blocks/column/column-item/factory.d.ts.map +1 -1
  3. package/build/blocks/column/column-item/factory.js +2 -1
  4. package/build/blocks/column/column-item/icon.d.ts +2 -0
  5. package/build/blocks/column/column-item/icon.d.ts.map +1 -0
  6. package/build/blocks/column/column-item/icon.js +2 -0
  7. package/build/blocks/column/column-item/index.d.ts.map +1 -1
  8. package/build/blocks/column/column-item/index.js +2 -0
  9. package/build/blocks/food-menu/food-menu-group/FoodMenuPreview.d.ts +6 -0
  10. package/build/blocks/food-menu/food-menu-group/FoodMenuPreview.d.ts.map +1 -0
  11. package/build/blocks/food-menu/food-menu-group/FoodMenuPreview.js +167 -0
  12. package/build/blocks/food-menu/food-menu-group/FoodMenuSelectModal.d.ts +7 -0
  13. package/build/blocks/food-menu/food-menu-group/FoodMenuSelectModal.d.ts.map +1 -0
  14. package/build/blocks/food-menu/food-menu-group/FoodMenuSelectModal.js +56 -0
  15. package/build/blocks/food-menu/food-menu-group/constants.d.ts +8 -0
  16. package/build/blocks/food-menu/food-menu-group/constants.d.ts.map +1 -0
  17. package/build/blocks/food-menu/food-menu-group/constants.js +7 -0
  18. package/build/blocks/food-menu/food-menu-group/edit.d.ts +7 -0
  19. package/build/blocks/food-menu/food-menu-group/edit.d.ts.map +1 -0
  20. package/build/blocks/food-menu/food-menu-group/edit.js +36 -0
  21. package/build/blocks/food-menu/food-menu-group/factory.d.ts +4 -0
  22. package/build/blocks/food-menu/food-menu-group/factory.d.ts.map +1 -0
  23. package/build/blocks/food-menu/food-menu-group/factory.js +57 -0
  24. package/build/blocks/food-menu/food-menu-group/icon.d.ts +2 -0
  25. package/build/blocks/food-menu/food-menu-group/icon.d.ts.map +1 -0
  26. package/build/blocks/food-menu/food-menu-group/icon.js +2 -0
  27. package/build/blocks/food-menu/food-menu-group/index.d.ts +6 -0
  28. package/build/blocks/food-menu/food-menu-group/index.d.ts.map +1 -0
  29. package/build/blocks/food-menu/food-menu-group/index.js +7 -0
  30. package/build/blocks/food-menu/food-menu-group/types.d.ts +19 -0
  31. package/build/blocks/food-menu/food-menu-group/types.d.ts.map +1 -0
  32. package/build/blocks/food-menu/food-menu-group/types.js +1 -0
  33. package/build/blocks/form/form-group/index.js +1 -1
  34. package/build/blocks/gallery/gallery-group/constants.d.ts +1 -19
  35. package/build/blocks/gallery/gallery-group/constants.d.ts.map +1 -1
  36. package/build/blocks/gallery/gallery-group/constants.js +1 -8
  37. package/build/blocks/gallery/gallery-group/edit.d.ts.map +1 -1
  38. package/build/blocks/gallery/gallery-group/edit.js +3 -19
  39. package/build/blocks/gallery/gallery-group/index.js +1 -1
  40. package/build/blocks/gallery/gallery-item/index.js +1 -1
  41. package/build/blocks/gallery/gallery-strip-group/constants.d.ts +34 -0
  42. package/build/blocks/gallery/gallery-strip-group/constants.d.ts.map +1 -0
  43. package/build/blocks/gallery/gallery-strip-group/constants.js +17 -0
  44. package/build/blocks/gallery/gallery-strip-group/edit.d.ts +7 -0
  45. package/build/blocks/gallery/gallery-strip-group/edit.d.ts.map +1 -0
  46. package/build/blocks/gallery/gallery-strip-group/edit.js +34 -0
  47. package/build/blocks/gallery/gallery-strip-group/factory.d.ts +5 -0
  48. package/build/blocks/gallery/gallery-strip-group/factory.d.ts.map +1 -0
  49. package/build/blocks/gallery/gallery-strip-group/factory.js +28 -0
  50. package/build/blocks/gallery/gallery-strip-group/index.d.ts +5 -0
  51. package/build/blocks/gallery/gallery-strip-group/index.d.ts.map +1 -0
  52. package/build/blocks/gallery/gallery-strip-group/index.js +7 -0
  53. package/build/blocks/gallery/gallery-strip-group/types.d.ts +18 -0
  54. package/build/blocks/gallery/gallery-strip-group/types.d.ts.map +1 -0
  55. package/build/blocks/gallery/gallery-strip-group/types.js +1 -0
  56. package/build/blocks/groups/text-and-media-group/index.d.ts.map +1 -1
  57. package/build/blocks/groups/text-and-media-group/index.js +2 -0
  58. package/build/blocks/hero/hero-item/index.js +1 -1
  59. package/build/blocks/review/review-summary-item/constants.d.ts +3 -0
  60. package/build/blocks/review/review-summary-item/constants.d.ts.map +1 -0
  61. package/build/blocks/review/review-summary-item/constants.js +2 -0
  62. package/build/blocks/review/review-summary-item/edit.d.ts +6 -0
  63. package/build/blocks/review/review-summary-item/edit.d.ts.map +1 -0
  64. package/build/blocks/review/review-summary-item/edit.js +17 -0
  65. package/build/blocks/review/review-summary-item/factory.d.ts +5 -0
  66. package/build/blocks/review/review-summary-item/factory.d.ts.map +1 -0
  67. package/build/blocks/review/review-summary-item/factory.js +23 -0
  68. package/build/blocks/review/review-summary-item/index.d.ts +5 -0
  69. package/build/blocks/review/review-summary-item/index.d.ts.map +1 -0
  70. package/build/blocks/review/review-summary-item/index.js +5 -0
  71. package/build/blocks/review/review-summary-item/types.d.ts +11 -0
  72. package/build/blocks/review/review-summary-item/types.d.ts.map +1 -0
  73. package/build/blocks/review/review-summary-item/types.js +1 -0
  74. package/build/blocks/urgency/urgency-item/constants.d.ts +56 -0
  75. package/build/blocks/urgency/urgency-item/constants.d.ts.map +1 -0
  76. package/build/blocks/urgency/urgency-item/constants.js +32 -0
  77. package/build/blocks/urgency/urgency-item/edit.d.ts +7 -0
  78. package/build/blocks/urgency/urgency-item/edit.d.ts.map +1 -0
  79. package/build/blocks/urgency/urgency-item/edit.js +83 -0
  80. package/build/blocks/urgency/urgency-item/factory.d.ts +5 -0
  81. package/build/blocks/urgency/urgency-item/factory.d.ts.map +1 -0
  82. package/build/blocks/urgency/urgency-item/factory.js +31 -0
  83. package/build/blocks/urgency/urgency-item/index.d.ts +5 -0
  84. package/build/blocks/urgency/urgency-item/index.d.ts.map +1 -0
  85. package/build/blocks/urgency/urgency-item/index.js +5 -0
  86. package/build/blocks/urgency/urgency-item/types.d.ts +22 -0
  87. package/build/blocks/urgency/urgency-item/types.d.ts.map +1 -0
  88. package/build/blocks/urgency/urgency-item/types.js +1 -0
  89. package/build/components/EntitySelectModal/EntitySelectModal.d.ts +6 -0
  90. package/build/components/EntitySelectModal/EntitySelectModal.d.ts.map +1 -1
  91. package/build/components/EntitySelectModal/EntitySelectModal.js +3 -2
  92. package/build/components/GalleryImagesAppender/index.d.ts +5 -0
  93. package/build/components/GalleryImagesAppender/index.d.ts.map +1 -0
  94. package/build/components/GalleryImagesAppender/index.js +12 -0
  95. package/build/constants.d.ts +19 -0
  96. package/build/constants.d.ts.map +1 -1
  97. package/build/constants.js +8 -0
  98. package/build/index.d.ts +8 -0
  99. package/build/index.d.ts.map +1 -1
  100. package/build/index.js +4 -0
  101. package/build/utils/plural.d.ts +3 -0
  102. package/build/utils/plural.d.ts.map +1 -0
  103. package/build/utils/plural.js +7 -0
  104. package/package.json +1 -1
  105. package/src/blocks/column/column-item/factory.tsx +2 -1
  106. package/src/blocks/column/column-item/icon.tsx +10 -0
  107. package/src/blocks/column/column-item/index.ts +2 -0
  108. package/src/blocks/food-menu/food-menu-group/FoodMenuPreview.tsx +380 -0
  109. package/src/blocks/food-menu/food-menu-group/FoodMenuSelectModal.tsx +69 -0
  110. package/src/blocks/food-menu/food-menu-group/constants.ts +9 -0
  111. package/src/blocks/food-menu/food-menu-group/edit.tsx +153 -0
  112. package/src/blocks/food-menu/food-menu-group/editor.scss +269 -0
  113. package/src/blocks/food-menu/food-menu-group/factory.tsx +66 -0
  114. package/src/blocks/food-menu/food-menu-group/icon.tsx +8 -0
  115. package/src/blocks/food-menu/food-menu-group/index.ts +17 -0
  116. package/src/blocks/food-menu/food-menu-group/types.ts +21 -0
  117. package/src/blocks/form/form-group/index.ts +1 -1
  118. package/src/blocks/gallery/gallery-group/constants.ts +1 -8
  119. package/src/blocks/gallery/gallery-group/edit.tsx +3 -39
  120. package/src/blocks/gallery/gallery-group/editor.scss +0 -12
  121. package/src/blocks/gallery/gallery-group/index.ts +1 -1
  122. package/src/blocks/gallery/gallery-item/index.ts +1 -1
  123. package/src/blocks/gallery/gallery-strip-group/constants.ts +22 -0
  124. package/src/blocks/gallery/gallery-strip-group/edit.tsx +112 -0
  125. package/src/blocks/gallery/gallery-strip-group/editor.scss +36 -0
  126. package/src/blocks/gallery/gallery-strip-group/factory.tsx +32 -0
  127. package/src/blocks/gallery/gallery-strip-group/index.ts +11 -0
  128. package/src/blocks/gallery/gallery-strip-group/types.ts +20 -0
  129. package/src/blocks/groups/text-and-media-group/index.ts +2 -0
  130. package/src/blocks/hero/hero-item/index.ts +1 -1
  131. package/src/blocks/review/review-summary-item/constants.ts +2 -0
  132. package/src/blocks/review/review-summary-item/edit.tsx +86 -0
  133. package/src/blocks/review/review-summary-item/editor.scss +44 -0
  134. package/src/blocks/review/review-summary-item/factory.tsx +27 -0
  135. package/src/blocks/review/review-summary-item/index.ts +9 -0
  136. package/src/blocks/review/review-summary-item/types.ts +11 -0
  137. package/src/blocks/urgency/urgency-item/constants.ts +39 -0
  138. package/src/blocks/urgency/urgency-item/edit.tsx +319 -0
  139. package/src/blocks/urgency/urgency-item/editor.scss +99 -0
  140. package/src/blocks/urgency/urgency-item/factory.tsx +36 -0
  141. package/src/blocks/urgency/urgency-item/index.ts +9 -0
  142. package/src/blocks/urgency/urgency-item/types.ts +23 -0
  143. package/src/components/EntitySelectModal/EntitySelectModal.tsx +19 -7
  144. package/src/components/GalleryImagesAppender/editor.scss +15 -0
  145. package/src/components/GalleryImagesAppender/index.tsx +29 -0
  146. package/src/constants.ts +9 -0
  147. package/src/editor.scss +5 -0
  148. package/src/index.ts +13 -0
  149. package/src/utils/plural.ts +13 -0
@@ -0,0 +1,380 @@
1
+ import apiFetch from '@wordpress/api-fetch'
2
+ import {Button, Spinner} from '@wordpress/components'
3
+ import {__, sprintf} from '@wordpress/i18n'
4
+ import clsx from 'clsx'
5
+ import React, {type KeyboardEvent, useEffect, useRef, useState} from 'react'
6
+ import {plural} from '~/utils/plural'
7
+ import {icon as menuIcon} from './icon'
8
+ import type {FoodMenuGroupAttributes, FoodMenuGroupEmptyBehaviour, FoodMenuGroupMode} from './types'
9
+
10
+ type PreviewItem = {
11
+ id: string
12
+ food_id: number
13
+ title: string
14
+ unit: string
15
+ price: string
16
+ note: string
17
+ allergens: number[]
18
+ missing?: boolean
19
+ }
20
+
21
+ type PreviewCourse = {
22
+ title: string
23
+ items: PreviewItem[]
24
+ }
25
+
26
+ type PreviewMenu = {
27
+ id: number
28
+ title: string
29
+ kind: string
30
+ courses: PreviewCourse[]
31
+ dishCount: number
32
+ reservation: {state: 'open' | 'closed'; deadline: string} | null
33
+ }
34
+
35
+ type PreviewDay = {
36
+ date: string | null
37
+ label: string
38
+ menu: PreviewMenu | null
39
+ }
40
+
41
+ type PreviewResponse = {
42
+ today: string
43
+ week: number | null
44
+ days: PreviewDay[]
45
+ emptyMessage: Record<FoodMenuGroupMode, string>
46
+ hasPermanent: boolean
47
+ }
48
+
49
+ const BASE = 'brilo-food-menu-preview'
50
+
51
+ /**
52
+ * The menu is assembled on the server — the week in the site's time zone,
53
+ * which days have a menu, whether ordering is open — so the preview asks the
54
+ * block's own endpoint instead of rebuilding that from post records. It shows
55
+ * structure and state, not the front-end markup: the theme renders blocks
56
+ * through its own renderer, which `ServerSideRender` cannot reach.
57
+ */
58
+ function useMenuPreview(mode: FoodMenuGroupMode, menuIds: number[], showPrices: boolean, showAllergens: boolean) {
59
+ const [data, setData] = useState<PreviewResponse | null>(null)
60
+ const [isLoading, setIsLoading] = useState(true)
61
+ const [hasError, setHasError] = useState(false)
62
+ const [attempt, setAttempt] = useState(0)
63
+
64
+ const params = new URLSearchParams({
65
+ mode,
66
+ show_prices: showPrices ? '1' : '0',
67
+ show_allergens: showAllergens ? '1' : '0',
68
+ })
69
+
70
+ if ('manual' === mode && 0 < menuIds.length) {
71
+ params.set('ids', menuIds.join(','))
72
+ }
73
+
74
+ const key = params.toString()
75
+
76
+ // `attempt` is what "Zkusit znovu" bumps: same request, run again.
77
+ // biome-ignore lint/correctness/useExhaustiveDependencies: attempt re-runs the effect on retry
78
+ useEffect(() => {
79
+ let cancelled = false
80
+ setIsLoading(true)
81
+ setHasError(false)
82
+
83
+ apiFetch<PreviewResponse>({path: `/brilo/v1/food-menu/preview?${key}`})
84
+ .then((result) => {
85
+ if (!cancelled) {
86
+ setData(result)
87
+ }
88
+ })
89
+ .catch(() => {
90
+ if (!cancelled) {
91
+ setData(null)
92
+ setHasError(true)
93
+ }
94
+ })
95
+ .finally(() => {
96
+ if (!cancelled) {
97
+ setIsLoading(false)
98
+ }
99
+ })
100
+
101
+ return () => {
102
+ cancelled = true
103
+ }
104
+ }, [key, attempt])
105
+
106
+ return {data, isLoading, hasError, retry: () => setAttempt((value) => value + 1)}
107
+ }
108
+
109
+ const WarnIcon = () => (
110
+ <svg className={`${BASE}__empty-icon`} viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
111
+ <path d="M10 3 2 17h16L10 3Z" />
112
+ <path d="M10 8v4M10 14.5v.5" />
113
+ </svg>
114
+ )
115
+
116
+ function PreviewEmpty({title, text, children}: {title: string; text?: string; children?: React.ReactNode}) {
117
+ return (
118
+ <div className={`${BASE}__empty`}>
119
+ <WarnIcon />
120
+ <div>
121
+ <b>{title}</b>
122
+ {text}
123
+ {children}
124
+ </div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ /**
130
+ * Says what the page does instead, in the editor's own setting, so a missing
131
+ * day is not a surprise on the live site. Derived from attributes only, so
132
+ * changing the setting updates it without a new request.
133
+ */
134
+ function emptySentence(behaviour: FoodMenuGroupEmptyBehaviour, emptyText: string, mode: FoodMenuGroupMode, data: PreviewResponse): string {
135
+ const hint = __('Změnit lze v panelu „Když není co zobrazit“.', 'brilo-blocks')
136
+
137
+ if ('hide' === behaviour) {
138
+ return `${__('Blok se na webu nevykreslí.', 'brilo-blocks')} ${hint}`
139
+ }
140
+
141
+ if ('permanent' === behaviour) {
142
+ const sentence = __('Na webu se místo něj nabídne stálá nabídka s poznámkou nad ní.', 'brilo-blocks')
143
+ const fallback = data.hasPermanent ? '' : ` ${__('Žádná stálá nabídka ale není, ukáže se věta.', 'brilo-blocks')}`
144
+
145
+ return `${sentence}${fallback} ${hint}`
146
+ }
147
+
148
+ const text = emptyText.trim() || data.emptyMessage[mode]
149
+
150
+ return `${sprintf(__('Na webu se místo něj ukáže věta „%s“', 'brilo-blocks'), text)} ${hint}`
151
+ }
152
+
153
+ function NoMenuNotice({attributes, data}: {attributes: FoodMenuGroupAttributes; data: PreviewResponse}) {
154
+ return (
155
+ <PreviewEmpty
156
+ title={__('Na tento den není lístek', 'brilo-blocks')}
157
+ text={emptySentence(attributes.emptyBehaviour, attributes.emptyText, attributes.mode, data)}
158
+ />
159
+ )
160
+ }
161
+
162
+ function sourceLabel(mode: FoodMenuGroupMode, day: PreviewDay, data: PreviewResponse): string {
163
+ if ('today' === mode) {
164
+ return __('Dnešní lístek', 'brilo-blocks')
165
+ }
166
+
167
+ if ('week' === mode) {
168
+ return sprintf(__('Týden %d', 'brilo-blocks'), data.week ?? 0)
169
+ }
170
+
171
+ if (!day.menu) {
172
+ return ''
173
+ }
174
+
175
+ return 'daily' === day.menu.kind && day.date ? sprintf(__('Denní · %s', 'brilo-blocks'), day.date) : __('Trvalý', 'brilo-blocks')
176
+ }
177
+
178
+ function PreviewRows({menu, showPrices, showAllergens}: {menu: PreviewMenu; showPrices: boolean; showAllergens: boolean}) {
179
+ return (
180
+ <>
181
+ {menu.courses.map((course, courseIndex) => (
182
+ // biome-ignore lint/suspicious/noArrayIndexKey: courses carry no id; their order is what the menu editor set
183
+ <div className={`${BASE}__section`} key={`${menu.id}-${courseIndex}`}>
184
+ {course.title && <div className={`${BASE}__section-title`}>{course.title}</div>}
185
+
186
+ {course.items.map((item) => (
187
+ <div className={clsx(`${BASE}__row`, !showPrices && `${BASE}__row--no-price`)} key={item.id || `food-${item.food_id}`}>
188
+ <span className={`${BASE}__unit`}>{item.unit}</span>
189
+
190
+ {item.missing ? (
191
+ <span className={clsx(`${BASE}__name`, `${BASE}__name--missing`)}>
192
+ {sprintf(__('Jídlo #%d (smazáno)', 'brilo-blocks'), item.food_id)}
193
+ </span>
194
+ ) : (
195
+ <span className={`${BASE}__name`}>
196
+ {item.title}
197
+ {showAllergens && 0 < item.allergens.length && <span className={`${BASE}__chip`}>{item.allergens.join(', ')}</span>}
198
+ {item.note && <small className={`${BASE}__note`}>{item.note}</small>}
199
+ </span>
200
+ )}
201
+
202
+ {showPrices && <span className={`${BASE}__price`}>{item.price}</span>}
203
+ </div>
204
+ ))}
205
+ </div>
206
+ ))}
207
+ </>
208
+ )
209
+ }
210
+
211
+ function PreviewDayTabs({days, selected, onSelect}: {days: PreviewDay[]; selected: number; onSelect: (index: number) => void}) {
212
+ const tabs = useRef<Array<HTMLButtonElement | null>>([])
213
+
214
+ const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
215
+ const step = {ArrowRight: 1, ArrowLeft: -1}[event.key]
216
+
217
+ if (!step) {
218
+ return
219
+ }
220
+
221
+ event.preventDefault()
222
+ const next = (selected + step + days.length) % days.length
223
+ onSelect(next)
224
+ tabs.current[next]?.focus()
225
+ }
226
+
227
+ return (
228
+ <div className={`${BASE}__days`} role="tablist" aria-label={__('Dny týdne', 'brilo-blocks')} onKeyDown={onKeyDown}>
229
+ {days.map((day, index) => {
230
+ const [weekday, ...date] = day.label.split(' ')
231
+
232
+ return (
233
+ <button
234
+ key={day.date ?? index}
235
+ ref={(element) => {
236
+ tabs.current[index] = element
237
+ }}
238
+ type="button"
239
+ role="tab"
240
+ aria-selected={index === selected}
241
+ tabIndex={index === selected ? 0 : -1}
242
+ className={`${BASE}__day`}
243
+ onClick={() => onSelect(index)}
244
+ >
245
+ <i className={clsx(`${BASE}__dot`, !day.menu && `${BASE}__dot--off`)} aria-hidden="true" />
246
+ {`${weekday.slice(0, 2)} ${date.join(' ')}`}
247
+ {!day.menu && <span className="screen-reader-text">{__('(bez lístku)', 'brilo-blocks')}</span>}
248
+ </button>
249
+ )
250
+ })}
251
+ </div>
252
+ )
253
+ }
254
+
255
+ type CardProps = {
256
+ attributes: FoodMenuGroupAttributes
257
+ data: PreviewResponse
258
+ day: PreviewDay
259
+ tabs?: React.ReactNode
260
+ }
261
+
262
+ function PreviewCard({attributes, data, day, tabs}: CardProps) {
263
+ const {mode, showPrices, showAllergens, showReservation} = attributes
264
+ const {menu} = day
265
+ const source = sourceLabel(mode, day, data)
266
+ const reservation = showReservation ? menu?.reservation : null
267
+
268
+ return (
269
+ <div className={`${BASE}__card`}>
270
+ <div className={`${BASE}__head`}>
271
+ <div className={`${BASE}__title`}>
272
+ {menuIcon}
273
+ <span>{day.label}</span>
274
+ {day.date === data.today && <span className={`${BASE}__pill`}>{__('dnes', 'brilo-blocks')}</span>}
275
+ </div>
276
+
277
+ <div className={`${BASE}__meta`}>
278
+ {source && <span className={clsx(`${BASE}__pill`, `${BASE}__pill--muted`)}>{source}</span>}
279
+ {'open' === reservation?.state && (
280
+ <span className={clsx(`${BASE}__pill`, `${BASE}__pill--ok`)}>{__('Rezervace otevřené', 'brilo-blocks')}</span>
281
+ )}
282
+ {'closed' === reservation?.state && (
283
+ <span className={clsx(`${BASE}__pill`, `${BASE}__pill--muted`)}>{__('Rezervace uzavřené', 'brilo-blocks')}</span>
284
+ )}
285
+ </div>
286
+ </div>
287
+
288
+ <div className={`${BASE}__body`}>
289
+ {tabs}
290
+
291
+ <div role={tabs ? 'tabpanel' : undefined}>
292
+ {menu ? (
293
+ <PreviewRows menu={menu} showPrices={showPrices} showAllergens={showAllergens} />
294
+ ) : (
295
+ <NoMenuNotice attributes={attributes} data={data} />
296
+ )}
297
+ </div>
298
+
299
+ {menu && (
300
+ <div className={`${BASE}__foot`}>
301
+ <span>
302
+ {[
303
+ `${menu.dishCount} ${plural(menu.dishCount, __('jídlo', 'brilo-blocks'), __('jídla', 'brilo-blocks'), __('jídel', 'brilo-blocks'))}`,
304
+ showAllergens ? __('alergeny v závorce', 'brilo-blocks') : __('alergeny skryté', 'brilo-blocks'),
305
+ showPrices ? __('ceny zobrazeny', 'brilo-blocks') : __('ceny skryté', 'brilo-blocks'),
306
+ ].join(' · ')}
307
+ </span>
308
+
309
+ {showReservation && <span>{sprintf(__('Uzávěrka rezervací: %s', 'brilo-blocks'), menu.reservation?.deadline || '—')}</span>}
310
+ </div>
311
+ )}
312
+ </div>
313
+ </div>
314
+ )
315
+ }
316
+
317
+ function WeekPreview({attributes, data}: {attributes: FoodMenuGroupAttributes; data: PreviewResponse}) {
318
+ const [selectedDate, setSelectedDate] = useState<string | null>(null)
319
+
320
+ if (0 === data.days.length) {
321
+ return null
322
+ }
323
+
324
+ // Today when it falls in the week, Monday otherwise; the choice is UI state
325
+ // only and is not saved with the block.
326
+ const pick = [selectedDate, data.today].map((date) => data.days.findIndex((day) => day.date === date)).find((index) => 0 <= index)
327
+ const selected = pick ?? 0
328
+
329
+ return (
330
+ <PreviewCard
331
+ attributes={attributes}
332
+ data={data}
333
+ day={data.days[selected]}
334
+ tabs={<PreviewDayTabs days={data.days} selected={selected} onSelect={(index) => setSelectedDate(data.days[index].date)} />}
335
+ />
336
+ )
337
+ }
338
+
339
+ export function FoodMenuPreview({attributes}: {attributes: FoodMenuGroupAttributes}) {
340
+ const {mode, menuIds, showPrices, showAllergens} = attributes
341
+ const {data, isLoading, hasError, retry} = useMenuPreview(mode, menuIds, showPrices, showAllergens)
342
+
343
+ if (hasError) {
344
+ return (
345
+ <div className={BASE}>
346
+ <PreviewEmpty title={__('Náhled se nepodařilo načíst.', 'brilo-blocks')}>
347
+ <Button variant="secondary" size="small" className={`${BASE}__retry`} onClick={retry}>
348
+ {__('Zkusit znovu', 'brilo-blocks')}
349
+ </Button>
350
+ </PreviewEmpty>
351
+ </div>
352
+ )
353
+ }
354
+
355
+ // The first load only: a reload keeps the previous card, so toggling a
356
+ // setting does not collapse the block and make it jump.
357
+ if (!data) {
358
+ return (
359
+ <div className={BASE}>
360
+ <div className={clsx(`${BASE}__card`, `${BASE}__card--loading`)}>
361
+ <div className={`${BASE}__head`}>
362
+ <Spinner />
363
+ </div>
364
+ </div>
365
+ </div>
366
+ )
367
+ }
368
+
369
+ let content: React.ReactNode
370
+
371
+ if ('week' === mode) {
372
+ content = <WeekPreview attributes={attributes} data={data} />
373
+ } else if (0 === data.days.length) {
374
+ content = <NoMenuNotice attributes={attributes} data={data} />
375
+ } else {
376
+ content = data.days.map((day) => <PreviewCard key={day.menu?.id ?? day.label} attributes={attributes} data={data} day={day} />)
377
+ }
378
+
379
+ return <div className={clsx(BASE, isLoading && `${BASE}--loading`)}>{content}</div>
380
+ }
@@ -0,0 +1,69 @@
1
+ import type {View} from '@wordpress/dataviews/wp'
2
+ import {__, sprintf} from '@wordpress/i18n'
3
+ import {createEntitySelectModal} from '~/components/EntitySelectModal/EntitySelectModal'
4
+
5
+ type MenuRecord = {
6
+ id: number
7
+ title: {rendered: string}
8
+ meta?: {
9
+ brilo_food_menu_kind?: string
10
+ brilo_food_menu_date?: string
11
+ }
12
+ }
13
+
14
+ const DEFAULT_VIEW: View = {
15
+ type: 'table',
16
+ search: '',
17
+ filters: [],
18
+ page: 1,
19
+ perPage: 20,
20
+ sort: {field: 'title', direction: 'asc'},
21
+ fields: ['kind'],
22
+ titleField: 'title',
23
+ }
24
+
25
+ /**
26
+ * A permanent menu has no date, so the kind column carries the day: the
27
+ * titles of a week of daily menus look alike and the date is what the editor
28
+ * actually picks by.
29
+ */
30
+ function describe(menu: MenuRecord): string {
31
+ const date = menu.meta?.brilo_food_menu_date
32
+
33
+ return 'daily' === menu.meta?.brilo_food_menu_kind && date
34
+ ? sprintf(__('Denní · %s', 'brilo-blocks'), date)
35
+ : __('Trvalý', 'brilo-blocks')
36
+ }
37
+
38
+ /**
39
+ * Manual brilo_food_menu picker (table view, search, sort, checkbox selection,
40
+ * reorderable selection list) with title/kind columns. See `EntitySelectModal`
41
+ * for the shared DataViews wiring.
42
+ */
43
+ export const FoodMenuSelectModal = createEntitySelectModal<MenuRecord>({
44
+ endpoint: '/wp/v2/brilo_food_menu?per_page=100&orderby=title&order=asc&_fields=id,title,meta',
45
+ defaultView: DEFAULT_VIEW,
46
+ buttonLabel: __('Vybrat lístky', 'brilo-blocks'),
47
+ modalTitle: __('Výběr lístků', 'brilo-blocks'),
48
+ emptyLabel: __('Zatím nevybrán žádný lístek.', 'brilo-blocks'),
49
+ // The block previews the picked menus in order right under the button.
50
+ listInModal: true,
51
+ getItemLabel: (item) => item.title.rendered,
52
+ matchesSearch: (item, needle) => item.title.rendered.toLowerCase().includes(needle),
53
+ sortValue: (item, field) => ('kind' === field ? describe(item) : item.title.rendered),
54
+ fields: [
55
+ {
56
+ id: 'title',
57
+ label: __('Název', 'brilo-blocks'),
58
+ enableHiding: false,
59
+ enableSorting: true,
60
+ render: ({item}: {item: MenuRecord}) => item.title.rendered || __('(bez názvu)', 'brilo-blocks'),
61
+ },
62
+ {
63
+ id: 'kind',
64
+ label: __('Typ', 'brilo-blocks'),
65
+ enableSorting: true,
66
+ render: ({item}: {item: MenuRecord}) => describe(item),
67
+ },
68
+ ],
69
+ })
@@ -0,0 +1,9 @@
1
+ export const FOOD_MENU_GROUP_MODES = ['manual', 'today', 'week'] as const
2
+ export const FOOD_MENU_GROUP_LAYOUTS = ['slider', 'list'] as const
3
+
4
+ /** What the block does on a day with no menu to show. */
5
+ export const FOOD_MENU_GROUP_EMPTY_BEHAVIOURS = ['message', 'permanent', 'hide'] as const
6
+
7
+ export const FOOD_MENU_GROUP_MODE = FOOD_MENU_GROUP_MODES[0]
8
+ export const FOOD_MENU_GROUP_LAYOUT = FOOD_MENU_GROUP_LAYOUTS[0]
9
+ export const FOOD_MENU_GROUP_EMPTY_BEHAVIOUR = FOOD_MENU_GROUP_EMPTY_BEHAVIOURS[0]
@@ -0,0 +1,153 @@
1
+ import {InspectorControls, useBlockProps} from '@wordpress/block-editor'
2
+ import {
3
+ PanelBody,
4
+ __experimentalText as Text,
5
+ TextareaControl,
6
+ ToggleControl,
7
+ __experimentalToggleGroupControl as ToggleGroupControl,
8
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
9
+ } from '@wordpress/components'
10
+ import {__, sprintf} from '@wordpress/i18n'
11
+ import clsx from 'clsx'
12
+ // biome-ignore lint/correctness/noUnusedImports: required in scope for classic JSX transform
13
+ import React from 'react'
14
+ import {plural} from '~/utils/plural'
15
+ import {FoodMenuPreview} from './FoodMenuPreview'
16
+ import type {FoodMenuGroupAttributes, FoodMenuGroupOptions} from './types'
17
+
18
+ const MODE_HINTS: Record<FoodMenuGroupAttributes['mode'], string> = {
19
+ manual: __('Zobrazí se ručně vybrané lístky.', 'brilo-blocks'),
20
+ today: __('Zobrazí se denní lístek s dnešním datem.', 'brilo-blocks'),
21
+ week: __('Zobrazí se denní lístky aktuálního pracovního týdne (pondělí až pátek).', 'brilo-blocks'),
22
+ }
23
+
24
+ const EMPTY_HINTS: Record<FoodMenuGroupAttributes['emptyBehaviour'], string> = {
25
+ message: __('Návštěvník uvidí krátkou větu, že lístek zatím není vypsaný.', 'brilo-blocks'),
26
+ permanent: __('Místo chybějícího denního lístku se nabídne stálá nabídka. Když žádná není, ukáže se věta.', 'brilo-blocks'),
27
+ hide: __('Blok ze stránky zmizí. Stránka pak může zůstat prázdná.', 'brilo-blocks'),
28
+ }
29
+
30
+ function pickedNote(count: number): string {
31
+ if (0 === count) {
32
+ return __('Zatím nevybrán žádný lístek.', 'brilo-blocks')
33
+ }
34
+
35
+ const picked = plural(
36
+ count,
37
+ __('Vybrán %d lístek', 'brilo-blocks'),
38
+ __('Vybrány %d lístky', 'brilo-blocks'),
39
+ __('Vybráno %d lístků', 'brilo-blocks'),
40
+ )
41
+
42
+ return `${sprintf(picked, count)} · ${__('pořadí změníte ve výběru lístků', 'brilo-blocks')}`
43
+ }
44
+
45
+ export function createFoodMenuGroupEdit(options: FoodMenuGroupOptions) {
46
+ return function Edit({
47
+ attributes,
48
+ setAttributes,
49
+ }: {
50
+ attributes: FoodMenuGroupAttributes
51
+ setAttributes: (a: Partial<FoodMenuGroupAttributes>) => void
52
+ }) {
53
+ const {mode, layout, menuIds, showPrices, showAllergens, showReservation, emptyBehaviour, emptyText} = attributes
54
+
55
+ const blockProps = useBlockProps({className: clsx(options.blockClassName ?? 'g-food-menu', `--${layout}`)})
56
+
57
+ return (
58
+ <>
59
+ <InspectorControls>
60
+ <PanelBody title={__('Nastavení lístku', 'brilo-blocks')} initialOpen>
61
+ <ToggleGroupControl
62
+ label={__('Zdroj lístků', 'brilo-blocks')}
63
+ help={MODE_HINTS[mode]}
64
+ value={mode}
65
+ onChange={(value) => setAttributes({mode: value as FoodMenuGroupAttributes['mode']})}
66
+ isBlock
67
+ >
68
+ <ToggleGroupControlOption value="manual" label={__('Ruční výběr', 'brilo-blocks')} />
69
+ <ToggleGroupControlOption value="today" label={__('Dnešní', 'brilo-blocks')} />
70
+ <ToggleGroupControlOption value="week" label={__('Týden', 'brilo-blocks')} />
71
+ </ToggleGroupControl>
72
+
73
+ <ToggleGroupControl
74
+ label={__('Rozvržení', 'brilo-blocks')}
75
+ help={__('Slider se použije jen tehdy, když je lístků víc než jeden.', 'brilo-blocks')}
76
+ value={layout}
77
+ onChange={(value) => setAttributes({layout: value as FoodMenuGroupAttributes['layout']})}
78
+ isBlock
79
+ >
80
+ <ToggleGroupControlOption value="slider" label={__('Slider', 'brilo-blocks')} />
81
+ <ToggleGroupControlOption value="list" label={__('Seznam', 'brilo-blocks')} />
82
+ </ToggleGroupControl>
83
+
84
+ <ToggleControl
85
+ __nextHasNoMarginBottom
86
+ label={__('Zobrazit ceny', 'brilo-blocks')}
87
+ checked={showPrices}
88
+ onChange={(value) => setAttributes({showPrices: value})}
89
+ />
90
+
91
+ <ToggleControl
92
+ __nextHasNoMarginBottom
93
+ label={__('Zobrazit alergeny a legendu', 'brilo-blocks')}
94
+ checked={showAllergens}
95
+ onChange={(value) => setAttributes({showAllergens: value})}
96
+ />
97
+
98
+ <ToggleControl
99
+ __nextHasNoMarginBottom
100
+ label={__('Umožnit rezervaci obědu', 'brilo-blocks')}
101
+ help={__(
102
+ 'Tlačítko rezervace se ukáže jen u denních lístků a jen když jsou rezervace zapnuté v nastavení modulu.',
103
+ 'brilo-blocks',
104
+ )}
105
+ checked={showReservation}
106
+ onChange={(value) => setAttributes({showReservation: value})}
107
+ />
108
+ </PanelBody>
109
+
110
+ <PanelBody title={__('Když není co zobrazit', 'brilo-blocks')} initialOpen={false}>
111
+ <ToggleGroupControl
112
+ label={__('Chování', 'brilo-blocks')}
113
+ help={EMPTY_HINTS[emptyBehaviour]}
114
+ value={emptyBehaviour}
115
+ onChange={(value) => setAttributes({emptyBehaviour: value as FoodMenuGroupAttributes['emptyBehaviour']})}
116
+ isBlock
117
+ >
118
+ <ToggleGroupControlOption value="message" label={__('Zpráva', 'brilo-blocks')} />
119
+ <ToggleGroupControlOption value="permanent" label={__('Stálá nabídka', 'brilo-blocks')} />
120
+ <ToggleGroupControlOption value="hide" label={__('Skrýt', 'brilo-blocks')} />
121
+ </ToggleGroupControl>
122
+
123
+ {'hide' !== emptyBehaviour && (
124
+ <TextareaControl
125
+ __nextHasNoMarginBottom
126
+ label={__('Vlastní text zprávy', 'brilo-blocks')}
127
+ help={__('Když zůstane prázdné, použije se věta podle zvoleného zdroje lístků.', 'brilo-blocks')}
128
+ rows={2}
129
+ value={emptyText}
130
+ onChange={(value) => setAttributes({emptyText: value})}
131
+ />
132
+ )}
133
+ </PanelBody>
134
+ </InspectorControls>
135
+
136
+ <div {...blockProps}>
137
+ {'manual' === mode && (
138
+ <div className="brilo-food-menu-preview__pick">
139
+ {options.renderMenusControl?.({
140
+ selectedItems: menuIds,
141
+ onChange: (ids: number[]) => setAttributes({menuIds: ids}),
142
+ })}
143
+
144
+ <Text variant="muted">{pickedNote(menuIds.length)}</Text>
145
+ </div>
146
+ )}
147
+
148
+ <FoodMenuPreview attributes={attributes} />
149
+ </div>
150
+ </>
151
+ )
152
+ }
153
+ }