@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.
- package/build/.tsbuildinfo +1 -1
- package/build/blocks/column/column-item/factory.d.ts.map +1 -1
- package/build/blocks/column/column-item/factory.js +2 -1
- package/build/blocks/column/column-item/icon.d.ts +2 -0
- package/build/blocks/column/column-item/icon.d.ts.map +1 -0
- package/build/blocks/column/column-item/icon.js +2 -0
- package/build/blocks/column/column-item/index.d.ts.map +1 -1
- package/build/blocks/column/column-item/index.js +2 -0
- package/build/blocks/food-menu/food-menu-group/FoodMenuPreview.d.ts +6 -0
- package/build/blocks/food-menu/food-menu-group/FoodMenuPreview.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/FoodMenuPreview.js +167 -0
- package/build/blocks/food-menu/food-menu-group/FoodMenuSelectModal.d.ts +7 -0
- package/build/blocks/food-menu/food-menu-group/FoodMenuSelectModal.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/FoodMenuSelectModal.js +56 -0
- package/build/blocks/food-menu/food-menu-group/constants.d.ts +8 -0
- package/build/blocks/food-menu/food-menu-group/constants.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/constants.js +7 -0
- package/build/blocks/food-menu/food-menu-group/edit.d.ts +7 -0
- package/build/blocks/food-menu/food-menu-group/edit.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/edit.js +36 -0
- package/build/blocks/food-menu/food-menu-group/factory.d.ts +4 -0
- package/build/blocks/food-menu/food-menu-group/factory.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/factory.js +57 -0
- package/build/blocks/food-menu/food-menu-group/icon.d.ts +2 -0
- package/build/blocks/food-menu/food-menu-group/icon.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/icon.js +2 -0
- package/build/blocks/food-menu/food-menu-group/index.d.ts +6 -0
- package/build/blocks/food-menu/food-menu-group/index.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/index.js +7 -0
- package/build/blocks/food-menu/food-menu-group/types.d.ts +19 -0
- package/build/blocks/food-menu/food-menu-group/types.d.ts.map +1 -0
- package/build/blocks/food-menu/food-menu-group/types.js +1 -0
- package/build/blocks/form/form-group/index.js +1 -1
- package/build/blocks/gallery/gallery-group/constants.d.ts +1 -19
- package/build/blocks/gallery/gallery-group/constants.d.ts.map +1 -1
- package/build/blocks/gallery/gallery-group/constants.js +1 -8
- package/build/blocks/gallery/gallery-group/edit.d.ts.map +1 -1
- package/build/blocks/gallery/gallery-group/edit.js +3 -19
- package/build/blocks/gallery/gallery-group/index.js +1 -1
- package/build/blocks/gallery/gallery-item/index.js +1 -1
- package/build/blocks/gallery/gallery-strip-group/constants.d.ts +34 -0
- package/build/blocks/gallery/gallery-strip-group/constants.d.ts.map +1 -0
- package/build/blocks/gallery/gallery-strip-group/constants.js +17 -0
- package/build/blocks/gallery/gallery-strip-group/edit.d.ts +7 -0
- package/build/blocks/gallery/gallery-strip-group/edit.d.ts.map +1 -0
- package/build/blocks/gallery/gallery-strip-group/edit.js +34 -0
- package/build/blocks/gallery/gallery-strip-group/factory.d.ts +5 -0
- package/build/blocks/gallery/gallery-strip-group/factory.d.ts.map +1 -0
- package/build/blocks/gallery/gallery-strip-group/factory.js +28 -0
- package/build/blocks/gallery/gallery-strip-group/index.d.ts +5 -0
- package/build/blocks/gallery/gallery-strip-group/index.d.ts.map +1 -0
- package/build/blocks/gallery/gallery-strip-group/index.js +7 -0
- package/build/blocks/gallery/gallery-strip-group/types.d.ts +18 -0
- package/build/blocks/gallery/gallery-strip-group/types.d.ts.map +1 -0
- package/build/blocks/gallery/gallery-strip-group/types.js +1 -0
- package/build/blocks/groups/text-and-media-group/index.d.ts.map +1 -1
- package/build/blocks/groups/text-and-media-group/index.js +2 -0
- package/build/blocks/hero/hero-item/index.js +1 -1
- package/build/blocks/review/review-summary-item/constants.d.ts +3 -0
- package/build/blocks/review/review-summary-item/constants.d.ts.map +1 -0
- package/build/blocks/review/review-summary-item/constants.js +2 -0
- package/build/blocks/review/review-summary-item/edit.d.ts +6 -0
- package/build/blocks/review/review-summary-item/edit.d.ts.map +1 -0
- package/build/blocks/review/review-summary-item/edit.js +17 -0
- package/build/blocks/review/review-summary-item/factory.d.ts +5 -0
- package/build/blocks/review/review-summary-item/factory.d.ts.map +1 -0
- package/build/blocks/review/review-summary-item/factory.js +23 -0
- package/build/blocks/review/review-summary-item/index.d.ts +5 -0
- package/build/blocks/review/review-summary-item/index.d.ts.map +1 -0
- package/build/blocks/review/review-summary-item/index.js +5 -0
- package/build/blocks/review/review-summary-item/types.d.ts +11 -0
- package/build/blocks/review/review-summary-item/types.d.ts.map +1 -0
- package/build/blocks/review/review-summary-item/types.js +1 -0
- package/build/blocks/urgency/urgency-item/constants.d.ts +56 -0
- package/build/blocks/urgency/urgency-item/constants.d.ts.map +1 -0
- package/build/blocks/urgency/urgency-item/constants.js +32 -0
- package/build/blocks/urgency/urgency-item/edit.d.ts +7 -0
- package/build/blocks/urgency/urgency-item/edit.d.ts.map +1 -0
- package/build/blocks/urgency/urgency-item/edit.js +83 -0
- package/build/blocks/urgency/urgency-item/factory.d.ts +5 -0
- package/build/blocks/urgency/urgency-item/factory.d.ts.map +1 -0
- package/build/blocks/urgency/urgency-item/factory.js +31 -0
- package/build/blocks/urgency/urgency-item/index.d.ts +5 -0
- package/build/blocks/urgency/urgency-item/index.d.ts.map +1 -0
- package/build/blocks/urgency/urgency-item/index.js +5 -0
- package/build/blocks/urgency/urgency-item/types.d.ts +22 -0
- package/build/blocks/urgency/urgency-item/types.d.ts.map +1 -0
- package/build/blocks/urgency/urgency-item/types.js +1 -0
- package/build/components/EntitySelectModal/EntitySelectModal.d.ts +6 -0
- package/build/components/EntitySelectModal/EntitySelectModal.d.ts.map +1 -1
- package/build/components/EntitySelectModal/EntitySelectModal.js +3 -2
- package/build/components/GalleryImagesAppender/index.d.ts +5 -0
- package/build/components/GalleryImagesAppender/index.d.ts.map +1 -0
- package/build/components/GalleryImagesAppender/index.js +12 -0
- package/build/constants.d.ts +19 -0
- package/build/constants.d.ts.map +1 -1
- package/build/constants.js +8 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/utils/plural.d.ts +3 -0
- package/build/utils/plural.d.ts.map +1 -0
- package/build/utils/plural.js +7 -0
- package/package.json +1 -1
- package/src/blocks/column/column-item/factory.tsx +2 -1
- package/src/blocks/column/column-item/icon.tsx +10 -0
- package/src/blocks/column/column-item/index.ts +2 -0
- package/src/blocks/food-menu/food-menu-group/FoodMenuPreview.tsx +380 -0
- package/src/blocks/food-menu/food-menu-group/FoodMenuSelectModal.tsx +69 -0
- package/src/blocks/food-menu/food-menu-group/constants.ts +9 -0
- package/src/blocks/food-menu/food-menu-group/edit.tsx +153 -0
- package/src/blocks/food-menu/food-menu-group/editor.scss +269 -0
- package/src/blocks/food-menu/food-menu-group/factory.tsx +66 -0
- package/src/blocks/food-menu/food-menu-group/icon.tsx +8 -0
- package/src/blocks/food-menu/food-menu-group/index.ts +17 -0
- package/src/blocks/food-menu/food-menu-group/types.ts +21 -0
- package/src/blocks/form/form-group/index.ts +1 -1
- package/src/blocks/gallery/gallery-group/constants.ts +1 -8
- package/src/blocks/gallery/gallery-group/edit.tsx +3 -39
- package/src/blocks/gallery/gallery-group/editor.scss +0 -12
- package/src/blocks/gallery/gallery-group/index.ts +1 -1
- package/src/blocks/gallery/gallery-item/index.ts +1 -1
- package/src/blocks/gallery/gallery-strip-group/constants.ts +22 -0
- package/src/blocks/gallery/gallery-strip-group/edit.tsx +112 -0
- package/src/blocks/gallery/gallery-strip-group/editor.scss +36 -0
- package/src/blocks/gallery/gallery-strip-group/factory.tsx +32 -0
- package/src/blocks/gallery/gallery-strip-group/index.ts +11 -0
- package/src/blocks/gallery/gallery-strip-group/types.ts +20 -0
- package/src/blocks/groups/text-and-media-group/index.ts +2 -0
- package/src/blocks/hero/hero-item/index.ts +1 -1
- package/src/blocks/review/review-summary-item/constants.ts +2 -0
- package/src/blocks/review/review-summary-item/edit.tsx +86 -0
- package/src/blocks/review/review-summary-item/editor.scss +44 -0
- package/src/blocks/review/review-summary-item/factory.tsx +27 -0
- package/src/blocks/review/review-summary-item/index.ts +9 -0
- package/src/blocks/review/review-summary-item/types.ts +11 -0
- package/src/blocks/urgency/urgency-item/constants.ts +39 -0
- package/src/blocks/urgency/urgency-item/edit.tsx +319 -0
- package/src/blocks/urgency/urgency-item/editor.scss +99 -0
- package/src/blocks/urgency/urgency-item/factory.tsx +36 -0
- package/src/blocks/urgency/urgency-item/index.ts +9 -0
- package/src/blocks/urgency/urgency-item/types.ts +23 -0
- package/src/components/EntitySelectModal/EntitySelectModal.tsx +19 -7
- package/src/components/GalleryImagesAppender/editor.scss +15 -0
- package/src/components/GalleryImagesAppender/index.tsx +29 -0
- package/src/constants.ts +9 -0
- package/src/editor.scss +5 -0
- package/src/index.ts +13 -0
- 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
|
+
}
|