Skip to content

Commit 85b4edd

Browse files
danielroewojtekmaj
authored andcommitted
refactor: calculate translations within selectedFacetIds
1 parent 7dbd791 commit 85b4edd

6 files changed

Lines changed: 370 additions & 202 deletions

File tree

app/components/compare/FacetSelector.vue

Lines changed: 30 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,36 @@
11
<script setup lang="ts">
2-
import type { ComparisonFacet } from '#shared/types'
3-
import {
4-
FACET_INFO,
5-
FACETS_BY_CATEGORY,
6-
CATEGORY_ORDER,
7-
getFacetDescriptionKey,
8-
getFacetLabelKey,
9-
} from '#shared/types/comparison'
10-
11-
const { isFacetSelected, toggleFacet, selectCategory, deselectCategory } = useFacetSelection()
12-
13-
// Enrich facets with their info for rendering
14-
const facetsByCategory = computed(() => {
15-
const result: Record<
16-
string,
17-
{ facet: ComparisonFacet; info: (typeof FACET_INFO)[ComparisonFacet] }[]
18-
> = {}
19-
for (const category of CATEGORY_ORDER) {
20-
result[category] = FACETS_BY_CATEGORY[category].map(facet => ({
21-
facet,
22-
info: FACET_INFO[facet],
23-
}))
24-
}
25-
return result
26-
})
2+
const {
3+
isFacetSelected,
4+
toggleFacet,
5+
selectCategory,
6+
deselectCategory,
7+
facetsByCategory,
8+
categoryOrder,
9+
getCategoryLabel,
10+
} = useFacetSelection()
2711
2812
// Check if all non-comingSoon facets in a category are selected
2913
function isCategoryAllSelected(category: string): boolean {
3014
const facets = facetsByCategory.value[category] ?? []
31-
const selectableFacets = facets.filter(f => !f.info.comingSoon)
32-
return selectableFacets.length > 0 && selectableFacets.every(f => isFacetSelected(f.facet))
15+
const selectableFacets = facets.filter(f => !f.comingSoon)
16+
return selectableFacets.length > 0 && selectableFacets.every(f => isFacetSelected(f.id))
3317
}
3418
3519
// Check if no facets in a category are selected
3620
function isCategoryNoneSelected(category: string): boolean {
3721
const facets = facetsByCategory.value[category] ?? []
38-
const selectableFacets = facets.filter(f => !f.info.comingSoon)
39-
return selectableFacets.length > 0 && selectableFacets.every(f => !isFacetSelected(f.facet))
22+
const selectableFacets = facets.filter(f => !f.comingSoon)
23+
return selectableFacets.length > 0 && selectableFacets.every(f => !isFacetSelected(f.id))
4024
}
4125
</script>
4226

4327
<template>
4428
<div class="space-y-3" role="group" :aria-label="$t('compare.facets.group_label')">
45-
<div v-for="category in CATEGORY_ORDER" :key="category">
29+
<div v-for="category in categoryOrder" :key="category">
4630
<!-- Category header with all/none buttons -->
4731
<div class="flex items-center gap-2 mb-2">
4832
<span class="text-[10px] text-fg-subtle uppercase tracking-wider">
49-
{{ $t(`compare.facets.categories.${category}`) }}
33+
{{ getCategoryLabel(category) }}
5034
</span>
5135
<button
5236
type="button"
@@ -57,9 +41,7 @@ function isCategoryNoneSelected(category: string): boolean {
5741
: 'text-fg-muted/60 hover:text-fg-muted'
5842
"
5943
:aria-label="
60-
$t('compare.facets.select_category', {
61-
category: $t(`compare.facets.categories.${category}`),
62-
})
44+
$t('compare.facets.select_category', { category: getCategoryLabel(category) })
6345
"
6446
:disabled="isCategoryAllSelected(category)"
6547
@click="selectCategory(category)"
@@ -76,9 +58,7 @@ function isCategoryNoneSelected(category: string): boolean {
7658
: 'text-fg-muted/60 hover:text-fg-muted'
7759
"
7860
:aria-label="
79-
$t('compare.facets.deselect_category', {
80-
category: $t(`compare.facets.categories.${category}`),
81-
})
61+
$t('compare.facets.deselect_category', { category: getCategoryLabel(category) })
8262
"
8363
:disabled="isCategoryNoneSelected(category)"
8464
@click="deselectCategory(category)"
@@ -90,33 +70,31 @@ function isCategoryNoneSelected(category: string): boolean {
9070
<!-- Facet buttons -->
9171
<div class="flex items-center gap-1.5 flex-wrap" role="group">
9272
<button
93-
v-for="{ facet, info } in facetsByCategory[category]"
94-
:key="facet"
73+
v-for="facet in facetsByCategory[category]"
74+
:key="facet.id"
9575
type="button"
96-
:title="
97-
info.comingSoon ? $t('compare.facets.coming_soon') : $t(getFacetDescriptionKey(facet))
98-
"
99-
:disabled="info.comingSoon"
100-
:aria-pressed="isFacetSelected(facet)"
101-
:aria-label="$t(getFacetLabelKey(facet))"
76+
:title="facet.comingSoon ? $t('compare.facets.coming_soon') : facet.description"
77+
:disabled="facet.comingSoon"
78+
:aria-pressed="isFacetSelected(facet.id)"
79+
:aria-label="facet.label"
10280
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs rounded border transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
10381
:class="
104-
info.comingSoon
82+
facet.comingSoon
10583
? 'text-fg-subtle/50 bg-bg-subtle border-border-subtle cursor-not-allowed'
106-
: isFacetSelected(facet)
84+
: isFacetSelected(facet.id)
10785
? 'text-fg-muted bg-bg-muted border-border'
10886
: 'text-fg-subtle bg-bg-subtle border-border-subtle hover:text-fg-muted hover:border-border'
10987
"
110-
@click="!info.comingSoon && toggleFacet(facet)"
88+
@click="!facet.comingSoon && toggleFacet(facet.id)"
11189
>
11290
<span
113-
v-if="!info.comingSoon"
91+
v-if="!facet.comingSoon"
11492
class="w-3 h-3"
115-
:class="isFacetSelected(facet) ? 'i-carbon:checkmark' : 'i-carbon:add'"
93+
:class="isFacetSelected(facet.id) ? 'i-carbon:checkmark' : 'i-carbon:add'"
11694
aria-hidden="true"
11795
/>
118-
{{ $t(getFacetLabelKey(facet)) }}
119-
<span v-if="info.comingSoon" class="text-[9px]"
96+
{{ facet.label }}
97+
<span v-if="facet.comingSoon" class="text-[9px]"
12098
>({{ $t('compare.facets.coming_soon') }})</span
12199
>
122100
</button>

app/composables/useFacetSelection.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
1-
import type { ComparisonFacet } from '#shared/types'
2-
import { ALL_FACETS, DEFAULT_FACETS, FACET_INFO } from '#shared/types/comparison'
1+
import type { ComparisonFacet, FacetInfo } from '#shared/types'
2+
import {
3+
ALL_FACETS,
4+
CATEGORY_ORDER,
5+
DEFAULT_FACETS,
6+
FACET_INFO,
7+
FACETS_BY_CATEGORY,
8+
} from '#shared/types/comparison'
39
import { useRouteQuery } from '@vueuse/router'
410

11+
/** Facet info enriched with i18n labels */
12+
export interface FacetInfoWithLabels extends Omit<FacetInfo, 'id'> {
13+
id: ComparisonFacet
14+
label: string
15+
description: string
16+
}
17+
518
/**
619
* Composable for managing comparison facet selection with URL sync.
720
*
821
* @param queryParam - The URL query parameter name to use (default: 'facets')
922
*/
1023
export function useFacetSelection(queryParam = 'facets') {
24+
const { t } = useI18n()
25+
26+
// Helper to build facet info with i18n labels
27+
function buildFacetInfo(facet: ComparisonFacet): FacetInfoWithLabels {
28+
return {
29+
id: facet,
30+
...FACET_INFO[facet],
31+
label: t(`compare.facets.items.${facet}.label`),
32+
description: t(`compare.facets.items.${facet}.description`),
33+
}
34+
}
35+
1136
// Sync with URL query param (stable ref - doesn't change on other query changes)
1237
const facetsParam = useRouteQuery<string>(queryParam, '', { mode: 'replace' })
1338

14-
// Parse facets from URL or use defaults
15-
const selectedFacets = computed<ComparisonFacet[]>({
39+
// Parse facet IDs from URL or use defaults
40+
const selectedFacetIds = computed<ComparisonFacet[]>({
1641
get() {
1742
if (!facetsParam.value) {
1843
return DEFAULT_FACETS
@@ -40,21 +65,26 @@ export function useFacetSelection(queryParam = 'facets') {
4065
},
4166
})
4267

68+
// Selected facets with full info and i18n labels
69+
const selectedFacets = computed<FacetInfoWithLabels[]>(() =>
70+
selectedFacetIds.value.map(buildFacetInfo),
71+
)
72+
4373
// Check if a facet is selected
4474
function isFacetSelected(facet: ComparisonFacet): boolean {
45-
return selectedFacets.value.includes(facet)
75+
return selectedFacetIds.value.includes(facet)
4676
}
4777

4878
// Toggle a single facet
4979
function toggleFacet(facet: ComparisonFacet): void {
50-
const current = selectedFacets.value
80+
const current = selectedFacetIds.value
5181
if (current.includes(facet)) {
5282
// Don't allow deselecting all facets
5383
if (current.length > 1) {
54-
selectedFacets.value = current.filter(f => f !== facet)
84+
selectedFacetIds.value = current.filter(f => f !== facet)
5585
}
5686
} else {
57-
selectedFacets.value = [...current, facet]
87+
selectedFacetIds.value = [...current, facet]
5888
}
5989
}
6090

@@ -69,36 +99,50 @@ export function useFacetSelection(queryParam = 'facets') {
6999
// Select all facets in a category
70100
function selectCategory(category: string): void {
71101
const categoryFacets = getFacetsInCategory(category)
72-
const current = selectedFacets.value
102+
const current = selectedFacetIds.value
73103
const newFacets = [...new Set([...current, ...categoryFacets])]
74-
selectedFacets.value = newFacets
104+
selectedFacetIds.value = newFacets
75105
}
76106

77107
// Deselect all facets in a category
78108
function deselectCategory(category: string): void {
79109
const categoryFacets = getFacetsInCategory(category)
80-
const remaining = selectedFacets.value.filter(f => !categoryFacets.includes(f))
110+
const remaining = selectedFacetIds.value.filter(f => !categoryFacets.includes(f))
81111
// Don't allow deselecting all facets
82112
if (remaining.length > 0) {
83-
selectedFacets.value = remaining
113+
selectedFacetIds.value = remaining
84114
}
85115
}
86116

87117
// Select all facets globally
88118
function selectAll(): void {
89-
selectedFacets.value = DEFAULT_FACETS
119+
selectedFacetIds.value = DEFAULT_FACETS
90120
}
91121

92122
// Deselect all facets globally (keeps first facet to ensure at least one)
93123
function deselectAll(): void {
94-
selectedFacets.value = [DEFAULT_FACETS[0] as ComparisonFacet]
124+
selectedFacetIds.value = [DEFAULT_FACETS[0] as ComparisonFacet]
95125
}
96126

97127
// Check if all facets are selected
98-
const isAllSelected = computed(() => selectedFacets.value.length === DEFAULT_FACETS.length)
128+
const isAllSelected = computed(() => selectedFacetIds.value.length === DEFAULT_FACETS.length)
99129

100130
// Check if only one facet is selected (minimum)
101-
const isNoneSelected = computed(() => selectedFacets.value.length === 1)
131+
const isNoneSelected = computed(() => selectedFacetIds.value.length === 1)
132+
133+
// Get translated category name
134+
function getCategoryLabel(category: FacetInfo['category']): string {
135+
return t(`compare.facets.categories.${category}`)
136+
}
137+
138+
// All facets with their info and i18n labels, grouped by category
139+
const facetsByCategory = computed(() => {
140+
const result: Record<string, FacetInfoWithLabels[]> = {}
141+
for (const category of CATEGORY_ORDER) {
142+
result[category] = FACETS_BY_CATEGORY[category].map(buildFacetInfo)
143+
}
144+
return result
145+
})
102146

103147
return {
104148
selectedFacets,
@@ -111,6 +155,10 @@ export function useFacetSelection(queryParam = 'facets') {
111155
isAllSelected,
112156
isNoneSelected,
113157
allFacets: ALL_FACETS,
158+
// Facet info with i18n
159+
getCategoryLabel,
160+
facetsByCategory,
161+
categoryOrder: CATEGORY_ORDER,
114162
}
115163
}
116164

app/pages/compare.vue

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
<script setup lang="ts">
2-
import {
3-
getFacetDescriptionKey,
4-
getFacetLabelKey,
5-
} from '#shared/types/comparison'
62
import { useRouteQuery } from '@vueuse/router'
73
84
definePageMeta({
@@ -27,7 +23,7 @@ const packages = computed({
2723
},
2824
})
2925
30-
// Facet selection
26+
// Facet selection and info
3127
const { selectedFacets, selectAll, deselectAll, isAllSelected, isNoneSelected } =
3228
useFacetSelection()
3329
@@ -131,13 +127,13 @@ useSeoMeta({
131127
<CompareComparisonGrid :columns="packages.length" :headers="gridHeaders">
132128
<CompareFacetRow
133129
v-for="facet in selectedFacets"
134-
:key="facet"
135-
:label="$t(getFacetLabelKey(facet))"
136-
:description="$t(getFacetDescriptionKey(facet))"
137-
:values="getFacetValues(facet)"
138-
:facet-loading="isFacetLoading(facet)"
130+
:key="facet.id"
131+
:label="facet.label"
132+
:description="facet.description"
133+
:values="getFacetValues(facet.id)"
134+
:facet-loading="isFacetLoading(facet.id)"
139135
:column-loading="columnLoading"
140-
:bar="facet !== 'lastUpdated'"
136+
:bar="facet.id !== 'lastUpdated'"
141137
:headers="gridHeaders"
142138
/>
143139
</CompareComparisonGrid>
@@ -147,13 +143,13 @@ useSeoMeta({
147143
<div class="md:hidden space-y-3">
148144
<CompareFacetCard
149145
v-for="facet in selectedFacets"
150-
:key="facet"
151-
:label="$t(getFacetLabelKey(facet))"
152-
:description="$t(getFacetDescriptionKey(facet))"
153-
:values="getFacetValues(facet)"
154-
:facet-loading="isFacetLoading(facet)"
146+
:key="facet.id"
147+
:label="facet.label"
148+
:description="facet.description"
149+
:values="getFacetValues(facet.id)"
150+
:facet-loading="isFacetLoading(facet.id)"
155151
:column-loading="columnLoading"
156-
:bar="facet !== 'lastUpdated'"
152+
:bar="facet.id !== 'lastUpdated'"
157153
:headers="gridHeaders"
158154
/>
159155
</div>

shared/types/comparison.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,6 @@ export const FACET_INFO: Record<ComparisonFacet, Omit<FacetInfo, 'id'>> = {
7272
},
7373
}
7474

75-
export function getFacetLabelKey(facet: ComparisonFacet): string {
76-
return `compare.facets.items.${facet}.label`
77-
}
78-
79-
export function getFacetDescriptionKey(facet: ComparisonFacet): string {
80-
return `compare.facets.items.${facet}.description`
81-
}
82-
8375
/** All facets in display order */
8476
export const ALL_FACETS: ComparisonFacet[] = Object.keys(FACET_INFO) as ComparisonFacet[]
8577

0 commit comments

Comments
 (0)