Skip to content

Commit fee2c4b

Browse files
committed
Hide feature block and sections when empty
1 parent 9efb652 commit fee2c4b

File tree

3 files changed

+201
-126
lines changed

3 files changed

+201
-126
lines changed

src/app/[locale]/(app)/explore/_components/features-block.tsx

Lines changed: 114 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { useTranslations } from 'next-intl'
55
import { FC, PropsWithChildren } from 'react'
66
import { MarkdownContent } from '~/components/generic/markdown-content'
77
import { IntlMessageKeys } from '~/helpers/types'
8-
import { pick } from '~/helpers/utilities'
98
import { Features } from '~/server/db/constants/features'
109
import {
1110
featureDisplayGroups,
1211
getCompositeFeatureKey,
12+
getCompositeFeatureValues,
1313
getIconForFeature,
14-
getMoreInfoContent,
14+
useFeatureDisplay,
1515
} from '~/server/db/constants/features-display-data'
1616

1717
export const FeaturesBlock: FC<{ features: Features; className?: string }> = ({
@@ -20,118 +20,124 @@ export const FeaturesBlock: FC<{ features: Features; className?: string }> = ({
2020
}) => {
2121
const t = useTranslations('data.features')
2222

23+
const { allValuesNull, allValuesNullInGroup, getMoreInfoContent } =
24+
useFeatureDisplay(features)
25+
26+
if (allValuesNull) return null
27+
2328
return (
2429
<Card className={className} radius="lg" shadow="sm">
2530
<CardBody className="gap-2">
26-
{featureDisplayGroups.map((group) => (
27-
<FeatureList key={group.key} title={t(`titles.${group.key}`)}>
28-
{group.featureDisplays.map((featureDisplay) => {
29-
if ('hidden' in featureDisplay && featureDisplay.hidden) {
30-
return null
31-
}
32-
33-
switch (featureDisplay.type) {
34-
case 'number':
35-
case 'text': {
36-
const value = features[featureDisplay.key]
37-
if (value === null || value === undefined) return null
38-
39-
if ('showRaw' in featureDisplay && featureDisplay.showRaw) {
40-
return (
41-
<FeatureItem
42-
key={featureDisplay.key}
43-
icon={featureDisplay.icon}
44-
text={String(value)}
45-
moreInfo={getMoreInfoContent(featureDisplay, features)}
46-
/>
47-
)
48-
} else {
49-
return (
50-
<FeatureItem
51-
key={featureDisplay.key}
52-
icon={featureDisplay.icon}
53-
text={t(
54-
`values.${`${featureDisplay.type}.${featureDisplay.key}` as IntlMessageKeys<'data.features.values'>}`,
55-
{
56-
value,
57-
}
58-
)}
59-
moreInfo={getMoreInfoContent(featureDisplay, features)}
60-
/>
61-
)
62-
}
63-
}
64-
case 'markdown': {
65-
const value = features[featureDisplay.key]
66-
if (value === null || value === undefined) return null
67-
return (
68-
<MarkdownFeatureItem
69-
key={featureDisplay.key}
70-
icon={featureDisplay.icon}
71-
label={t(`labels.${featureDisplay.key}`)}
72-
content={value}
73-
/>
74-
)
75-
}
76-
case 'enum': {
77-
const value = features[featureDisplay.key]
78-
if (value === null || value === undefined) return null
79-
80-
return (
81-
<FeatureItem
82-
key={featureDisplay.key}
83-
icon={getIconForFeature(featureDisplay, value)}
84-
text={t(
85-
`values.enum.${`${featureDisplay.key}.${value}` as IntlMessageKeys<'data.features.values.enum'>}`
86-
)}
87-
moreInfo={getMoreInfoContent(featureDisplay, features)}
88-
/>
89-
)
90-
}
91-
case 'boolean': {
92-
const value = features[featureDisplay.key]
93-
if (value === null || value === undefined) return null
94-
95-
return (
96-
<BooleanFeatureItem
97-
key={featureDisplay.key}
98-
icon={getIconForFeature(featureDisplay, true)}
99-
iconOff={getIconForFeature(featureDisplay, false)}
100-
text={t(`values.boolean.${featureDisplay.key}.${value}`)}
101-
value={value}
102-
moreInfo={getMoreInfoContent(featureDisplay, features)}
103-
/>
104-
)
105-
}
106-
case 'composite': {
107-
const rawValues = pick(
108-
features,
109-
featureDisplay.keys
110-
) as Parameters<(typeof featureDisplay)['transformValues']>[0]
111-
112-
if (
113-
featureDisplay.showIf &&
114-
!featureDisplay.showIf(rawValues)
115-
) {
31+
{featureDisplayGroups.map(
32+
(group) =>
33+
!allValuesNullInGroup[group.key] && (
34+
<FeatureList key={group.key} title={t(`titles.${group.key}`)}>
35+
{group.featureDisplays.map((featureDisplay) => {
36+
if ('hidden' in featureDisplay && featureDisplay.hidden) {
11637
return null
11738
}
11839

119-
const values = featureDisplay?.transformValues(rawValues)
120-
121-
const key = getCompositeFeatureKey(featureDisplay.keys)
122-
return (
123-
<FeatureItem
124-
key={key}
125-
icon={featureDisplay.icon}
126-
text={t(`values.composite.${key}`, values)}
127-
moreInfo={getMoreInfoContent(featureDisplay, features)}
128-
/>
129-
)
130-
}
131-
}
132-
})}
133-
</FeatureList>
134-
))}
40+
switch (featureDisplay.type) {
41+
case 'number':
42+
case 'text': {
43+
const value = features[featureDisplay.key]
44+
if (value === null || value === undefined) return null
45+
46+
if (
47+
'showRaw' in featureDisplay &&
48+
featureDisplay.showRaw
49+
) {
50+
return (
51+
<FeatureItem
52+
key={featureDisplay.key}
53+
icon={featureDisplay.icon}
54+
text={String(value)}
55+
moreInfo={getMoreInfoContent(featureDisplay)}
56+
/>
57+
)
58+
} else {
59+
return (
60+
<FeatureItem
61+
key={featureDisplay.key}
62+
icon={featureDisplay.icon}
63+
text={t(
64+
`values.${`${featureDisplay.type}.${featureDisplay.key}` as IntlMessageKeys<'data.features.values'>}`,
65+
{
66+
value,
67+
}
68+
)}
69+
moreInfo={getMoreInfoContent(featureDisplay)}
70+
/>
71+
)
72+
}
73+
}
74+
case 'markdown': {
75+
const value = features[featureDisplay.key]
76+
if (value === null || value === undefined) return null
77+
return (
78+
<MarkdownFeatureItem
79+
key={featureDisplay.key}
80+
icon={featureDisplay.icon}
81+
label={t(`labels.${featureDisplay.key}`)}
82+
content={value}
83+
/>
84+
)
85+
}
86+
case 'enum': {
87+
const value = features[featureDisplay.key]
88+
if (value === null || value === undefined) return null
89+
90+
return (
91+
<FeatureItem
92+
key={featureDisplay.key}
93+
icon={getIconForFeature(featureDisplay, value)}
94+
text={t(
95+
`values.enum.${`${featureDisplay.key}.${value}` as IntlMessageKeys<'data.features.values.enum'>}`
96+
)}
97+
moreInfo={getMoreInfoContent(featureDisplay)}
98+
/>
99+
)
100+
}
101+
case 'boolean': {
102+
const value = features[featureDisplay.key]
103+
if (value === null || value === undefined) return null
104+
105+
return (
106+
<BooleanFeatureItem
107+
key={featureDisplay.key}
108+
icon={getIconForFeature(featureDisplay, true)}
109+
iconOff={getIconForFeature(featureDisplay, false)}
110+
text={t(
111+
`values.boolean.${featureDisplay.key}.${value}`
112+
)}
113+
value={value}
114+
moreInfo={getMoreInfoContent(featureDisplay)}
115+
/>
116+
)
117+
}
118+
case 'composite': {
119+
const values = getCompositeFeatureValues(
120+
featureDisplay,
121+
features
122+
)
123+
124+
if (!values) return null
125+
126+
const key = getCompositeFeatureKey(featureDisplay.keys)
127+
return (
128+
<FeatureItem
129+
key={key}
130+
icon={featureDisplay.icon}
131+
text={t(`values.composite.${key}`, values)}
132+
moreInfo={getMoreInfoContent(featureDisplay)}
133+
/>
134+
)
135+
}
136+
}
137+
})}
138+
</FeatureList>
139+
)
140+
)}
135141
</CardBody>
136142
</Card>
137143
)

src/server/db/constants/features-display-data.ts

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ import {
3131
IconToolsKitchen2Off,
3232
IconWalk,
3333
} from '@tabler/icons-react'
34-
import { InferInsertModel, InferSelectModel } from 'drizzle-orm'
34+
import { useMemo } from 'react'
3535
import { Join } from 'ts-toolbelt/out/String/Join'
36+
import { pick } from '~/helpers/utilities'
3637
import {
38+
FeaturesInsert,
39+
FeaturesSelect,
3740
PriceUnit,
3841
amountOfPeople,
3942
difficulty,
4043
groundType,
4144
placeToArriveFrom,
4245
priceUnit,
4346
} from '~/server/db/constants/features'
44-
import { features } from '~/server/db/schema'
4547

4648
const typeFeatureDisplay = <F extends AnyFeature>(feature: F) => feature
4749

@@ -244,17 +246,6 @@ export const featureDisplayGroups = [
244246
featureDisplays: AnyFeature[]
245247
}[]
246248

247-
export function getMoreInfoContent(
248-
featureDisplay: AnyFeature,
249-
features: Features | null | undefined
250-
) {
251-
if (!features) return null
252-
if (!('moreInfoFeatureKey' in featureDisplay)) return null
253-
if (!featureDisplay.moreInfoFeatureKey) return null
254-
255-
return features[featureDisplay.moreInfoFeatureKey]
256-
}
257-
258249
export function getIconForFeature<F extends EnumFeature | BooleanFeature>(
259250
featureDisplay: F,
260251
value: string | boolean | null | undefined
@@ -269,11 +260,87 @@ export function getCompositeFeatureKey<Keys extends Array<string>>(keys: Keys) {
269260
return keys.join('-') as Join<Keys, '-'>
270261
}
271262

263+
function getCompositeFeatureRawValues<T extends CompositeFeature>(
264+
featureDisplay: T,
265+
features: Features
266+
) {
267+
return pick(features, featureDisplay.keys) as Parameters<
268+
NonNullable<T['transformValues']>
269+
>[0]
270+
}
271+
272+
function shouldShow<T extends CompositeFeature>(
273+
featureDisplay: T,
274+
rawValues: Parameters<NonNullable<T['transformValues']>>[0]
275+
) {
276+
if (!featureDisplay.showIf) return true
277+
return featureDisplay.showIf(rawValues)
278+
}
279+
280+
export function getCompositeFeatureValues<T extends CompositeFeature>(
281+
featureDisplay: T,
282+
features: Features
283+
) {
284+
const rawValues = getCompositeFeatureRawValues(featureDisplay, features)
285+
if (!shouldShow(featureDisplay, rawValues)) return null
286+
287+
if (!featureDisplay?.transformValues) return rawValues
288+
return featureDisplay?.transformValues(rawValues)
289+
}
290+
291+
export function useFeatureDisplay(features: Features | null | undefined) {
292+
const allValuesNullInGroup = useMemo(() => {
293+
return Object.fromEntries(
294+
featureDisplayGroups.map((group) => [
295+
group.key,
296+
group.featureDisplays.every(featureDisplayIsEmpty),
297+
])
298+
) as Record<(typeof featureDisplayGroups)[number]['key'], boolean>
299+
}, [features])
300+
301+
const allValuesNull = useMemo(() => {
302+
return featureDisplayGroups.every(
303+
(group) => allValuesNullInGroup[group.key]
304+
)
305+
}, [allValuesNullInGroup])
306+
307+
function getMoreInfoContent(featureDisplay: AnyFeature) {
308+
if (!features) return null
309+
if (!('moreInfoFeatureKey' in featureDisplay)) return null
310+
if (!featureDisplay.moreInfoFeatureKey) return null
311+
312+
return features[featureDisplay.moreInfoFeatureKey]
313+
}
314+
315+
function featureDisplayIsEmpty(featureDisplay: AnyFeature) {
316+
if (!features) return true
317+
318+
if ('hidden' in featureDisplay && featureDisplay.hidden) {
319+
return true
320+
}
321+
322+
if (featureDisplay.type === 'composite') {
323+
const rawValues = getCompositeFeatureRawValues(featureDisplay, features)
324+
return !shouldShow(featureDisplay, rawValues)
325+
}
326+
327+
return (
328+
features[featureDisplay.key] === null ||
329+
features[featureDisplay.key] === undefined
330+
)
331+
}
332+
333+
return {
334+
allValuesNull,
335+
allValuesNullInGroup,
336+
getMoreInfoContent,
337+
featureDisplayIsEmpty,
338+
}
339+
}
340+
272341
// ------------------- types -------------------
273342

274-
type Features =
275-
| InferSelectModel<typeof features>
276-
| InferInsertModel<typeof features>
343+
type Features = FeaturesInsert | FeaturesSelect
277344

278345
type FeatureKey = Exclude<keyof Features, 'id'>
279346

src/server/db/constants/features.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InferSelectModel } from 'drizzle-orm'
1+
import { InferInsertModel, InferSelectModel } from 'drizzle-orm'
22
import { features } from '../schema'
33

44
// Don't reorder these values, they are used to generate the database enum.
@@ -45,4 +45,6 @@ export const placeToArriveFrom = [
4545
] as const
4646
export type PlaceToArriveFrom = (typeof placeToArriveFrom)[number]
4747

48-
export type Features = InferSelectModel<typeof features>
48+
export type FeaturesSelect = InferSelectModel<typeof features>
49+
export type FeaturesInsert = InferInsertModel<typeof features>
50+
export type Features = FeaturesSelect

0 commit comments

Comments
 (0)